As you may know, Dialogs have been deprecated and are going away. I have a few customers who’ve asked me to replace their existing dialogs with something that would allow them to get the same result without losses in functionality.
Microsoft recommends 2 techniques to replace Dialogs – BPF and Canvas Apps (embedded or opened by the Url).
BPF just can’t do what I need.
Canvas Apps looks like a good solution but with the ease of customizing, I experienced issues with support and moving Canvas Apps between environments and sending data back to the calling source.
So I decided to choose the third option – the development of HTML/JS Web Resources.
If you found this page – heads up – I have an updated version of this post here.
Tools and technologies
Let me explain my approach. I plan to develop rich Html/Js webresources with UCI lookalike controls. That’s why I added “Office UI Fabric” to the list. “Office UI Fabric” is a “React”-based framework – that’s why “React” is in the list as well.
To quickly begin app development, I use the “Create React App” package because it contains the initial version of the application, configured webpack, and much more so there is no need to do a lot of the initial configuration.
Last but not least – “Visual Studio Code” – that’s the editor I use to work with the code for this project.
Initialization of the project
Before initialization of the project, it’s required to install npm package “create-react-app”. In order to do that, I run the following command from the terminal:
npm install -g create-react-app
Once it is installed, I navigate to the folder of the project and run the following command:
npx create-react-app abcustomdialog --template typescript
After the project is initialized, I open it using “Open Folder” menu item of VSCode. That’s how the initial structure of the project could look like:
To check that everything works the way it should, I run the following command from the terminal:
npm start
As a result, in the browser I see the following application:
Obviously, I don’t need everything that is added to the project by default, so I delete unneeded items and remove all places where those items are referenced. The following screenshot demonstrates the comparison of the project’s structure before and after the “clean-up”:
After all modifications, files look like this:
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>
I deleted all of the content from “App.css”. And because of this reason I don’t provide the content of it here.
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') );
During the run of “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 office-ui-fabric-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
Let me start from “App.tsx” – the following listings contain all of the code that is required and contains comments (if I missed anything, feel free to ask for clarification in comments):
import React from 'react'; import './App.css'; //those components are required because I use it in dialog form import { Stack, PrimaryButton, TextField, DatePicker, initializeIcons } from 'office-ui-fabric-react'; //initialization of icons - without calling this function icons for DatePicker would not be shown initializeIcons(); //this interface contains description of state and properties for Dialog application export interface IABCustomDialogState { //text input text: string | undefined; //date input date: Date | undefined; } class ABCustomDialog extends React.Component<IABCustomDialogState, IABCustomDialogState> { constructor(props: IABCustomDialogState) { super(props); //passing of data from properties to state of control during initialization this.state = props; } //this function is used to control formatting of datetime field private 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; } //heart of application that returns React markup render() { return ( <> <TextField label="Label of the Text Input" value={this.state.text} onChange={(event: any, newvalue: string | undefined) => { this.setState({ text: newvalue }); }} /> <DatePicker label="Label of the Date Input" value={this.state.date} onSelectDate={(newValue: Date | undefined | null) => { this.setState({ date: newValue ? newValue : undefined }); }} formatDate={this.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 window.returnValue = this.state; window.close(); }} /> <PrimaryButton text="Cancel" onClick={() => { //This code closes the dialog window window.close(); }}/> </Stack> </div> </>); } } export default ABCustomDialog;
I used a special CSS class for the “footer” div. Here it is:
.footerDiv { position: absolute; bottom: 0; width: 98%; }
The next place I have to modify is the code that “starts” the application – “index.tsx”. Here is the code:
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; //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 const data = JSON.parse(params.data); //rendering of application and passing parameters inside ReactDOM.render( <App text={data.text} date={new Date(data.date)} />, document.getElementById('root') );
After this change, the App will stop rendering and the browser will display a bunch of errors but never mind – it’s ok – the next tests I will do will be tests from CE.
Build and deployment
To build the project I run the following command:
npm run build
Once the project is built, a new folder is created in the project’s folder – “build”. Here is what it could look like:
That’s not the perfect folder structure to maintain. In order to change it, it would be required to change webpack config. To do that I open \node_modules\reacts-scripts\config\webpack.config.js and look for the following part of the configuration and comment it out to change the file to:
Also I apply following changes to have consistent naming of css and js parts of the project:
After changes are applied, I run the build command using the following script:
npm run build
Now “build” folder should look like the following:
The last change in the files I will make before the deployment to CE is the following change in “index.html” from the “build” folder – it’s basically a change of references for .js and .css files from absolute to relative:
Now those 3 files can be imported to CE. I created a solution and added 3 Web Resources to it:
Invoking the Dialog Window
Using the following code, I invoke this dialog from the form or ribbon script. As a parameter for the function I pass in formContext:
var AB = AB || {}; AB.ContactRibbon = (function(){ function openDialog(formContext){ var data = { text: formContext.data.entity.getPrimaryAttributeValue(), date: formContext.getAttribute("createdon").getValue() }; var dialogParameters = { pageType: "webresource",//required webresourceName: "ab_/ABCustomDialog/index.html",//Html Webresource that will be shown data: JSON.stringify(data) }; var navigationOptions = { target: 2,//use 1 if you want to open page inline or 2 to open it as dialog width: 400, height: 300, position: 1, title: "My Custom Dialog" }; Xrm.Navigation.navigateTo(dialogParameters, navigationOptions).then( function (returnValue) { console.log(returnValue); //Add your processing logic here }, function (e) { Xrm.Navigation.openErrorDialog(e); }); } return { OpenDialog: openDialog }; })();
Demonstration
The following GIF demonstrates how it would look like if this dialog is called on click of a ribbon button:
Hi,
nice read!
What do you prefer, using plain html like in your other recent blog post (https://butenko.pro/2019/12/23/xrm-navigation-navigateto-gotchas-tricks-and-limitations/) or using react?
Are there any advantages or disadvatanges to either way?
Cheers
The end result will be html/js webresource anyway – cool thing of Office UI Fabric is that developer gets awesome “UCI-lookalike” controls, React allows to concentrate on functionality and not spend a lot of time on data-binding.
Andrew
Hi Andrew
Looks nice and I wanted to try it myself.
However I got struck editing the webpack.config.js as the images are too small for me to see clearly and have probably made a mistake and the build doesn’t produce the files as expected.
Could you please post larger ones or the before and after text?
Cheers
Richard,
Try to zoom in using the browser (Ctrl + Mouse Wheel) – it will be easier to see details of changes.
Andrew
You can open an image by right click + open the image in the new tab. Remove resize parameter from the URL and you will get a full resolution image.
Before: https://i2.wp.com/butenko.pro/wp-content/uploads/2020/04/A8917154-658D-4273-AD7C-13A123575334.gif?resize=640%2C360&ssl=1
After: https://i2.wp.com/butenko.pro/wp-content/uploads/2020/04/A8917154-658D-4273-AD7C-13A123575334.gif
Hi Andrew,
Thank you for sharing, it inspires me a lot.
Mehdi,
You are welcome! I’m glad you liked the post.
Andrew
Hey mate,
You have inspired me since 2009. I just want to say thank you.
Dan,
Your comment and your inspiration is the reason why I keep doing what I was and am doing. Thanks for your warm words!
Andrew
Hi Andrew, a really great post.
Just a little comment to polish. There is no need to use this line of code (“window.parent.$(“h1[data-id=’defaultDialogChromeTitle’]”, window.parent.document).html(“My Custom Dialog”);”) to change the web resource Display name. You can just web resource display name in solution to “My Custom Dialog” and it will appear on the modal. Hope it helps and thanks again!
Michael,
Thanks. I noticed that as well but I haven’t updated the post yet.
Andrew
HEllo Michael,
Thanks for the tip!
I’d actually love some help with configuring Babel and Webpack for a Dynamics CRM 2011,
if you can post a quick-guide on the hows it would surely by of great help!
thank you,
Ofek
Is it possible to add optionset as a controller
Muhammed,
Absolutely. In order to do that you will have to query metadata to get the optionset values/labels and use this control – https://developer.microsoft.com/en-us/fluentui#/controls/web/dropdown
Andrew
Hey Andrew,
Thanks for posting. Do you see any usage of Dialog fluent ui component to ease action handlers and avoid DOM manipulation? Component url:
https://developer.microsoft.com/en-us/fluentui#/controls/web/dialog
Cheers,
Eryk
Erik,
I don’t think it could be used within CDS scripts but it could be used in Html/Js Webresources and PCF controls.
Andrew
Hello Andrew,
Will this approach works for updating the field of multiple records at once?
Thanks!
Yasaswini,
Can you please explain your usecase? I’m not sure I understand how do you plan to use Html/JS webresource in this situation.
Thanks,
Andrew
Hi,
I am trying and stuck at point where it is showing error at $. Could you please tell me what should i do here? installed jquery and added under import statements.
Ramakrishna,
Did you add //@ts-ignore directive before lines with $ symbol?
Also there is an alternative way on how it’s possible to close the form/set the label – check the updated post.
Thanks,
Andrew
Hi Andrew,
first of all: thank you for the great post!
I’m trying to use the same approach in order to have the web resource send back a return value, but no matter what I do the success promise I pass to navigateTo.then() always gets a result containing {returnValue: null}.
Do you happen to know what could cause such an issue?
The CRM I’m using is Dynamics 365 9.2.21012.00146, and the app is recognized as Unified Interface.
Thank you in advance.
Romeo
Hello Romeo,
I used that approach literally the last week and the code worked without any issue.
Here is the “Dialog” side:
window.returnValue = this.state;
window.close();
Here is the “Dialog Invoker” side:
Xrm.Navigation.navigateTo(dialogParameters, navigationOptions).then(
function (returnValue) {
if (!returnValue || !returnValue.returnValue || !returnValue.returnValue.queueId) {
return;
}
var queueId = returnValue.returnValue.queueId;
…
Andrew
Hi Andrew,
~1 year ago the solution like this was be created.
but after the last update “9.2.20123.00143” our website doesn’t return anything anymore.
can you please verify that this still is working ?
Thanks,
Andre
Hello Andre,
It seems that this approach doesn’t work anymore. I will have to update the article and return the approach that used session storage to return results back.
Andrew
Hi Andrew,
I am not happy about the normal sessionState, because on some validateion tools of dynamics it’s define as critical error. also on the mobile deveices this could be a problem. maybe the better solution is to use “Microsoft.CIFramework”
– getSession
– createSession
– …
I will also design try to design a workaround.
Andre
Andre,
Please feel free to share once you’re done with the workaround.
Andrew
Hey,
running into the same issue, that the value is not passed anymore using window.returnValue.
Is there a simple workaround yet?
Thanks!
Serf,
I updated the code to use sessionStorage to return the response to the calling part.
Andrew
Well, thank you for the reply, I actually didn’t manage to understand what the problem is and sidestepped the problem by setting/reading the sessionState.
In the future I hope to have the time to get back to the issue and understand what the issue was in case somebody stumbles in this article while investigating the same issue.
Keep up the good work 🙂
Romeo
Hi Andre/Andrew,
Thanks for your comment and solution. I have followed the same article and implemented it was working fine.
But now, I am also facing the above issue that, we are always getting returnValue as “null”
could you please help us with a workaround if any?
Hello Vivek,
I will update the post to use the solution that I used before – usage of the session storage to pass the result to the calling code.
Andrew
Hi Andrew,
I have used the session Storage to pass the result in the calling code. It’s working as expected.
But I would like to know if there is any other approach because I have seen that Andre Grumbach mentioned that it will create a problem if we use this approach.
Vivek Mugundu.
Great article, Thank you for sharing this article.
You are welcome! I’m glad you found it helpful!
Hi, thanks for this great article! To change the static output I went for rewire (https://github.com/oklas/react-app-rewired).
1. npm i react-app-rewired
2. create a ‘config-overrides.js’ in the root directory of your react app and insert this:
const MiniCssExtractPlugin = require(‘mini-css-extract-plugin’);
module.exports = (config, env) => {
config.optimization.runtimeChunk = false;
config.optimization.splitChunks = {
cacheGroups: {
default: false,
},
};
config.output.filename = ‘static/js/bundle.js’;
var cssPluginIndex = config.plugins.findIndex(plugin => plugin instanceof MiniCssExtractPlugin);
config.plugins[cssPluginIndex] = new MiniCssExtractPlugin({
filename: ‘static/css/[name].css’,
chunkFilename: ‘static/css/[name].chunk.css’,
});
return config;
};
3. in package.json change “script” > “build” to “react-app-rewired build”
4. npm run build
Martin,
Thanks a lot! Beautiful minds think alike! I got a similar suggestion from one of my collegues. I just don’t have time to update the post in order to include those recommendations.
Andrew
Hi,
Is it possible to get non-minified version of build scripts? Please help!
Hello Ankita,
Potentially there is a way but I didn’t go superdeep in this.
Andrew
Can you please suggest some code example while react is used webresource in the form where we use formcontext. Onload the react loads before even it set window.formContext.
Hello Aditya,
I will put the suggested content on the list of posts for the future.
But generally speaking, if you plan to use the data from formContext – just render the control after formContext is passed into Html Webresource.
Thanks,
Andrew
Hey Andrew, thank you for this UI full post.
I would like to extend this with formContext and use of Xrm.WebApi in it, but not sure how to use formContext within App.js. Can you pls share some code examples or related articles?
Hello Rohan,
Quick question – how do you plan to use that webresource? Do you plan to embed it in form/navigation or open it as a dialog?
Andrew
As a requirement, I will open it as a dialog using Xrm.Navigation.navigateTo. I need to use Xrm.WebApi once the dialog opened and on submit button click, need to create record in Dynamics. I tried declare global object and assigned formContext to parent object from opening JS. But when I use the same in App.js, not able to build the code as it is showing build error “Unexpected use of ‘parent’ no-restricted-globals”.
I decided to open it is a Dialog using Xrm.Navigation.navigateTo. Once dialog opens, need to use XrmWebApi to retrive data and on submit button click need to creare record in dynamics.
I tried creating globel property like parent.getContext() from calling JS and used it in App.js. But unable to build the project as getting error “Property ‘getFormContext’ does not exist on type ‘Window & typeof globalThis'”.
Hello,
In this case, your best shot is to use ClientGlobalContext.js.aspx – https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/getglobalcontext-clientglobalcontext.js.aspx
An alternate way is to use direct calls to webapi without using Xrm.WebApi.
Andrew
I thoroughly enjoyed reading your insightful blog post about developing custom HTML/JS web resources with the assistance of modern frameworks. Your comprehensive guide provides an excellent resource for both seasoned developers and newcomers to the web development landscape.