HTML Templates and CSS in SharePoint Framework Apps

“The time has come,” the Walrus said,
“To talk of many things:
Of template files, cascading styles,
And bundles made of strings,
And why SharePoint is boiling hot,
And whether pigs have wings.”

Like many SharePoint developers of late, I’ve been exploring the SharePoint Framework (SPFx), doing experiments, and porting code over to the new web part model. Something that perplexed me at first is how to handle HTML templates.

On the client side, I mostly work in AngularJS 1.x (Angular 2 isn’t ready for web parts yet since it only allows one instance of an app to run on a page at a time). And we Angular developers just love our templates! So how to manage them in the SPFx development environment? This article will show you how, and it will also clarify the mystery of CSS modules, which end up being related to HTML templates.

Starting with Knockout

The SPFx team doesn’t include a Yeoman template for Angular, but they do include another popular MV* style framework: KnockoutJS. Looking at the Knockout code is educational for Angular developers as well; if you want to check it out without spinning up your development environment, I posted the starting Knockout project for your reference. Notice there’s an HTML template under /src/webparts/ with a filename ending in .template.html. It turns out it’s that simple! Just make a file ending in .template.html, and you can read it into your code with a single call to require(), such as:


const htmlTemplate : string = require("./main.template.html");

Through the wonders of WebPack, you can enjoy the convenience of CommonJS modules in the web browser! SPFx uses WebPack as its module builder; you can break your project into lots of little files, each of which is an isolated module, and WebPack bundles them all together for deployment. Then it provides the hooks for your modules to expose and reference information in other modules. You reference modules in your code using the requires() function or the TypeScript imports statement.

Modules can be referenced by path name as you see above. In this case, WebPack bundles up the HTML file in a module so the HTML string can be obtained using the require() call. You can see this on line 69 of the Knockout web part.

Bringing it to Angular

There isn’t an official Angular template for SPFx, so I usually follow Waldek Mastykarz’s excellent blog article to get an Angular SPFx project started. This begins with the simplest SPFx starting project: the “No Framework” template.

The
The “No javaScript web framework” Yeoman template

Once you’ve followed Waldek’s steps and have an Angular web part, you can add an HTML template just as the Knockout example showed. Here’s a simple example of an Angular SPFx web part using an HTML template. The template is in /src/webparts/ngTemplateSample/templates, and is called main.template.html. This little template file references a controller, and is ready for rendering by AngularJS.

<div ng-controller="MainController as vm" ng-class="vm.Styles.webPart">
<h1 ng-class="vm.Styles.greeting">
        Hello{{vm.GetSeparator()}}{{vm.MessageTo}}!</h1>
Who should I say hello to?
    <input type="text" ng-model="vm.MessageTo" /></div>

Thelloangularspfxhis renders as a simple “hello world” type application – it’s intentionally very minimalist. It’s just enough to show how to connect a controller to a view that’s in an HTML template.

At the core of every SPFx web part is a single DOM element, which is accessible from the ClientWebPart base class as this.domELement. The web part’s render function reads in the HTML as a string, injects it into the DOM element, and binds it to the Angular application.

public render(): void {

  if (!this.renderedOnce) {
    this.domElement.innerHTML = require("./templates/main.template.html");

    angular.module('templateSample1', [])
           .constant ('Styles', styles)
           .controller('MainController', MainController);
    angular.bootstrap(this.domElement, ['templateSample1']);
  }
}

All that’s left is the controller, which does very little in this case.

// ViewModel definition
export interface IMainViewModel {
 MessageTo: string;
 GetSeparator: () => string;
 Styles: any;
}

export class MainController implements IMainViewModel {
  public static $inject = ["Styles"];

  // ViewModel implementation
  public MessageTo: string;
  public GetSeparator: () => string = this.getSeparator;
  constructor (public Styles: any, public MessageText: string) { }

  private getSeparator() : string {
    return this.MessageTo ? ' ' : '';
  }
}

I like to use a TypeScript interface to represent each ViewModel, so that’s the first thing here. In addition to the MessageTo property, there’s a function called GetSeparator that decides if there should be a space after the word “Hello”, and a property called Styles.

What’s with the Styles? It’s time to talk about CSS modules.

CSS Modules

What if I called my CSS class “Bruce” and you called yours “Bruce,” and Marc Anderson  called his CSS class “Bruce” as well? That might cause a bit of confusion! Since SPFx runs directly on the page, a name conflict is certainly a possibility. One solution to this is CSS Modules.

My example includes a small .scss file containing some CSS. Since scss is a superset of CSS, you can just put CSS in there.

.webPart {
 background-color: lightgray;
 padding: 0 5px 5px 5px;
}

.greeting {
 color: green;
}

The thing is, by the time your code is bundled and executed, fun mini-GUIDs have been appended to all your class names, like this:

.webPart_ec08603d {
 background-color: lightgray;
 padding: 0 5px 5px 5px;
}

.greeting_c56cd566 {
 color: green;
}

As a result, if the HTML in your template was expecting the unaltered class names, your CSS won’t work. This happened to me when I was porting existing widgets to the SharePoint Framework, where the style names were hard-coded into the HTML.

The SPFx build process generates a TypeScript file that lets you translate from the plain to the uniquified class names. You won’t find it in my repo because it’s generated at build time, but here it is:

require('NgTemplateSample.module.css');

const styles = {
 webPart: 'webPart_ec08603d',
 greeting: 'greeting_c56cd566',
};

export default styles;

As you can see, they’re using modules again! The first line references the .css file that was compiled from the .scss file; this causes WebPack to bring it into the bundle. Then it declares the style mappings and exports them so other modules can reference them. The default keyword tells the module loader that the styles object is the default object to hand any code that requires the module.

The main web part file references the module using a TypeScript import statement and is handed the default export, which is the styles object. This import statement is included automatically from the Yeoman template. Then my code passes the styles object into the Angular app as a constant.

import styles from './NgTemplateSample.module.scss';
// ...
public render(): void {
  // ...
  angular.module('templateSample1', [])
    .constant ('Styles', styles)
    .controller('MainController', MainController);
  angular.bootstrap(this.domElement, ['templateSample1']);
}

The controller makes sure the styles are part of the ViewModel.

export interface IMainViewModel {
  // ...
  Styles: any; // Collection of CSS style names
}

export class MainController implements IMainViewModel {
  // ...
  public static $inject = ["Styles"];
  constructor (public Styles: any, public MessageText: string) { }
  // ...
}

Now the HTML view can use Angular data binding to access the style name mappings in the ViewModel. It’s been a long way ’round, but we’re back at the HTML template!

<div ng-controller="MainController as vm" ng-class="vm.Styles.webPart">
<h1 ng-class="vm.Styles.greeting">
    Hello{{vm.GetSeparator()}}{{vm.MessageTo}}!</h1>
<!-- ... --></div>

If CSS modules seem like more trouble than they’re worth, or you’re porting a web part that hard-codes the class names, fear not! There’s a work-around:

  • Rename your .scss file to remove the word “module”
  • Be sure to reference the .scss file in your web part to load it into the bundle. You can place this in the main web part file, or any file that’s part of the bundle. For example,
          import ‘./NgTemplateSample.scss’;

    In this case we don’t need anything from the .scss file, so there’s no local variable to set, but it needs to be imported or required so WebPack includes the CSS.

  • Now you can just use the plain class names in your code

But Bob, I want more than one template!

Yeah like I said, Angular devs love their templates! The code in this article so far only dealt with the starting HTML that the Angular app binds to. There may be directives, ng-includes, or other templates that need to be included in the bundle and loaded at runtime. So here’s a second example that shows how to do this. main.template.html has been changed to move the input box into a second template using ng-include.

<div ng-controller="MainController as vm" ng-class="vm.Styles.webPart">
<h1 ng-class="vm.Styles.greeting">
    Hello{{vm.GetSeparator()}}{{vm.MessageTo}}!</h1>
<div ng-include="'form-template'"></div>
</div>

Getting additional templates into the bundle is easy enough – just add more .template.html files. And, as you might expect, the same require() call will get you the HTML as a string. The question is: how do you get the string into your Angular code?

One approach is to use Angular’s template cache service, $templateCache. This allows you to preload Angular’s template cache with the HTML string. I generally load it while I’m configuring the rest of the Angular app; here’s a snippet from the render() method.

this.domElement.innerHTML = require("./templates/main.template.html");

angular.module('templateSample2', [])
  .constant ('Styles', styles)
  .controller('MainController', MainController)
  .run(['$templateCache', ($templateCache) => {
    $templateCache.put("form-template",
                       require("./templates/form.template.html"));
  }]);

angular.bootstrap(this.domElement, ['templateSample2']);

Angular executes the run() block after the other blocks are loaded and the dependency injector is up and running. This is a good place for last-minute initialization. It references the $templateCache service and uses the put method to place the HTML string into the cache.

Now, the template can be used by name without any further ado.

<div ng-include="'form-template'"></div>

Why do we like templates so much anyway?

Well, maybe it’s just me who likes templates, but I don’t think so. Maybe there’s just something satisfying about editing a file in its native format (HTML). Maybe it’s habit. I’m learning React now so who knows, I may abandon them entirely! But for now I use them, and I hope this article helps you use them well.

3 thoughts on “HTML Templates and CSS in SharePoint Framework Apps

  1. This is excellent info, Bob! (Especially since I always call my CSS ‘Bruce’.)

    For some reason, the SPFx team seems loathe to provide an Angular 1.x generator for SPFx, and you’ve basically given us one here by adding to Waldek’s good work. Nice!

    As you’ve been doing, I expect many people’s first true experience with SPFx won’t be writing new Web Parts, but porting existing code into it. I certainly won’t be using SPFx in any serious way until I can take an existing AngularJS 1.x or KnockoutJS WebPart I’ve previously built and deploy it into “production” reliably.

    This helps me see that path.

    M.

    Like

  2. Hi Bob,

    I’ve had good success using two approaches to handle template in Angular applications. The first one you mentioned is to use require(‘path/to.html’) and webpack will pack the contents of the HTML directly into a module. I use this directly with the template property of a component or directive.

    There’s an example here: https://github.com/johnnliu/contract-register/blob/master/src/app/contracts/contractList.js

    This approach is most modular and best for going forward, but there are problems for existing applications that either has used ng-include to build composite apps, or has a lot of legacy code that you don’t want to change all the templateUrl to template. Then the approach I’ve settled on is to use ngTemplateLoader in webpack to handle these – all the html templates are loaded into their own module and automatically loaded into the $templateCache.

    Between both approaches I’ve upgraded several Angular apps (v1.1~1.5) to webpack and have them ready for SPFx. Just adding 2c

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s