vxh.viet
7/19/2018 - 7:52 AM

Angular Stucture and Best Practices

Angular Project Structure and Best Practices

SOURCE, SOURCE, SOURCE

Feature Modules

In Angular, every module which is not the AppModule is technically a Feature Module, and it has the following caveats:

  • It must declare all the components, directives and pipe it needs.
  • It must import CommonModule instead of BrowserModule: While BrowserModule must be imported in AppModule (it’s required in order to run the app in the browser), this module must not be imported elsewhere: instead, we must import CommonModule, which contains Angular’s common directives, such as ngIf, ngFor, ngClass, etc… BrowserModule also re-exports CommonModule, so that you can use this directives in AppModule too.
  • It doesn’t bootstrap anything: The only module responsible for bootstrapping a component is, obviously, AppModule
  • We use Feature Modules to define one unique feature of our app which could be a collection of screens. In general, each module should have its own routing. By organizing this way , there’s no need for the parent module to import your child module’s components to put them in the routing configuration. For example, from localhost/contacts/ on, the ContactsModule will be responsible for its routes. In addition to that, by having its own routing, modules can be lazily loaded (Gist, Cacher)
  • Advance: see the SOURCE to see how we can use the loadChildren keyword without actually Lazy Loading the module and define a preloading strategy for lazily loaded modules:

Example of a feature module:

import { ProductComponent } from "./product.component";
import { BestProductComponent } from "./best-product/best-product.component";
import { ProductListComponent } from "./product-list/product-list.component";
import { ProductDetailComponent } from "./product-detail/product-detail.component";
import { SharedModule } from "@shared/shared.module";

const ProductRoutes: Routes = [
  {
    path: "",
    component: ProductListComponent,
    pathMatch: "full"
  },
  {
    path: "all-products",
    component: ProductListComponent
  },
  {
    path: "product/:id",
    component: ProductDetailComponent
  }
];


@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild(ProductRoutes),
    SharedModule,
  ],
  declarations: [
    ProductComponent,
    BestProductComponent,
    ProductListComponent,
    ProductDetailComponent,
  ],
  exports: [BestProductComponent]
})
export class ProductModule {}

Summary:

  • Feature modules should only import services from CoreModule. If feature module A needs to import service from feature module B consider moving that service into core.
  • Rule of thumb is to try to create features which don’t depend on any other features just on services provided by CoreModule and components provided by SharedModule.
  • We should lazy load our feature modules whenever possible. Theoretically only one feature module should be loaded synchronously during the app startup to show initial content. Every other feature module should be loaded lazily after user triggered navigation.
  • Extra: Ideally feature module will only need have access to services from the CoreModule and components from SharedModule. Sometimes this might not be enough to solve particular business case and we will also need some kind of shared feature module which is providing functionality for a limited subset of other feature modules. Like this:

Core Module

The answer to the question “Where should I put all my global services?” would be: AppModule. This is because services are (in general) app-scoped, which means that they can be accessed from every module.

As briefly described in the Lazy Loading article (Gist, Cacher), every lazy module has its own injector! What that means is that a service provided in a lazy module is only accessible in that module. But it can still access the services previously provided by non-lazy modules (such as AppModule)!

So technically, global singleton services such as AuthService or UserService in AppModule, since they’ll be available to everyone. However, we really don’t want our AppModule to be a complete mess… What Angular recommends is to put all of our global services in a separated module, called CoreModule, and import it ONLY in AppModule. This way is the same as providing the services in AppModule directly!

In order to prevent inexperienced developers from re importing the CoreModule we use this little trick: inside our CoreModule, we can… inject CoreModule! If Angular injects it correctly, it means that a CoreModule has been already created, and we can throw an error:

@NgModule({
  providers: [
    // Your services
  ]
})
export class CoreModule {
constructor(@Optional() @SkipSelf() core: CoreModule) {
    if (core) {
      throw new Error('CoreModule should only be imported once in App Module');
    }
  }
}

Summary:

  • Consider making CoreModule a pure services module with no declarations (no components, pipes, etc)
  • Should be imported only ONCE in AppModule.

Shared Modules

A shared module is the perfect place to declare components, pipes, directives in order to make them reusable: this way, you won’t re-import the same components in every module, you’ll just import the shared module.

Example shared module:

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HttpClientModule,
    RouterModule,
  ],
  declarations: [
    NoProductsFoundComponent,
    PriceFormatPipe,
    ProductCellComponent,
    ProductCarouselComponent
  ],
  exports: [
    NoProductsFoundComponent,
    PriceFormatPipe,
    ProductCellComponent,
    ProductCarouselComponent
  ],
  providers: [
    // services should not be declared here
  ]
})
export class SharedModule {
  // this should be use in AppModule only.
  // for lazy loaded modules, we should import SharedModule normally.
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: SharedModule,
      providers: [
        // If you have a very good reason to have services in shared module,
        // use this to declare your services. Otherwise, consider moving the service
        // to Core Module.
        // For more explanation, See: https://stackoverflow.com/a/46622924/1602807
      ]
    };
  }
}

Still remember that:

  • Lazy modules have their own injector which means if they import a module which provides some services, they’ll create their own instances of those services.
  • Services and components have different scopes.

So how can we import only the component, directive, pipe part of shared module into lazy modules and import the service part to AppModule?

Angular gives us a special interface we can use to attach services to modules, it’s called ModuleWithProviders, here it is:

export interface ModuleWithProviders {
    ngModule: Type<any>;
    providers?: Provider[];
}

So we don’t provide our services in the SharedModule metadata; instead, we define a static method inside the module, which returns the SharedModule istelf AND the array of providers just like in the example!

Now in the AppModule you can import SharedModule.forRoot(), while in all the other modules you can import SharedModule (this is exactly how RouterModule works).

This trick is for the case you absolutely need to have a mixed shared module. To make matters simpler, just try to keep Shared Module free from services.

Summary:

  • All the dumb components, directives and pipes should be implemented here.
  • These components don’t import and inject services from core or other feature modules in their constructors.
  • They should receive all data though attributes in the template of the component using them (using @Input() and @Output() decorator).
  • This all sums up to the fact that SharedModule doesn’t have any dependency to the rest of our application.
  • It is also the perfect place to import and re-export Angular Material components.
  • You should import this SharedModule into the specific Feature Modules as needed. You DO NOT import the SharedModule into your main AppModule or CoreModule.

Note for App wide components:

For app wide components such as Header and Footer, we can consider putting them in AppModule. If we want to clean up AppModule even more, we can put those in CoreModule.

Only app wide components should be put in AppModule or CoreModule.

For the first page, Index or Home, we should put it in AppModule.