Angular2 – Using Angular Modules & Optimizing Apps

The idea behind modules

Captura de pantalla 2017-05-24 a las 15.34.54

  • Using Modules in Angular is a way of doing easier to maintain an large app.
  • We will modify our recipe book app to use multiple modules.
  • NOTE: The imports in a component or service are not related to modules, it is a JS stuff to work.

Understanding the App Module

  • Lets see the meaning of the properties of the NgModule in the app.module.ts
@NgModule({
  declarations: [
...
  imports: [
...
  providers: [ShoppingListService,RecipesService, DataStorageService, AuthService, AuthGuard],
  bootstrap: [AppComponent]
})
  • declarations : component , directives or pipes this module use.
  • imports: other modules this module use (some Angular built in modules). When we import another module what that module exports. For example FormsModule export a set of directives , and importing it we can use all of that directives.
  • providers: which service we are using in this module. Inject here will be provided for the whole app.
  • bootstrap: whats our root component.

Understanding Features Modules

  • It is about put all classes relate to a feature inside a Module, in our example Recipes Module can be a candidate.

Creating a Recipe Feature Module

  • We create a recipe/recipe.module.ts to move all recipe related components to the new feature module.
  • We also move DropdownDirective and ReactiveFormsModule into recipe.module.ts because these only been used in the recipes module.
  • BrowserModule needs to be imported only in the app.module. It contains all elements of CommonModule and additional stuff to start the app. But use CommonModule for common directives in the others modules.
  • IMPORTANT: You must not declare the same component in two different modules.

recipe/recipe.module.ts

import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { RecipeListComponent } from './recipe-list/recipe-list.component';
import { RecipeItemComponent } from './recipe-item/recipe-item.component';
import { RecipeDetailComponent } from './recipe-detail/recipe-detail.component';
import { RecipesComponent } from './recipes.component';
import { RecipeStartComponent } from './recipe-start/recipe-start.component';
import { RecipeEditComponent } from './recipe-edit/recipe-edit.component';

@NgModule({
  declarations: [
    RecipesComponent,
    RecipeListComponent,
    RecipeItemComponent,
    RecipeDetailComponent,
    RecipeStartComponent,
    RecipeEditComponent
  ],
  imports: [
    CommonModule,
    ReactiveFormsModule
  ]
})

export class RecipeModule {}

But now we have a problem with the routing.  Recipe module doesn’t have access to the AppRoutingModule, it is in the app.module and don’t travel down to the recipe.module.

We will resolve it in the next point.

NOTE: We don’t move RecipeService from app.module because it is used in others parts of the app

NOTE: Don’t forget to import CommonModule in recipe.module. It gives you access to the common directives: ngClass , ngFor, etc

Registering routes in Features Modules

  • The problem we havce is we are using router-outlet in some of the templates in recipe module but we are declaring AppRoutingModule.
  • The solution is to create recipe/recipe-routing.module.ts for get all root configuration from the parent component and add all recipe relate routes.
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/Router';
import { RecipesComponent } from './recipes.component';
import { RecipeStartComponent } from './recipe-start/recipe-start.component';
import { RecipeDetailComponent } from './recipe-detail/recipe-detail.component';
import { RecipeEditComponent } from './recipe-edit/recipe-edit.component';
import { AuthGuard } from '../auth/auth-guard.service';

const recipeRoutes: Routes = [
  { path: 'recipes', component: RecipesComponent, children: [
    { path: '', component: RecipeStartComponent},
    { path: 'new', component: RecipeEditComponent, canActivate: [AuthGuard]},
    { path: ':id', component: RecipeDetailComponent},
    { path: ':id/edit', component: RecipeEditComponent, canActivate: [AuthGuard]}
  ]},
];

@NgModule({
  imports: [
    RouterModule.forChild(recipeRoutes)
  ],
  exports: [RouterModule]

})
export class RecipeRoutingModule {
}
  • The key thing is to use forChild instead of forRoot (only in the app routing) — IMPORTANT
    RouterModule.forChild(recipeRoutes)

for link the new recipe routes.

And reference it from recipe/recipe.module.ts

...
import {RecipeRoutingModule} from './recipe-routing.module';

...

  ],
  imports: [
    CommonModule,
    ReactiveFormsModule,
    RecipeRoutingModule
  ]
  • Now we have another issue: DropdownDirective is not working in the recipe module
  • IMPORTANT: A Module can only use components in its declaration and items imported in “imports”

Understanding Shared Modules

  • We will need to move DropdowDirective to a Shared Module

Captura de pantalla 2017-05-25 a las 13.58.59

Creating a Shared Module

shared/shared.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { DropdownDirective } from './dropdown.directive';

@NgModule({
  declarations: [
    DropdownDirective
  ],
  exports: [
    CommonModule,
    DropdownDirective
  ]
})

export class SharedModule {}

Creating a Shopping List Feature Module

  • We create two new features modules: auth/auth.module.ts and shopping-list/shopping-list.module.ts.

Loading components via Selectors vs Routing

  • We cannot use app-shopping-list selector in app.component.html because it is not declared as part of the app.module.ts.
  • IMPORTANT: only use selectors declared in the same module as the template.
  • But with routes it is different. We can declare different routing files and use them anywhere if there is any place in the app where has been declared.

A Common Gotcha

  • Make sure to import FormsModule in the modules where you are using forms directives such as ng-submit,etc,

Creating the Auth Feature Module

 

Understanding Lazy Loading

Captura de pantalla 2017-05-25 a las 15.24.23

  • Lazy loading is about don’t load all the features modules at the beginning. It can happen user won’t visit some feature never so all the components related to that feature are not needed.

 

Adding Lazy Loading to the recipes module

  • We want Recipes Module to be lazily loaded.
  • The first thing to do is create a home/home.component to set it up as the entrance point of our app instead of been redirected to the recipes.
  • Then we remove RecipeModule reference from the app.module.
  • Then we modify routes in the app-routing.module.ts:
const appRoutes: Routes = [
  { path: '', component: HomeComponent},
  { path: 'recipes', loadChildren: './recipes/recipe.module#RecipeModule'}
];
  • With the “loadChildren” option we reference what module we want to load as the user visit ‘/recipes’

NOTE: in the recipe-routing.module.ts we now have:

const recipeRoutes: Routes = [
  { path: '', component: RecipesComponent, children: [
    { path: '', component: RecipeStartComponent},
    { path: 'new', component: RecipeEditComponent, canActivate: [AuthGuard]},
    { path: ':id', component: RecipeDetailComponent},
    { path: ':id/edit', component: RecipeEditComponent, canActivate: [AuthGuard]}
  ]},
];
  • We can check in the console how lazy load some “chunks” of our app since we click in recipes:

Captura de pantalla 2017-05-25 a las 16.09.42

Protecting Lazy Load Routes with canLoad

What if you want to use route protection (canActivate  to be precise) on lazily loaded routes?

You can add canActivate to the lazy loaded routes but that of course means, that you might load code which in the end can’t get accessed anyways. It would be better to check that BEFORE loading the code.

You can enforce this behavior by adding the canLoad  guard to the route which points to the lazily loaded module:

{ path: 'recipes', loadChildren: './recipes/recipes.module#RecipesModule', canLoad: [AuthGuard] }

In this example, the AuthGuard  should implement the CanLoad interface.

 

How Modules and Services work together

  • Services (LogService for example) can be injected in the Lazy Loaded modules automatically without need to specify a providers array. This is because lazy modules use the root injector which can be referenced from the whole app.

Captura de pantalla 2017-05-25 a las 16.35.20

  • Let see another use case: the lazy module has its own provider as the other modules. Then the Lazy Load Module will create a child injector and the LogService will have another instance different from the one at app level:

Captura de pantalla 2017-05-25 a las 16.40.27

  • Keep in mind : when providers array it is declared on a lazy module a child injector will be created
  • And if we want to have a LogService only for a module (Module scope) then use the providers array in the component instead of the module
  • A third use case can be use a shared module to give the LogService instance to the lazy module:

Captura de pantalla 2017-05-25 a las 16.44.09

  • But in this case a child injector will be created too, because lazy module is importing a module which has a providers array.

IMPORTANT: Don’t provide services in shared modules. Especially not , if you plan to use them in lazy loaded modules

Understanding the Core Module

  • Now we want to leave the app.module as lean as is possible. So let see what components belong to the core of the app: Header and Home.

Creating a Basic Core Module

  • We create a core folder and we put inside header and home folders.
  • We fix routes
  • We create a core.module.ts:

core/core.module.ts

import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

import { HomeComponent } from './home/home.component';
import { HeaderComponent } from './header/header.component';

import { SharedModule } from '../shared/shared.module';

import { AppRoutingModule } from '../app-routing.module';

@NgModule({
  declarations: [
    HomeComponent,
    HeaderComponent
  ],
  imports: [
    SharedModule,
    AppRoutingModule
  ],
  exports: [
    HeaderComponent,
    AppRoutingModule
  ]
})

export class CoreModule {}

 NOTE : in the core.module we export Header component because we use the app-header selector in the app.component.html. But we don’t need to export HomeComponent because is only used in routes and in this case is visible from the whole app.

Restructuring services to use the child injector

  • We continue doing leaner the app.module.
  • This time we are moving all the services in the providers array to the core.module. These services will still be available in the whole app because the core module is loaded at the beginning si it is in the Root injector.
  • The AuthGuard service is only used in recipe module so we can define it only in that module.

core/core.module.ts

  providers: [ShoppingListService,RecipesService, DataStorageService, AuthService],

recipe/recipe.module.ts

  providers: [AuthGuard]

NOTE: Adding the AuthGuard service only to the recipe module we are improving the speed of the app in the bootstrap as Recipe module is lazy loaded.

Using Ahead-of-time compilation

  • This is NOT related to compilation from Typescript to Javascript.
  • This is about Angular needs to compile HTML templates to convert them to Js objects
  • This step can be done before the app is released (without loosing the fact of some templates are dynamic).

Captura de pantalla 2017-05-25 a las 17.58.01

  • Using AoT has advantages:
    • Faster startup since Parsing and Compilation doesn’t happen in the browser
    • Templates get checked during development: all the console errors will trigger in the build process not in the user browser.
    • Smaller file size as unused features can be stripped out and the Compiler itself isn’t shipped.

 

How to use AoT compilation with the CLI

  • The CLI command to run on terminal is:
ng build --pro --aot
  • The –pro flag is top minify
  • The –aot flag is for Ahead of Time compilation
  • Some errors can be triggered with this command you should resolve to get it completed.
  • Once the command is completed a dist folder will be generated and you can check size of the app files: main.js and vendor.js and compare it with the sizes in the chrome console on development time.

Preloading Lazy Loading routes

  • We can preload all the lazy loading modules , not at the time the app start but during user is using the app.
  • In the app-routing.module.ts, we need to specify the preloadingStrategy:
@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes, {preloadingStrategy: PreloadAllModules})
  ],
  exports: [RouterModule]
})
  • Then in debug console –> network, we can see how chunk file is loaded after the main and vendor js files

Captura de pantalla 2017-05-26 a las 10.03.15