Angular2 – Load component’s template and styles by convention

in Front-End Web Technologies

Below is a common definition of Angular2 component.

clock.ts

@Component({
     selector: 'clock',
     template: require("./clock.html!text"),
     styles: [require("./clock.css!text")]
 })
 export class ClockComponent {
     constructor() {
     }
 }

We use the require function to load the component’s template and styles definitions.

While the code is straightforward it contains some redundant information. For example, if we decide to rename the clock.ts file to myClock.ts then we probably also rename the HTML and CSS files to myClock.html and myClock.css respectively.

Once we rename the CSS and HTML files we also need to fix our code as below.

@Component({
     selector: 'my-clock',
     template: require("./myClock.html!text"),
     styles: [require("./myClock.css!text")]
 })
 export class MyClockComponent {
     constructor() {
     }
 }

Assuming Angular2 has support for defining template and styles by convention than the component definition could be much simpler.

@Component({
     selector: 'clock',
 })
 export class ClockComponent {
     constructor() {
     }
 }

Unfortunately, Angular2 does not support that yet. Let’s do it our self. (BTW, Angular does offer a provider named ViewResolver. However, it does not support asynchronous loading of template and styles).

First, we need to override the Component decorator so each component registration is controllered by us.

export function Component(metadata: ComponentDecoratorCtorParameters) {
     components.push(metadata);
 
     metadata.moduleId = SystemJSExtensions.getExecutingModule().name;
 
     return function (target: Function) {
         (<any>metadata).target = target;
     }
 }

Actually we are not really overriding Angular2’s Component decorator but rather define a new one. The new Component definition accepts the same options as Angular2 original decorator and stores the metadata into a global variable named components. 

Since we are just defining a new Component decorator we need to fix all components to use the new decorator.

import {Component} from "../fx/annotations";
 
 @Component({
     selector: 'clock',
 })
 export class ClockComponent {
     constructor() {
     }
 }

Using some monkey patching we can “steal” the module URL of the current executing JavaScript file from SystemJS. Here is the the trick.

(function() {
      "use strict";
      
      var stack = [];
  
      var SystemJSExtensions = window.SystemJSExtensions = {
          getExecutingModule: function() {
              return stack[stack.length-1];
          }
     };
 
     hook(System.constructor.prototype, "instantiate", function(original) {
         return function(load) {
             var promise = original.apply(this, arguments);
 
             hook(load.metadata.entry, "execute", function(original) {
                 return function () {
                     stack.push(load);
 
                     var res = original.apply(this, arguments);
 
                     stack.pop();
 
                     return res;
                 };
             });
 
             return promise;
         }
     });
 
     function hook(obj, name, func) {
         var original = obj[name];
         obj[name] = func(original);
     }
 })();

Once we have knowledge of all components and their module URL we can bootstrap our application by first loading all component’s template and styles and only then let Angular run its own bootstrapping logic.

  export function bootstrap(appComponentType: Type): Promise<ComponentRef<any>> {
      console.log("Registered components: " + components.length);
  
      var promises = [];
  
      for(let metadata of components) {
          let promiseTemplate = Promise.resolve(true);
          if(!metadata.template && !metadata.templateUrl) {
              var templateUrl = new URI(metadata.moduleId).suffix("html");
             console.log("  Loading template: " + templateUrl);
             promiseTemplate = System.import(templateUrl + "!text").then(function (template) {
                 metadata.template = template;
 
                 console.log("  Template loaded: " + template);
             });
         }
 
         let promiseStyles = Promise.resolve(true);
         if(!metadata.styles && !metadata.styleUrls) {
             var stylesUrl = new URI(metadata.moduleId).suffix("css");
             console.log("  Loading styles: " + stylesUrl);
             promiseStyles = System.import(stylesUrl + "!text").then(function (styles) {
                 metadata.styles = [styles];
 
                 console.log("  Styles loaded: " + styles);
             });
         }
 
         promises.push(Promise.all([promiseTemplate, promiseStyles]).then(function() {
             if(metadata.encapsulation === undefined) {
                 metadata.encapsulation = ViewEncapsulation.None;
             }
 
             Component(metadata)((<any>metadata).target);
         }));
     }
 
     return Promise.all(promises).then(function() {
         return bootstrap(appComponentType);
     });
 }


The magic resides at lines 38-40. Only after all templates and styles were loaded and injected to the metadata object, we let Angular bootstrap the application. At that step Angular just “sees” a metadata object filled with templates and styles and does its usuall magic.

Full source code can be found at the Github repository https://github.com/oricalvo/blog-angular2-template-by-convention.

 

Contact us
You might also like
Share: