Split module into single component angular modules (SCAMs)

Reading Time: 6 minutes

Loading

Introduction

When Angular application grows, developers start to build new components and add them into modules. When we put every component into a single module, not only the module becomes bloated but also cannot identify related resources (components, pipes, directives, etc) that can bundle together. In order to avoid bloated module in an Angular application, we can split module into single component angular modules by grouping related resources together.

Moreover, SCAM with reusable components can easily import into other modules for reuse that is a benefit. After we move the resources to the SCAM that they belong to, the original large module can remove unneeded imports and keep the essential Angular dependencies and child modules only.

In this post, we learn the process of splitting a module into single angular component modules (SCAM)S and import them into parent module that uses the simple components to build complex ones.

let's go

Single feature module before Single Component Angular Modules (SCAMs)

The initial architecture of Ng Spanish Menu places all components in the feature module, FoodModule. The disadvantage is I cannot establish the parent-child relationships between the components to draw the hierarchical component tree. The good design is to break FoodModule into several smaller feature modules with tightly-coupled components encapsulated into the same one. Moreover, feature module grants the ability to export shareable component(s) and keep the rest as internal.

import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'

import { FoodCardComponent } from './food-card'
import { FoodChoiceComponent } from './food-choice'
import { FoodChoiceFormComponent } from './food-choice-form'
import { FoodMenuComponent } from './food-menu'
import { FoodMenuCardComponent } from './food-menu-card'
import { FoodMenuOptionComponent } from './food-menu-option'
import { FoodQuestionComponent } from './food-question'
import { FoodRoutingModule } from './food-routing.module'
import { FoodShellComponent } from './food-shell'
import { FoodTotalComponent } from './food-total'

@NgModule({
  declarations: [
    FoodCardComponent,
    FoodQuestionComponent,
    FoodChoiceComponent,
    FoodMenuComponent,
    FoodMenuCardComponent,
    FoodTotalComponent,
    FoodChoiceFormComponent,
    FoodShellComponent,
    FoodMenuOptionComponent,
  ],
  imports: [CommonModule, ReactiveFormsModule, FoodRoutingModule],
})
export class FoodModule {}

After reviewing the code, the following digram displays the hierarchical component tree.

Start the process of Single Component Angular Modules

I can replace FoodModule into four modules:

  • FoodTotalModule containing FoodTotalComponent
  • FoodCardModule containing FoodCardComponent
  • FoodChoiceModule containing FoodChoiceComponent and FoodChoiceFormComponent
  • FoodMenuModule containing FoodMenuComponent, FoodMenuOptions, FoodMenuCardComponent, FoodQuestionComponent and FoodChoiceModule

Create single component angular modules for single component

First, I create the standalone modules for FoodTotalComponent and FoodCardComponent respectively. It is because they are simple and independent from the tree of FoodMenuComponent.

import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'

import { FoodTotalComponent } from './food-total.component'

@NgModule({
  declarations: [FoodTotalComponent],
  imports: [CommonModule, ReactiveFormsModule],
  exports: [FoodTotalComponent],
})
export class FoodTotalModule {}

FoodTotalComponent has a reactive form with a dropdown; therefore, I import both FormModule and ReactiveFormsModule into FoodTotalModule. Then, export FoodTotalComponent such that FoodShellComponent has access to it.

src/app/food/food-total
├── food-total.component.spec.ts
├── food-total.component.ts
├── food-total.module.ts
├── food-total.stories.ts
└── index.ts

Next, repeat the same process to make FoodCardModule

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

import { FoodCardComponent } from './food-card.component'

@NgModule({
  declarations: [FoodCardComponent],
  imports: [CommonModule],
  exports: [FoodCardComponent],
})
export class FoodCardModule {}

FoodCardComponent is a simple presentational component; it does not require other dependencies.

src/app/food/food-card
├── food-card.component.spec.ts
├── food-card.component.ts
├── food-card.module.ts
├── food-card.stories.ts
└── index.ts

Create single component angular module for multiple components

Despite the word “single“, there is no restriction on the number of components in SCAM and the size depends on the context. In some scenarios, authors build component C from A and B but they don’t want A and B to be available elsewhere. Therefore, the SCAM should encapsulate A, B and C and export C only. When other modules import this SCAM, they cannot use A and B directly. Otherwise, Angular compiler outputs error messages.

Based on this definition, FoodChoiceModule is consisted of FoodChoiceComponent and FoodChoiceFormComponent. Furthermore, FoodChoiceFormComponent is the child and is used internally in the module.

import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'

import { FoodChoiceFormComponent } from '../food-choice-form'
import { FoodChoiceComponent } from './food-choice.component'

@NgModule({
  declarations: [FoodChoiceComponent, FoodChoiceFormComponent],
  imports: [CommonModule, ReactiveFormsModule],
  exports: [FoodChoiceComponent],
})
export class FoodChoiceModule {}

We declare both components in the declarations array but we only find FoodChoiceComponent in the exports array.

Folder structure of FoodChoiceFormComponent.

src/app/food/food-choice-form
├── food-choice-form.component.html
├── food-choice-form.component.spec.ts
├── food-choice-form.component.ts
├── food-choice-form.stories.ts
├── food-choice-form.type-guard.ts
├── index.ts
└── interfaces
    ├── form-value-quantity.interface.ts
    └── index.ts

Folder structure of FoodChoiceModule

src/app/food/food-choice
├── food-choice.component.html
├── food-choice.component.scss
├── food-choice.component.spec.ts
├── food-choice.component.ts
├── food-choice.module.ts
├── food-choice.stories.ts
├── food-choice.type-guard.ts
├── index.ts
└── interfaces
    ├── index.ts
    └── simple-change.interface.ts

We continue to create FoodMenuModule that depends on FoodChoiceModule.

we can fix all accessibility violations

Let’s remind the readers the module architecture of FoodMenuModule. The building blocks of FoodMenuComponent are FoodMenuCardComponent, FoodMenuOptionsComponent, FoodQuestionComponent and FoodChoiceComponent. The last component is exported from FoodChoiceModule.

import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'

import { FoodChoiceModule } from '../food-choice'
import { FoodMenuCardComponent } from '../food-menu-card'
import { FoodMenuOptionsComponent } from '../food-menu-options'
import { FoodQuestionComponent } from '../food-question'
import { FoodMenuComponent } from './food-menu.component'
import { RenderOptionPipe } from './render-menu-option.pipe'

@NgModule({
  declarations: [
    FoodMenuComponent,
    FoodQuestionComponent,
    FoodMenuCardComponent,
    FoodMenuOptionsComponent,
    RenderOptionPipe,
  ],
  imports: [CommonModule, FoodChoiceModule, ReactiveFormsModule],
  exports: [FoodMenuComponent],
})
export class FoodMenuModule {}
src/app/food/food-menu
├── food-menu.component.html
├── food-menu.component.spec.ts
├── food-menu.component.ts
├── food-menu.module.ts
├── food-menu.stories.ts
├── index.ts
├── render-menu-option.pipe.spec.ts
└── render-menu-option.pipe.ts

Lastly, we assemble FoodShellModule by importing FoodTotalModule and FoodMenuModule.

Assemble FoodShellModule

FoodShellComponent is a shell application that make use of components in FoodTotalModule and FoodMenuModule to build the food menu. Therefore, the imports array contains both of them in addition to CommonModule.

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

import { FoodMenuModule } from '../food-menu'
import { FoodTotalModule } from '../food-total'
import { FoodShellComponent } from './food-shell.component'

@NgModule({
  declarations: [FoodShellComponent],
  imports: [CommonModule, FoodTotalModule, FoodMenuModule],
  exports: [FoodShellComponent],
})
export class FoodShellModule {}
src/app/food/food-shell
├── food-shell.component.spec.ts
├── food-shell.component.ts
├── food-shell.module.ts
├── food-shell.stories.ts
└── index.ts

Take a final look of FoodModule and AppModule

Finally, we look at the declarations of FoodModule and AppModule.

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

import { FoodRoutingModule } from './food-routing.module'
import { FoodShellModule } from './food-shell'

@NgModule({
  declarations: [],
  imports: [CommonModule, FoodRoutingModule, FoodShellModule],
})
export class FoodModule {}

The declarations array is empty whereas the initial version declares all components here. The imports array removes FormsModule and ReactiveFormsModule because they are pushed down to SCAMS and FoodShellModule takes their places.

AppModule remains unchanged despite the code refactoring (I was genuinely surprised after the exercise).

import { AppComponent } from './app.component'
import { AppRoutingModule } from './app-routing.module'
import { FoodModule } from './food'

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule, AppRoutingModule, FoodModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Final thoughts

Single Component Angular Module (SCAM) is the new way to build modules and Angular application. SCAM is small module that can import into other modules that actually use it to build reusable or complex components. Therefore, this pattern identifies dependencies between modules and helps move imports from top-level module (FoodModule in this example) to SCAMs (FoodTotalModule FoodMenuModule and FoodChoiceModule).

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

Resources:

  1. Github Repository: https://github.com/railsstudent/ng-spanish-menu
  2. Angular APAC w/ Pankaj Parkar: Angular APAC w/ Pankaj Parkar – Lazy Loading Recipes
  3. Introduction to Modules: https://angular.io/guide/architecture-modules