Split module into single component angular modules (SCAMs)

Reading Time: 6 minutes

 139 total views,  1 views today

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

Functional composition with compose and pipe in lodash/fp

Reading Time: 3 minutes

 107 total views,  1 views today

What is functional composition?

Functional composition is the technique of composing multiple functions into a complex function. In Mathematic definition,

f(x) = x + 1
g(x) = x * 2
h(x) = f(g(x)) = f(x * 2) = (x * 2) + 1

f(x) and g(x) are functions and h(x) is the composed function of f and g.

Functional composition is an efficient technique in processing list data and it is preferred over calling chained array methods to achieve the same result.

The code snippet below uses Array.filter, Array.map and Array.reduce to calculate the total, 105.

const multThree = (num: number) => num * 3
const addOne = (num: number) => num + 1
const isEven = (num: number) => num % 2 === 0
const combineSum = (acc: number, num: number) => acc + num

const data = [1,2,3,4,5,6,7,8,9,10]
const total = data.filter(isEven)
 .map(addOne)
 .map(multThree)
 .reduce(combineSum)

The chained methods produces intermediate arrays before we calculate the total.

[1,2,3,4,5,6,7,8,9,10].filter(number => number % 2 === 0) => [2,4,6,8,10]
[2, 4, 6, 8, 10].map(num => num + 1) => [3,5,7,9,11]
[3,5,7,9,11].map(num => num * 3) => [9,15,21,27,33]
[9,15,21,27,33].reduce((acc, num) => acc + num, 0) => 105

Array method chaining has the following drawback:

  • Produce the side effect of intermediate arrays and can lead to memory issue with big input array
  • Developers have to trace each step to visualize the input and output of it
  • Authors think in iterative approach and not functional approach

Functional composition can solve this problem because it combines functions to create a new function. When we feed list data to the new function, it manipulates the data once and combines the new result with the previous result.

In this post, we will see how lodash/fp offers compose, pipe and the counterpart of Array.methods to build a composed function. The composed function is capable of accepting input list and returning the final output by processing the list once.

let's go

Functional composition with compose

compose executes functions from right to left direction. If the functions are f(g(x)), we call compose(g(x), f(x)) in this manner.

Let’s rewrite the above function with high order function, compose.

import { compose, map, filter, reduce } from 'lodash/fp'

const composeFunction = compose(
    reduce(combineSum),
    map(multThree), 
    map(addOne), 
    filter(isEven)
)

const total2 = composeFunction(data)
console.log('with lodash/fp compose', total2)

First, we import compose, map, filter and reduce from ‘lodash/fp’. Since compose executes from right to left direction, reduce is the first argument of compose where as filter is the last one.

In the code, we use compose to combine filter, map and reduce to build a composed function called composeFunction.

When we feed data to composedFunction, composedFunction traverses the list once to calculate the total of 105.

composedFunction([1,2,3,4,5,6,7,8,9,10])
=> 9 + 15 + 21, 27, 33 
=> 105

The benefit of composedFunction is the removal of the the side effect of producing intermediate arrays.

Functional composition with pipe

Next, we rewrite composedFunction with pipe and called it pipeFunction

Similar to compose, pipe is a high order function that flows data through functions from left to right direction.

import { pipe, map, filter, reduce } from 'lodash/fp'

const pipeFunction = pipe(
    filter(isEven),
    map(addOne), 
    map(multThree), 
    reduce(combineSum),
)

const total4 = pipeFunction(data)
console.log('with lodash/fp pipe', total4)

The arguments of pipe are reversed but pipeFunction should achieve the same result as composedFunction.

pipeFunction([1,2,3,4,5,6,7,8,9,10])
=> 9 + 15 + 21, 27, 33 
=> 105

Finally, you can run the Stackblitz demo to play with the examples of compose, pipe and native methods of Array.

Final thoughts

Function composition has these benefits:

  • Remove the side effect of producing intermediate arrays
  • Developers do not have to trace the steps to derive the output list that becomes the input list of the next chained array method
  • Codes adopt functional approach and are point-free

In conclusion, I highly recommend developers to understand and practice functional programming in JavaScript and use the tool to replace array chaining to improve the efficiency of list processing.

Resources:

  1. Functiona light JS: https://github.com/getify/Functional-Light-JS/blob/master/manuscript/ch4.md/#chapter-4-composing-functions
  2. lodash/fp guide: https://github.com/lodash/lodash/wiki/FP-Guide
  3. ramdaJs: https://ramdajs.com/docs/#compose
  4. Stackblitz: https://stackblitz.com/edit/typescript-tmi9eg

Customize template with ngTemplateOutlet and ngTemplate in Angular

Reading Time: 4 minutes

 305 total views

Introduction

When Angular components require to render ngTemplates programmatically, ngif-then-else construct takes care of most of the scenarios. However, ngIf is lack of passing context that ngTemplateOutlet directive supports. If either template depends on inputs or calculated data of component, then we can pass the values to them via template context of ngTemplateOutlet directive.

The usage of ngTemplateOutlet is shown as follows:

<ng-container *ngTemplateOutlet="templateRefExp; context: contextExp"></ng-container>

that is the syntactic sugar of

<ng-container [ngTemplateOutlet]="templateRefExp" [ngTemplateOutletContext]="contextExp"></ng-container>

In this post, we learn how to use ngTemplateOutlet directive in a <ng-container> element, assign different templates to the directive given the result of the ternary expression. We can supply inputs to template context and the rendered ngTemplate can use the data in the context to render the content subsequently.

let's go

Final output after using ngTemplateOutlet

Customize ngContainer to host ngTemplateOutlet

First, we add <ng-container> element in food-menu.component.html to host a ngTemplateOutlet directive. The directive receives an instance of ngTemplate based on the result of the ternary expression. When the expression is true, the directive gets “hasFood” template. On the other hand, it gets “noFood” template when the expression is false.

<ng-container *ngTemplateOutlet="data.menuItems.length > 0 ? hasFood : noFood; context: { data }"></ng-container>

Moreover, we pass the data object to the template context for both templates to access its values.

context: { data }

For you information, data is an object that has two properties: menuItems and option. MenuItems is an array that stores the information of menu items and their choices. Option stores the selected value of the dropdown.

data: {
   menuItems: [ 
     { question: '...', choices: [...] }, 
     { question: '...', choices: [...] } 
   ],
   option: 'AVAILABLE'
}

Define hasFood ngTemplate to assign to the ngTemplateOutlet directive

Then, we define hasFood template that is displayed when the condition, data.menuItems.length > 0, is met.

Since ngTemplateOutlet has a context expression, let-data="data" allows us to access the data object in the context. Next, we iterate the array to display each menu item in <app-food-menu-card> component. <app-food-question> prompts user to select food with a question while <app-food-choice> provides an input field to enter quantity to order.

<ng-template #hasFood let-data="data">
  <app-food-menu-card *ngFor="let menuItem of data.menuItems; index as i; trackBy: menuItemTrackByFn">
    <app-food-question [question]="menuItem.question" head></app-food-question>
    <ng-container *ngFor="let choice of menuItem.choices; index as j; trackBy: choiceTrackByFn" body>
      <app-food-choice
        [choice]="choice"
        [qtyMap]="qtyMap"
        (foodChoiceAdded)="handleFoodChoiceSub$.next($event)"
      ></app-food-choice>
    </ng-container>
  </app-food-menu-card>
</ng-template>
we can fix all accessibility violations

Define noFood ngTemplate to assign to the ngTemplateOutlet directive

First ngTemplate is ready and we need to create the second ngTemplate, noFood. This template shows a simple text when the menuItems array has no item.

<ng-template #noFood let-data="data">No food or drink that is {{ data.option | renderMenuOption }}.</ng-template>
export enum MENU_OPTIONS {
  ALL = 'ALL',
  AVAILABLE = 'AVAILABLE',
  SOLD_OUT = 'SOLD_OUT',
  LOW_SUPPLY = 'LOW_SUPPLY',
}

If you are curious of data.option, it is a value of MENU_OPTIONS enum. The enum has four member values: ‘ALL’, ‘AVAILABLE’, ‘LOW_SUPPLY’ or ‘SOLD_OUT’ that are in uppercase. Due to the casing and underscore format of the member values, we will create a custom pipe to transform the value to normal English words.

Build custom pipe to transform value in ngTemplate noFood

Finally, use Angular CLI to generate the boilerplate code for the custom pipe

ng g pipe RenderOptionPipe
import { Pipe, PipeTransform } from '@angular/core'

import { MENU_OPTIONS } from '../enums'

@Pipe({
  name: 'renderMenuOption',
})
export class RenderOptionPipe implements PipeTransform {
  transform(value: MENU_OPTIONS): string {
    if (value === MENU_OPTIONS.AVAILABLE) {
      return 'available'
    } else if (value === MENU_OPTIONS.LOW_SUPPLY) {
      return 'low supply'
    }

    return 'sold out'
  }
}

Three outcomes:

  • All food is sold out (quantity = 0)
  • All food is available (quantity > 0)
  • None of the food is low supply

Final code in template

<div class="food-menu" *ngIf="menuItems$ | async as data; else notAvailable">
  <app-food-menu-option (menuOptionSelected)="menuOptionSub$.next($event)"></app-food-menu-option>
  <ng-container *ngTemplateOutlet="data.menuItems.length > 0 ? hasFood : noFood; context: { data }"></ng-container>
</div>

<ng-template #notAvailable>No menu</ng-template>
<ng-template #hasFood let-data="data">
  <app-food-menu-card *ngFor="let menuItem of data.menuItems; index as i; trackBy: menuItemTrackByFn">
    <app-food-question [question]="menuItem.question" head></app-food-question>
    <ng-container *ngFor="let choice of menuItem.choices; index as j; trackBy: choiceTrackByFn" body>
      <app-food-choice
        [choice]="choice"
        [qtyMap]="qtyMap"
        (foodChoiceAdded)="handleFoodChoiceSub$.next($event)"
      ></app-food-choice>
    </ng-container>
  </app-food-menu-card>
</ng-template>
<ng-template #noFood let-data="data">No food or drink that is {{ data.option | renderMenuOption }}.</ng-template>

Final thoughts

When a component requires to render conditional templates, ngIf may not be the right approach especially when the templates expect inputs from the component. A robust solution is to host ngTemplateOutlet directive in ng-container element, and assign templates and context to the directive in a ternary expression.

The result of the ternary expression controls which template to display; the template can access variables in the template context and use the values in elements.

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. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. ngTemplateOutlet documentation: https://angular.io/api/common/NgTemplateOutlet
  3. ngTemplateOutput: The secret to customization: https://indepth.dev/posts/1405/ngtemplateoutlet

How to perform accessibility testing in Angular and Storybook

Reading Time: 4 minutes

 348 total views

Introduction

Accessibility testing is an important aspect of Angular and Storybook development because applications should be accessible to disabled individuals. For example, people who suffer from color blindness can not see red and green, and developers should render texts in different colors. When developing accessible applications in Angular, we can add aria attributes to HTML templates and apply colors with high contrast to satisfy accessibility compliances.

While working on this Angular project, accessibility is not a top priority of mine because my main focus is functionality. Moreover, I am not expert of this area and would require tool to help me identify accessibility violations.

Fortunately, Storybook provides a11y add-on that runs story books against WCAG standard to identify violations to help developers build accessible components in Angular.

In this post, we learn the installation Storybook a11y add-on, verify the results of accessibility testing, fix violations and eventually pass the WCAG rules in both Angular and Storybook.

let's go

Caveats of Storybook add-ons

Storybook application and its add-ons need to have the same version. In my example, the version of a11y add-on that I used is 6.3.12.

Install Storybook a11y add-on

First, we install the new dependency with the exact version, 6.3.12.

npm install --save-exact @storybook/addon-a11y@6.3.12

Then, append "@storybook/addon-a11y" to addons array within ./storybook/main.js.

module.exports = {
  "addons": [
     ... existing addons ...
     "@storybook/addon-a11y"
   ]
}

Launch Storybook application

// package.json 

"scripts": {    
    "docs:json": "compodoc -p ./tsconfig.json -e json -d .",
    "storybook": "npm run docs:json && start-storybook -p 6006",
}

Second, we type npm run storybook on the terminal to launch the Storybook application.

we can fix all accessibility violations

Verify the results of accessibility testing in Storybook

If a11y is set up properly, we will see a new Accessibility tab on the panel. When clicking the tab, we can see three new sub-tabs: Violations, Passed and Incomplete. Our objective is to achieve 0 violation and test result is either passed or incomplete.

A couple of stories share the following violation:

“Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds”.

When we check the “Highlight results” checkbox, we see a dashed rectangle on the Submit button. If we change the background color of the button, the accessibility rule should pass.

Similarly, the add-on reports a violation in the story of Food Menu Options. The rule, “Ensures select element has an accessible name”, describes the select input is missing at least arial-label attribute.

To validate my assumption, expand the accordion to read supplementary explanation. The content panel contains “More info…” link that links to WGAC rule page.

Click the link to navigate to the rule page that describes various examples to fix the problem.

Next, we fix the accessibility issues such that none of the storybooks has violations.

Fix accessibility violations in Storybook

Make background color of button to dark indigo

Due to the violations, we apply dark background color in .btn-indigo class in the global stylesheet.

// style.scss

.btn-indigo {
  @apply rounded-md p-2 bg-indigo-600 hover:bg-indigo-400 text-white;
}

The old value is bg-indigo-500 and the new value is bg-indigo-600

Add aria-label to select food input

Next, we add arial-label attributes to the elements of the template to fix the other violation.


// food-menu-options.component.ts

<section class="flex justify-end p-3 pr-0" aria-label="select food section">
  <form [formGroup]="form" aria-label="select food form">
      <select class="pl-2 w-[200px] text-base" name="option" formControlName="option" aria-label="select food">
         <option value="all" aria-label="show all">Show all</option>
         <option value="available" aria-label="show available">Show available only</option>
         <option value="soldOut" aria-label="show sold out">Show sold out</option>
      </select>
   </form>
</section>

Re-test accessibility in Storybook

Neither story has violation and the application practices good web accessibility.

Final thought

Storybook facilitates development of web accessibility by offering a11y add-on to illustrate violations. Error messages are clear and provide visual aid to highlight elements that cause the violations. Then, developers can analyze the information and tackle the violations iteratively until the components comply to the WCAG standard.

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 Storybook.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Tailwind: https://tailwindcss.com/docs/installation
  3. Storybook Addon PostCSS: https://storybook.js.org/addons/@storybook/addon-postcss

How to render Tailwind CSS in Angular and Storybook

Reading Time: 2 minutes

 201 total views

Introduction

In previous post, we learned how to apply Tailwind CSS to Angular application. However, the Tailwind CSS does not render in Storybook components properly.

It is because we need to install PostCSS add-on to configure PostCSS and add Tailwind to the Storybook.

In this post, we learn how to install storybook PostCSS add-on and create a PostCSS configuration to add Tailwind to Storybook and render their CSS in Storybook components.

Precondition

Angular application must have Tailwind installed before we can proceed to render Tailwind CSS in Storybook.

Install Storybook PostCSS add-on

Storybook recommends to install addon-postcss to project. Let’s install @storybook/addon-postcss as development dependency of the project.

npm install -save-dev @storybook/addon-postcss

Within ./storybook/main.js, append "@storybook/addon-postcss" to addons array

module.exports = {
  "addons": [
     ... existing addons ...
     "@storybook/addon-postcss"
   ]
}

PostCSS Configuration

Create postcss.config.js at root level of the project,

module.exports = {
  plugins: [
    require('autoprefixer'),
  ],
}

Verify the result in the Storbook

npm run storybook
Using PostCSS preset with postcss@7.0.39

indicates PostCSS is loaded to Storybook successfully

Open the storybooks of FoodCard Component

Storybook components render Tailwind CSS property after the configuration

We can use Storybook to visualize the functionality and styling of components.

Final thought

When Storybook components broke and did not render Tailwind CSS, I panicked because I was not sure how to support Tailwind in them. Storybook PostCSS add-on simplifies the configuration and with a few steps, Storybook renders the components with the expected styling.

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 Storybook.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Tailwind: https://tailwindcss.com/docs/installation
  3. Storybook Addon PostCSS: https://storybook.js.org/addons/@storybook/addon-postcss

Dynamically import module in Angular

Reading Time: 5 minutes

 335 total views

Introduction

The elements of Spanish menu application (https://github.com/railsstudent/ng-spanish-menu) are primarily texts and buttons, and the user interface looks plain on first glance. I want to make it interesting by rendering an icon when quantity is below threshold.

This is the final output:

The exclamation icon is loaded from angular-fontawesome followed by message, “Low Supply”.

I worked on the implementation twice:

Initially, static import FontAwesomeModule to the application, and used ng-if to conditionally render the icon and text. The solution had little code changes but the drawback was an additional of 32 kilobytes to the bundle size. The margin of increase is a lot considering the application is small and I am using only one icon of the library.

As the result of this discovery, the final design dynamically creates FaIconComponent and inserts it to an instance of ViewContainerRef. Then, inject Renderer2 and append “Low Supply” child to div parent.

This post will explain how I made the enhancement with the naive approach, what I discovered and the benefits of creating dynamic components in Angular.

Install Angular Fontawesome in Angular

Firstly, we have to install angular-fontawesome schematics into the Angular application.

ng add @fortawesome/angular-fontawesome@0.9.0

Add font-awesome icon statically

Secondly, import FontAwesomeModule in food-choice module such that all icons are available to render in template.

food-choice.module.ts

import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'

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

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

Thirdly, update component and template to display the icon and text conditionally.

// environment.ts
export const environment = {
  production: false,
  baseUrl: '/.netlify/functions',
  lowSupplyPercentage: 0.4,
}
// food-choice.component.ts

public ngOnInit(): void {
    this.remained = this.qtyMap ? this.qtyMap[this.choice.id] || 0 : 0
    this.minimumSupply = Math.ceil(this.remained * environment.lowSupplyPercentage)
}
// file-choice.component.html

<div class="flex items-center" *ngIf="remained > 0 && remained <= minimumSupply">
   <fa-icon [icon]="faExclamationTriangle" class="text-red-500 text-[1.35rem] mr-2"></fa-icon>
    <span class="text-red-500 text-xl">Low supply</span>
</div>

Lastly, I examine the impacts of angular-fontawesome on the bundle size. The bundle size should increase but the degree of decrease is my major focus.

Install source-map-explorer to analyze the bundle of the project

npm i --save-dev source-map-explorer

Build the project and enable source-map flag

ng build --source-map=true

Finally, analyze the source map to gather information on the size of different packages.

./node_modules/.bin/source-map-explorer ./dist/ng-spanish-menu/main.<hash sum>.js

The bottom right displays the size of angular-fontawesome and it is roughly the same size as rxjs. I have to improve the bundle size because one icon leads to a slightly bloated main.js.

Create dynamic fontawesome icon and text

This approach requires more steps than its counterpart but the bundle size shrinks eventually and the benefits outweigh the extra efforts.

Firstly, add a template reference (#lowSupplyRef) to the div parent. I will use the reference to append the “Low Supply” text later on.

// font-choice.template.html
<div class="flex items-center grow" #lowSupplyRef></div>

Secondly, define a viewContainerRef inside the div element to host instances of font-awesome icon.

// font-choice.template.html
<div class="flex items-center grow" #lowSupplyRef>
   <ng-container #viewContainerRef></ng-container>
</div>

Inside the component, declare a componentRef variable to hold a reference to font-awesome icon.

// food-choice.component.ts

public componentRef: ComponentRef<unknown> | null = null

Use @ViewChild() decorator to obtain viewContainerRef and lowSupplierRef.

// food-choice.component.ts

@ViewChild('viewContainerRef', { read: ViewContainerRef, static: true })
public viewContainerRef: ViewContainerRef

@ViewChild('lowSupplyRef', { read: ElementRef, static: true })
public lowSupplierRef: ElementRef

Next, define a function to create a dynamic font-awesome icon and insert it to viewContainerRef.

private async displayLowSupplyIcon() {
    const faExclamationTriangle = (await import('@fortawesome/free-solid-svg-icons')).faExclamationTriangle
    const FaIconComponent = (await import('@fortawesome/angular-fontawesome')).FaIconComponent
    const resolvedFaIconComponent = this.componentFactoryResolver.resolveComponentFactory(FaIconComponent)
    const faIconComponentRef = this.viewContainerRef.createComponent(resolvedFaIconComponent)
    faIconComponentRef.instance.icon = faExclamationTriangle
    faIconComponentRef.instance.classes = ['text-red-500', 'text-[1.35rem]', 'mr-2']
    faIconComponentRef.instance.render()
    this.componentRef = faIconComponentRef
}

The first import() statement imports the exclamation icon.

const faExclamationTriangle = (await import('@fortawesome/free-solid-svg-icons')).faExclamationTriangle

The next two lines of code create a FaIconComponent component.

const FaIconComponent = (await import('@fortawesome/angular-fontawesome')).FaIconComponent
const resolvedFaIconComponent = this.factoryResolver.resolveComponentFactory(FaIconComponent)

Then, we create an instance of ComponentRef<FaIconComponent>, assign the icon, specify tailwind CSS classes and render the svg.

const faIconComponentRef = this.viewContainerRef.createComponent(resolvedFaIconComponent)
faIconComponentRef.instance.icon = faExclamationTriangle
faIconComponentRef.instance.classes = ['text-red-500', 'text-[1.35rem]', 'mr-2']
faIconComponentRef.instance.render()
this.componentRef = faIconComponentRef

Next, define another function to append the “Low Supply” text to lowSupplierRef.

private renderLowSupplyText() {
    const lowSupplySpanElement = this.renderer.createElement('span')
    lowSupplySpanElement.classList.add('text-red-500', 'text-xl')
    lowSupplySpanElement.innerText = 'Low Supply'
    this.renderer.appendChild(this.lowSupplierRef.nativeElement, lowSupplySpanElement)
}

When quantity is low and icon has not rendered, render both icon and the text, and trigger change detection.

private async displayLowSupplyComponent() {
  if (!this.componentRef) {
     await this.displayLowSupplyIcon()

     this.renderLowSupplyText()
     this.cdr.detectChanges()
  }
}

When quantity reaches zero, destroys the components and clears viewContainerRef to prevent memory leak.

private destroyComponents() {
    if (this.componentRef) {
      this.componentRef.destroy()
    }

    if (this.viewContainerRef) {
      this.viewContainerRef.clear()
    }

    Array.from(this.lowSupplierRef.nativeElement.children).forEach((child) => {
      this.renderer.removeChild(this.lowSupplierRef.nativeElement, child)
    })
}

private async handleLowSupply() {
    if (this.remained <= 0) {
      this.destroyComponents()
    } else if (this.remained > 0 && this.remained <= this.minimumSupply) {
      await this.displayLowSupplyComponent()
    }
}

Finally, we call handleLowSupply() in ngOnInit and ngOnChanges.

public async ngOnInit(): Promise<void> {
    this.remained = this.qtyMap ? this.qtyMap[this.choice.id] || 0 : 0
    this.minimumSupply = Math.ceil(this.remained * environment.lowSupplyPercentage)

    await this.handleLowSupply()
}

public async ngOnChanges(changes: SimpleChanges): Promise<void> {
    ... omitted ...

    await this.handleLowSupply()
}

Study the bundle size

We change many codes and keep the same user interface. Did the efforts significantly reduce the bundle size?

Re-run the commands below

ng build --source-map=true
./node_modules/.bin/source-map-explorer ./dist/ng-spanish-menu/main.<hash sum>.js

The bundle size increases by 3 kilobytes and angular-fontawesome library is removed from the source map.

Dynamic import does not add angular-fontawesome to main.js and instead, it splits into a couple of lazy chunk files (457.5da21ff230e58ed7c939.js and 859.106542046a8d67d7e411.js).

Final thought

Static import third-party library increases the bundle size of Angular application and importing a large library can contribute to a big bundle. In this example, the naive approach led to a 10% increase of the bundle size.

Thanks to dynamic import, ComponentFactoryResolver and ViewComponentRef classes, I can load icon on the fly, achieve the same result yet the bundle size increases by a few kilobytes.

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 web technologies.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. ComponentFactoryResolver: https://angular.io/api/core/ComponentFactoryResolver
  3. ViewContainerRef: https://angular.io/api/core/ViewContainerRef
  4. Renderer2: https://angular.io/api/core/Renderer2

Tailwind CSS in JIT mode with Angular

Reading Time: 3 minutes

 168 total views

Introduction

Tailwind CSS (https://tailwindcss.com/) is a utility-first CSS framework with out-of-the-box classes for UI components.

For example, <p class="pl-2">hello world</> is equivalent to <p style="padding-left: 0.5rem;">hello world</h> and we achieve the effect without writing inline-style or custom class.

However, our components may require one-off style that Tailwind does not support and we cannot justify to include it in theme configuration.

Fortunately, Tailwind enables Just-In-Time (JIT) mode that generates the styles on demand. This feature allows dynamic style such as “w-[200px]” to fix the width of a card to 200px.

This post will show you the installation of Tailwind CSS, enable Just-In-time mode and add dynamic styles to style a div element.

Install Tailwind CSS in Angular

Firstly, we have to install the dependency of tailwind and tailwind plugins into the Angular application.

npm install --save-dev tailwindcss
npm install --save-dev @tailwindcss/forms @tailwindcss/typography
npx tailwindcss init

The npx command generates tailwind.config.js with default values

Enable Just-In-Time mode

Secondly, we have to enable JIT mode of tailwind; therefore, mode: 'jit‘ is added to tailwind.config.js.

module.exports = {
  mode: 'jit',
  content: ["./src/**/*.{html,ts}"],
  darkMode: 'media', // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/typography'),
    require('@tailwindcss/forms'),
  ],
}

Then, we add file path in content array to purge all html and typescript files to maintain small bundle size in production. The last step of configuration is to import typography and forms plugins by require('@tailwindcss/typography') and require('@tailwindcss/forms')

Add Tailwind Base Styles to Angular

It is important to put tailwind directives at the beginning of the global stylesheet, style.scss. These are base styles that are accessible to all UI components of the application.

@tailwind base;
@tailwind components;
@tailwind utilities;

Finally, we are ready to generate on-demand style in our component.

Generate on-demand styles in Angular Component

Our use case is to set the width of food-card component to 300 pixels wide. The html template of food-card component looks like the following:

<div style="display: flex; flex-direction: column; margin-right: 0.5rem; border: 2px solid black; width: 300px;">
      <label name="name" class="item card-row">
        <span class="field">Name:</span>
        <span class="field-text">{{ ordered.name }}</span>
      </label>
      <label class="item card-row" name="description">
        <span class="field">Description:</span>
        <span class="field-text">{{ ordered.description }}</span>
      </label>
</div>

Except width: 300px;, we can substitute the rest with tailwind utility classes.

I don’t believe the width style should be part of the configuration because it is used one time. The just-in-time mode provides the solution we need; w-[300px] generates custom class at run time and changes the width to 300 pixels wide.

After the modification;

<div class="flex flex-col mr-2 border-solid border-2 border-black w-[300px]">
      <label name="name" class="item card-row">
        <span class="field">Name:</span>
        <span class="field-text">{{ ordered.name }}</span>
      </label>
      <label class="item card-row" name="description">
        <span class="field">Description:</span>
        <span class="field-text">{{ ordered.description }}</span>
      </label>
</div>

Verify the result

The final stage is to verify the custom style actually works and the div component has the expected width.

We can open Chrome inspector, hover on the <div> element and inspect its width property. As the image indicates, we have successfully fix the width to 300 pixels.

Final thought

I really like to use Tailwind in Angular because it saves the efforts of authoring custom css classes in SCSS files. After switching to tailwind, I got rid of most of the SCSS files and replaced the styles with the counterpart utility classes. When utility class is unavailable, it is convenient to generate the custom class on the fly with arbitrary value. Authoring UI components can’t get any easier in web development.

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 web technologies.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Tailwind: https://tailwindcss.com/docs/installation

Automate release management in Angular

Reading Time: 4 minutes

 118 total views

Introduction

Whether it is enterprise application or open source project, they will schedule for release to announce new features and bug fixes. The project will need new version and tag after the release to keep track of changes that occur between two releases.

When manager ask development team the deliverable of the current release; change log becomes handy because developers can present it to manager to review.

However, making a change log by hand is time consuming because developers have to go through git log and choose changes that have great values such as enhancements and bug fixes.

Developers are lazy and if they can find tool that can generate change log, they will use it without hesitation.

standard-version is an open source library that fulfills the requirement and you will love it if you have already written commit messages according to commitlint convention.

The effort to add standard-version is minimal and you can find all the steps in this blog post.

Install dev dependencies

npm i --save-dev standard-version

Add scripts to package.json that run to cut new release for an Angular application

// package.json
{
  "release": "standard-version -t '' -a",
  "release:patch:dryrun": "npm run release  -- --dry-run --release-as patch",
  "release:patch": "npm run release  -- --no-verify --release-as patch",
}

npm run release uses standard-version to cut new release, generate CHANGELOG.md and add new tag. The flag -a commits all generated changes while -t '' replaces the prefix of tag from v to blank string.

npm run release:patch:dryrun tests patch release in dry mode. When the script is successful, the simulation bumps the version of package.json and package-lock.json, outputs git commits to CHANGELOG.md and tag the new version to main branch.

Example of standard-version in dry run mode:

Configurations of of standard-version

standard-version supports lifecycle scripts and one of them is posttag that is executed after adding new tag. The posttag event is the ideal hook to push both commit and tag to repository automatically.

Moreover, the library has the option to show the type of commits in the change log. My preference is to output features and bug fixes to the file to highlight important changes in the new release.

We can find the configurations in .versionrc.json:

// .versionrc.json
{
  "types": [
    { "type": "feat", "section": "Features" },
    { "type": "fix", "section": "Bug fixes" }
  ],
  "scripts": {
    "posttag": "git push --follow-tags --force"
  }
}

Commit messages begin with feat: prefix belongs to Feature section whereas fix: prefix goes to Bug fixes section.

git push --follow-tags --force command force push commit and tag to my repository.

Cut a new release

The next feature of the project is adoption of Tailwind CSS and apply their CSS styles to beautify the components. The results look good in storybook; therefore, I decide to create a 0.0.7 release.

The release process is straightforward: npm run release:patch script automates the process from bump version, tagging to change log update.

The –no-verify flag ensures that husky hooks do not run in the commit step of the release and the release process is expected to complete quickly.

Change log accumulates features and bug fixes in release 0.0.7.

If I click any git commit id, I will see all the changes in the files.

One script replaces all the manual work of version management.

Final thought

In the past, developers are responsible for post-release activities such as version increment and adding version tag. Sometimes, developers forget because they are busy to implement new features and fix bugs. Nowadays, there are plenty of tools to facilitate release management and developers can issue a single script to automate the mentioned tasks. There is no excuse of not defining the standard procedure of release at work or in open source projects

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular, architecture and good engineering practice.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Standard version: https://github.com/conventional-changelog/standard-version

Improve Angular code with Betterer

Reading Time: 5 minutes

 275 total views

Introduction

In software development, when developers work on a project for a period of time, they tend to add code smell into the code that Eslint rules identify as problems. When architect adds a new Eslint rule to fix these errors, npm run lint reports error messages in terminal that require immediate attention.

I have two approaches to handle Eslint errors at work.

When npm run lint returns few errors, I fix all of them at once, verify on my machine and push the codes to remote repository.

When the number of errors is significant, I choose a few to fix, turn off the Eslint rule and git push the code to remote repository, Then, I enable the rule, re-run npm run lint and repeat the process. Therefore, the process continues until the Eslint rule passes and I can enable the rule permanently in the project.

Approach two is tedious and it disrupts the flow of developer by enabling and disabling the rule to fix issues. Fortunately, Craig Spence created betterer to improve the code base incrementally and I can throw approach two out of the window.

Add code smell to Angular

I add code smell to the project to demonstrate betterer. The example calls async/await statements in a for loop to sum some numbers in ngOnit function.

sum = 0
async ngOnInit(): Promise<void> {
  for (let i = 1; i <= 4; i++) {
    this.sum += await this.square(i)
    this.sum += await this.cube(i)
  }
}

private async square(num: number): Promise<number> {
  return Promise.resolve(num * num)
}

private async cube(num: number): Promise<number> {
  return Promise.resolve(num * num * num)
}

Executing await in a loop is code smell and Eslint even has a no-await-in-loop rule to check for its existence.

Next, I will show the readers how to write betterer test to look for await in a loop and to improve the test values by refactoring the code.

Install dependencies

npx @betterer/cli init
npm i --save-dev @betterer/angular @betterer/eslint

The npx command generates betterer script in package.json and creates a blank betterer.ts for the tests as a result.

package.json
{
  "betterer": "betterer"
}

Write betterer tests for Eslint and Angular

Writing tests for Eslint and Angular is very simple because betterer library provides eslint and angular functions that accept the name of the rule and its options.

// betterer.ts

import { angular } from '@betterer/angular'
import { eslint } from '@betterer/eslint'

export default {
  'no more await in loop': () => eslint({ 'no-await-in-loop': 'error' }).include('src/**/*.ts'),
  'stricter template compilation': () =>
    angular('./tsconfig.json', {
      strictTemplates: true,
    }).include('src/**/*.ts', 'src/**/*.html'),
}

eslint({ 'no-await-in-loop': 'error' }).include('src/**/*.ts') test looks for await in any loop in TypeScript files of src folder and outputs error messages.

Then, we execute npm run betterer to run the tests and store the initial values in betterer.results

“no more await in loop test” fails and finds two await calls in the for loop. Therefore, our task is to refactor ngOnInit to get rid of them.

(Optional) Run betterer in pre-commit hook

If your project has husky installed, you can run betterer precommit in pre-commit hook to update the values in betterer.result file

Precommit requires configurations in package.json, gitignore file and eventually pre-commit husky file

// package.json

{
    ....
    "betterer": "betterer --cache",
    "betterer:precommit": "betterer precommit --cache"
}

Betterer normal and betterer precommit tests cache ids to .betterer.cache to bypass checking on unmodified files. Therefore, the tests complete faster and developers do not have to wait forever.

We add betterer.cache to gitignore to ensure we don’t not accidentally commit it to our repo

// .gitignore

.betterer.cache
// pre-commit  husky file

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged
npm run betterer:precommit

Whenever we commit changes, betterer re-run the tests and update the values in .betterer.results file

Refactor code to improve the values of betterer

// food-shell.component.ts
const sumAndCubePromises = await Promise.all(
  [1, 2, 3, 4].map(async (i) => (await this.square(i)) + (await this.cube(i))),
)

this.sum = sumAndCubePromises.reduce((acc, value) => acc + value)

I refactor the codes to create all promises and obtains the results in await Promise.all(). The result is an array of numbers and we compute the total by Array.reduce().

Code rewrite eliminates both for loop and await calls; thus, the betterer values get better.

The “no more await in loop” test has met its goal while “stricter template compilation” test stays the same.

Since the code base does not have await in loop, I can delete the test and add the eslint rule in the configuration file

Remove betterer test that has met it’s goal
Add no-await-in-loop rule in eslintrc.json configuration file

After I commit the latest changes, betterer is run again to update betterer.results file.

The terminal displays the result of the only test and the test value stays the same.

Bonus: Resolve conflicts in betterer.results

We have a large team at work and we constantly deal with merge conflicts in .betterer.results file. Fortunately, the library provides merge functionality to resolve them automatically.

{
   ....
   "betterer:merge": "betterer merge"
}

Type npm run merge and the conflicts should disappear. As precaution, we run npm run betterer to recalculate the values before code push.

Final thought

Betterer allows developers to add tests to improve codes in stages. These tests stay on until the issues are resolved. When one issue is fully resolved, we can remove the corresponding test and include the rule to eslintrc configuration file. One benefit is to make better codes without widespread changes. The other benefit is prevent massive code refactoring that breaks existing functionality.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular, architecture and good engineering practice.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Betterer: https://phenomnomnominal.github.io/betterer/docs/introduction
  3. Angular Nation and Betterer w/Craig Spence: https://www.youtube.com/watch?v=BCdDEhNWpUU
  4. Craig Spence’s twitter: https://twitter.com/phenomnominal

Angular and Storybook – Mock Data in Component

Reading Time: 5 minutes

 277 total views

Introduction

After building a few presentational components, I am ready to build a container component, food menu component, with them. Food menu component is responsible for displaying an option dropdown and a list of menu items.

This is the component tree of food menu component.

The functions of food menu component are to retrieve menu data from Netlify function, listen to selected option from the dropdown, filter the data and eventually pass it to food menu card to display.

Visualizing container component in Storybook involves mock data because we don’t want to pull data from real data sources. Angular makes it easy because Angular provides dependency injection to inject mock service to provide fake data.

The process is similar to mock data in Angular unit testing except the result is seen visually and I am going to show the readers how to do it.

Create Food menu component in food module

ng g c foodMenu --module=food
// food-menu.component.ts 
... omit import statement ...

import { Choice, MenuItem, OrderedFoodChoice } from '../interfaces'
import { FoodService } from '../services'

@Component({
  selector: 'app-food-menu',
  templateUrl: './food-menu.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FoodMenuComponent implements OnInit, OnDestroy {
  @Output()
  addDynamicFoodChoice = new EventEmitter<OrderedFoodChoice>()

  menuItems$: Observable<MenuItem[] | undefined>
  handleFoodChoiceSub$ = new Subject<OrderedFoodChoice>()
  menuOptionSub$ = new BehaviorSubject<string>(MenuOptions.all)
  unsubscribe$ = new Subject<boolean>()

  qtyMap: Record<string, number> | undefined

  constructor(private service: FoodService) {}

  ngOnInit(): void {
    const menuUrl = `${environment.baseUrl}/menu`

    this.menuItems$ = combineLatest([
      this.service.getFood(menuUrl),
      this.menuOptionSub$,
      this.service.quantityAvailableMap$,
    ]).pipe(
      map(([menuItems, option]) => ({
        menuItems,
        option,
      })),
      map(({ menuItems, option }) => this.filterMenuItems(menuItems, option)),
      takeUntil(this.unsubscribe$),
    )

    this.service.quantityAvailableMap$.pipe(takeUntil(this.unsubscribe$)).subscribe((updatedQtyMap) => {
      if (!updatedQtyMap) {
        this.qtyMap = undefined
      } else {
        this.qtyMap = {
          ...updatedQtyMap,
        }
      }
    })

    this.handleFoodChoiceSub$
      .pipe(
        tap(({ id, quantity }) => this.service.updateQuantity(id, quantity)),
        takeUntil(this.unsubscribe$),
      )
      .subscribe((choice) => this.addDynamicFoodChoice.emit(choice))
  }

  .. omit trivial logic ...

  filterMenuItems(menuItems: MenuItem[] | undefined, option: string): MenuItem[] | undefined {
    ... return filtered menu item ...
  }
}

FoodMenuComponent injects FoodService that retrieves data from Netlify function and calculates remaining quantity of the menu items. Furthermore, the html template resolves the observable, this.menuItem$, to render the filtered menu items in the presentational components.

Set up mock service and data

Rather than using FoodService in Storybook, we create a mock service, MockFoodService, and implement the same functions that exist in FoodService.

// mock.ts

export class MockFoodService {
  private quantityAvailableSub$ = new BehaviorSubject<Record<string, number> | undefined>(undefined)
  quantityAvailableMap$ = this.quantityAvailableSub$.asObservable()

  constructor(private menuItems?: MenuItem[]) {}

  private buildQtyMap(): Record<string, number> | undefined {
    if (!this.menuItems) {
      return undefined
    }
    return this.menuItems.reduce((acc, mi) => {
      mi.choices.forEach(({ id, quantity }) => {
        acc[id] = quantity
      })
      return acc
    }, {} as Record<string, number>)
  }

  getFood(): Observable<MenuItem[] | undefined> {
    const qtyMap = this.buildQtyMap()
    this.quantityAvailableSub$.next(qtyMap)
    return of(this.menuItems)
  }

  updateQuantity(id: string, quantity: number): void {
    const qtyAvailableMap = this.quantityAvailableSub$.getValue()
    if (qtyAvailableMap) {
      const oldQty = qtyAvailableMap[id]
      const nextQty = oldQty - quantity
      if (nextQty >= 0) {
        this.quantityAvailableSub$.next({
          ...qtyAvailableMap,
          [id]: nextQty,
        })
      }
    }
  }

  private getLatestQtyMap() {
    const qtyAvailableMap = this.quantityAvailableSub$.getValue()
    if (!qtyAvailableMap) {
      const qtyMap = this.buildQtyMap()
      this.quantityAvailableSub$.next(qtyMap)
    }
    return this.quantityAvailableSub$.getValue()
  }

  getQuantity(id: string): number {
    const qtyAvailableMap = this.getLatestQtyMap()
    if (qtyAvailableMap) {
      return qtyAvailableMap[id] || 0
    }
    return 0
  }
}

The constructor of MockFoodService expects MenuItem array; therefore, we prepare static mock data in a separate file.

// constants.ts

export const MockData: MenuItem[] = [
  {
    id: '1',
    question: 'Which appetizer(s) do you wish to order?',
    choices: [
      {
        id: 'a',
        name: 'Egg salad',
        quantity: 10,
        ... other properties ...
      },
      {
        id: 'd',
        name: 'Buffalo Chicken Wings',
        quantity: 10,
        ... other properties ...
      },
      {
        id: 'b',
        name: 'Oven Baked Zucchini Chips',
        quantity: 10,
        ... other properties ...
      },
    ],
  },
  {
    id: '2',
    question: 'Which dessert(s) do you wish to order?',
    choices: [
      {
        id: 'a1',
        name: 'Ice cream',
        quantity: 10,
        ... other properties ...
      },
      {
        id: 'b1',
        name: 'Tiramisu',
        quantity: 10,
        ... other properties ...
      },
    ],
  },
]

Mock service and data is in place and we can finally add new storybook to visualize the food menu.

Add Storybook for Food Menu Component

Create food-menu.stories.ts under the food-menu folder

// food-menu.storeis.ts

import { HttpClientModule } from '@angular/common/http'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { Meta, moduleMetadata, Story } from '@storybook/angular'

import { FoodChoiceComponent } from '../food-choice'
import { FoodChoiceFormComponent } from '../food-choice-form'
import { FoodMenuCardComponent } from '../food-menu-card'
import { FoodMenuOptionComponent } from '../food-menu-option'
import { FoodQuestionComponent } from '../food-question'
import { FoodService } from '../services'
import { MockData, MockFoodService, SoldOutMockData } from '../storybook-mock'
import { FoodMenuComponent } from './food-menu.component'

export default {
  title: 'Food Menu',
  component: FoodMenuComponent,
  decorators: [
    moduleMetadata({
      declarations: [
        FoodChoiceComponent,
        FoodQuestionComponent,
        FoodChoiceFormComponent,
        FoodMenuCardComponent,
        FoodMenuOptionComponent,
      ],
      imports: [ReactiveFormsModule, FormsModule, HttpClientModule],
      providers: [
        {
          provide: FoodService,
          useFactory: () => new MockFoodService(MockData),
        },
      ],
    }),
  ],
  argTypes: { onClick: { action: 'clicked' } },
} as Meta

const Template: Story<FoodMenuComponent> = (args: FoodMenuComponent) => ({
  props: args,
})

export const Menu = Template.bind({})

The magic of the storybook is the providers array of moduleMetadata where we use useFactory to inject an instance of MockFoodService for FoodService.

providers: [
  {
      provide: FoodService,
      useFactory: () => new MockFoodService(MockData),
   },
],

When Storybook invokes this.service.getFood() and this.service.quantityAvailableMap$ in ngOnInit() of FoodMenuComponent, it actually invokes the implementation defined in MockFoodService. Therefore, there is no dependency between Storybook and Netlify function.

After laying down all the hard work, we can launch the storybook application and see the new story in action.

Start storybook application

npm run storybook

Click the title Food Menu -> Menu and view the dropdown, menu questions and items.

When dropdown value is “Show all”, the menu displays all questions and choices.

Next, I select “Show sold out” in the dropdown and the menu displays an appetizer that is no longer served.

Last, I select “Show available only” in the dropdown and the menu displays appetizers and desserts that have positive quantity.

Final thought

Storybook has the ability to inject mock service for container component. Story loads fake data from mock service on behalf of the container component and pass it down to presentational components to display. Then, we can interact with components to confirm the expected behavior in the story. When the results are satisfactory, we can even publish the new story to Chromatic for collaboration (refer to “Angular and Storybook – Publish to Chromatic”).

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

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Storybook: Create a template component: https://storybook.js.org/docs/angular/workflows/stories-for-multiple-components#creating-a-template-component