August 1, 2017

1567 words 8 mins read

SharePoint Framework and Angular - Walkthrough of the SPFx Angular Boilerplate

SharePoint Framework and Angular - Walkthrough of the SPFx Angular Boilerplate

In this post, we will cover the general concepts used to make the SharePoint Framework Angular Boilerplate work and how it solves the challenges that were preventing the production use of both technologies together.

This post is part of a series that contains the following posts :

Getting Angular in the project First of all, we had to add Angular to the project. As you can see in the

package.json, we added all the Angular related modules and its dependencies.

{
  "name": "spfx-angular-boilerplate",
  "version": "0.0.1",
  "private": true,
  "engines": {
    "node": ">=0.10.0"
  },
  "dependencies": {
    "@angular/common": "^4.3.1",
    "@angular/compiler": "^4.3.1",
    "@angular/core": "^4.3.1",
    "@angular/http": "^4.3.1",
    "@angular/platform-browser": "^4.3.1",
    "@angular/platform-browser-dynamic": "^4.3.1",
    "@angular/router": "^4.3.1",
    "core-js": "^2.4.1",
    "reflect-metadata": "^0.1.9",
    "rxjs": "5.0.1",
    "systemjs": "0.19.40",
    "zone.js": "0.8.4",
    "@microsoft/sp-core-library": "~1.1.0",
    "@microsoft/sp-webpart-base": "~1.1.1",
    "@types/webpack-env": ">=1.12.1 <1.14.0",
    "sp-pnp-js": "^2.0.6"
  },
  "devDependencies": {
    "@microsoft/sp-build-web": "~1.1.0",
    "@microsoft/sp-module-interfaces": "~1.1.0",
    "@microsoft/sp-webpart-workbench": "~1.1.0",
    "gulp": "~3.9.1",
    "@types/chai": ">=3.4.34 <3.6.0",
    "@types/mocha": ">=2.2.33 <2.6.0"
  },
  "scripts": {
    "build": "gulp bundle",
    "clean": "gulp clean",
    "test": "gulp test"
  }
}

Fixing some complaints of the bundling process

Then, some complaints from the webpack bundler started to happen during the bundling process of the project. We had to add a setting to the tsconfig.json file to skip the library check. That prevents TypeScript transpilation error and will “assume” that all the libraries are valid. The resulting tsconfig.json is the following.

{
  "compilerOptions": {
    "target": "es5",
    "forceConsistentCasingInFileNames": true,
    "module": "commonjs",
    "jsx": "react",
    "declaration": true,
    "sourceMap": true,
    "skipLibCheck": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "types": ["es6-promise", "es6-collections", "webpack-env"]
  }
}

Then, we have to instruct webpack of where the application source files are located. That was done through the gulpfile.js. Adding the ContextReplacementPlugin was necessary to fix the request dependency bug in Angular 5644:15-36 Critical dependency: the request of a dependency is an expression. The resulting gulpfile.js is the following.

'use strict';

const gulp = require('gulp');
const path = require('path');
const build = require('@microsoft/sp-build-web');
const webpack = require('webpack');

/**
 * Fixing the "5644:15-36 Critical dependency: the request of a dependency is an expression" warning
 * Linked to an existing bug/problem in Angular https://github.com/angular/angular/issues/11580
 */
build.configureWebpack.mergeConfig({
  additionalConfiguration: (generatedConfiguration) => {
    generatedConfiguration.plugins.push(
      new webpack.ContextReplacementPlugin(/angular(\\|\/)core/, path.resolve(__dirname, './src'))
    );

    return generatedConfiguration;
  },
});

build.initialize(gulp);

The last piece that needs to happen is to externalize the Zone.js component of the bundle. This is EXTREMELY IMPORTANT. Without this, the code will work without any problem but will break as soon as another webpart built with Angular but from another project will be dropped on the same page as yours. The key to this boilerplate is to keep a single Zone.js instance to be loaded so all the webparts share the same.

To do that, we had to modify the config\config.json file and add the following to the externals section.

{
  "entries": [
    {
      "entry": "./lib/webparts/basicAngular/BasicAngularWebPart.js",
      "manifest": "./src/webparts/basicAngular/BasicAngularWebPart.manifest.json",
      "outputPath": "./dist/basic-angular.bundle.js"
    }
  ],
  "externals": {
    "zone.js": {
      "path": "https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.12/zone.min.js",
      "globalName": "zone.js"
    }
  },
  "localizedResources": {
    "basicAngularStrings": "webparts/basicAngular/loc/{locale}.js"
  }
}

The project is now ready to build webparts using Angular as everything will be bundled and will allow multiple webparts, multiple instances of the same webparts and multiple webparts from multiple projects!

Using the BaseAngularWebPart

There is a lot of magic happening in the BaseAngularWebPart. This class is responsible for 3 major things. First of all, it renders the webparts by specifying its “DOM” element to be dynamic for every webpart instance running. The following snippet highlights this method.

/**
 * Render the web part. This causes the Angular2 app to be bootstrapped which
 * in turn bootsraps the Angular2 web part root component.
 */
public render(): void {
  // @todo: most likely we need to make this width:100%
  this.domElement.innerHTML = `<angular-${this.context.instanceId} />`;
  this._bootStrapModule();
}

Then, once the “DOM” element is rendered, we want to bootstrap our NgModule to take over and render the actual set of components. The following snippet does it using the Zone.js component and dos a dynamic bootstrapping of our module.

/**
 * Bootstrap the root component of the web part.
 */
private _bootStrapModule(): void {
  var self = this;
  platformBrowserDynamic().bootstrapModule(self._getModule()).then(
    ngModuleRef => {

      if(self._app["_rootComponents"] != undefined && self._app["_rootComponents"].length > 0) {
        self._component = self._app['_rootComponents'][0]['_component'] as KProperties;
        self._zone.run(() => { console.log('Outside Done!'); });
      }
    }, err => {
      console.log(err);
    }
  );
}

Finally, we need to find the Root Component that will be used and create the NgModule with all its required settings and dependencies. The actual SharePoint Framework WebPart will instruct the BaseAngularWebPart of which of the set of components is the Root Component, which are the Providers, which are the Sub Components and what are the Routes to bundle in the Application.

/**
 * Get the NgModule reference that will act as the root of this web part.
 */
private _getModule(): any {
  const component: any = this.rootComponentType.getComponent(this.context.instanceId);
  const declarations = this.appDeclarationTypes.concat(component);
  const routes = this.routes;
  const providers = this.providers;
  const webPart = this;
  /**
  * Our goal is to define a single module class definition to be instantiated for each
    webpart (like instances of a class). When an instance of the module class is bootstrapped Angular2
    will create an annotation and attach it to the module class. However, when multiple instances of the
    same module class are bootstrapped, only the first annotation associated with the module class will be parsed.
    This results in any other module class instances on the page to not function.
    To allow multiple modules of the same class definitoin on one page to work, we need to define the
    class in a closure to create a new environment for each instance class, so that each annotation
    object will be parsed.
  */
  const AppModule = (() => {
    function AppModule(applicationRef, ngZone) {
      webPart._app = applicationRef; // applicationRef gives us a reference to the Angular2 component's properties
      webPart._zone = ngZone;
    }
    // We now attach required metadata for Angular2 that is allowable within a clousure
    const AppModule1 = Reflect.decorate([
      NgModule({
        imports: [BrowserModule, FormsModule, HttpModule, routes],
        declarations: declarations,
        bootstrap: [component],
        exports: [RouterModule],
        providers: providers
      }),

      Reflect.metadata('design:paramtypes', [ApplicationRef, NgZone]) // This allows Angular2's DI to inject dependencies
    ], AppModule);
    return AppModule1;
  })();
  return AppModule;
}

With that class, any SharePoint Framework WebPart can be coded using a set of Angular components.

Inheriting from the BaseAngularWebPart

The only requirement is to inherit from the BaseAngularWebPart and to instruct the Framework of some of the required elements to be used within the previous logic. First of all, it is required to inherit from the BaseAngularWebPart and specify in there what will be the base Application Component.

// ...
export default class BasicAngularWebPart extends BaseAngularWebPart<IBasicAngularWebPartProps, AppComponent> {
  // ...
}

Then, we need to return a set of properties that will be used by the BaseAngularWebPart abstract class so it can build a proper NgModule. You will not be able to build your WebPart without those as they are abstract methods to override.

protected get rootComponentType(): any {
  return AppComponent;
}

protected get appDeclarationTypes(): any {
  return [
    HomeComponent,
    ListComponent
  ];
}

protected get routes(): any {
  return AppRoutes;
}

protected get providers(): any {
  if (Environment.type === EnvironmentType.Local) {
    return [
      ConfigurationService,
      { provide: ItemsService, useClass: MockItemsService },
      { provide: APP_INITIALIZER, useFactory: (configurationService: ConfigurationService) => () => configurationService.load({
        mocked: true,
        listName: this.properties.listName,
        description: this.properties.description,
        styles: styles
      }), deps: [ConfigurationService], multi: true }
    ];
  } else if (Environment.type == EnvironmentType.SharePoint || Environment.type == EnvironmentType.ClassicSharePoint) {
    return [
      ConfigurationService,
      { provide: ItemsService, useClass: ItemsService },
      { provide: APP_INITIALIZER, useFactory: (configurationService: ConfigurationService) => () => configurationService.load({
        mocked: false,
        listName: this.properties.listName,
        description: this.properties.description,
        styles: styles
      }), deps: [ConfigurationService], multi: true }
    ];
  }
}

Configuring your routes

To use a generic approach for your components and be as isolated from the SharePoint Framework as possible, the management of components (what to load when) is done through the Angular routing system. Providing the routing ahead of time will allow us to not only have a great navigation between components, but also to write all of our components with Angular code only. Nothing in the components will have a reference to the SharePoint Framework because of that.

import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home';

const routes: Routes = [{ path: '', component: HomeComponent }];

export const AppRoutes: any = RouterModule.forRoot(routes, { useHash: true });

Creating your Application Component

Finally, the only missing piece is the Root Application Component that has to be created and that is an empty component with a simple router outlet. That will allow us to have a full routing system that manages our components and will keep our Angular code totally separated from the SharePoint Framework code.

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

export class AppComponent {
  public static getComponent(selectorId: string): any {
    return Component({
      selector: `angular-${selectorId}`,
      template: `<router-outlet></router-outlet>`,
    })(class AppComponentInner {});
  }
}

Video Walkthrough

What’s next?

That’s it? It’s that “simple”? Well… Surprisingly, it was not that much complex to get it working and the result is exactly what we’re looking for : Coding Angular Components in a SharePoint Framework Webpart!

In the next post, we will create your first SharePoint Framework Solution using Angular!

Oh! And (finally), happy Angular coding on the SharePoint Framework!

comments powered by Disqus