Angular within Dynamics CRM 2016

See Github Angular CRM Dialog Example - NgCrmEg

Introduction

Angular is a robust Single Page Application framework that leverages the high-performance JavasScript engine built into most modern Web Browsers. This post addresses integrating the most recent version, Angular 5.x into a Dynamics CRM

Solution and specifically into a CRM Entity Form. Why would you want to do this? It gives you a thoroughly modern (and testable) UI for CRM Customizations via the CRM Web Resource HTML feature. With Angular, you code the UI and business logic in an MVVM/MVC pattern and then integrate it into your existing CRM Solution application.

Background

A Brief History of Angular

Angular has been around since 2009 as a Development tool, first as GetAngular and then as AngularJs at Google. The Angular concept is simple and sublime. You start with a single web page as the entry point into an entire application. So, an "index.html" references the necessary JavaScript files and CSS to kickstart the entire thing. A single HTML element (known as a directive, but now as a component), say "<app-root></app-root>", is interpreted by the Angular JavaScript core libraries as the trigger to launch the entire application. Views are defined as HTML snippets (bound to a controller - JS/TS class file - thus composing directive/components), in which HTML elements reference still more directive/components - Turtles all the way down. See John Papa, "HTML Fragments are Views", for further details. AngularJs leveraged all of the advanced design patterns to bring the Web kicking and screaming into the modern age - testability via Test Driven Development and Dependency Injection, et al. Angular 2.x through 5.x is the rewrite of AngularJs - bigger, better, faster. Well, maybe. It does simplify many of the pain points of AngularJs, and it adds the advantage of TypeScript (a type-safe Object Oriented JavaScript-ish language) that is then transpiled into type-safe JavaScript code.

 AngularJs in CRM

Rami Mounla documents the process of adding an AngularJs SPA into CRM in his PakT book "Microsoft Dynamics 365 Extensions Cookbook" (also available on Amazon). I used his book as a starting point and assumed that introducing Angular 5.x into CRM 2016 would follow a similar recipe.

 Tools

I used the following toolset (in addition to Dynamics CRM 2016):

  • Angular 5 - recommend that you use the latest toolset, which requires Node and Node Package Manager (NPM)
  • Visual Studio Code - a great open source code editor/IDE managed by Microsoft. It is soooo good and clean. https://code.visualstudio.com/ and customize with Angular and additional TypeScript extensions as you see fit.
    • Recommended Extensions - anything by Papa or Wahlin appropriate to the version of Angular is a good starting point.
  • Fiddler - owned and maintained byTelerik. Same great tool as always. https://www.telerik.com/fiddler
  • Command Prompt - I am using Windows, so "cmd" and run as Administrator (saves some problems with '-g' for "ng" commands.
  • Chrome Browser - I use Chrome mainly because I have Edge open to many pages of Developer goodies, and reserve Chrome only for development. It makes the process less confusing when I have a single Browser Icon for viewing the application while debugging.

 Overview

Create a sample SPA application to host in a CRM Web Resource using the Angular CLI. Keep it simple stupid (KISS). Create a simple testable UI that uses Mock data, and get working within the CRM Entity Form. Then add the CRM features and hooks using an Injectable Service. Rely on the "ng" command line tool to build out both Development and Production builds. Deploy Production build to CRM (with some modification to the "index.html"), but debug with the Development build.

Recipe - Let's Do This

Angular Starter Application

If you have an application in hand and know what you are doing, then skip this section, and go right to the Preparation of Angular Application for CRM Integration section.

  • Open a Command prompt, and navigate down to the directory for building your application. Install Angular and client if needed:

npm install -g @angular/cli

  • Create the application (be patient) - Note that you should follow Angular convention of snake case (lower case with dash separating phrases):

ng new ng-crm-eg

  • CD to the app folder:

cd ng-crm-eg

  • And serve it up to verify that all is good. Creates a Dev Dev build and reports what port it is serving the application to:

ng serve

  • Visit the page in a browser. The command output will inform you of the port your Application will be served on (4200 for my ngeg example). I will use Chrome. So open Chrome and enter "localhost:4200" as the URL. You should see the sample application served up:


  • You could stop right there and make this your sample CRM application to integrate. But it would not serve much good. So let's be more realistic and introduce a button and a cool dialog via the Google Material Design button and dialog components. Start by adding the Material Design features. First "Ctrl-C" at the command prompt to end the "ng serve" server.
  • Install Material Design Dialog (https://material.angular.io/components/dialog/overview) bits:

[code language="text"]

npm install —save @angular/material @angular/cdk

npm install —save @angular/animations

[/code]

  • Add the Material Design Stylesheet reference into the "index.html" header. Open Visual Studio Code and open the Directory where you have created your application. You have 2 choices: open at the application level or the source level. Each has an advantage. For a large application you would probably want to open at the source level. But for the demo and learning your way around the various "JSON" configuration files, open at the application level.
    • File / Open Folder: "C:\Projects\ngeg\ng-crm-eg", and "Select Folder". Your Source will be under the "src" folder. Open the "src" folder, and click on "index.html". Near the end of the "head" element section add the folliowing link reference:

[code language="html"]

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

[/code]

  • Save (Ctl-S).
  • At this point you have installed the Material Design and referenced the Fonts and other bits as a CSS Resource on the Google API CDN.
  • Now open "main.ts", and observe line with:

[code language="javascript"]

platformBrowserDynamic().bootstrapModule(AppModule)

[/code]

This tells the application to bootstrap "AppModule" or "app-module" in snake-case.

 
 

  • Now open the "app" folder and click on "app.module.ts". It contains an export of "AppModule", so yes the "main.ts" comes here, and then this bootstraps to: "AppComponent".
  • Add the following import lines:

[code language="javascript"]

import { BrowserAnimationsModule } from
'@angular/platform-browser/animations';

import { FormsModule, ReactiveFormsModule } from
'@angular/forms';

import { MatButtonModule } from
'@angular/material';

import { MatDialogModule } from
'@angular/material/dialog';

import { MatInputModule } from
'@angular/material/input';

import { MatFormFieldModule } from
'@angular/material/form-field';

[/code]

  • Add the following lines to the "imports [" array. Put them first so the comma is taken care of:

[code language="javascript"]

BrowserAnimationsModule,

FormsModule, ReactiveFormsModule,

MatButtonModule, MatDialogModule, MatInputModule, MatFormFieldModule,

[/code]

  • Add the following "entryCompnents" array above the imports array:

[code language="javascript"]

entryComponents: [

SampleDialogComponent

],

[/code]

  • Save (Ctl-S)
  • Open the "app.component.ts", and stub in the Open Dialog button method:

[code language="javascript"]

openSampleDialog(): void {

/// todo: Load Dialog here.

}

[/code]

  • Replace the default "title = 'app';" class instance variable line with the following:

[code language="javascript"]

inputFromDialog = '';

[/code]

  • Save. Open the "app.component.css". Let's add some style to the Button we plan to drop in:

[code language="css"]

.button-text {

padding-left: .5em;

}

.valign-center {

display: inline-flex;

vertical-align: middle;

align-items: center;

}

.button-container {

width: 100%;

}

button {

float: right;

}

[/code]

  • Save. Open the "app.component.html". Now replace all HTML elements with the following:

[code language="html"]

<div
class="button-container">

<button (click)="openSampleDialog()"
class="valign-center"
>

<i
class="material-icons md-18">person_add</i>

<span
class="button-text">Open Dialog</span>

</button>

</div>

<div *ngIf="inputFromDialog.length !== 0"
>

<label>Input from Dialog:</label>

<span>{{ inputFromDialog }}</span>

</div>

[/code]

 
 

  • Save. Now at the Command Prompt, enter "ng serve", and refresh your Chrome browser. You should have a button aligned to the right side of the page.


  

  • Now we will create the Sample Dialog Component. Ctl-C to stop the "ng" server. And generate a new component:

    "ng g component sample-dialog"

  • In VS Code, open the newly generated component, "app/sample-dialog/sample-dialog.component.html". Replace the sample HTML with:

[code language="html"]

<h2
mat-dialog-title>

Sample Input Dialog

</h2>

<div>

<mat-label>Parent Entity ID:</mat-label>

<span>{{ parentEntityId }}</span>

</div>

<form>

<div>

<mat-label>Sample Input</mat-label>

<input
type="text"
matInput
dense
name="sampleInput" [(ngModel)]="sampleInput"

aria-label="Sample Input"
>

</div>

<div
style="float:right; padding-right:1em;">

<button
title="OK - Save Sample Input"
id="saveInputBtn"

[disabled]="isValidInput()"

(click)="saveSampleInput()">OK</button>

<button
title="Cancel"
id="cancelBtn"

(click)="cancelSampleInput()">Cancel</button>

</div>

</form>

[/code]

 
 

  • Save. Open "app/sample-dialog/sample-dialog.component.ts", and replace the default "import" statement (line 1) with:

[code language="javascript"]

import { Component, OnInit, Inject } from
'@angular/core';

import { MatDialogRef, MAT_DIALOG_DATA } from
'@angular/material/dialog';

[/code]

  • Replace the "constructor statement with the following:

[code language="javascript"]

constructor(

public
dialogRef: MatDialogRef<SampleDialogComponent>,

@Inject(MAT_DIALOG_DATA) public
dlgData: any,

) {

}

[/code]

  • Under the class Export statement ("export
    class…
    "),
    add the following Class Instance variables:

[code language="javascript"]

sampleInput = '';

parentEntityId = '';

[/code]

  • Add the following 3 class methods:

[code language="javascript"]

isValidInput(): boolean {

return
this.sampleInput.length === 0;

}

saveSampleInput() {

this.dlgData.sampleInput = this.sampleInput;

this.dialogRef.close(this.dlgData);

}

cancelSampleInput() {

this.dialogRef.close(this.dlgData);

}

[/code]

  • Save. Now wire-up the "app.component" to the Dialog. Open the "app/app.component.ts" file, and add the following "import" statements:

[code language="javascript"]

import { MatDialog } from
'@angular/material';

import { SampleDialogComponent } from
'./sample-dialog/sample-dialog.component';

[/code]

 
 

  • After the "export class…" statement add the following class instance variable:

[code language="javascript"]

inputFromDialog = '';

[/code]

  • Add the following constructor:

[code language="javascript"]

constructor( public
dialog: MatDialog ) {}

[/code]

  • Replace the stubbed in method "openSampleDialog" with the following:

[code language="javascript"]

openSampleDialog(): void {

const
multiAssignDlg = this.dialog.open(SampleDialogComponent, {

data: { sampleInput:
this.inputFromDialog }

});

multiAssignDlg.afterClosed().subscribe(res
=> {

// Collect data output from Dialog

if (res && res.sampleInput) {

this.inputFromDialog = res.sampleInput;

}

});

}

[/code]

  • Save. Start the "ng serve". You now have a button that pops a dialog that allows you to input a sample data item, and sends it back to the calling form. It should look as follows:


  

 At this point the Sample Input Dialog is nearly ready to deploy as a starter application in a CRM Entity form. Stop the ng server via Ctrl-C in the Command Prompt window.

 Preparation of Angular Application for CRM Integration

Before you can deploy the Angular Application into the CRM environment, you must do a Production build. This is a necessary step for continued development as well. The purpose is to get the JavaScript packages transpiled from TypeScript on the server in the "Web Resources" folder. We will use a Fiddler trick via the "AutoResponder" to further develop and debug the Angular SPA.

 Before starting the build, we must make an adjustment to the "src/index.html" file. Open it in VS Code, and find the "base" element within "head". Notice that the "href" attribute is set to "/". This is great for Angular Routing but very bad for CRM Web Resources. It turns all of the relative "href"s to start at the CRM Domain name instead of the CRM Instance and Solution with Web Resource folder. So, remove the forward-slash. And Save.

At the Command Prompt, issue the "ng build --prod" "ng build --prod --aot --output-hashing none" command. When it finishes running, you will have a new folder, "dist. There are several files that will need to be deployed to the CRM folder using a special naming convention that allows the files to be easily replaced and tied together in a neat little package. On a side note, why not deploy the –dev ( or straight-up "ng build") "dist" output? For one the "vendor" bundle is too large for CRM. So you need it minified and uglified to get the size down to the CRM 2016 limits. Besides the application will perform much better when the Web Resources are smaller. So moving along, let's modify the "index.html" to work properly in CRM. Before we do this, be forewarned that the "ng build" owns the "dist" folder and will overwrite the output on the next build. So, create a "CrmHack" folder and copy the "index.html" there. Once copied, you can open the new copy in VS Code and make the following modifications:

  • All elements are on the same line. Prettify the HTML so that it is easier to work with. Set the cursor at the start, and Ctrl-Shift-End to mark all text. Ctrl-K-F to format. Ctrl-S to save.
  • Now remove the unique IDs from the bundle names as follows: SKIP this step (revised ng build with "—ouput-hashing none" takes care of it)
    • styles.9c0ad738f18adc3d19ed.bundle.css => styles.bundle.css
    • inline.b65a7ec0a869cec063fe.bundle.js => inline.bundle.js
    • polyfills.f20484b2fa4642e0dca8.bundle.js => polyfills.bundle.js
    • main.23e048cb814f19c6e7fb.bundle.js => main.bundle.js
  • And Save.

CRM Deployment

Naming conventions are very important at this stage. Think of your Angular SPA as a Control. Place all of the bundles under the same Control folder in the CRM Solution Web Resources. Also place the "CrmHack/index.html" file there. This is important, because you will get name collisions with any other Angular SPA apps deployed to the same Solution. Putting all of the resources in the same folder also solves the problem relative URL references.

In Web Resources, chose "New". For Name, after the grayed out button with your prefix, enter "/", then control-name, "/", then bundle name without the unique ID into the Input field. The convention is "PREFIX/<control-name>/<bundle-name>". For example "crmpref_/sampledialog/styles.bundle.css". Follow this naming convention for all of the CSS and JS resources in the "dist" folder. Now do the same for the "index.html file located in the "CrmHack" folder.

Now you are ready to install the "crmpref_/sampledialog/index.html" Web Resource into an Entity Form. If you have followed all of the steps correctly, the Dialog will display when the "Open Dialog" button is clicked just as when served from the ng command in the command prompt. At this point you have a sample application that displays a dialog and nothing more. So where is the CRM Integration? And what about debugging?

CRM Debugging

You cannot properly integrate your Angular SPA into CRM without debugging, and a Production build is hardly the right place to start. So we will back up a little, or more properly go sideways. We will now use the ng command to generate a development build, and then side-load that into the CRM environment using Fiddler.

At the Command prompt, issue the following command: "xcopy dist fdist\". This will create a copy of the sources to a folder not under "ng" control. Why is this important? Fiddler during side-loading via the AutoResponder holds files open, even when you turn off the AutoResponder and stop capturing. 2 steps forward, 1 step back. And so it goes. Now you are ready to create a Fiddler AutoResponder Rule and start debugging.

Fiddler AutoResponder

Find the Fiddler "AutoResponder" tab. Click it and Add a Rule". In the Rule editor, you will create regular expression rules that rout your "index.html" and your JavasScript bundles (no CSS in the dev build) to the CRM Web Resource requests. Note that all slashes and periods are back-slashed to avoid being interpreted as regex operators. So pattern is "regex:", followed by ".*" (any character, 0 or more), followed by the CRM prefix, followed by a forward-slash, followed by control-name, followed by a forward-slash, and followed by the file name. Whew. See, naming conventions are important. Start with the "index.html".

regex:.*crmpre_\/sampledialog\/index\.html

And map to the file in your "fdist" folder. Repeat for the bundles, with a change. We will capture the bundle name "(.*) and feed it to the mapping via "$1".

regex:.*crmpre_\/sampledialog\/(.*)\.bundle\.js

<Full path to fdist>\$1.bundle.js

Enable the Rules. Unmatched Requests passt - check. Export your rules so you can reload them later. Start capturing via Fiddler. Hit your CRM Entity page, and now it loads the "index.html" and bundles from your "fdist" folder. The only catch is that you must rerun "ng build" and "xcopy dist fdist\" each time you wish to test a code change. You can add a "debugger;" stop in your code, rebuild, and Xcopy. Then Ctrl-Shift-I in Chrome to Debug, F5 to refresh, and you should stop at your "debugger" statement.

CRM Integration

Now that you are successfully debugging, it is time to start the true CRM integration. Let's try to follow best practice by isolating our CRM access into a Service. All CRM Entities have an Entity ID field. So we will access the parent Entity Form and show the Entity ID in our Sample Dialog.

First create an "XrmIntegration" service. At the command prompt, "ng g service services/xrm-integration". This will create the service shell into a services folder. We will treat the new service as a Singleton. So keep it simple and just copy the following code into the "app/services/xrm-integration.service.ts" file using VS Code:

[code language="javascript"]

import { Injectable } from
'@angular/core';

@Injectable()

export
class
XrmIntegrationService {

private
static
xrmParent: any = null;

private
static
xrmParentChecked = false;

constructor() { }

getParentXrm(): any {

if (!XrmIntegrationService.xrmParentChecked && XrmIntegrationService.xrmParent === null) {

if (('parent'
in
window) && 'Xrm'
in
window.parent ) {

XrmIntegrationService.xrmParent = window.parent['Xrm'];

}

XrmIntegrationService.xrmParentChecked = true;

}

return
XrmIntegrationService.xrmParent;

}

isGuidEmpty(value) {

return
value === null
// NULL value

|| value === undefined
// undefined

|| value === 'undefined'
// undefined

|| value.length === 0
// Array is empty

|| value === '00000000-0000-0000-0000-000000000000'; // Guid empty

}

}

[/code]

The above code implements 2 static variables, and 2 accessor methods, one to get the Parent XRM the other to test for empty GUIDs (copied from here with some modification to TypeScript). Save. Now add the Service to the Providers in the "app.module.ts" by copying the class name "XrmIntegrationService" and pasting into the "providers" in the open "app.module.ts":

[code language="javascript"]

providers: [XrmIntegrationService],

[/code]

Click the "Light Bulb" and choose "Import from XrmIntegrationService…". Save.

Now add XRM Integration into the "app/sample-dialog/sample-dialog.component.ts" file. First inject the service "private xrmIntegrationSvc: XrmIntegrationService" into the constructor, and click the "Light Bulb", "Import Module…":

[code language="javascript"]

constructor(

public dialogRef: MatDialogRef<SampleDialogComponent>,

@Inject(MAT_DIALOG_DATA) public dlgData: any,

private xrmIntegrationSvc: XrmIntegrationService

) {

}

[/code]

Now find the "ngOnInit" event method, and replace with the following code to use the XRM Integration Service to get the parent Entity ID:

[code language="javascript"]

ngOnInit() {

const parentXrm = this.xrmIntegrationSvc.getParentXrm();

if (parentXrm !== null) {

this.parentEntityId = parentXrm.Page.data.entity.getId();

} else {

this.parentEntityId = 'No XRM Integration';

}

}

[/code]

Save. And "ng serve". Refresh your Chrome browser. Open the Dialog:


  

Ok, it looks good. Let's try our Entity Form using Fiddler to supply our Development build. At the command prompt, Ctrl-C to end the "ng serve", then:

[code language="text"]

ng build

[/code]

Then

[code language="javascript"]

xcopy dist fdist\

[/code]

In Fiddler, start capturing with your AutoResponder entries enabled. Load an entity where you have added the HTML Web Resource. Click the "Open Dialog" button and you should now see the Parent Entity ID.

If all went well, stop capturing in Fiddler. And prepare to deploy a Production build. At the command prompt, "ng build --prod --aot --output-hashing none". When the build completes, replace all of the previously uploaded Web Resource bundles with the newly generated bundles. DO NOT replace the "index.html". Test your Entity again without using Fiddler. Your Angular CRM is now ready to demo.

1 thought on “Angular within Dynamics CRM 2016”

  1. I used MS OneNote (uses MS Word) to publish this to WordPress. Not completely happy, but it is probably my own fault. I may republish this Post tomorrow.

Comments are closed.