Smarter ideas worth writing about.

Creating Angular 2 Web Parts for SharePoint

In this article, we will be discussing how to create and load multiple Angular 2 web parts on a page in SharePoint 2013. We will take the following steps to create our solution:

  1. Technologies and Starting Points
  2. Goals and Challenges
  3. Adding Web Part Code
  4. Debugging
  5. Enabling Prod Mode
  6. Bundling
  7. Uploading to SharePoint
  8. Conclusion

Technologies and Starting Points

As of the writing of this blog post, Angular 2 is in Release Candidate 4. It is possible (or perhaps even likely) that some of the functionality and steps detailed in this article will change. We will assume knowledge of SharePoint 2013 development tasks including how to update master pages and add web parts. Knowledge of Angular 2, TypeScript and JavaScript in general are highly recommended in order to create useful web parts that are more complex than the simple example demonstrated in this article. Knowledge of SystemJS and SystemJS builder will be required to properly bundle and load code for the browser, however, this article will cover some of the more challenging portions of this task.

One of the most difficult questions when starting a project is what template or starter solution to begin with. In this article, I started with a combination of the Angular 2 Quickstart and Dan Wahlin’s Angular2-JumpStart. The Angular 2 Quickstart gives the extreme basics needed to start an Angular 2 applications and doesn’t include much of the unnecessary functionality for web parts such as routing. Dan’s Angular2-JumpStart gives us a good beginning on the necessary functionality for bundling.

For our file structure, we will create the following basic folders and files (the link to the final source code can be found at the bottom of this article):



We have our typical files necessary for an Angular 2 application including package.json, tsconfig.json, typings.json, and the node_modules folder. We keep our primary source code within the src folder with all Angular 2/TypeScript code in the app subfolder.

Notice that in our completed file structure, we have systemjs.config.js and index.html files in both the root folder and the src folder. The files in the src folder are used for debugging and bundling the code.  The files in the root folder can be used to test our bundled files that will be created in the dist folder.

Goals and Challenges

Before we go too deep into our solution, we need to review the goals for our solution and the challenges that we must overcome. Our goals:

  • Create web parts for Angular 2 to be hosted in SharePoint 2013
  • One or more web parts may be on a page within SharePoint
  • Angular 2 code should be bundled so that only the necessary code should be loaded on a page (i.e. we don’t want the code for web parts that aren’t on our page to load)

Based on these goals, we run into several challenges. Most Angular 2 examples are created as single page applications. Because an Angular 2 application is bootstrapped to one HTML tag (such as <my-app> within the Angular 2 Quickstart), we can’t create a single application that encompasses all web parts, but instead we must create multiple applications. We will create an Angular application for each web part.

Bundling will be a challenge as well. We will have to only load the code for the web parts that are used on the page. We may only bootstrap components that are present on the current page. We will bundle the code for each web part separately so that we can load JavaScript files based on which web parts are included on the page.  We will discuss bundling in more detail later in this article.

Adding Web Part Code

If you have any code or subfolders in the src/app folder (such as those you might create via the Angular 2 Quickstart tutorial), remove them.

We need a location to store our web part code, so we will create a folder under src/app titled wp1. Create a file in the wp1 folder named wp1.component.ts. This file will contain the code for our web part. The contents of this file should be as follows:

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

@Component({
    selector: 'web-part-1',
    template: '<h1>{{text}}</h1>'
})
export class WebPart1Component {
    text: string = 'Web Part 1 Loaded';
}

This simplistic code will create an h1 heading and will display “Web Part 1 Loaded” in the heading once Angular has loaded and processed the code.

Next we create a main.ts file under the wp1 folder. This will be the entry point for the Angular application within our web part. The contents of this file should be as follows:

import {bootstrap} from '@angular/platform-browser-dynamic';

import {WebPart1Component} from './wp1.component';

bootstrap(WebPart1Component);

Here we are importing our component file created before, then bootstrapping the application with this component.

Next we will update our systemjs.config.js file within our src folder. We use the file created within the Angular 2 Quickstart as a starting point. We need to update the map and packages section to include the new wp1 code (and remove the app sections as we removed the app code before). Our systemjs.config.js file should now look like the following:

(function(global) {
  // map tells the System loader where to look for things
  var map = {
    'wp1':                        'src/app/wp1',
    '@angular':                   'node_modules/@angular',
    'rxjs':                       'node_modules/rxjs'
  };
  // packages tells the System loader how to load when no filename and/or no extension
  var packages = {
    'wp1':                        { main: 'main.js',  defaultExtension: 'js' },
    'rxjs':                       { defaultExtension: 'js' }
  };

    var ngPackageNames = [
    'common',
    'compiler',
    'core',
    'http',
    'platform-browser',
    'platform-browser-dynamic',
    'router',
    'router-deprecated',
    'upgrade',
  ];
  // Add package entries for angular packages
  ngPackageNames.forEach(function(pkgName) {
    packages['@angular/'+pkgName] = { main: 'bundles/' + pkgName + '.umd.js', defaultExtension: 'js' };
  });
  var config = {
    map: map,
    packages: packages
  }
  System.config(config);
})(this);

Next, we need to update the index.html file from the Angular 2 Quickstart within the src folder to include our wp1 files and the selector tag for the web part (and again remove any references to app from the Quickstart code). When completed, the index.html file will contain the following:

<html>
  <head>
    <title>Angular 2 SharePoint Web Parts</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- 1. Load libraries -->
     <!-- Polyfill(s) for older browsers -->
    <script src="node_modules/core-js/client/shim.min.js"></script>
    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/reflect-metadata/Reflect.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>
    <!-- 2. Configure SystemJS -->
    <script src="systemjs.config.js"></script>
    <script>
      System.import('wp1').catch(function(err){ console.error(err); });
    </script>
  </head>
  <!-- 3. Display the application -->
  <body>
    <web-part-1>Loading...</web-part-1>
  </body>
</html>

At this point, we have a working solution with one web part. Next, we will step through how to debug our application via NPM.

Debugging

We will use NPM scripts to test out and debug the solution we have created so far. The scripts available to run are defined in the package.json file under the scripts section:


  "scripts": {
    "start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite\" ",
    "lite": "lite-server",
    "postinstall": "typings install",
    "bundle": "tsc && node bundle.js",
    "bundle:prod": "tsc && node bundle.js --prod",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "typings": "typings"
  },

To run a script, use the name of the script after “npm run” in a terminal window. To run our application, we can type “npm run start” and the TypeScript code will be compiled into JavaScript (the “tsc” portion of the script), a watch will be started to recompile when TypeScript code is updated (the “npm run tsc:w” part) and the lite-server will be started opening a browser window with browser-sync functionality (the “npm run lite” part).  Note than “npm start” also runs the start script. Because of the watch and browser-sync functionality, code can be edited and the browser window will refresh when code is changed.

Through the browser window, functionality can be tested to make sure it works as expected. Also, make sure to check developer tools in IE or Chrome to ensure that no unexpected errors are being thrown to the console. Exit the terminal command to end debugging and browser-sync.

Once we are sure that our code is working, we should follow the steps under the Adding Web Part Code to add wp2 as a duplicate of wp1. Make sure to change every section referring to web part 1 in the steps above to web part 2.

After all updates are made, run “npm start” again. At this point, you should see the following in your browser window:

Enabling Angular 2 Production Mode

While early beta versions of Angular 2 enabled production mode by default, the most recent versions have production mode turned off by default. This requires us to specify that we want to use production mode. When production mode is off, additional change detection and deep object comparisons run to help detect bugs that will cause issues later in production. In order to enable production mode, it is recommended to include the enableProdMode function call just before the application is bootstrapped. This would change our wp1/main.ts to the following:


import {bootstrap} from '@angular/platform-browser-dynamic';

import {WebPart1Component} from './wp1.component';
import {enableProdMode} from '@angular/core';

enableProdMode();
bootstrap(WebPart1Component);

Since we would want to make sure production mode is turned on for every web part, it is tempting to add this code to the main.ts file for each web part. However, if we do this, we get the following error:

Cannot enable prod mode after platform setup.

This error occurs because the one global platform object is created and shared for all Angular applications that occupy the same browser window. Once bootstrap has been run once, no additional configuration to this global platform object can be made. For more details on bootstrapping, see the official Angular documentation on the bootstrap function.

Instead, we should break this functionality into a separate folder on the same level as wp1 (called prodMode) and add a main.ts with the following code:

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

enableProdMode();

We must also add the definitions for the new prodMode code within the map and packages section of our systemjs.config.js file.  We will not import the prodMode code via our index.html file since we are using that file for our debugging and production will use different HTML to bootstrap the web parts.

Bundling

When we run our code with npm start, each TypeScript file is converted to a JavaScript file and is loaded in the browser. Also, all of the Angular 2 files and any other packages within node_modules are loaded that we use are loaded into the browser. Due to the number and size of these files, the performance of our solution will be less than ideal in a production environment. To solve this, we will bundle our files to reduce the number of files necessary along with minifying and mangling those files to produce a smaller download size for clients.

Our goal with bundling is to create 4 different files. We would like one file for each web part (wp1.min.js and wp2.min.js). We want one file to turn on production mode (prodMode.min.js) so that we can load that separately from any individual web part.  The fourth file is the common.min.js file. This file will contain all of the Angular 2 code and other third party node_module code. We will also set this file up to contain any common code between web part 1 and 2. This will allow us to load only the necessary code for a page depending on whether one or both web parts are used on a page.

We create a bundle.js file to handle the bundling. This bundle.js file uses SystemJS Builder to create the bundles we define. Let’s take a look at the code and then walk through the pieces:


var SystemBuilder = require('systemjs-builder');
var argv = require('yargs').argv;
var builder = new SystemBuilder();

  builder.loadConfig('src/systemjs.config.js')
    .then(function() {
        /**** Bundle Common Files into common bundle ****/
        var depOutputFile = argv.prod ? 'dist/common.min.js' : 'dist/common.js';
        return builder.bundle('(wp1 & wp2)', depOutputFile, {
            minify: argv.prod,
            mangle: argv.prod,
            sourceMaps: argv.prod,
            rollup: argv.prod
        });
    })
    .then(function() {
        /**** Bundle ProdMode Files into prodMode bundle ****/
        var appSource = argv.prod ? 'prodMode - dist/common.min.js' : 'prodMode - dist/common.js';
        var appOutputFile = argv.prod ? 'dist/prodMode.min.js' : 'dist/prodMode.js';
        return builder.bundle(appSource, appOutputFile, {
            minify: argv.prod,
            mangle: argv.prod,
            sourceMaps: argv.prod,
            rollup: argv.prod
        });
    })
    .then(function() {
        /**** Bundle WP1 Files into wp1 bundle ****/
        var appSource = argv.prod ? 'wp1 - dist/common.min.js' : 'wp1 - dist/common.js';
        var appOutputFile = argv.prod ? 'dist/wp1.min.js' : 'dist/wp1.js';
        return builder.bundle(appSource, appOutputFile, {
            minify: argv.prod,
            mangle: argv.prod,
            sourceMaps: argv.prod,
            rollup: argv.prod
        });
    })
    .then(function() {
        /**** Bundle WP2 Files into wp2 bundle ****/
        var appSource = argv.prod ? 'wp2 - dist/common.min.js' : 'wp2 - dist/common.js';
        var appOutputFile = argv.prod ? 'dist/wp2.min.js' : 'dist/wp2.js';
        return builder.bundle(appSource, appOutputFile, {
            minify: argv.prod,
            mangle: argv.prod,
            sourceMaps: argv.prod,
            rollup: argv.prod
        });
    })
    .then(function() {
        console.log('bundle built successfully');
    }); 

We will be starting the bundle process using either “npm start bundle” for a development bundle (non-minified, non-mangled) or “npm start bundle:prod” for a production bundle (minified, mangled).

After ensuring that we have SystemJS Builder loaded, we load the systemjs.config.js file for configuration.

Next, we will build our bundle of common files which will contain the Angular 2 source and other node_module files. We start with the following line of code:

var depOutputFile = argv.prod ? 'dist/common.min.js' : 'dist/common.js';

This sets our file for output based on whether we have specified this build as a production build or not.

Next we build the bundle file using the builder.bundle method.

return builder.bundle('(wp1 & wp2)', depOutputFile, {
            minify: argv.prod,
            mangle: argv.prod,
            sourceMaps: argv.prod,
            rollup: argv.prod
        });

The first argument for the method “(wp1 & wp2)” uses bundle arithmetic to identify all common code between web part 1 and web part 2. Bundle Arithmetic is similar to set theory. For more details about how this works, see the Bundle Arithmetic section for SystemJS Builder. We use this to create the output file with minification, mangling, and other properties set based on whether we are creating a production build.

We follow the same concepts as above to build the prodMode, wp1, and wp2 files. The major difference between these and the common bundle is the bundle arithmetic. As an example, the bundle arithmetic for wp1 in a production build would be

wp1 - dist/common.min.js

This gets all of the dependencies and files needed for web part 1, then removes everything we included in our common bundle. This ensures that no code is repeated between the wp1 bundle and the common bundle.

When completed, the bundled JavaScript files will be created in the dist directory (along with .map files if a production bundle was specified).

Deploying to SharePoint

Each of the bundled JavaScript files as well as several polyfills for older browsers need to be uploaded to SharePoint. While these can be uploaded in different places, for simplicity we will put them in a document library titled SiteAssets. Within this document library, we create 2 folders: vendor and dist. The vendor file includes the files and structure for all of the polyfills listed in the Angular2 quickstart (shim.min.js, zone.js, reflect.js, system.src.js). It is recommended to keep the folder structure under node_modules the same in order to prevent confusion in the future as the polyfills are updated. The dist file will contain our bundled files that were created in the dist folder previously.

Next we need to create a master page to include the polyfills as well as our common and prodMode bundles. For ease of use, copy the seattle.html file in the Master Page Gallery, then rename the file as seattleAngular.html. Add the following lines of code just before the end of the head tag. Note that the code below is for a SharePoint site at /sites/a2webpart.

<script src="/sites/a2webpart/SiteAssets/vendor/core-js/client/shim.min.js"></script>
<script src="/sites/a2webpart/SiteAssets/vendor/zone.js/dist/zone.js"></script>
<script src="/sites/a2webpart/SiteAssets/vendor/reflect-metadata/Reflect.js"></script>
<script src="/sites/a2webpart/SiteAssets/vendor/systemjs/dist/system.src.js"></script>
<script src="/sites/a2webpart/SiteAssets/dist/common.min.js"></script>
<script src="/sites/a2webpart/SiteAssets/dist/prodMode.min.js"></script>

Save the updated master page, upload it to the Master Page Gallery, then set the master page for the site to the seattleAngular master page. (Note that this loads Angular on each page in the site.  Another option to load Angular only on specific pages would include using the PlaceHolderAdditionalPageHead control on a Page Layout.)

Next, we will create our systemjs.config.js and place it in the root of the SiteAssets document library. This will contain the following:


function(global) {

  // map tells the System loader where to look for things
  var map = {
    'prodMode':                   'src/app/prodMode',
    'wp1':                        'src/app/wp1',
    'wp2':                        'src/app/wp2',
    '@angular':                   'node_modules/@angular',
    'rxjs':                       'node_modules/rxjs'
  };

  // packages tells the System loader how to load when no filename and/or no extension
  var packages = {
    'prodMode':                   { main: 'main.js',  defaultExtension: 'js' },
    'wp1':                        { main: 'main.js',  defaultExtension: 'js' },
    'wp2':                        { main: 'main.js',  defaultExtension: 'js' },
    'rxjs':                       { defaultExtension: 'js' }
  };

  var packageNames = [
    'common',
    'compiler',
    'core',
    'http',
    'platform-browser',
    'platform-browser-dynamic',
    'router',
    'router-deprecated',
    'upgrade',
  ];

  // add package entries for angular packages in the form '@angular/common': { main: 'index.js', defaultExtension: 'js' }
  packageNames.forEach(function(pkgName) {
    packages['@angular/'+pkgName] = { main: 'bundles/' + pkgName + '.umd.js', defaultExtension: 'js' };
  });

  var config = {
    bundles: {
      'dist/common.min' : [
        '@angular/common',
        '@angular/compiler',
        '@angular/core',
        '@angular/http',
        '@angular/platform-browser',
        '@angular/platform-browser-dynamic',
        '@angular/router',
        '@angular/router-deprecated',
        '@angular/testing',
        '@angular/upgrade'
      ],
     'dist/prodMode.min': ['src/app/prodMode'],
     'dist/wp1.min': ['src/app/wp1'],
     'dist/wp2.min': ['src/app/wp2']
    },
    typescriptOptions: {
        "module": "system",
        "sourceMap": true
    },
    map: map,
    packages: packages
  }

  // filterSystemConfig - index.html's chance to modify config before we register it.
  if (global.filterSystemConfig) { global.filterSystemConfig(config); }

  System.config(config);

})(this);

This SystemJS configuration file sets up all of the mappings needed to point references to the proper bundle.

For the individual web parts, we will use the content editor web part and use a text file to hold the contents for the web part. Create a wp1.txt file in the root of the SiteAssets document library and add the following code:

<script src="/sites/a2webpart/SiteAssets/dist/wp1.min.js"></script>
<web-part-1>Loading...</web-part-1>
<script>
    System.import('src/app/wp1').catch(function(err){ console.error(err);  });
</script>

This code will link to the wp1 bundle, add our selector that our web part will use for the Angular application, then import and run the web part 1 code (including the bootstrap function). We now only need to add the content editor web part and link to the text file under the Content Link setting to display the Angular application web part.

We can now duplicate the steps for web part 2 and the wp2 bundle to display both web parts on the same page.

Conclusion

Hopefully, after all of these steps, you have learned how to create and add multiple Angular 2 web parts onto a page in a SharePoint 2013 environment. While this is a starting point and works today, understand that updates to Angular 2 may affect how this solution works in the future.




The source code for this article is at https://github.com/CardinalNow/Angular2-WebParts.

Share:

About The Author

Managing Consultant

Bart is a Managing Consultant in the Application Development practice of Cardinal’s Raleigh/Durham office.