Angular 6 App Structure With Multiple Modules

Since I couldn't find any resources on Angular 5 app structure with multiple modules, I decided that whilst rebuilding an AngularJS app, I would implement a multiple-module architecture and document it. Later, I updated the architecture to be compliant with the Angular 6 major release. Below is the approach I took, with some justifications for the decisions I took. In the near future I plan to write more posts which get in to the more granular details of each area of the application and the motivations behind each decision.

An Angular 6 multi-module starter template is available on GitHub here.

Overview

From a high-level perspective, this is what the folder structure looks like:

app/
├── app.component.html
├── app.component.scss
├── app.component.spec.ts
├── app.component.ts
├── app.module.spec.ts
├── app.module.ts
├── app-routing.module.ts
├── core/
│   ├── core-routing.module.ts
│   ├── core.module.spec.ts
│   ├── core.module.ts
│   ├── login/
│   ├── services/
│   └── header/
├── admin/
│   ├── admin-routing.module.ts
│   ├── admin.module.spec.ts
│   ├── admin.module.ts
│   ├── admin.component.module.ts
│   ├── admin.component.spec.ts
│   ├── admin.component.html
│   ├── admin.component.scss
│   ├── manage-users/
│   └── services/
├── form/
│   ├── form-routing.module.ts
│   ├── form.module.spec.ts
│   ├── form.module.ts
│   ├── form.component.module.ts
│   ├── form.component.spec.ts
│   ├── form.component.html
│   ├── form.component.scss
│   ├── summary/
│   └── services/
└── shared/
    ├── shared.module.spec.ts
    ├── shared.module.ts
    ├── components/
    ├── models/
    ├── components/
    ├── directives/
    └── services/

The AppModule which occupies the root of the folder is purposely kept as bare as possible. It's role is simply to bootstrap the Angular application, and provide the root-level router-outlet. This approach also leaves open the possibility of running multiple, independent Angular applications through the same base URL. It also introduces the idea of building a router-driven Angular application.

Everything runs through the CoreModule which is placed in its own directory within the app directory. Coming from AngularJS, it feels weird not to create a sibling directory for the CoreModule, but this is done to keep in line with the Angular CLI conventions. Each sub-directory within the app folder shown above should be a module.

I did experiment with following AngularJS conventions and put modules outside of the app directory, and this works fine. It kept the root app directory clean but raised concerns of automatic updates through the Angular CLI in the future. I found that Angular documentation, and the Angular CLI by default, created new modules within the app directory - so this is the convention I followed.

The declaration for CoreModule in app/core/core.module.ts must import all other sub-modules of the application excluding the SharedModule (unless it is actually needed - explained later). The purpose of CoreModule is to hold the root components, services and features of the application such as a universal login screen, global navbar/header, global footer, authentication and authentication guards. Where lazy-loaded is needed, the other modules can easily be lazy-loaded in using the following code in the core-routing.module.ts file:

{
    path: 'admin',
    canActivate: [AuthGuardService],
    loadChildren: '../admin/admin.module#AdminModule'
},
{
    path: 'form',
    loadChildren: '../form/form.module#FormModule'
},
{
    path: 'login',
    component: LoginComponent
},
{
    path: '**',
    component: NotFoundComponent
}

To help simplify the concept, CoreModule in this approach takes on the role of the root AppModule but is not the module which gets bootstrapped by Angular at run-time.

The CoreModule can also manage other core features for your app such as the Error 404 page through the NotFoundComponent shown above.

The above routing definitions illustrate that the CoreModule handles the routing of the application. In theory we should be able to import a new Core2Module in to AppModule which may represent a version two of the application, and the implementation of this app would have no impact on the app running via CoreModule.

The FormModule is publicly accessible, and the AdminModule is protected by an Authentication Guard as illustrated in the routes shown above. The beauty of this model is that you can easily take out the AdminModule or FormModule without breaking any other part of your app.

For example, if you were to remove the AdminModule you only need to delete the app/admin directory and remove the admin route declared above. Then, any users navigating to the Admin section of the site will be shown the Not Found page via the NotFoundComponent.

Since the application is driven by the Router, each module has a root component which contains the router-outlet. This is key to enabling decoupled modules (essential for clean Unit Testing), and navigation to be powered by Components and the Router rather than ngIfs.

Routing

Since the navigation and the rendering of components is heavily driven by the Router in this model. The root AppComponent contains no routes. When the application bootstraps, the CoreRoutingModule (declared in app/core/core-routing.module.ts) kicks in and loads the Core components.

If the user navigates to /form the CoreModule lazy-loads the FormModule module along with its components and uses the routes declared in FormRoutingModule to navigate and display content within the /form URL. All this works by exporting the FormRoutingModule from the FormModule declaration, and then importing FormModule in to CoreModule.

If the user navigates to /admin (which is a protected area of the application), the AuthGuardService from the CoreModule checks the conditions of canActivate and only lazy-loads the AdminModule if the user is authenticated. Similar to the FormModule, the AdminModule has its own routing configuration declared in AdminRoutingModule and this controls the content displayed within the /admin URL path.

Each submodule declares its own routes like so (AdminRoutingModule taken as an example):

const routes: Routes = [
    {
        path: '',
        component: AdminComponent,
        children: [
            { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
            { path: 'dashboard', component: AdminDashboardComponent },
            { path: 'manage-users', component: ManageUsersComponent }
        ]
    }
];
@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class AdminRoutingModule { }

The root path of the AdminRoutingModule is declared blank as this will match to the /admin route declared in CoreRoutingModule. It loads the AdminComponent which is simply a html template file containing a router-outlet. This file can also contain an Admin Navbar and other static content if you wish.

The important bit here is that the RouterModule from the Angular library is imported as forChild for all modules except CoreModule. The CoreModule imports it's routes as forRoot.

You then also want to export the RouterModule after initialising it with your routes so that when the AdminRoutingModule is imported in to the AdminModule, and the AdminModule is imported in to the CoreModule, the CoreModule will have access to the routes declared in the sub-modules and they can compliment the routes declared for the Core of your application.

Sharing components, services, directives and more

If you've paid attention so far, you'll already know the answer to this. The SharedModule is where any shared components, pipes/filters and services will go. The SharedModule can be imported in to any other module that requires its components, pipes and/or services. Just be sure to export anything you want to share with other modules. A reminder that services do not need to be exported - they just need to be declared under providers in the module declaration.

The SharedModule doesn't have a root component or any routing declarations because it only contains components that other modules will import to use. There are no views or logic in the SharedModule.

Be aware, though… Non of the services in the SharedModule will be persistent. So do not use it to store data that needs to be access across various modules, but each instance of the service imported from the SharedModule will be different! How do we solve this? Keep reading…

Persistent services and app-wide singletons

If you are using a service to store data while your Angular app is running you'll need to use a service which is instantiated and not discarded throughout the duration of the application runtime.

A service in the AdminModule is only available until the user navigates out of the /admin path. Any other module (say FormModule that imports that service, will have a fresh version of that service as if it was instantiated with new MyService().

We get around this by putting services that need to be persistent in the CoreModule because our entire app runs through the CoreModule. From the minute the app is launched, to when the page is closed CoreModule is being used to run the application.

That means any services declared within app/core/services and then added to the list of providers in app/core/core.module.ts will be accessible to all other modules and contain persistent data.

Universal Navbar/Header and Footer

One of the biggest challenges with this model was trying to figure out how to handle displaying a dynamic header/navbar depending on which component/page was active, and changing the navbar links displayed to the user based on whether they were authenticated (logged-in) or not.

The solution is actually very obvious… Once its pointed out!

We start by creating a component called Header in the CoreModule (app/core/header). This module is then declared and exported from CoreModule as seen below:

@NgModule({
    imports: [
        ...
    ],
    declarations: [HeaderComponent],
    exports: [
        ...
        HeaderComponent
    ],
    providers: [
        ...
    ]
})
export class CoreModule { }

Then, when CoreModule is imported in to the root AppModule, the HeaderComponent is available as a directive because of the export above.

This means that in app/app.component.html we now have a router-outlet and app-header element.

If you reload your app you will see that the header now appears at the top of every page of your application. Next, you need to make the content dynamic…

The contents of the app/core/header folder can be as follows:

header/
├── header.component.html
├── header.component.scss
├── header.component.spec.ts
├── header.component.ts
└── navigation-links.ts

Inside the navigation-links.ts file we can export a static Array of objects that represent all navigation menu items. This can easily be replaced by a service which retrieves objects for all the pages - but since I didn't have one available, and the navigation links for this project weren't too complicated I decided to hard-code them.

Here is a simplified version of the contents of navigation-links.ts:

export const navigationLinks = [
    {
        name: 'Dashboard',
        routerLink: '/dashboard',
        roles: ['Admin', 'RegUser'],
        order: 0,
        overrideFunction: function() { console.log("override function clicked"); }
    }
]

Then, within the header.component.ts we can use the power of the Angular Router to subscribe to URL changes to determine where in the application the user is and filter which navigation links are displayed.

Alternatively, if you are storing user token data somewhere, you can query this to determine the user's role and show only navigation links that match with the user's role.

You'll need to store the active navigation links as a class property, and within your navbar template you'll be able to loop through them all to display them. To keep things clean in the HTML, I would suggest you create a class variable called activeLinks: Array and set its values from the navigationLinks constant above every time the URL changes. You can subscribe to URL changes within the ngOnInit() function using the following code:

this.router.events
.subscribe(
    event => {
        if (event instanceof NavigationEnd) {
            this.setUserRoleFromUrl(event.urlAfterRedirects);
            this.setNavLinksFromUserRole(this.userRole);
        }
    }
);

The same concept can be applied for a footer.

Loading spinner

One of my biggest gripes with Angular is the lack of a native loading-bar/spinner. I opted for ng-http-loader by Michel Palourdio, imported it into CoreModule and placed the loader alongside the router-outlet in app/app.component.html. The plugin shows a loading spinner any time a HTTP call is made within the application. It takes away localised spinners which result in cleaner code lower-down, and by using interceptors it uses one global spinner component to handle loading.