HowTo: Angular Elements in SharePoint Framework Projects - One Big Project

by Andrew Connell

Last updated September 22, 2024


Configure the SPFx Project to Build Angular

Before this can work, there are two things you need to do. First, you need to configure your SPFx project to support Angular v6. How do you do this? Do the following three things:

  1. Add necessary Angular packages to the project (both those needed for production & development to build)
// in addition to the SPFx packages, you need the following
"dependencies": {
     "@angular/common": "6.1.3",
     "@angular/compiler": "6.1.3",
     "@angular/core": "6.1.3",
     "@angular/elements": "6.1.3",
     "@angular/platform-browser": "6.1.3",
     "@angular/platform-browser-dynamic": "6.1.3",
     "@webcomponents/custom-elements": "1.2.0",
     "@webcomponents/webcomponentsjs": "2.1.1",
     "core-js": "2.5.7",
     "rxjs": "6.2.2",
     "zone.js": "0.8.26"
},
"devDependencies": {
     "uglifyjs-webpack-plugin": "1.3.0",
     "webpack-bundle-analyzer": "2.13.1"
}
  1. Modify the gulpfile.js to account for some special build stuff steps:
// add the following before the existing line: build.initialize(gulp);

const webpack = require('webpack');
const path = require('path');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const bundleAnalyzer = require('webpack-bundle-analyzer');

build.configureWebpack.mergeConfig({
     additionalConfiguration: (generatedConfiguration) => {
       const lastDirName = path.basename(__dirname);
       const dropPath = path.join(__dirname, 'temp', 'stats');
       generatedConfiguration.plugins.push(
         new bundleAnalyzer.BundleAnalyzerPlugin({
           openAnalyzer: false,
           analyzerMode: 'static',
           reportFilename: path.join(dropPath, `${lastDirName}.stats.html`),
           generateStatsFile: true,
           statsFilename: path.join(dropPath, `${lastDirName}.stats.json`),
           logLevel: 'error'
         })
       );

const contextPlugin = new webpack.ContextReplacementPlugin(/\@angular(\\|\/)core(\\|\/)(fesm5|fesm2015|fesm2020|esm2015|esm2020|esm5)/,
         path.join(__dirname, './client')
       );
       generatedConfiguration.plugins.push(contextPlugin);

for (let i = 0; i < generatedConfiguration.plugins.length; i++) {
         const p = generatedConfiguration.plugins[i];
         if (p.options && p.options.mangle) {
           generatedConfiguration
             .plugins
             .splice(i, 1, new UglifyJSPlugin({ uglifyOptions: { mangle: true } }));
           break;
         }
       }

return generatedConfiguration;
     }
});
  1. Update the TypeScript project file tsconfig.json to include the “emitDecoratorMetadata”: true property.

These steps will set up your project to include the necessary Angular libraries and configure the SPFx build process to include Angular in the build.

Use Angular Elements to Bootstrap your Angular app as a Custom Element

The second step is to create your custom element using Angular Elements. To do this, create it like you’d create any other Angular v6 based application. But, when it comes to bootstrapping it, your root AppModule will look a bit different. This is where Angular Elements comes into play.

Assuming your custom element is called AppComponent, your root app module will look like this:

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

@NgModule({
  declarations: [ AppComponent ],
  imports: [ BrowserModule ],
  entryComponents: [ AppComponent ]
})
export class AppModule {
  constructor(private injector: Injector) {}

public ngDoBootstrap() {
    if (!customElements.get('app-hello-world')) {
      const AppElement = createCustomElement(AppComponent, { injector: this.injector });
      customElements.define('app-hello-world', AppElement);
    }
  }
}

Notice the addition of the ngDoBootstrap() method. This will imperatively bootstrap the custom element when the script file loads in the browser.

When you test your project, you’ll notice it appears to get hung at one point when you get to the webpack step… just be patient. Angular v6 uses webpack 4 which is much faster than webpack 3 which your SPFx project is using, so it will take a moment to finish the entire build and bundling process. But when it’s finished, you’ll see it work in the local workbench!

But, There’s a Catch

Actually, two big catches…

  1. You can’t use the Angular CLI to create, run, debug, test and build your project; everything is done in the SPFx project and not in an Angular project.
  2. The resulting payload is huge. In this very simple example, I can’t get it down below 2.6MB ( 518kB gzipped)! That’s way too big for a client application.

My Opinion: One Project is a Bad Approach

Because of these two drawbacks, I don’t recommend this approach. First, the side of the payload is way too big for a web application. The entire payload of a web page shouldn’t be 2.7MB, much less a single component on it.

The other reason I don’t like it is that you can’t use the Angular CLI.