Angular: extract a module with existing components and important lesson about NgModule
I often see Angular applications with bad architecture, especially no separation on modules. As a result, all components of an application are put in a single app/components
folder which makes it hard to separate concerns. Which components are grouped together? Which components require a service? Which component is merely a part of another component? Are there any dumb components?
In this article, I’ll show how easy it is to refactor an Angular application and extract a new module with existing components. I’ll describe a simple case, with no global services.
Table of contents
Initial state
I had a simple Angular 8 application with:
- all components, including
HomeComponent
andAppComponent
, inapp/components
AppComponent
is just the router:<router-outlet></router-outlet>
HomeComponent
is the entry component for the router and displays all other components- there is only one module:
AppModule
which imports the Angular Material and all my components
Move files
I started with creating a subdirectory app/course-editor
and moving all the components except for AppComponent
to app/course-editor/components
. I also moved the corresponding model classes from app/model
to app/course-editor/model
. Thus I had all related files in a separate folder.
Note: I'll soon be sharing short, practical tips on Angular — a good way to pick up something new if you're interested.
VS Code automatically updated all (almost all) references while moving, so it was quick. Git commit.
Create Angular module
Now that files are in a subfolder, I encapsulated them in a new Angular module.
I created CourseEditorModule
in app/course-editor/course-editor.module.ts
with:
- all components from
app/course-editor/components
in thedeclarations
section HomeComponent
in thedeclarations
andexports
section
Information
A short reminder about what are the Angular module sections for:
declarations
– list all components that will be used by components using this module, including the ones listed in this listimports
– list all modules that are included altogether, e.g. Material libraries or other modules from my applicationexports
– list all components that will be available to other modules thatimport
this moduleproviders
– list all services defined in this module, though@Injectable({ providedIn: foo })
is the preferred waybootstrap
– list the component that is the root for the application. There will be only onebootstrap
section with one component in a typical applications, sometimes there can be more modules with other bootstraps, e.g. in case of debug-time client-side rendering and production server-side rendering
And here the complex part begins. The application is not going to compile soon.
I removed all the components that I moved to the new module, from AppModule
.
Create new main component in new module
It was not necessary in this case, as there was already a main component – HomeComponent
. Otherwise, I’d just create a new component by extracting the relevant parts from HomeComponent
, add it in app/course-editor/components
and declare and export it in CourseEditorModule
.
Extract a Shared Module
Usually, there are parts that are used in both AppModule
and CourseEditorModule
. In my case it was the Angular Material library modules, like BrowserModule
, MatButtonModule
, and so on. Sometimes it is tricky to refactor the existing modules such that you don’t introduce a circular dependency, e.g. AppModule
imports CourseEditorModule
because it needs to access the HomeComponent
, but CourseEditorModule
needs to import AppModule
, because it provides MatButtonModule
. Even worse things happen to services, which may stop being singletons…
I extracted all Angular Material modules and all my “utilities” used (possibly) everywhere in the application from AppModule
to SharedModule
in app/shared.module.ts
.
Advice
Avoid naming a module CommonModule
. There is already one in Angular, frequently used. More about it in the last chapter.
Note that previously in AppModule
I had (after removing all course-editor components):
I imported e.g. MatButtonModule
and it (with its Material Button, i.a. mat-button
directive) was available to all the components declared here. So AppComponent
could use the Material Button.
When I extract the common libraries to a new module, it is not enough to import them. Actually, it is not necessary if there are no new declared components. Instead, I have to export them and make them available to components declared in the modules that import SharedModule
.
However, if there were new components declared in SharedModule
, I would also have to import the modules that are accessed by these components.
See the example of my case:
Differentiate between two kinds of imports, though. import { MatButtonModule } from '@angular/material/button'
is a language syntax to load MatButtonModule
. However, @NgModule({ imports: [ MatButtonModule ] })
is a framework construct to make MatButtonModule
available to all components listed in the declarations
section. Since there was no component using anything from Angular Material, I could skip the imports
section.
After refactoring, AppModule
looks now as follows:
Finally, CourseEditorModule
also imports SharedModule
in its imports
section.
Note that I avoided importing CourseEditorModule
. It is automatically instantiated by the routing mechanism:
Well done. The application works again (just remember to relaunch it).
CommonModule
All Angular applications require CommonModule, which provides directives like ngIf
or ngFor
. If you get the error:
ERROR in Can't bind to 'ngIf' since it isn't a known property of 'div'. ("<div id="error" [ERROR ->]*ngIf="modelService.error.getValue()">
<mat-icon>error</mat-icon>
Error when {{ modelService.erro")
Property binding ngIf not used by any directive on an embedded template. Make sure that the property name is spelled correctly and all directives are listed in the "@NgModule.declarations". ("[ERROR ->]<div id="error" *ngIf="modelService.error.getValue()">
<mat-icon>error</mat-icon>
Error when {{ m")
Can't bind to 'ngIf' since it isn't a known property of 'app-category-editor'.
1. If 'app-category-editor' is an Angular component and it has 'ngIf' input, then verify that it is part of this module.
2. If 'app-category-editor' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.
3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. ("
<app-category-editor fxFlex="1 0 1"
[ERROR ->]*ngIf="!!modelService.getSelectedTopic()"
></app-category-editor>
")
Property binding ngIf not used by any directive on an embedded template. Make sure that the property name is spelled correctly and all directives are listed in the "@NgModule.declarations". ("
></app-categories-editor>
[ERROR ->]<app-category-editor fxFlex="1 0 1"
*ngIf="!!modelService.getSelectedTopic()"
></app-category-ed")
or
ERROR in Can't bind to 'ngForOf' since it isn't a known property of 'button'. ("
<mat-action-list cdkDropList (cdkDropListDropped)="drop($event)">
<button mat-list-item
[ERROR ->]*ngFor="let topic of topics"
(click)="selected(topic)"
then it means that app-category-editor
component is declared in a module that does not import CommonModule
.
Note that CommonModule
is re-exported by BrowserModule and BrowserAnimationsModule. So importing any of these three will be enough.