Tech tutorials Angular 2 Tutorial, SharePoint 2013 Web Parts
By Insight Editor / 5 Jul 2016 , Updated on 16 May 2019 / Topics: Application development
By Insight Editor / 5 Jul 2016 , Updated on 16 May 2019 / Topics: Application development
In this article, we’ll be discussing how to create and load multiple Angular 2 web parts on a page in SharePoint 2013. We’ll address the following items to create our solution:
As of this writing, Angular 2 is in Release Candidate 4. It’s possible (or perhaps even likely) some of the functionality and steps detailed in this article will change. We’ll 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 application 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’ll create the following basic folders and files (the link to the final source code can be found at the bottom of this article):
As the final step of manipulating the data, we loop through all possible combinations of leaders and status values while counting up matching records in our data retrieved from the REST calls. This data goes into our seriesData array.
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.
Before we go too deep into our solution, we need to review the goals for our solution and the challenges we must overcome. Our goals:
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 within the Angular 2 Quickstart), we can’t create a single application that encompasses all web parts. Instead, we must create multiple applications. We’ll create an Angular application for each web part.
Bundling will be a challenge as well. We’ll 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’ll 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’ll discuss bundling in more detail later in this article.
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’ll 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’re importing our component file created before, then bootstrapping the application with this component.
Next, we’ll 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’ll step through how to debug our application via NPM.
We’ll use NPM scripts to test and debug the solution we’ve 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 that “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, be sure to check developer tools in Internet Explorer or Google Chrome to ensure no unexpected errors are being thrown to the console. Exit the terminal command to end debugging and browser-sync.
Once we’re sure our code is working, we should follow the steps under the “Adding web part code” section to add wp2 as a duplicate of wp1. Be 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:
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’s 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 want to make sure production mode is turned on for every web part, it’s 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 won’t import the prodMode code via our index.html file since we’re using that file for our debugging, and production will use different HTML to bootstrap the web parts.
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 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’ll 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 four different files. We’d 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’ll also set this file up to contain any common code between web parts 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’ll start 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 we have SystemJS Builder loaded, we load the systemjs.config.js file for configuration.
Next, we’ll 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’re 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 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).
Each of the bundled JavaScript files, as well as several polyfills for older browsers, need to be uploaded to SharePoint. These can be uploaded in different places, but for simplicity, we’ll put them in a document library titled SiteAssets.
Within this document library, we create two folders: vendor and dist. The vendor file includes the files and structure for all of the polyfills listed in the Angular 2 QuickStart (shim.min.js, zone.js, reflect.js, system.src.js). It’s 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’ll 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’ll use the content editor web part and 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 the 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.
Hopefully, after all of these steps, you’ve 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, updates to Angular 2 may affect how this solution works in the future.