Angular 2 with Handcrafted Tools, Century-Old Techniques and ES5

The Idea

Writing an Angular 2 application in ES5 is a tricky subject, and I have to be careful about the tone I adopt when talking about this approach. Using a modern build system that leverages ES6 or TypeScript with live reloading, scaffolding, linting, deployment options, pre-processors, test runners, etc. is a superior course of action and should be the default choice. With that said, there are legitimate reasons why it makes more sense to start with ES5 when building an Angular 2 application. The one scenario that I have run into quite a bit is when a large organization has a sizeable AngularJS 1.x application written in ES5, and they are looking for a migration path that doesn’t require introducing a ton of new paradigms to upper management as they propose to tear everything down. Using the new angular.component syntax and component driven architecture, you can write an AngularJS 1.5 application that is surprisingly close to an Angular 2 application when written in ES5.

My official stance is that if you have to write an Angular 2 application in ES5, I am sorry. I want to be sensitive to developers who are in this position, and so if I come across as sarcastic, I am also sorry. It is all in good fun and so let’s get started on how to write an Angular 2 application in ES5.

The Application

Angular 2 Website Screenshot

I have taken the Angular 2 project I covered in Get Started with Angular 2 by Building a Simple Website and converted it into ES5. I am not going to get into every little detail of Angular 2 but instead, focus on covering the specific ES5 implementation details.

Code

Setting up

Since we are using pure ES5, there is no need for any build tools. We will manually reference our JavaScript resources in index.html, just like in the good ol’ days! Because Angular 2 modules are packaged individually, we need to include them all separately. Look for files that end in *.umd.js, which are written in plain JS and eliminate the need for module loaders. Because we are not using a module loader, order matters!

<body>
  <app>Loading...</app>
  <!-- Dependencies -->
  <script src="//npmcdn.com/rxjs@5.0.0-beta.7/bundles/Rx.umd.js"></script>
  <script src="//npmcdn.com/reflect-metadata@0.1.3"></script>
  <script src="//npmcdn.com/zone.js@0.6.12"></script>
  <script src="//npmcdn.com/@angular/core@2.0.0-rc.2/bundles/core.umd.js"></script>
  <script src="//npmcdn.com/@angular/common@2.0.0-rc.2/bundles/common.umd.js"></script>
  <script src="//npmcdn.com/@angular/compiler@2.0.0-rc.2/bundles/compiler.umd.js"></script>
  <script src="//npmcdn.com/@angular/platform-browser@2.0.0-rc.2/bundles/platform-browser.umd.js"></script>
  <script src="//npmcdn.com/@angular/platform-browser-dynamic@2.0.0-rc.2/bundles/platform-browser-dynamic.umd.js"></script>
  <script src="//npmcdn.com/@angular/router@2.0.0-rc.2/bundles/router.umd.js"></script>
  <!-- Our Code -->
  <script src="app/common/state.service.js"></script>
  <script src="app/common/experiments.service.js"></script>
  <script src="app/home/home.component.js"></script>
  <script src="app/about/about.component.js"></script>
  <script src="app/experiments/experiment-details/experiment.detail.component.js"></script>
  <script src="app/experiments/experiments.component.js"></script>
  <script src="app/app.component.js"></script>
  <script src="app/boot.js"></script>
</body>

Bootstrapping

To bootstrap our application, we are going to add a DOMContentLoaded event listener to our document to tell us when it is safe to bootstrap our application. In the event handler, we will call ng.platformBrowserDynamic.bootstrap and pass in our root component and its dependencies. You will notice that everything is wrapped in an IIFE, and we are passing in window.app which is what we will attach to our Angular application. For the sake of readability, we are are going to omit the IIFE wrappers out of our remaining snippets.

app/boot.js

(function(app) {
  document.addEventListener('DOMContentLoaded', function() {
    ng.platformBrowserDynamic.bootstrap(app.AppComponent, []);
  });
})(window.app || (window.app = {}));

Root Component

An Angular 2 component in TypeScript consists of a class definition and component metadata as you can see in the snippet below.

@Component({
  selector: 'app',
  templateUrl: require('app/app.component.html'),
  styleUrls: [require('app/app.component.css')]
})
export class AppComponent {}

The shape is similar in ES5 but with a more explicit syntax. We are manually calling ng.core.Component and ng.core.Class to provide us with a component we can use in our application. We then store the resulting object to app.AppComponent which we reference directly when we bootstrap the application in this line of code ng.platformBrowserDynamic.bootstrap(app.AppComponent, []);.

app/app.component.js

app.AppComponent = ng.core
  .Component({
    selector: 'app',
    templateUrl: 'app/app.component.html',
    styleUrls: ['app/app.component.css']
  })
  .Class({
    constructor: function() {}
  });

And then to complete the connection, we add our root component selector to our index.html.

<body>
  <app>Loading...</app>
</body>

Subcomponents

To illustrate how to use subcomponents in ES5, we are going to start by examining the app.ExperimentDetailComponent subcomponent. Notice that we are defining our inputs and outputs as an array of strings instead of using the TypeScript decorators.

app/experiments/experiment-details/experiment.detail.component.js

app.ExperimentDetailComponent = ng.core
  .Component({
    selector: 'experiment',
    templateUrl: 'app/experiments/experiment-details/experiment.detail.component.html',
    styles: [ ... ],
    inputs: ['experiment']
  })
  .Class({
    constructor: function () {},
    doExperiment: function () {
      this.experiment.completed += 1;
    }
  });

To consume the ExperimentDetailComponent within the ExperimentsComponent, we need to add the directives property to our component configuration object and pass in the ExperimentsDetailsComponent.

app/experiments/experiments.component.js

app.ExperimentsComponent = ng.core
  .Component({
    selector: 'experiments',
    templateUrl: 'app/experiments/experiments.component.html',
    directives: [app.ExperimentDetailComponent]
  })
  .Class({
    constructor: function() {
      this.title = 'Experiments Page';
      this.body =  'This is the about experiments body';
      this.message = '';
      this.experiments = [];
    },
    ngOnInit: function() {},
    updateMessage: function(m) {}
  })

The other detail that I want to call out is that our class object is just a key-value map of the methods that we need to expose to our component. The interesting part is that the properties we need to define generally happen within the constructor method of the object or sometimes implicitly within other methods.

Services

Because services are strictly imperative in nature, we define those slightly different than a component. We will start with a constructor function and then augment our service by adding additional methods to it via the prototype chain. In the code below, we initialize ExperimentsService with an array of experiment objects and then add a getExperiments method to it by attaching a function to app.ExperimentsService.prototype.getExperiments that returns this.experiments.

app/common/experiments.service.js

app.ExperimentsService = function () {
  this.experiments = [
    {name: 'Experiment 1', description: 'This is an experiment', completed: 0},
    {name: 'Experiment 2', description: 'This is an experiment', completed: 0},
    {name: 'Experiment 3', description: 'This is an experiment', completed: 0},
    {name: 'Experiment 4', description: 'This is an experiment', completed: 0}
  ];
};
app.ExperimentsService.prototype.getExperiments = function () {
  return this.experiments;
};

To make ExperimentsService available to our application, we will add it to the providers array on our root component.

app/app.component.js

app.AppComponent = ng.core
  .Component({
    selector: 'app',
    templateUrl: 'app/app.component.html',
    styleUrls: ['app/app.component.css'],
    providers: [ app.ExperimentsService ]
  })
  .Class({
    constructor: function() {}
  });

In ES6 and TypeScript, dependency injection happens at the class constructor. In ES5, we approximate that by converting the constructor property on our class object from a function to an array that contains our dependencies and the constructor function as the last argument. This is fairly similar to how we did it in Angular 1.x and so nothing too crazy here.

app/experiments/experiments.component.js

app.ExperimentsComponent = ng.core
  .Component({
    selector: 'experiments',
    templateUrl: 'app/experiments/experiments.component.html',
    directives: [app.ExperimentDetailComponent]
  })
  .Class({
    constructor: [app.ExperimentsService, function(experimentsService) {
      this.experiments = [];
      this.experimentsService = experimentsService;
    }],
    ngOnInit: function() {
      this.experiments = this.experimentsService.getExperiments();
    }
  });

Even though I am totally addicted to the TypeScript / ES6 sugar, I really like how close my code gets when I write Angular 1.x applications in an Angular 2 style. The building blocks are really close, the only real difference is the mechanisms that tie the pieces together.

Routes

To wrap up this magical mystery tour, let’s talk about routes. We are going to cover the RC2 router and revisit this section at a future date when things are a bit more stable. Because routes are an application wide dependency, we are going to add ng.router.ROUTER_PROVIDERS to our bootstrap call.

app/boot.js

document.addEventListener('DOMContentLoaded', function() {
  ng.platformBrowserDynamic.bootstrap(app.AppComponent, [ng.router.ROUTER_PROVIDERS]);
});

We will also add ng.router.ROUTER_DIRECTIVES to our directives array on our root component. This will allow us to use routerLink in our template to navigate between routes. To define our routing table, we first call ng.router.Routes and pass in an object that maps URL paths to a component which will return a partially applied function that we will call and pass in app.Component as its parameter. This applies the routing table to app.Component and returns it intact and unharmed.

app/app.component.js

app.AppComponent = ng.core
  .Component({
    selector: 'app',
    templateUrl: 'app/app.component.html',
    styleUrls: ['app/app.component.css'],
    directives: [ ng.router.ROUTER_DIRECTIVES ],
    providers: [ app.ExperimentsService ]
  })
  .Class({
    constructor: function() {}
  });

app.AppComponent = ng.router.Routes([
  {path: '/',            component: app.HomeComponent},
  {path: '/home',        component: app.HomeComponent},
  {path: '/about',       component: app.AboutComponent},
  {path: '/experiments', component: app.ExperimentsComponent},
  {path: '/*',           component: app.HomeComponent }
])(app.AppComponent);

In our app.component.html file, we will first add router-outlet to the layout and then update our header to navigate to their respective routes.

app/app.component.html


<header id="header">
<h1 id="logo">
    <a [routerLink]="['/home']"></a>
  </h1>
<div id="menu">
    <a [routerLink]="['/home']" class="btn">Home</a>
    <a [routerLink]="['/about']" class="btn">About</a>
    <a [routerLink]="['/experiments']" class="btn">Experiments</a>
  </div>
</header>
<div id="container">
  <router-outlet></router-outlet>
</div>

Conclusion

Writing an Angular 2 application in ES5 would not be my first choice because I am inherently lazy and I like abstracting out the hard bits into sugar coated candies that I do not have to think about. And then there is reality. I really REALLY like Angular 2 and it has changed the way the way that I write all my web applications and I encourage everyone to embrace Angular 2 patterns no matter what version they are working with. Using ES5 provides a strong strategic advantage when a piece by piece upgrade is the only option and / or getting executive buy-in will be hard.

Be sure to grab the project code and let me know what you think!

Code

Leave a Comment