Converting an AngularJS 1.5 app to Angular 4

Hey! Angular 4 is out, and it’s good. So, let’s get good at Angular 4 together, by converting my excellent website satsuki.tv from Angular 1.5 to Angular 4.

Angular 4 (and 2) is a big jump from 1.x. There are major shifts in the way apps are written. Angular 4 is best built in TypeScript (the docs are primarily in TypeScript). There’s even a command line interface (CLI) called ng to help build out a project structure quickly.

What’s that? You don’t know TypeScript? Me either. It’s okay — we’ll make it through this. The jump from JavaScript to TypeScript is a lot easier with the Angular™ Command Line Interface™ ng than you may expect. Let’s look at the main things you’ll need to learn for TypeScript.


Welcome to JavaScript, but with Typing

Remember var ? This is them now… feel old yet?

const is good when you aren’t going to be changing the value of the variable. You can still change the value’s properties though.
let replaces var in pretty much all other situations.

Reiterating the image descriptions: var becomes let and const depending on what you’re doing, and we also start adding types because TypeScript! Here’s a little cheatsheet for some common types:

  • Integers and floats: number
  • Strings: string
  • Random objects: any

It’s tempting to make plain-old-javascript-objects; however, it’s good to consider turning random objects, that have all the same data, into classes that can be imported, constructed, and all that good stuff. You’ll get a little benefit of code completion if your IDE/Editor supports TypeScript like VS Code does. There’s also the benefit of having a well described API for those objects by creating a “Single Source of Truth” when you make a class.

Other important TypeScript nuggets:

  • ES6-style classes are most of what you’ll be working in! If you are unfamiliar with them, check out some basic syntax here.
An example of an annotation for an angular component
  • Annotations are how you tell Angular what things are! If you’re from Java-land or Python-land this might look familiar.
Some examples of imports
  • You have to import any non-built-in classes you’re using in every file you’re using them. Some IDEs that support TypeScript may take care of these imports for you, but often times you’ll just get a red squiggly under your missing class.
Oops, I forgot to import this!
  • Injecting services is done in the constructor() and you can use some TypeScript syntactic sugar to automagically create and assign them to class properties by specifying public or private before the variable name.
Notice the word “private” before tumblrService. In the rest of my class, I can access this.tumblrService!

Major Version Change = Major Idea Changes

Services

The biggest ideological shift I found is that the control over whether a service is a singleton has now transferred to the implementor.

This is a warning message on the console when you generate a service. It’s telling you that you have to decide how it will be provided: by module, or by component!

That’s a gosh darn mouthful — let’s try to break it down. The way writing services in AngularJS 1.x works is that you are able to enforce it as a singleton simply by what you return inside app.factory().

  • Does the service definition return a constructor? Then people using it can make their own, hence not a singleton.
  • Does the service definition return an object? Then people can safely assume it’s a singleton and share data across it.

It’s an extremely common pattern to simply return an object inside your app.factory() call, since services are often used to share data or functionality across components instead of using $scope or component bindings.

In Angular 4, the service you create can be a singleton or not depending on how a developer provides it to their module or component.

  • If the service is provided at the module level only, it is a singleton by default. Any components using it will all use the same copy (as long as they do not declare that they are providing their own).
  • If it is provided at the component level, it is a singleton only to that component’s tree. Another catch: If your component tree root is a page/view in a router outlet, it creates a new service every time you switch pages.

Okay but what does this mean for me, the AngularJS 1.x dev?

What this means is that if you want to keep the AngularJS 1.x behavior, you have to provide your services at the module level, as displayed in the screenshot below.

Providing a service in a module, which provides the service as a singleton down the entire module tree.

Components and Controllers, oh my

In Angular 4, components are created as classes instead of an object with a controller function. This allows for components to have a more Java-esque structure — by that I mean they have class properties and function declarations. Components are also in a tree structure instead of utilizing scope.

Out:

  • Controllers in all their forms
  • Scope (seriously, all of it)

In:

  • Components (without controllers)
  • Component trees

Okay but what does this mean for me, the AngularJS 1.x dev?

It means that your components that used scoped variables to pull data from above it in the scope tree should instead use a service, component bindings, or the component tree structure (by requiring a parent component, for example).

It means that any HTML that uses ng-controller should be converted into a component instead. If you’re using controller composition in AngularJS 1.x you will likely need to use inheritance or composition for your components in TypeScript/Angular 4.

It also means that scope is really really gone. Yes, $scope. Yes, that includes $scope.$watch. Anything using $scope.$watch will have to be refactored. Sometimes its as easy as using an event binding, maybe you end up using ngOnChanges(), maybe you have to do something like setTimeout() . Ultimately the days of setting your own watchers is gone. Some additional information about the internals at this hyperlink.

If your code is organized neatly, the component transfer from 1.5 to 4 will include moving the initial property declarations out into class property definitions and/or the constructor, and declaring your functions within the class instead of using object function assignments. For the most part, my AngularJS 1.5 code was assigning a lot of $ctrl.fnDoSomething = function() { / *something */ } inside of a controller function. Basically, you’ll just be doing the same stuff except in a class.

If you’re not using components, and still on directives (e.g. Angular 1.4 or lower), things are a bit more involved but the concepts will mostly be the same if you’re not relying too much on scope.

The templates are a-changing

A big gripe I’ve had with AngularJS 1.5 component templates is that they used $ctrl. to refer to the component controller, and yet in the component controller you would use this.. Also, don’t get me started on the old style of using vm to refer to the controller.

Referring to the same object with different names depending on whether you were in HTML or JS always seemed messy to me, and was definitely confusing to newcomers on AngularJS 1.5 projects. I started enforcing the placement of var $ctrl = this in all AngularJS 1.5 component controllers, and thus using $ctrl. to refer to the controller in both HTML and JS. This significantly reduced confusion as to what was available in the template and what you had to create in the controller.

Angular 4 is a bit different and takes care of this gripe for me. Since components are objects with properties and functions, those properties and functions are bound to the template without any prefix. So if your component has a property named isTweetLoaded, then you use it in the template by name only:isTweetLoaded… no prefix!

That being said, you still have to use this.isTweetLoaded in the component code, but this is a general JavaScript/TypeScript convention that many folks will be used to, and is certainly an easier pill to swallow when you don’t have to use $ctrl. in the template. Recall that in Angular 1.x, un-prefixed variables in templates refers to variables on the current $scope but Angular 4 has removed $scope and stray controllers in favor of a component tree structure.


Getting Started with the Conversion

Enough talk! First, install this! https://github.com/angular/angular-cli

A common roadblock with Angular development has been getting a development environment set up. The ng CLI linked above was very useful in setting up the project with everything I needed to get started. Theng new PROJECT-NAME command takes care of most of that roadblock. The project created by ng has a few commands that may help:

  • ng serve or npm start which watches and compiles your code in real-time, hosting it on port 4200 by default. This allows you to develop your app locally and painlessly!
  • ng build or npm run build which creates the distributable files to be hosted publicly!
  • ng test or npm test which starts the Angular test suite, featuring karma, protractor, and all that good stuff!

Much of the compilation is done by webpack. If that scares you then please don’t worry — it’s already set up and you shouldn’t have to touch anything!

Making things in Angular 4

Now that you have a project, lets make some components. First, cd or change your terminal directory into your project. Now, running ng generate component my-component-name will create files in the src/ directory for your new component. It’ll create css, html, and ts (TypeScript) files, as well as a spec.ts file for tests.

Something to note is that, by default, your generated component element names (also known as selectors) will be prefixed with app-. For example, generating MyComponent would create an HTML element named <app-my>. Although my instinct was to fight this and name my component selectors without that prefix, I do think it can be important; especially for something like a NavComponent, which may normally interact poorly with the existing <nav> element. In this case the CLI is auto-generating an <app-nav> element which shouldn’t collide with any existing non-Angular elements.

I created a component for each page of my site to start with (Home, Projects, Blog, Blog Post). I copied my HTML in and started with converting the templates.

Converting Old Component Templates

Remember from before where I was talking about how template code has no $scope or scoped variables anymore? All the properties you’ll use from your component are un-prefixed — so get rid of those $ctrl. prefixes in the template. If you were using scoped variables you’ll need to retrieve them from somewhere else, like a service, component binding, or parent component! Accessing parent components is beyond the scope of this post, but we’ll touch on injecting services briefly below.

Template code has the same basic ideology but different syntax. Below are some common directive changes that should help out:

  • ng-hide="$ctrl.isHiding" => [hidden]="isHiding"
  • ng-bind-html="$ctrl.someHtmlString" => [innerHtml]="someHtmlString"
  • ng-model="ctrl.form.field" => [(ngModel)]="form.field"
  • ng-repeat="user in $ctrl.users" => *ngFor="let user of users"
  • ng-click="$ctrl.doSomething()" => (click)=>"doSomething()"

Notice that the square braces [] are used to denote an input, where the component is feeding the “HTML attribute” or directive with a value. The parentheses () are used to denote an output, where the HTML element is feeding the angular component with an event. You can read more on the angular documentation! Putting [()] creates a portal to another dimension, where two-way binding occurs. More on that in the hyperlinked post.

JavaScript to TypeScript

For the most part, converting JavaScript to TypeScript will be changing the syntax such that it fits into a class definition and changing var declarations to let or const. Otherwise, there’s a few other things to note.

Your services are injected in the constructor, and for each parameter you can insert an access modifier such as private or public in front of it to automatically create a property with the same name in your component. It’s great syntactic sugar that eliminates a rote pattern.

An example of injecting 3 services into a component by putting “private” in front of the name of variable name.

Many service names have changed. For example:$element => ElementRef and $http => Http. I’ve found that googling something to the effect of “what’s $http in Angular 4?” usually yields decent results. If that fails, try the same search query except “Angular 2.” 😜

$scope.$apply() is gone! If you’re using some non-Angular library, you’ll have add private changeRef: ChangeDetectorRef (and import that type from @angular/core) to your constructor. Then whenever changes occur outside Angular, you call this.changeRef.detectChanges();.

TypeScript doesn’t like it when you use window.GLOBALVARIABLE and it’s not healthy if you plan on doing more with your Angular 4 code (see angular-universal). So in order to use libraries that place themselves as a global on the window object, like the twttr library, you will want to add declare var twttr: any; to the top of your component file to indicate that there exists an untyped global named twttr. Then you can check for its existence just like you would in JavaScript: if (twttr) { /* do stuff */ }

Promises to Observables

You may be thinking “But Cecelia, I just learned Promises for AngularJS 1.x and now I have to learn Observables for Angular 4?” And frankly, I’m pretty miffed, too; however, for the purposes of this guide, and for many Angular projects, it will be a matter of changing a few words.

I need to make an HTTP call to Tumblr to grab some posts. Specifically, I need to get JSONP. Let’s make some changes to our project:

  1. Add this to app.module.ts:
    import { JsonpModule } from ‘@angular/http’;
  2. Add JsonpModule to imports in @NgModule()
  3. Generate a service with ng generate service tumblr-service
  4. Provide that service in app.module.ts
  5. You’ll need Jsonp and Observable so add these to tumblr.service.ts:
    import { Jsonp } from ‘@angular/http’;
    import { Observable } from ‘rxjs/Rx’;
  6. Inject the Jsonp service into the TumblrService constructor.

My service will have two functions:

  • getPost(id:string): Observable<any>
  • getPosts(): Observable<any[]>

You can view the real code on GitHub, but I’ve shortened the code for this guide. This is how I’m making a call to Tumblr and then returning my posts as an array:

return this.jsonp
.request(tumblrUrl)
.map(response => {
return response.json().response.posts;
});

getPost() will just call getPosts() and filter out the one post we need using .map() again. This is pretty similar to using .then() with Promises, and in many cases you will be using Observables just like you were using Promises.

The one tricky difference (Thanks Emily) is that in components where you’ll be calling this.tumblrService.getPosts(), you'll need to use .subscribe() to actually retrieve the data. Observables are lazy, meaning that they don't actually execute any code until something subscribe()s to it. So instead of doing Promise.then().then().then(), you would do Observable.map().map().subscribe().

Observables can be more complicated, but this small tutorial should help those who already have grasped the basics of using Promises.

Routing

A picture is worth a thousand words! Here’s the full router code configuration for my website.

See the picture above for how my basic routing configuration looks. RouterModule comes from @angular/router so you’ll have to import that into app.module.ts. You’ll also need to add a <router-outlet></router-outlet> element in app.component.ts. The view is rendered as a sibling below the routerOutlet. This is key for styling: don’t expect anything to be a child of routerOutlet.

For larger apps, the Angular 4 router does support more configuration; however, for my small website, this configuration is sufficient.

Adding router links to your templates is done by adding something like routerLink="/path/to/route" or[routerLink]="['/path', variable]" to your element. There’s also routerLinkActive="active" which adds the active class to the element when the path specified in routerLink is active.


Results

The result was that I have the same site except it’s using Angular 4 now. I’m also a little bit smarter and a little bit cooler. ng build gives me a small handful of flattened files (in addition to my assets) to upload to my server and everything works (after testing it all locally, and having everything not work many many times).

There’s still automated tests cases to be built out and getting this all reintegrated with Travis CI — perhaps that will be another post. The site is basically 4 components with 1 service and 1 non-Angular library. It took less than a week of working on it in my spare time to get the site rebuilt and deployed, plus a bit of time afterwards for code cleanup. It certainly helps that I was not adding any new functionality.

If you were to perform this refactor on a larger project, I would recommend pausing feature development until the first stable release of the refactored app in Angular 4. If that’s not possible: I’ve heard that there are ways to have Angular 4 and 1.6 running in tandem — your mileage may vary.

You can view the entire codebase for my site on GitHub: @satsukitv/satsuki.tv.