Component refactoring
Angular Best Practices SPA Typescript Visual Code

How to Modularize Angular Application Components

Welcome to today’s post.

Today I will show how to convert an Angular application to make use of a feature that allows us to move related components into respective feature component modules.

When we start developing applications with Angular, the most obvious way we structure our application components is to create them within the applications root folder. Once the number of components within our application gets to a large size and the application loading starts getting slower, we will need to move many of the related components into modules.

One of the main benefits of modularizing our application is to improve the performance of our application when we are loading the components from the application’s root module.

One key feature available in Angular is the lazy-loading of feature modules.

By default, all modules in Angular are eagerly loaded, which is when the application starts loading.

When the number of routes in our application increases to a large size, this is when we can make use the lazy-loading design pattern, which allows us to load NgModules when only as needed. 

The benefit of this controlled loading is the bundle sizes will be smaller and reduce loading times. Suppose we have an application that has a number of logically-related components that are housed under the app root folder:

We would like to move these components into their own feature module and only load them when necessary.

Presently, our app routing modules routes all the account_* components.

Our app module app-routing.module.ts would look something like this:

const routes: Routes = [
    { path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
    { path: '', redirectTo: 'home', pathMatch: 'prefix'},
    { path: 'login', component: LoginComponent },    
    { path: 'about', component: AboutComponent },
    { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] },
	  . . . 
    { path: 'account-users', component: AccountUsersComponent, 
        canActivate: [AuthGuard] },
    { path: 'account-user-new', component: AccountUserNewComponent, 
        canActivate: [AuthGuard] },
    { path: 'account-user-edit/:id', component: AccountUserEditComponent, 
        canActivate: [AuthGuard] },
    { path: 'account-user-view/:id', component: AccountUserViewComponent, 
        canActivate: [AuthGuard] }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Our app module currently imports all account_* components as shown:

@NgModule({
    declarations: [
      AppComponent,
      MenuComponent,
      AboutComponent,
      LoginComponent,
      DashboardComponent,
      HomeComponent,
  	    . . .
      AccountUsersComponent,
      AccountUserNewComponent,
      AccountUserEditComponent,
      AccountUserViewComponent,
    	. . .
    ],
    imports: [
	  . . .
    ],
    providers: [
	  . . .
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

Next, we will create a module to contain our feature.

It is quite straightforward to generate the scaffold for the feature module using Angular CLI.

Creating a feature module

To create a feature module to contain our accounts components we run the NG command as follows:

ng generate module accounts --route accounts --module app.module

This will generate an accounts folder with our lazy-loadable feature module accounts AccountsModule defined in accounts.module.ts and routing module AccountsRoutingModule in accounts-routing.module.ts.

With lazy-loading, the account* components will no longer be loaded within the root module app.module.ts. They will instead be loaded within the routes array within app.module.ts.

The output from the feature module generation is shown below:

>ng generate module accounts --route accounts --module app.module     

CREATE src/app/accounts/accounts-routing.module.ts (352 bytes)
CREATE src/app/accounts/accounts.module.ts (363 bytes)
CREATE src/app/accounts/accounts.component.html (23 bytes)
CREATE src/app/accounts/accounts.component.spec.ts (642 bytes)
CREATE src/app/accounts/accounts.component.ts (277 bytes)
CREATE src/app/accounts/accounts.component.css (0 bytes)
UPDATE src/app/app-routing.module.ts (3047 bytes)

Our new accounts routing module imports a singe route to the default accounts component.

The updated routing module accounts-routing.module.ts is below:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { AccountsComponent } from './accounts.component';

const routes: Routes = [{ path: '', component: AccountsComponent }];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class AccountsRoutingModule { }

Our feature module file is then amended to include declarations from the account_* components that will be exported out to our main module when loaded. We also remove the default AccountsComponent from the imports and declarations.

The accounts module accounts.module.ts with our imported components is below:

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

import { AccountsRoutingModule } from './accounts-routing.module';
import { AccountUserEditComponent } from 
    './account-user-edit/account-user-edit.component';
import { AccountUserNewComponent } from 
  	 './account-user-new/account-user-new.component';
import { AccountUsersComponent } from 
  	 './account-users/account-users.component';
import { AccountUserViewComponent } from 
  	 './account-user-view/account-user-view.component';

@NgModule({
  declarations: [
    AccountUserEditComponent,
    AccountUserNewComponent,
    AccountUsersComponent,
    AccountUserViewComponent
  ],
  imports: [
    CommonModule,
    AccountsRoutingModule
  ]
})
export class AccountsModule { }

Our app routing module app-routing.module.ts is amended as shown:

const routes: Routes = [
    { path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
    { path: '', redirectTo: 'home', pathMatch: 'prefix'},
    { path: 'login', component: LoginComponent },    
    { path: 'about', component: AboutComponent },
    { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] },
	  . . . 
    { path: 'account-users', component: AccountUsersComponent, 
      canActivate: [AuthGuard] },
    { path: 'account-user-new', component: AccountUserNewComponent, 
      canActivate: [AuthGuard] },
    { path: 'account-user-edit/:id', component: AccountUserEditComponent, 
      canActivate: [AuthGuard] },
    { path: 'account-user-view/:id', component: AccountUserViewComponent, 
      canActivate: [AuthGuard] }
    { path: 'accounts', loadChildren: () => 
      import('./accounts/accounts.module').then(m => m.AccountsModule) }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }

The feature components are still within the app root folder. We next move these component folders into the account feature module sub-folder.

After moving the account* component folders into the account feature module folder, our folder structure looks as shown:

Our next task is to move references to the components from the app module into the accounts feature module.

First, we move the account_* component imports and route paths from app-routing.module.ts into accounts-routing.module.ts.

After removing the default component that was generated, our accounts routing module definition file should look as shown:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard } from '../security/auth.guard';

import { AccountUsersComponent } from './account-users/account-users.component';
import { AccountUserNewComponent } from './account-user-new/account-user-new.component';
import { AccountUserViewComponent } from './account-user-view/account-user-view.component';
import { AccountUserEditComponent } from './account-user-edit/account-user-edit.component';

const routes: Routes = 
[
    { path: '', component: AccountUsersComponent, canActivate: [AuthGuard] },
    { path: 'account-users', component: AccountUsersComponent, 
      canActivate: [AuthGuard] },
    { path: 'account-user-new', component: AccountUserNewComponent, 
      canActivate: [AuthGuard] },
    { path: 'account-user-edit/:id', component: AccountUserEditComponent, 
      canActivate: [AuthGuard] },
    { path: 'account-user-view/:id', component: AccountUserViewComponent, 
      canActivate: [AuthGuard] }
];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class AccountsRoutingModule { }

The app module is where we next make changes by removing imports of our account_* components.

Our app module will look as shown with the declarations and imports of our account_* references removed and the new AccountsModule included as an import.

@NgModule({
    declarations: [
      AppComponent,
      MenuComponent,
      AboutComponent,
      LoginComponent,
      DashboardComponent,
      HomeComponent,
        . . .
	    . . .
    ],
    imports: [
	    . . .
    ],
    providers: [
	  . . .
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

If we have used any other modules that our component HTML templates are dependent on, then these will also need to be imported into the feature module and any other modules that depend on those modules.

The next change we make is to re-link the routes from our application menus to our newly created feature module.

// route to admin users form.
adminusers()
{
    this.router.navigate(['account-users']);
}

Changed to:

// route to admin users form.
adminusers()
{
    this.router.navigate(['account']);
}

Once we have finished re-building our app you will notice the module has been separated into its own chunk:

After running the app and you open pages that are part of the new feature, if you experience the following error:

ERROR Error: Uncaught (in promise): Error: Template parse errors: ‘mat-card-title’ is not a known element:

Then import the libraries that are required by your component HTML templates.

If you get errors like the one below:

ERROR Error: Uncaught (in promise): Error: Type [component name] is part of the declarations of 2 modules: AppModule and AccountsModule! Please consider moving [component name] to a higher module that imports AppModule and AccountsModule. You can also create a new NgModule that exports and includes [component name] then import that NgModule in AppModule and AccountsModule.

In this case, move [component name] into its own module as we did above and export it, then import the module into the dependency modules.

For example, we import the following modules:

import { ComponentsModule } from './components/components.module';
import { ValidatorsModule } from './validators/validators.module';

into our app module and our feature module as they are used by both.

Then in our modules we import the dependent modules:

@NgModule({
    declarations: [
	   . . . 
    ],
    imports: [
	  ...
      ComponentsModule,
      ValidatorsModule
    ],

The final task is to update import references within unit test source *spec.ts files as these will break if we are also running unit tests or end to end tests. Re-running the tests (using ng test) will also be part of our verification following the remediation tasks after all import references have been run.

Once we have imported our dependent modules, we can use directives within those modules in our HTML templates without errors.

After the creation of a few feature and container modules the pattern will be second nature and will be a good practice to adopt.

That’s all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial