Understanding @NgModule (http://g.co/ng/modules)

Previous work

Overview

@NgModule

declarations

imports

exports

providers

entryComponents

schemas

Examples

Module Declaration:

Dynamic bootstrap:

Offline-compile bootstrap:

Legacy bootstrap:

Lazy Loading Modules

Lifecycle

Deprecation

Why

Two Scopes

Breaks ES6 Mental Model

Nobody Will Use It

Transition Period

Seeing too much:

Accidental Component Kidnapping

Replacing platform directives with modules

Rejected Ideas

Pointer from @Component to @NgModule

Testing with @NgModules

Issues

Outstanding

NOT Blocking RC5

Resolved

Previous work

Overview

The @NgModule is needed to:

@NgModule

class NgModule {

  declarations: Array<ComponentType | DirectiveType | PipeType>;

  imports: Array<ModuleType | ModuleWithProviders>;

  exports: Array<ComponentType | DirectiveType | PipeType | ModuleType>;

  providers: Array<Providers | Array<any> >;

  entryComponents: Array<ComponentType>;

  schemas: Array<any>;

}

An NgModule provides a compilation environment for components it contains. In practice NgModule defines which set of components, directives, and pipes are available to the template of the components owned by the module.

Each component (directive or pipe) must be owned by exactly one NgModule.

A component is owned by an NgModule if it is listed in its declarations property.

declarations

List of components, directives, and pipes which belong to this module. All declarations listed here are visible in this module without any explicit imports.

imports

Imports allows you make exported declarations from another module visible within the templates of components that are part of the current module. Importing a module does not automatically re-export the imported module's exports.

exports

List of all declarations which should be available to other modules, which import this module. A module in this list will have all of its exports re-exported using current module.

providers

A list of providers to configure the injector when this module is imported. Providers are always imported / merged from other modules.

entryComponents

A list of components for which ComponentFactory should be generated. ComponentFactory is used for dynamically loading components (and bootstrapping). The components listed here do not have to be declared in the current module (but have to be declared in some module and then imported into this module.)

schemas

PRE FINAL:

If omitted will only allow DOM elements schema (throw on unrecognized DOM elements and properties). If pointing to CUSTOM_ELEMENTS then the compiler will allow any property on kebab cased elements. (useful when working with web-components).

NOTE: this feature is needed to throw useful error messages during migration period. Without it a component which will be stolen (referred to in @Component.directives without havinig appropriate module imported) by a module would silently not include directives.

POST FINAL:

Allow definition a schema for custom elements. Will allow configuration of which custom elements are allowed, what are their properties, and their events.

Examples

Module Declaration:

Assume one has a component as declared here:

import {Component} from '@angular/core';

@Component({

  template: `

  <button md-button>material buttons are allowed</button>

  <input [ngModel]="exp" value="Forms are supported">

  <div *ngIf="true">core directives are present.</div>

`

})

class MyAppRootComponent {

}

The above application will need a main module definition such as here:

import {NgModule, ApplicationRef} from '@angular/core';

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

import {FormsModule} from '@angular/forms';

import {MaterialModule} from '@angular2-material/module';

import {MyAppRootComponent} from './my-app-root-component';

@NgModule({

  declarations: [MyAppRootComponent],

  imports: [BrowserModule, CommonModule, FormsModule, MaterialModule],

  entryComponents: [MyAppRootComponent]

})

class MyApplicationModule {

  constructor(appRef: ApplicationRef) {

    appRef.bootstrap(MyAppRootComponent);

  }

}

Notice that the main module imports CommonModule, FormsModule, MaterialModule. This allows the MyAppRootComponent to implicitly use md-button, ngModel, and ngIf without any additional imports on the @Component level.

Dynamic bootstrap:

import {MyApplicationModule} from './my-application-module';

import {platformBrowserDynamic} from '@angular/browser-platform-dynamic';

platformBrowserDynamic().bootstrapModule(MyApplicationModule);

In this example the application module is bootstrap using in the browser compiler.

Offline-compile bootstrap:

import {MyApplicationModuleFactory} from './my-application-module.ngfactory';

import {platformBrowser} from '@angular/browser-platform';

platformBrowser().bootstrapModuleFactory(MyApplicationModuleFactory);

In this example the application is bootstrapped using offline compilation. The difference is that we need to import the MyApplicationModuleFactory from a offline compiled / generated file and then bootstrap it. Unlike bootstrapModule the bootstrapModuleFactory is synchronous.

Legacy bootstrap:

import {bootstrap} from '@angular/browser-platform-dynamic';

import {MyAppRootComponent} from './my-app-root-component';

import {FormsModule} from '@angular/forms';

import {MaterialModule} from '@angular2-material/module';

bootstrap(MyAppRootComponent, {imports: [FormsModule, MaterialModule]});

The above bootstrap does not depend on a module. Rather the module is created implicitly in the background. Also notice that there is no need for the CommonModule, as that one is automatically included to match the old PLATFORM_DIRECTIVES behavior.

Lazy Loading Modules

Imports have dual purpose. They provide context to the compiler and at runtime they affect how injector provisions instances. Applications import a module into its root module and optionally the module can be imported into a child / lazy loaded modules.

Lazy loading adds a particular constraint on the module. This is because a third-party module (such as ionic) has to be imported in both the root module as well as the lazy loaded module (this is needed to load the directives). However only the root module should configure the ionic services. By supporting both the IonicModule as well as the IonicModule.with syntax we can satisfy both of the constraints. (Module.with() should be added to our style guide.)

Assume a third party Module such as:

@NgModule({

  declarations: IONIC_DIRECTIVES,

  imports: [ BrowserModule ],

  exports: [ IONIC_DIRECTIVES, BrowserModule ]

})

class IonicModule {

  constructor(@Optional() @SkipSelf() parent: IonicModule,

              appRef: ApplicationRef) {

    if (!parent) {

      // make sure to only bootstrap when the module is root module.

      // Don't run this in lazy loaded component.

      appRef.bootstrap(IonicAppRoot);

    }

  }

  static forRoot(mainComponent: Component, config: any) {

    return {

      module: IonicModule,

      providers: [

        {provide: IONIC_CONFIG, useValue: config},

        {provide: IONIC_MAIN_COM, useValue: mainComponent},

        {provide: ANALYZE_FOR_PRECOMPILE, useValue: mainComponent,

           multi: true},

        OtherIonicService,

        ...

      ]

    };

  }

}

Can then be used for root module to configure both the module exports as well as the providers as:

@NgModule({

  declarations: [MyRootComponent],

  imports: [

    IonicModule.forRoot(...ionic_config...),

    FirebaseModule.forRoot(...firebase_config...)

  ]

})

class MyAppModule {}

And for lazy loading module only load the module exports (not the providers) as:

@NgModule({

  declarations: [ LazyLoadedComponent ],

  imports: [ IonicModule ]

})

class MyLazyAppModule {}

Example with router

class RouterModule {

  forRoot(routes: any) {

    // add ANALYZE_WITH_PRECOMPILE provider

    // + Router provider

  }

  forChild(routes: any) {

    // just add ANALYZE_WITH_PRECOMPILE provider

  }

}

Lifecycle

A module including its imported and exported modules are created eagerly when NgModuleFactory.create is called. For some use cases, this is not enough and so we are exploring a more in depth lifecycle hooks in @NgModule Lifecycle Hooks.

Deprecation

The ability to import directives and pipes into components will be deprecated. This means that after deprecation the following properties will be removed: @Component.directives and @Component.pipes.

Why

Keeping @Component.directives/pipes causes the following issues:

Two Scopes

It creates two scopes: module scope and component scoped. The module scoped is very similar to how ES6 modules work. As a result, it is easy to explain to the user. We have to have it for dev ergonomics. The component scope is unique and harder to explain.

Breaks ES6 Mental Model

Having the component scope breaks the ES6 mental model. In ES6 to use a token you have to import it from a module. Tokens don't just appear out of nowhere. It is easy to explain that to use a material component you need to import the right module. Because that's what you would do with ES6.

Nobody Will Use It

Modules create small-enough scope to avoid collisions, and they are significantly more ergonomic. Because using modules is more ergonomic, the Component.directives option will not be used in practice. As a new Angular user I have to use modules to get my forms and common directives, so it is natural for me to add my own directives there.

Transition Period

During transition period the behavior of @Component.directives and @Component.pipes will change slightly by making more things available to component template then before, but we don't expect the change to result in changes to the application behavior.

Note: This section will discuss @Component.directives behavior, but same rules are applied to @Component.pipes.

During the transition it is possible to have components which are not explicitly declared in the @NgModule.declarations field to mimic the old behavior. The situation can arise when a component references another component through @Component.directives. In such a case the referenced component will be included in the same module's declarations as the referencing component.

Assume this old component declaration.

@Component({})

class MyComponent {}

@Component({

  directives: [ MyComponent ] // deprecated behavior

})

class MyAppRootComponent {}

To migrate to modules the developer will have to declare main module like so:

@NgModule({

  declarations: [ MyAppRootComponent ], // Include root component only.

  module: [ CommonModule ]

})

class MyAppModule {}

The above will cause MyAppRootComponent to be part of the MyAppModule because it is referenced in the declarations property. The MyComponent is referenced from MyAppRootComponent therefore it will also be implicitly included in the MyAppModule's declarations property. We only include the directives found via @Component.directives into the declarations of the module if none of the included modules already contains it in their declarations property. The end result is equivalent to:

@Component({})

class MyComponent {}

@Component({})

class MyAppRootComponent {}

@NgModule({

  declarations: [ MyAppRootComponent, MyComponent ],

  module: [CommonModule]

})

class MyAppModule {}

The resulting implicit changes are equivalent to what the developer will have to do explicitly during a proper migration.

There are two possible failure modes:

Seeing too much:

One important thing to notice is that it is possible that by hoisting the components from the @Component.directives to @NgModule.declarations that some components will have more components / directives available to them after the change. We don't think that this will result in a real world issues. As it is unlikely that:

Accidental Component Kidnapping

Assume this code:

@Component({

  directives: [ MdButton ]

})

class MyAppRootComponent {}

@NgModule({

  declarations: [ MyAppRootComponent ],

  module: [ CommonModule ]

})

class MyAppModule {}

In the above example the MyAppRootComponent imports MdButton, but the MyAppModule forgets to import MaterialDesignModule. The result is an incorrect assumption that the MdButton is a free floating component and that it should be included in the MyAppModule's declarations property, resulting in MdButton being compiled by MyAppModule rules rather then MaterialDesignModule. To better understand see this implicit change:

@Component({})

class MyAppRootComponent {}

@NgModule({

  declarations: [ MyAppRootComponent, MdButton ],

  module: [ CommonModule ]

})

class MyAppModule {}

The problematic part is because the component is compiled using different module it is possible that many of its needed imports are missing, resulting in incorrect behavior.

The correct behavior should have been:

@Component({})

class MyAppRootComponent {}

@NgModule({

  declarations: [ MyAppRootComponent],

  imports: [ MaterialDesignModule ]

  module: [ CommonModule ]

})

class MyAppModule {}

Kidnaping behavior is easy to achieve. To mitigate this we will make custom-elements support opt in using the schema property. By default the compilation mode will only allow DOM elements or angular components and properties. In this way if a component is accidentally stolen by a module the extraneous element names or attribute names will be flagged as invalid by the schema.  If the schema passes, the component is still stolen, but will most likely behave same as all of the directives are accounted for. (The component could inject missing providers from its true module but that will be discovered at runtime.)

If the developer is using web-components they will have to explicitly opt-into it, and then they run a higher risk of component theft as describe above. We think that there are only few applications which rely on web-components, and so only few applications will run the risk of component theft.

Replacing platform directives with modules

The current platform directives (configured via providers for PLATFORM_DIRECTIVES) will be deprecated and removed earlier than @Component.directives. The replacement is to import the correct module, e.g. FormsModule or RouterModule. The CommonModule that includes ngIf, ... is automatically included in the BrowserModule which is automatically used for bootstrap.

Rejected Ideas

Pointer from @Component to @NgModule

A reasonable question would be why is there no pointer from @Component to @NgModule?

Benefits of pointer from @Component to @NgModule:

Organizational reasons against pointer from @Component to @NgModule:

Technical reasons against pointer from @Component to @NgModule:

After looking at the benefits cost, we decided to opt to keep all of the information in the @NgModule.

Testing with @NgModules

See separate document. Initial implementation is in this PR.

Simplifying Bootstrap

With the apis as proposed above even the simplest app that uses NgModules requires developers to know about ApplicationRef and its bootstrap method. This creates a barrier for entry and forces us to document many advanced concepts early in the developer's journey.

One solution to this problem was to preserve the existing "bootstrap(MyComponent, {/*implicit module def */}" method (also called bootstrap junior) that could be used for these simple cases. The downside is that this api is only useful for plunkers and small apps and can't be used for offline compilation cases, forcing the developer to learn a very different way to bootstrap their app when they get more serious.

Alternative approach to that would be to allow declarative specification of the component to bootstrap within the module metadata:

// app.component.ts

@Component({ ... })

class AppComponent {}

// app.module.ts

@NgModule({

  imports: [BrowserModule]

  declarations: [AppComponent],

  bootstrap: [AppComponent]

})

class AppModule {}

// main.ts

if (prod) enableProdMode();

platformBrowserDynamic().bootstrapModule(AppModule);

// main-offline.ts

if (prod) enableProdMode();

platformBrowser().bootstrapFactory(AppModuleNgFactory);

The "bootstrap" property of NgModuleMetadata would cause the codegen to generate module factory that would be comparable to this manual bootstrap:

// app.component.ts

@NgModule({

  imports: [BrowserModule]

  declarations: [AppComponent],

})

class AppModule {

  ngDoBootstrap(applicationRef: ApplicationRef) {

    applicationRef.bootstrap(AppComponent);

  }

}

With this change we would make the bootstrap simple enough that we could drop the "bootstrap junior" api and have a single way to bootstrap the app for simple and advanced usecases.

Properties:

Issues

Outstanding

HOT STATUS:

ENDORSED:

---------------

THE LIST OF EVERYTHING:

NOT Blocking RC5

Resolved

class MyComponent {

  static final SYMBOL = new OpaqueSymbol(...);

}

@NgModule({

  providers: [{provide: MyComponent.SYMBOL, useValue: 123}]

})

class MyModule{}