SPFx Webpart With Angular Elements

Many developers want to know how to use angular components with SPFx Webpart.

Angular Elements allows us to export angular components as custom elements (web components). These custom elements we can use in the SPFx webpart.

So In this article, we will see step by step implementation of SPFx Webpart with angular elements.

Let’s start with the introduction of angular elements,

Angular Elements is one of the best features released in Angular 6. It allows us to create Web Components (Custom Elements) using an Angular.

Angular elements are ordinary Angular components packaged as custom elements, a web standard for defining new HTML elements in a framework-agnostic way.

This approach lets us develop reusable components in the way that’s familiar to us, and yet, embedding these in every kind of website (vanilla JavaScript, React, Vue, WordPress, etc. ).

Important features of Angular Elements:

  • They are Self Bootstrapping.
  • They actually host the Angular Component inside a Custom Element.
  • It bridges between the DOM APIs and Angular Components.
  • Anyone can use it without having the knowledge of how Angular works.

Now let’s start with creating an angular element and SPFx Webpart.

We required Angular CLI and PnP Generator for creating Angular Element application and SPFx Webbart with Angular Element Integration.

If you are looking for an initial SPFx Development setup, refer here: Prerequisites and Environment Setup for SPFx Development.

Angular CLI is command-line interface for Angular. This gives various commands to scaffold projects, generate components, services, modules, and many more things.

PnP/generator-SPFx provides improved governance for SharePoint Framework projects. it extends the capabilities for ReactJS, and Knockout projects and support for additional frameworks, such as HandlebarsJS, Aurelia, VueJS and Angular Elements.

We will add it globally. using the following command

npm install -g @angular/cli @pnp/generator-spfx@1.8.1

PnP Generator helps us to generate separate SPFx and Angular Element projects. This allows us to use Angular CLI in development. SPFx project folder is created with -spfx suffix. Output generated from Angular is bundled and imported in SPFx Web Part.

Note : Currently there are some vesrion mismatch issues with pnp-genartor@1.13.0 and Angular CLI 9. So if you want to use latest version on Angular CLI then you have to downgrade Pnp-Generator version. Because Angular CLI 9 is not supported in pnp-generator@1.13.0(As on writing this article).

Currently PnP Generator Team is working on this issues but for now we will downgrade PnP version and use latest Angular CLI so we will install PnP genartor@1.8.1 using npm install -g @pnp/generator-spfx@1.8.1

Once the environment setup is done we are ready to start with development. Now we will see the development of SPFx webpart with Angular Elements step by step.

We will create Angular Elements and SPFx webpart in a separate directory, we can create it using mkdir < directory - name >.

Now execute the following command  to create two separate projects for SPFx webpart with angular elements

yo @pnp/spfx

It will ask you some questions for configuration, give an answer as below.

SPFx webpart with Angular Elements | yo @pnp/spfx
SPFx webpart with Angular Elements | yo @pnp/spfx

After selecting all the required answers it will generate two separate projects (solutions) as shown below. One is for Angular Element and one for SPFx Webpart. SPFx webpart project will have suffix -spfx.

SPFx webpart with Angular Elements | Project Structure
SPFx webpart with Angular Elements | Project Structure

We will create a sample angular-element, we will create a To Do angular element application. which have features like Add, List, Update and Delete ToDo’s.

SPFx Webpart with Angular Elements | ToDo Component
SPFx Webpart with Angular Elements | ToDo Component

This ToDo Application will add/update/delete/get ToDos from SharePoint ListItem. We will do this using SharePoint REST Services. To connect to this REST Services we will create AppService in Angular Element Application.

In this application, we can pass a dynamic title and siteurl from SPFx Webpart using @Input directive. 

So our final ToDo Angular Element Folder Structure and Source will look like as below. 

model-service-structure

export interface ToDo {
    Id?: number;
    Title: string;
}

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';

import { AngularElementsWebPartComponent } from './angular-elements-web-part/angular-elements-web-part.component';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
  declarations: [
    AngularElementsWebPartComponent,
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    FormsModule
  ],
  providers: [],
  entryComponents: [AngularElementsWebPartComponent]
})
export class AppModule {
  constructor(private injector: Injector) {}
  ngDoBootstrap() {
    const el = createCustomElement(AngularElementsWebPartComponent, { injector: this.injector });
    customElements.define('app-angular-elements-web-part', el);
  }
}
import { Component, Input, OnInit, ViewEncapsulation, Output, EventEmitter, OnDestroy } from '@angular/core';
import { ToDo } from '../models/todo.model';
import { AppService } from '../services/app.service';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'app-angular-elements-web-part',
  templateUrl: './angular-elements-web-part.component.html',
  styleUrls: ['./angular-elements-web-part.component.scss'],
  encapsulation: ViewEncapsulation.Emulated
})

export class AngularElementsWebPartComponent implements OnInit {
  @Input() title: string;

  @Input()
  public set siteurl(url: string) {
    this.appService.setAPIUrl(url);
  }

  todos: ToDo[];
  editToDo: ToDo = undefined;
  submitted = false;

  constructor(private appService: AppService) {
  }

  ngOnInit() {
    this.appService.getToDos().then(todos => this.todos = todos);
  }

  delete(todo: ToDo, index: number) {
    const confirmation = confirm(`⚠ Are you sure ? Do you want to delete ${todo.Title} ?`);
    if (confirmation) {
      this.appService.deleteToDo(todo).then(result => {
        this.todos.splice(index, 1);
        alert(`⚠ ${todo.Title} is deleted...`);
      });
    }
  }

  submit(todoForm: NgForm) {
    const todo = todoForm.value;
    this.appService.addToDo(todo).then(result => {
      alert(`✅ Task: ${todo.Title} is added...`);
      this.todos.push(result);
      todoForm.resetForm();
    });
  }

  update(todo: ToDo) {
    this.appService.updateToDo(todo).then(response => {
      this.editToDo = undefined;
      alert(`✔ ToDo Updated...`);
    });
  }

  edit(todo: ToDo) {
    this.editToDo = { ...todo };
  }

}
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { ToDo } from '../models/todo.model';
import { map, tap, catchError } from 'rxjs/operators';
import { EMPTY } from 'rxjs';
@Injectable({
  providedIn: 'root'
})
export class AppService {

  private BASE_URL = 'http://localhost:8080/';

  constructor(private httpClient: HttpClient) {

  }

  setAPIUrl(url: string) {
    this.BASE_URL = url || this.BASE_URL;
  }

  getFormDigest() {

    const headers: HttpHeaders = new HttpHeaders({
      Accept: 'application/json;odata=verbose'
    });

    return this.httpClient.post<any>(`${this.BASE_URL}/_api/contextinfo`, {}, { headers })
      .pipe(map(data => data.d.GetContextWebInformation.FormDigestValue))
      .toPromise();
  }

  getToDos() {
    return this.httpClient.get<any>(`${this.BASE_URL}/_api/web/lists/getbytitle('To Do')/items?$select=Id,Title,Status`).pipe(
      map(response => response.value as ToDo[])
    ).toPromise();
  }

  async addToDo(toDo: ToDo) {

    const input = {
      __metadata: { type: 'SP.Data.To_x0020_DoListItem' },
      Title: toDo.Title
    };

    const headers: HttpHeaders = new HttpHeaders({
      'Content-Type': 'application/json;odata=verbose',
      Accept: 'application/json;odata=verbose',
      'X-RequestDigest': await this.getFormDigest()
    });

    return this.httpClient.post<any>(`${this.BASE_URL}/_api/web/lists/getbytitle('To Do')/items`, input, { headers }).pipe(
      map(p => p.d ? ({ Id: p.d.Id, Title: p.d.Title }) : null),
      catchError(error => {
        console.log(error);
        return EMPTY;
      })
    ).toPromise();
  }

  async updateToDo(todo: ToDo) {

    const { Title } = todo;
    const input = {
      __metadata: { type: 'SP.Data.To_x0020_DoListItem' },
      Title
    };

    const headers: HttpHeaders = new HttpHeaders({
      'X-HTTP-Method': 'MERGE',
      'IF-MATCH': '*',
      Accept: 'application/json;odata=verbose',
      'Content-Type': 'application/json;odata=verbose',
      'X-RequestDigest': await this.getFormDigest()
    });

    return this.httpClient.post<any>(`${this.BASE_URL}/_api/web/lists/getbytitle('To Do')/items(${todo.Id})`, input, { headers }).pipe(
      tap(response => console.log('Update Response => ', response))
    ).toPromise();
  }

  async deleteToDo(todo: ToDo) {

    const headers: HttpHeaders = new HttpHeaders({
      'X-HTTP-Method': 'DELETE',
      'IF-MATCH': '*',
      Accept: 'application/json;odata=verbose',
      'Content-Type': 'application/json;odata=verbose',
      'X-RequestDigest': await this.getFormDigest()
    });

    return this.httpClient.post<any>(`${this.BASE_URL}/_api/web/lists/getbytitle('To Do')/items(${todo.Id})`, {}, { headers }).pipe(
      tap(response => console.log('Delete Response => ', response))
    ).toPromise();
  }
}
<div class="container">
  <h1 class="bg-primary text-white p-2 text-center mt-2">{{ title || 'SPFx With Anular Elements - CRUD Demo'}}</h1>
  <div class="row">
    <div class="col-md-4">
      <div class="card">
        <div class="card-body">
          <h3>Add To Do</h3>
          <form #toDoForm="ngForm" *ngIf="!submitted">
            <div class="form-group">
              <label for="name">Task Name</label>
              <input type="text" class="form-control" name="Title" id="title" ngModel>
            </div>
            <button type="submit" class="btn btn-primary mr-2" (click)="submit(toDoForm)">Submit</button>
            <button type="reset" class="btn btn-light">Cancel</button>
          </form>
        </div>
      </div>
    </div>
    <div class="col-md-8">
      <div class="card">
        <div class="card-body">
          <h3>To Do Details</h3>
          <table class="table">
            <thead>
              <tr>
                <th>Task Id</th>
                <th>Title</th>
                <th colspan="2">Action</th>
              </tr>
            </thead>
            <tbody>
              <tr *ngFor="let todo of todos; let i=index">
                <td scope="row">{{todo?.Id}}</td>
                <ng-container *ngIf="!(editToDo?.Id===todo?.Id); else EditMode">
                  <td scope="row">{{todo.Title}}</td>
                  <td><button type="button" class="btn btn-primary" (click)="edit(todo)">Edit</button></td>
                  <td><button type="button" class="btn btn-danger" (click)="delete(todo, i)">Remove</button></td>
                </ng-container>
                <ng-template #EditMode>
                  <td scope="row">
                    <input type="text" class="form-control" name="Title" [(ngModel)]="todo.Title">
                  </td>
                  <td><button type="button" class="btn btn-success" (click)="update(todo)">Update</button></td>
                  <td><button type="button" class="btn btn-dark"
                      (click)="todos[i] = editToDo; editToDo = undefined">Cancel</button></td>
                </ng-template>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </div>

To use the angular elements in the SPFx webpart, we need to bundle it in one JS file.  while we generate a project using @pnp/spfx it also adds element-build.js script in angular elements project, also it add one script in package.json as 

"scripts": {
  ... ,
  "bundle": "ng build --prod --output-hashing none && node elements-build.js"
},

Execute this command 

npm run bundle

This will create a production build for the angular element and then bundle it in one file bundle.js. We will use this bundle.js and styles.css in SPFx Webpart

⚠ Note

We need to update elements-build.js files object as below.

const concat = require('concat');

(async function build() {
  const files = [
    './dist/AngularElements/runtime-es5.js',
    './dist/AngularElements/runtime-es2015.js',
    './dist/AngularElements/polyfills-es5.js',
    './dist/AngularElements/polyfills-es2015.js',
    './dist/AngularElements/main-es5.js',
    './dist/AngularElements/main-es2015.js'
  ];
  await concat(files, './dist/AngularElements/bundle.js');
})();

Because from Angular 8.3 angular build generates build files for es5 and es2015. In @PnP/generator-spfx@1.8.1 it is not updated yet. 

here replace AngularElements your angular element project name.

Now we will see how to use ToDo Angular Element in SPFx Webpart.

We need to do following things.

  1. Import bundle.js file and styles.css in our webpart.ts file as below: 
  2. Add angular element tag in render() method. In angular element tag, we will dynamically pass siteUrl and title

import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import {
  IPropertyPaneConfiguration,
  PropertyPaneTextField
} from '@microsoft/sp-property-pane';

import { escape } from '@microsoft/sp-lodash-subset';

import * as strings from 'AngularElementsWebPartStrings';

import 'angular-elements/dist/AngularElements/bundle.js';

require('../../../node_modules/angular-elements/dist/AngularElements/styles.css');

export interface IAngularElementsWebPartProps {
  title: string;
}

export default class AngularElementsWebPart extends BaseClientSideWebPart<IAngularElementsWebPartProps> {

  public render(): void {
    const siteUrl = this.context.pageContext.web.absoluteUrl;
    this.domElement.innerHTML = `<app-angular-elements-web-part id="aeForm" title="${ this.properties.title }" siteUrl="${siteUrl}"></app-angular-elements-web-part>`;
  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('description', {
                  label: strings.DescriptionFieldLabel
                })
              ]
            }
          ]
        }
      ]
    };
  }
}

Now we will serve this application using gulp serve command.

This will serve our application on

SharePoint-SiteURL + /_layouts/15/workbench.aspx

Once it successfully served on the localhost, we can deploy it on the SharePoint site.

We can deploy this solution on SharePoint site using two commands : gulp bundle --ship and then gulp package-solution --ship command. Refer SPFx Application Deployment article for more details.

Great ✨✨✨!!! We have successfully done SPFx webpart With Angular Elements.

You can find the complete source code of this application at the end of the article GitHub repository.

This application will give the following output.

SPFx Webpart with Angular Elements - ToDo Application
SPFx Webpart with Angular Elements - ToDo Application

GitHub Repository

https://github.com/chandaniprajapati/spfx-angular-elements

If you like this project, mark a ⭐ on GitHub Repository

Summary

In this article, we have step by step seen How to use SPFx Webpart With Angular Elements with sample application.

I hope you like this article, share it with others. Give your valuable feedback and suggestions in the comment section below.

You may also like the following articles...

Sharing is Caring.
Happy Coding 🙂

4 3 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments