Today we will learn how to create a custom web resource (HTML) using Angular, and show it in the side pane using Xrm.App.sidePanes.createPane method. The end result will be like the below picture:

Without further ado, let’s go make the Angular apps!
Angular Project
Before we begin, I want to give a shoutout to these 2 fantastic blog posts by Inogic (from 2019 and yet the foundation still valid):
I created the Angular project (you need to ensure that your machine has Node.Js + Angular) with the below command:

Once the project is created by the Angular CLI, we need to install some of the npm packages using the below command:
npm install -save @types/xrm
npm install –save-dev xrm-webapi
Open the index.html file and make the changes below:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Angular CRM Demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<script src="../../ClientGlobalContext.js.aspx" type="text/javascript" ></script>
</head>
<body>
<app-root></app-root>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</body>
</html>
The most important thing from the above HTML is on line 8 where we import the ClientGlobalContext.js.aspx. When deploying the web resource, we will name the web resource to the dev_/angular-crm-demo/[file] (virtual folder paths), it is necessary to change the import to “../../ClientGlobalContext.js.aspx“:

We also will use Bootstrap 5 to make the UI beautiful.
Next, I created the below app.service.ts component:
import { Injectable } from '@angular/core';
import * as WebAPI from "xrm-webapi";
@Injectable({
providedIn: 'root'
})
export class AppService {
private config = new WebAPI.WebApiConfig("9.2");
retrieveMultiple(entitySet: string, queryString?: string, queryOptions?: WebAPI.QueryOptions) {
return WebAPI.retrieveMultiple(this.config, entitySet, queryString, queryOptions);
}
}
For the demo, we only will use retrieveMultiple method. That’s why the AppService only exposes this single method.
For the app.component.ts, I changed the implementation like below:
import { Component } from '@angular/core';
import {AppService} from "./app.service";
import {RetrieveMultipleResponse} from "xrm-webapi";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
data: {start: Date, end: Date, name: string, qty: number, unitPrice: number, total: number}[] = [];
constructor(private appService: AppService) { }
ngOnInit() {
const context = Xrm.Utility.getGlobalContext();
console.log(`Logged in user: ${context.userSettings.userName}..`);
const id = this.getParameterByName("data") || '';
console.log(`Params detected ${id}..`);
const query = "?$select=ins_actualend,ins_actualstart,_ins_contact_value,ins_name,ins_qty,ins_total,ins_unitprice" +
(id ? `&$filter=_ins_contact_value eq ${id}`: "");
this.appService.retrieveMultiple("ins_calculates", query)
.then(result => this.loadTable(result));
}
getParameterByName(name: string, url = window.location.href) {
name = name.replace(/[\[\]]/g, '\\$&');
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
loadTable(result: RetrieveMultipleResponse) {
this.data = [];
for (const entity of result.value){
const ins_actualEnd = entity["ins_actualend"]; // Date Time
const ins_actualStart = entity["ins_actualstart"]; // Date Time
const ins_name = entity["ins_name"]; // Text
const ins_qty = entity["ins_qty"]; // Whole Number
const ins_total = entity["ins_total"]; // Currency
const ins_unitPrice = entity["ins_unitprice"]; // Currency
this.data.push({
name: ins_name,
start: ins_actualStart,
end: ins_actualEnd,
qty: ins_qty,
unitPrice: ins_unitPrice,
total: ins_total
});
}
}
}
Then for the app.component.html:
<div class="d-flex align-items-center justify-content-center vh-100">
<div class="container">
<h1>Calculate Table</h1>
<hr>
<div class="table-responsive-sm">
<table class="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Start</th>
<th scope="col">End</th>
<th scope="col">Qty</th>
<th scope="col">Unit Price</th>
<th scope="col">Total</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of data">
<th>{{ row.name }}</th>
<td>{{ row.start }}</td>
<td>{{ row.end }}</td>
<td>{{ row.qty}}</td>
<td>{{ row.unitPrice}}</td>
<td>{{ row.total}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
Because we injected AppService component, then we need to change our app.module.ts to:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import {AppService} from "./app.service";
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
],
providers: [ AppService ],
bootstrap: [AppComponent]
})
export class AppModule { }
And we are done with the Angular. You can run the “ng build” and Angular will create the dist folder. But, before we can deploy, we need to modify the index.html like below (I copy the HTML to another folder as when we run “ng build” command, the existing file will be deleted):

As you can see, the right side uses the “exact” name for all the created files (your naming in CRM will also need to be the same). Be careful with the naming, if the naming is wrong, you will get an error (as the file can’t be found).

In the above picture, you can see the mapping from the dist folder to the CRM web resource.
From this point, when you publish all and try to open the index.html path, you can see the result already:

Glue it together – Xrm.App.sidePanes
I created the below Javascript:
var Blog = Blog || {};
(function () {
this.onLoad = function () {
var navigate = function (pane) {
pane.navigate({
pageType: "webresource",
webresourceName: "/dev_/angular-crm-demo/index.html",
data: Xrm.Page.data.entity.getId().replace("{", "").replace("}", "")
});
};
var paneId = "CalculateView";
var panes = Xrm.App.sidePanes.getAllPanes();
if (panes && panes._collection && panes._collection[paneId]) {
var selectedPane = panes._collection[paneId];
// Refresh the Angular app
navigate(selectedPane);
} else {
Xrm.App.sidePanes.createPane({
title: "Calculate View",
paneId: paneId,
canClose: false,
width: 500
}).then((pane) => navigate(pane));
}
};
}).apply(Blog);
We need to call Xrm.App.sidePanes.createPane where you can see the detail parameter here. Once the pane is created, we can instruct the system to navigate to almost anything. You can call Dataverse Form, View, or in our demo, we can call web resource which passes the necessary parameter (current GUID of the contact). All the information related to the capabilities can be checked here. On the logic above, I added a function to basically refresh the Angular (to get the related data).
This is how I register the above Javascript:

And here is the result:

The source code for the Angular project can be checked here.
Happy CRM-ing!
One thought on “Model-Driven-Apps: How to use the side pane to show custom HTML”