Model-Driven-Apps: How to use the side pane to show custom HTML

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:

Demo result

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:

Create Angular project

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):

Changed the generated name to exact name.

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).

Deployment mapping

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:

Preview result

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:

Register the onLoad method

And here is the result:

Demo 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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.