Tuesday, August 23, 2016

Learning Angular 2: Upgrading to Angular 2 RC5

Version 0.0.3 of my GuildRunner sandbox Angular 2 application is now available.  All of the differences between this version and the previous version (minus the updates to the version number and the README file) are changes made to upgrade the application to use Angular 2 RC5 (release candidate 5).

While there were some changes to the router/routing syntax, the biggest change that comes with RC5 is the introduction of Angular modules and the @ngModule decorator.  There is a long documentation page about Angular modules in the official developer guide, but essentially Angular modules allow you to bundle sets of shared injectable dependencies into a single file that provides those dependencies "downstream".

For example, prior to the upgrade my MainNavigationComponent received the directives needed for routing (like RouterLink) via the "directives" metadata property (which also meant it had to be imported):


//main-navigation.component.ts (previous version)
import { ROUTER_DIRECTIVES } from '@angular/router';
...
@Component({
  ...
  directives: [ ROUTER_DIRECTIVES ]
})

As another example, the GuildsMasterComponent received the GuildService for its constructor method via the "providers" metadata property:


//guilds-master.component.ts (previous version)
import { GuildService } from '../guild.service';
...
@Component({
  ...
  providers: [ GuildService ]
})

Now both of those dependencies are declared in the new application-level Angular module - app.module.ts:


//app.module.ts
...
import { routing } from './app.routing';
import { GuildService } from './guild.service';
...
import { SandboxModule } from './sandbox/sandbox.module';
...
@NgModule({
  imports:      [
    BrowserModule,
    HttpModule,
    routing,  //Provides the routing directives as well as the route definitions
    SandboxModule
  ],

  declarations: [
    AppComponent,
    VersionComponent,
    MainNavigationComponent,
    HomeComponent,
    GuildsMasterComponent
  ],

  providers: [
    { provide: XHRBackend, useClass: InMemoryBackendService }, // in-mem server
    { provide: SEED_DATA,  useClass: InMemoryDataService },     // in-mem server data
    VersionService,
    GuildService //Provides the GuildService downstream
  ],

  bootstrap:    [
    AppComponent
  ],
})

Because the MainNavigationComponent and GuildsMasterComponent are included in the module via the "declarations" block, they are part of the feature bundle of this module, and so they have access, via dependency injection, to the routing and GuildService dependencies without the need for the "directives" or "providers" metadata properties of the @Component.

Note the four properties in this @ngModule decorator. The "declarations" property is where you list all of the components, directives, and custom pipes used in your module: anything your templates need in order to operate (example: the inclusion of the MainNavigationComponent in the declarations allows the AppComponent template to understand how to render the "<app-main-navigation>" tag in the AppComponent HTML). The "providers" property is where you list your module services (things that would be injected into your component as a constructor argument).

The "imports" property is where you list other modules that provide functionality (services, directives, etc.) to your feature module. Some of the modules may be Angular library modules, such as the required BrowserModule or the HttpModule needed for performing HTTP operations. But it can also include other modules in your application. Note the inclusion of the "SandboxModule" in this example:


//app.module.ts
import { SandboxModule } from './sandbox/sandbox.module';
...
@NgModule({
  imports:      [
    ...
    SandboxModule
  ],

That is a separate Angular module file dedicated to the "sandbox" feature area of my application:


//sandbox/sandbox.module.ts
import { NgModule }       from '@angular/core';
import { sandboxRouting } from './sandbox.routing'
import { GuildListComponent } from "./guild-list/guild-list.component";
import { SandboxService } from './sandbox.service';

@NgModule({
  imports: [
    sandboxRouting
  ],

  declarations: [
    GuildListComponent
  ],

  providers: [
    SandboxService
  ]
})

export class SandboxModule {}

This module encompasses the components, services, and the routing related to the sandbox feature area of the application, and it's integrated with the rest of the application simply by the fact that it's declared in the "imports" property of the main application Angular module. Note that it doesn't contain the fourth property seen in app.module.ts: the "bootstrap" property is mainly for declaring the top-level component of a given module, something the sandbox feature area doesn't have.

So the introduction of Angular modules adds a new organizational construct to Angular 2 and cuts down on typing since there's no need to add "directives" and "providers" properties to your components in order to perform dependency injection.

However, there is one small caveat, best explained by example. Even though my app.module.ts file declares the GuildService in its array of providers, and I no longer need to use the "providers" metadata property on my GuildsMasterComponent, I still need to import the GuildService:


//guilds-master.component.ts (new version)
import { GuildService } from '../guild.service';
...
export class GuildsMasterComponent implements OnInit {

  guilds: Guild[] = [];

  constructor( private guildService: GuildService ) { }

This puzzled me, and in perusing some of the updated documentation and tutorial examples I couldn't find an explanation for why that import was still necessary if the GuildsMasterComponent was getting its instance of the GuildService from the application Angular module.

But then I looked at the JavaScript being generated from the guilds-master.component.ts file. Here are the relevant lines from that JavaScript file, with the significant lines followed by comments:


//guilds-master.component.js
...
var guild_service_1 = require('../guild.service');  //Significant line #1
...
GuildsMasterComponent = __decorate([
        core_1.Component({
            moduleId: module.id,
            selector: 'app-guilds-master',
            templateUrl: 'guilds-master.component.html',
            styleUrls: ['guilds-master.component.css']
        }), 
        __metadata('design:paramtypes', [guild_service_1.GuildService]) //Significant line #2
    ], GuildsMasterComponent);
    return GuildsMasterComponent;

Those two significant lines are generated by Angular compiler based on the argument declaration of the GuildsMasterComponent constructor. If you try to type the "guildService" argument of the constructor as another data type (like "any"), the compiler won't know what object/export it's supposed to use. And you can't set the "guildService" argument to type GuildService without importing GuildService so that the TypeScript compiler can recognize the data type.

Two other tidbits:

  • When I initially created the separate sandbox Angular module (sandbox.module.ts), I did not create a separate routing file with a route configuration for the single sandbox route:  that route was still part of the application Angular module and the app-level routing.  But when I ran the application, that generated an error message that my single sandbox component was "part of the declaration of 2 modules."  Fortunately I found a Stack Overflow post that pointed out the need for separating routing configurations.

  • In an earlier blog post, I noted how my IntelliJ IDE would automatically add the "import {} from ..." statement for any component I added to my router configuration via auto-complete (where IntelliJ would provide me a list of options as I typed the component name, and I could select the one I wanted from the list using the Tab key).  I was happy to see that same convenience feature at work as I added components to the "declarations" list of my app module.

No comments:

Post a Comment