Sunday, October 16, 2016

Angular 2 and Aurelia, into the app code

In my last post, I discussed my attempts to convert the Gentelella Dashboard into React, Angular2 and Aurelia apps.  There were different levels of success, but in the end I concluded that Aurelia had offered advantages over Angular2 (which was a little more difficult to work with) and React (which was impossible to implement the Dashboard without a re-write).

In this post, I'd like to get into a few more specifics, with examples.  I'll walk through the application structure starting with the bootstrap and ending at the top level component.  While I'm not going into anything complex here, I think it's nice high level view on the differences in structure between the two frameworks.

Bootstrapping

First, when implementing either Aurelia, or Angular2, you need to bootstrap the application into a web page.  In the angular and Aurelia versions, the bootstrapping files are of similar complexity/structure.  Both do some configuration, and then move to the next step in the process.

Angular 2 Bootstrapping

/*
 * Angular bootstraping
 */
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { decorateModuleRef } from './app/environment';
import { bootloader } from '@angularclass/hmr';
/*
 * App Module
 * our top level module that holds all of our components
 */
import { AppModule } from './app';

/*
 * Bootstrap our Angular app with a top level NgModule
 */
export function main(): Promise<any> {
  return platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .then(decorateModuleRef)
    .catch(err => console.error(err));
}

// needed for hmr
// in prod this is replace for document ready
bootloader(main);

The important thing here is pulling in a definition of the AppModule, which contains references to all the components being injected into the main application.

Aurelia Bootstrapping

import {Aurelia} from 'aurelia-framework';
// we want font-awesome to load as soon as possible to show the fa-spinner
import 'font-awesome/css/font-awesome.css';
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap';
import '../styles/styles.css';

// comment out if you don't want a Promise polyfill (remove also from webpack.config.js)
import * as Bluebird from 'bluebird';
Bluebird.config({ warnings: false });

export async function configure(aurelia: Aurelia) {
  aurelia.use
    .standardConfiguration()
    .developmentLogging();

  // Uncomment the line below to enable animation.
  // aurelia.use.plugin('aurelia-animator-css');
  // if the css animator is enabled, add swap-order="after" to all router-view elements

  // Anyone wanting to use HTMLImports to load views, will need to install the following plugin.
  // aurelia.use.plugin('aurelia-html-import-template-loader')
 

  await aurelia.start();
  aurelia.setRoot('app');

  // if you would like your website to work offline (Service Worker), 
  // install and enable the @easy-webpack/config-offline package in webpack.config.js and uncomment the following code:
  /*
  const offline = await System.import('offline-plugin/runtime');
  offline.install();
  */
}

For Aurelia, the key line above is the setRoot, which invokes the root component for Aurelia. 

At this point they diverge.  The angular App module is a larger construct that knows a lot about the various components that are going to be used in the application.  This is an extra step in the load process between the bootstrap and the eventual top level component.  On the other hand, the Aurelia App just goes straight to the top level component, and its router.

Angular 2 App Module

import { NgModule, ApplicationRef } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { RouterModule } from '@angular/router';
import { removeNgStyles, createNewHosts, createInputTransfer } from '@angularclass/hmr';

/*
 * Platform and Environment providers/directives/pipes
 */
import { ENV_PROVIDERS } from './environment';
import { ROUTES } from './app.routes';
// App is our top level component
import { App } from './app.component';
import { APP_RESOLVER_PROVIDERS } from './app.resolver';
import { AppState, InteralStateType } from './app.service';
import { Home } from './home';
import { About } from './about';
import { Footer } from './controls/footer.component';
import { Sidebar } from './menu/sidebar.component';
import {FlotCmp} from './controls/network-activities.component'
import { Content } from './content';
import { NoContent } from './no-content';
import { XLarge } from './home/x-large';
import { FontAwesomeDirective } from 'ng2-fontawesome';

// Application wide providers
const APP_PROVIDERS = [
  ...APP_RESOLVER_PROVIDERS,
  AppState
];

type StoreType = {
  state: InteralStateType,
  restoreInputValues: () => void,
  disposeOldHosts: () => void
};

/**
 * `AppModule` is the main entry point into Angular2's bootstraping process
 */
@NgModule({
  bootstrap: [ App ],
  declarations: [
    App,
    About,
    Home,
    Content,
    Footer,
    Sidebar,
    FlotCmp,
    NoContent,
    XLarge,
    FontAwesomeDirective
  ],
  imports: [ // import Angular's modules
    BrowserModule,
    FormsModule,
    HttpModule,
    RouterModule.forRoot(ROUTES, { useHash: true })
  ],
  providers: [ // expose our Services and Providers into Angular's dependency injection
    ENV_PROVIDERS,
    APP_PROVIDERS
  ]
})
export class AppModule {
  constructor(public appRef: ApplicationRef, public appState: AppState) {}

  hmrOnInit(store: StoreType) {
    if (!store || !store.state) return;
    console.log('HMR store', JSON.stringify(store, null, 2));
    // set state
    this.appState._state = store.state;
    // set input values
    if ('restoreInputValues' in store) {
      let restoreInputValues = store.restoreInputValues;
      setTimeout(restoreInputValues);
    }

    this.appRef.tick();
    delete store.state;
    delete store.restoreInputValues;
  }

  hmrOnDestroy(store: StoreType) {
    const cmpLocation = this.appRef.components.map(cmp => cmp.location.nativeElement);
    // save state
    const state = this.appState._state;
    store.state = state;
    // recreate root elements
    store.disposeOldHosts = createNewHosts(cmpLocation);
    // save input values
    store.restoreInputValues  = createInputTransfer();
    // remove styles
    removeNgStyles();
  }

  hmrAfterDestroy(store: StoreType) {
    // display new elements
    store.disposeOldHosts();
    delete store.disposeOldHosts;
  }

}

Some of this is for supporting HMR, but in general this is a big chunk of code.  This module is a central host to the components that are getting loaded into the Angular application.  When you add new controls, you need to come here and add reference to them.  At the top of the file, you can see a number of components are first being imported, and then lower down they are also injected into the module itself.  The next step in the load process is handled via the @NgModule attribute, which has a reference to the bootstrap component, which is loaded first.

In Aurelia, the bootstrapper is where Aurelia is configured, and there's no equivalent central module to this.  The Aurelia bootstrap directly loads the app component which contains the top level router info.  That handles routing for the app template that is loaded up initially.   The difference is that the Angular2 component learns about its components from the App Module, whereas in Aurelia the HTML template does an import of the child components before it can refer to them.  Aurelia moves that composition responsibility to the template side, while it is embedded into the Angular 2 application code.

App Components

The next step in loading the application is starting up the top level App component.  As you can see below the components being used at top level for Angular 2 and Aurelia are actually quite similar.

Angular 2 App Component

/*
 * Angular 2 decorators and services
 */
import { Component, ViewEncapsulation } from '@angular/core';
import 'style!css!less!font-awesome-webpack/font-awesome-styles.loader!font-awesome-webpack/font-awesome.config.js';
import { AppState } from './app.service';

/*
 * App Component
 * Top Level Component
 */
@Component({
  selector: 'app',
  encapsulation: ViewEncapsulation.None,
  styleUrls: [
    './app.style.css'
  ],
  templateUrl: './app.component.html'
})
export class App {

  constructor(
    public appState: AppState) {

  }

  ngOnInit() {
    console.log('Initial App State', this.appState.state);
  }

}

The important thing to notice here is the dependency on the @Component attribute for defining core functionality for the component. The attribute for selector defines how the component can be referenced from html, and the templateURL defines the template that is linked to the component.

Aurelia App Component

import {Aurelia} from 'aurelia-framework';
import {Router, RouterConfiguration} from 'aurelia-router';

export class App {
  router: Router;

  configureRouter(config: RouterConfiguration, router: Router) {
    config.title = 'Aurelia';
    config.map([
      { route: ['', 'dashboard'], name: 'dashboard',      moduleId: './content/dashboard',      nav: true, title: 'Dashboard' },
      { route: 'inbox', name: 'inbox',      moduleId: './content/inbox',      nav: true, title: 'Inbox' },
    ]);

    this.router = router;
  }
}

With Aurelia there is no required attribute necessary to define the selector or the template.  Instead both can be defaulted to be defined by convention.  In this case, as the class name is "App", the template for the html is expected to be app.html, and the html tag will also be "app".  Also, if a css style file was used for just this component it would be defined in  a require from the HTML template, not in the script file.

One thing of note here is that this top level component directly contains the router for the component, whereas in the Angular 2 application the router is in a separate routes component.  There's some differences in how these routers work, but that's a worthy of a completely separate discussion.

HTML Templates

The final files I'd like to compare are the top-level App template files.  With these you can definitely see how Angular2 relies more on the script code files than Aurelia.

Angular 2 App HTML Template

<side-bar></side-bar>
<topnav-bar></topnav-bar>
<router-outlet></router-outlet>
<custom-footer></custom-footer>     

In Angular 2, the top level HTML template is just a list of three custom components and the Angular 2 router outlet.  They are all components that are defined elsewhere. While this is a very minimalistic approach from the template side, what is not obvious is where these custom tags are coming from. You first need to do a search for the selectors to find the component, and then track back from the component to the template if you want to see where are the behavior is coming from.

Aurelia App HTML Template

<template>
  <require from="./menu/topbar"></require>
  <require from="./menu/sidebar"></require>  
  <require from="./controls/custom-footer.html"></require>
  
  <div class="container body">
    <div class="main_container">        
      <sidebar router.bind="router"></sidebar>
      <topbar></topbar>
      <router-view></router-view> 
      <custom-footer></custom-footer>
    </div>
  </div>
     
</template>

There's a bit more template code here in Aurelia.  At the top are the require tags which point directly to the source for the custom tags.  There's also then some extra div tags that are moved here from index.html because of some differences in the way Angular 2 and Aurelia load up, but after that you essentially have same 3 custom components, and the router view for Aurelia.  So the main difference is that the template is more self-descriptive.  With the require tags, there's no mystery where you're pulling these components from.

Finally, one thing I do want to note is the custom-footer reference.  In this case, there was no behavior needed, so it is an HTML only component.  For the Angular 2 version I needed to actually create and import the component script class into the App Module in order to use this custom footer.

Conclusion

Hopefully this write up can be helpful to understand the different design approaches taken by the Angular 2 and Aurelia frameworks.  Aurelia in general has less boilerplate code, and eliminates need for extra steps you need to configure in Angular 2. I found this very powerful for rapid prototyping where I was just piecing together HTML templates to produce a mockup without needing any backing Typescript files.   It was possible to incrementally build up the application, instead of doing more work up front just to things working.

In the end, despite their differences, I do find Aurelia and Angular 2 to be similar enough that it is relatively easy to move between the two.  There's a lot of analogous behavior, and they both are approaching things from the same direction.  

No comments:

Post a Comment