I wrote the initial post on how to develop Html/JS webresources using modern frameworks almost 2 years ago. A few months back I got an email from my colleague Chris Groh who did a few brilliant recommendations on how to enhance the development process and in this post, I will provide an updated version of the steps and those golden tips.
Initialize
Create the folder for your web application and run the following command to initialize the project:
npx create-react-app abcustomdialog --template typescript
Yes, no additional installation of the “create-react-app” package is needed.
Open the project folder using VS Code. You should see something like the following:
In order to check that everything is configured the right way run the “npm start” command. Running this command will start “development server” and your browser should show the following app:
Project clean-up
Usage of the template brings a lot of files. As a next step it’s possible to perform the clean-up and remove files that won’t be used:
Also, I perform modifications to files:
index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>React App</title> </head> <body> <div id="root"></div> </body> </html>
App.tsx:
import React from 'react'; import './App.css'; function App() { return ( <> </> ); } export default App;
index.tsx:
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render( <App />, document.getElementById('root') );
Remove everything from App.css file.
During the run of the “npm start” command, a blank browser window is presented. So now, after all preparations have been completed, it’s possible to switch to the fun part that is the development of the application.
In my project, I plan to rely on Office UI Fabric (or Fluent UI how it is called now) so I run the following command to install the required packages:
npm install @fluentui/react
Scenario
With a click of the ribbon button, the user is presented with a modal dialog window that contains a text and date input control, initial values for fields are passed from the calling script. By click on the “OK” button located at the bottom of the dialog – the window is closed and user inputs are returned to the calling source. If the “Cancel” button is clicked – nothing happens.
Implementation
I will start from App.tsx – the following
import React from 'react'; import { TextField } from '@fluentui/react/lib/components/TextField/TextField'; import { DatePicker } from '@fluentui/react/lib/components/DatePicker/DatePicker'; import { initializeIcons } from '@fluentui/react/lib/Icons'; import { Stack } from '@fluentui/react/lib/components/Stack/Stack'; import { PrimaryButton } from '@fluentui/react/lib/components/Button'; import './App.css'; initializeIcons(); export interface IABCustomDialogProps { //text input text: string | undefined; //date input date: Date | undefined; } const ABCustomDialog: React.FunctionComponent<IABCustomDialogProps> = (props: IABCustomDialogProps) => { const [text, setText] = React.useState<string | undefined>(props.text); const [date, setDate] = React.useState<Date | undefined>(props.date); const formatDate = (value?: Date | undefined): string => { if (!value) { return ""; } let result = ("0" + (value.getMonth() + 1).toString()).slice(-2) + "/"; result += ("0" + value.getDate().toString()).slice(-2) + "/"; result += value.getFullYear().toString(); return result; } return ( <> <TextField label="Label of the Text Input" value={text} onChange={(event: any, newvalue: string | undefined) => { setText(newvalue); }} /> <DatePicker label="Label of the Date Input" value={date} onSelectDate={(newValue: Date | undefined | null) => { setDate(newValue ? newValue : undefined) }} formatDate={formatDate} /> <div className="footerDiv"> <Stack horizontal horizontalAlign={"end"} tokens={{ childrenGap: 10, padding: 10 }}> <PrimaryButton text="OK" onClick={() => { //this code on click of "OK" button returns current state to calling part //@ts-ignore window.returnValue = { text: text, date: date }; window.close(); }} /> <PrimaryButton text="Cancel" onClick={() => { //This code closes the dialog window window.close(); }} /> </Stack> </div> </>); } export default ABCustomDialog;
Here is the CSS I put to App.css:
.footerDiv { position: absolute; bottom: 0; width: 98%; }
Here is the code of index.tsx:
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; let data: any; if (window.location.hostname === "localhost") { data = { text: "Initial String", date: new Date() }; } else { //following code parses url of dialog to extract required data including "data" parameter const queryString = window.location.search.substring(1); let params: any = {}; const queryStringParts = queryString.split("&"); for (let i = 0; i < queryStringParts.length; i++) { const pieces = queryStringParts[i].split("="); params[pieces[0].toLowerCase()] = pieces.length === 1 ? null : decodeURIComponent(pieces[1]); } //deserializing of the data parameter if (params.data) { data = JSON.parse(params.data); } else { data = { text: "Initial String", date: new Date() }; } } //rendering of application and passing parameters inside ReactDOM.render( <App text={data.text} date={new Date(data.date)} />, document.getElementById('root') );
In order to see the first results, you can run the “npm start” command. In a nutshell, you should see something like presented on the following screenshot:
Build
In the original article, I was changing the webpack.config. That works when you’re the one-man-army. But when one works in a team and it’s possible that your teammate may have a need to apply some changes to your code- this way could go downside really fast. Chris suggested using a custom build script and “rewire” npm package to override settings used in the build process.
Install the “rewire” package using the following command from the terminal:
npm install rewire
Create a JS file with the following content and add it to your project (I used ./scripts/custom-build.js) in my case – this file used during the build process overrides the configuration of “chunking” and all files are built in one piece:
const rewire = require('rewire'); const defaults = rewire('react-scripts/scripts/build.js'); let config = defaults.__get__('config'); // Allow configuration for dev/non-dev based on command line parameter. const isDev = process.argv.indexOf('-dev') !== -1; if (isDev) { config.optimization.minimize = false; config.mode = "development"; } config.optimization.splitChunks = { cacheGroups: { default: false } }; config.optimization.runtimeChunk = false; config.output.filename = '[name].js' config.output.chunkFilename = '[name].chunk.js'; const minCssPlugin = config.plugins.find(x => x.constructor && x.constructor.name === 'MiniCssExtractPlugin'); minCssPlugin.options.filename = '[name].css' minCssPlugin.options.moduleFilename = () => 'main.css' config.module.rules.forEach(rule => { if (!Array.isArray(rule.oneOf)) return; rule.oneOf.forEach(x => { if (x.options && x.options.name === 'static/media/[name].[hash:8].[ext]') x.options.name = '[name].[ext]'; }); });
Add “homepage” to “package.json” file – that will configure react-scripts to use relative paths during the build process:
Replace standard build commands with the custom that was just added in “package.json”:
So from now, running “npm run build” will build the app that is not shrunk into chunks, and even better – using “npm run build:dev” will build a non-minified version of the code that is helpful during troubleshooting. Here is how the build folder will look like once the build is completed:
There is no need to change anything inside any of the files and the next step is an import of files to the system. The main requirement to that import – files have to be in one folder like it’s shown on the next screenshot:
Once files are imported to the system it’s up to use how to use it – as a resource embedded into the SiteMap or record page or in dialog scenarios. In order to get more clarity regarding the usage of Html WebResources as dialogs you can check my other posts:
Xrm.Navigation.navigateTo – gotchas, tricks and limitations
Development of custom Html/JS Webresources with help of modern frameworks
Cover photo by Christina @ wocintechchat.com on Unsplash
Have you tried “react-app-rewired” npm package? I have not tried “rewired” but felt like you need lot of configuration to start it up.
Danish,
I have not. I will put it to my long “To Check” list 🙂
Andrew
Thanks for a great job. I have a question. If the autosave is prevented how can I save the new value of the field using javascript?
I created a javascript Onload event but the record display unsaved form.
Thanks in advance
Hello Necdet,
Can you please explain your scenario in detail, I’m not sure that I fully understand it.
Andrew
Hi Andrew,
Thanks for the answer. The problem is already solved. But I have another problem. I added Web resources on the contact, account, and lead form. Which is displayed leaflet map and recorded with a marker. Marker is dependent on customer type and I query all records using the Rest API. We have over 100K records for each entity(account, contact, and lead) When I open the form the query needs over 2 minutes to display the map, and when I checked the check box of customer type (for marker) need the closely same time to display on the map.
My question is how can I query faster and display markers on the HTML web resource map without freezing the CRM form?
Thanks in advance
Hello,
I have few questions:
1. What kind of requests do you use to query records? Sync/Async? Usage of Async approach should not freeze the page so if you use Sync – refactor the code using Async instead.
2. Do you really need that 100k records for every of entities? I’m not sure what’s customer’s usecase for that scenario…
Andrew
Hello,
I use HttpRequest with pagination to query all records with required fields value (full name, contact, street, zip, city telephone, and related account customer type and website ) These fields I need to add popup when the user clicks on the marker it must display with the information. I use the async method.
The scenario is: When the user opens a contact form there is a Tab for the web resource that displays the open street map. The code queries all contacts with coordinate(lat, long) and adds in search circle (between 5 and 150 km) with different colors of markers depending on customer type.
From here users can select the records and add them to the marketing list. With 10K record working perfect but when I load on Product system were to exist over 100K records it is working very slow. It needs 3 minutes to load the map. There are checkboxes to select each customer type and for all separate checkboxes.
What can you advise me to accelerate my program? How fast can I query all records? Is it better to use FetchXML and HttpRequest together?
Thanks in advance
Hello Andrew,
Thanks for the answer.
I use Http request async to retrieve records. (with paging)
I must query all contact records while I retrieve the coordinate(lat, long) and with calculation select which are in the selected circle will be marked on the map. Users can click the marker and see the information of the records and also can add to the marketing list. Maybe I use the wrong logic? There are checkboxes which are depending on the customer type and when the user checked customer type these are will be displayed on the map.
The approach is: User can select a contact and search other contacts in a circle(between 5 to 150 km) and these are will be displayed on the map with a marker. When they click marker they can see the information about the record from CRM and if they want to create a marketing list they can add all contacts in a circle to a marketing list and create it. All contacts have a parent account and the account has the field which displays customer type of account with options set. (14 different types)
I hope I explain understandable. Sorry about my English thanks for your time and help.
Hello Necdet,
Somehow I missed your question. I believe that the best way to speed up the processing is to avoid “traveling” time that is related to the page-by-page approach.
The way that I used is the following:
1. Create an action that receives search parameters and returns a string.
2. Write a plugin to handle that message and move all the processing to the serverside – querying, filtering, e.t.c.
3. Build a result and serialize it into JSon format and return it.
4. Call that action from your form and use the result on the map.
Thanks,
Andrew
Thank you for your input!
This really helped us solve our task, although we had to replace fluentui with another framework, because it doesn’t support React 18 yet.
Hello Yosem,
I was building the dialog just yesterday and I experienced similar issues with the compatibility of React 18 and FluentUI. I downgraded the version of the react/react-dom to 17.0.2 and everything started to work again.
I’m glad you found the article helpful.
Andrew
Hi, thanks for the blog post and its been great help!
I would like to get some details on how do you deploy to the crm. from you blog post, you mentioned it should upload into same folder. i am a bit lost on this part. can you help? thanks!
regards,
Helicon
Hello Helicon,
In this case, I mentioned a virtual folder. Let’s say the html webresource was deployed to the system with the following name “ab_/Dialog/Dialog.html”. In this case, the folder will be “ab_/Dialog/” and JS/CSS files should be uploaded to the same virtual folder so “ab_/Dialog/Dialog.js” and “ab_/Dialog/Dialog.css”.
Andrew
Hi Andrew.
Thanks for the update. It was great help!
Regards
Helicon
Hello Andrew,
Thank you for this elegant solution.
Could you share your code invoking the dialog window? i tried via Xrm.Navigation.navigateTo like in your previous post (Development of custom Html/JS Webresources with help of modern frameworks).
But I can’t successfully manipulate the data inside returnValue. Are you still using this approach?
Hello,
At the moment I use sessionStorage to pass the parameters to the dialog and get the response from the dialog – https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
Thanks,
Andrew
Sorry I never got back to you. The error was a stupid syntax mistake on my side that I fixed shortly after that. Thank you for the help and the interesting article!
Hello Nicolas,
I’m glad you were able to resolve the issues you experienced.
Thanks,
Andrew
The index.html after build is not actually working and I have a blank dialog on dynamics 365 page. Could you help?
Did you deploy JS and CSS webresources?
Yes, but no solution yet
I would recommend opening the browser developer tools and checking if everything looks fine in it.
Hi, I have dropdown with 5 options but, when I open ab dialog by default choice value is empty. How to make 1 option default? Or, how to disable “Ok” button if choice value is empty?
Hello,
You can set the value of the dropdown by default (populate the state for the dropdown value on initiation).
Also, you can set the Disabled property for your OK button saying that button is disabled when the state for the dropdown value is null or undefined.
Thanks,
Andrew