Author nestjs library using Angular Schematics

Reading Time: 7 minutes

 58 total views

Introduction

When I create nestjs project to do proof of concept, I usually spend the first hour to install dependencies and add rules to eslintrc.js before writing any meaningful code. When I researched code generation in nestjs, I found out that angular schematics can author nestjs library to upgrade configurations and add new files. Therefore, I decide to design a library that updates prettier configuration and generates eslint prettier rule using schematics.

In this blog post, I author a nestjs library, test the library on nestjs application locally and publish it to npmjs.com.

let's go

Install essential dependencies to author nestjs library

npm install -g @angular-devkit/schematics-clinpm install --save-dev --save-exact cpx@1.5.0 lodash@4.17.21 rimraf@3.0.2 typescript@4.7.2
npm install --save-exact @angular-devkit/core@13.3.0 @angular-devkit/schematics@13.3.0 @schematics/angular@13.3.0

Generate new schematics project to author nestjs library

schematics blank --name=nest-prettier

My habit is to rename src to schematics

nest-prettier
├── package.json
├── package-lock.json
├── README.md
├── schematics
│   ├── collection.json
│   └── nest-prettier
│       ├── index_spec.ts
│       └── index.ts
└── tsconfig.json

Update collection.json

As the name implies, collection.json is a collection of schematics in the library. In this library, I have one schematic that is nest-add that I can install via

nest add nest-prettier

to apply changes within the the latest version of nestjs application

{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "nest-add": {
      "description": "Run prettier as an eslint rule.",
      "factory": "./nest-prettier/index#nestPrettier",
      "schema": "./nest-prettier/schema.json"
    }
  }
}

Factory method is nestPrettier in index.ts and schema.json contains a list of questions that prompts users to answer. When users answer them, properties are updated in the schematic context

// schema.ts 

export interface Schema {
  printWidth: number
  tabWidth: number
  useTabs: boolean
  semi: boolean
  singleQuote: boolean
  trailComma: string
  bracketSpacing: boolean
  eslintFileFormat: string
  quoteProps: string
  arrowParens: string
  requirePragma: boolean
  insertPragma: boolean
}

Define the build process of the schematics

When authoring nestjs library, we intend to build and publish the library to npmjs.com for other developers to use. First, we configure tsconfig.json and add new npm scripts in package.json.

// tsconfig.json
{
  "compilerOptions": {
    ...
    "rootDir": "schematics",
    "outDir": "dist",
    ...
  },
  "include": ["schematics/**/*"],
  "exclude": ["schematics/*/files/**/*"]
}

Second, We change “rootDir” from “src” to “schematics” and add "outDir": "dist" to build the schematic in dist/ directory. We modify include array to include files in schematics/ and excludes templates in files/ from compilation.

Then, we tell schematics where to find the collection.json in package.json.

"schematics": "./dist/collection.json",

nest-prettier is a dev dependency in nestjs application

"nest-add": {
  "save": "devDependencies"
},

Next, add prebuild, copy:collection, copy:schematics, postbuild and manual-test scripts in package.json

"scripts": {
    "prebuild": "rimraf dist",
    "build": "tsc -p tsconfig.json",
    "copy:collection": "cpx schematics/collection.json dist && cpx 'schematics/**/schema.json' dist",
    "copy:schematics": "cpx 'schematics/**/files/**' dist",
    "postbuild": "npm run copy:collection && npm run copy:schematics",
    "manual-test": "npm run build && schematics .:nest-add"
}

It is important to copy collection.json, schematic.json and templates in post build stage.

Finally, manual-test script permits us to test the schematic in debug mode without generating new output file.

Create factory method of the schematic

// nest-prettier/index.ts

export function nestPrettier(options: Schema): Rule {
  return (tree: Tree, context: SchematicContext) => {
    context.addTask(new NodePackageInstallTask())

    addDependencies(tree, context, [prettier, eslintConfigPrettier, eslintPluginPrettier])
    return chain([addPrettierConfig(options), addEsnlintPrettier(options)])
  }
}

Let’s outline the main flow of the schematic

  • Add NodePackageInstallTask to install the library that is mandatory
  • Update package.json of nestjs application to install prettier, eslint-config-prettier and eslint-plugin-prettier dev dendencies
  • Create a single rule that updates .prettierrc and generates the eslint prettier rule from eslintrc-prettier.template template

Implement new schematic rules

In this simple nestjs library, there are 2 rules:

addPrettierConfig – this rule overwrites .prettierrc with user inputs

addEslintPrettier – this rule generates the eslint prettier rule in eslintrc-prettier.template that we have to manually copy to .eslintrc.js. It is because schematics does not have API that can update javaScript file easily.

Overwrite Prettier configuration file

export function addPrettierConfig(options: Schema): Rule {
  return (tree: Tree, context: SchematicContext) => {
    const prettierOptions: Record<string, string | number | boolean> = {
      singleQuote: options.singleQuote,
      trailingComma: options.trailComma,
      tabWidth: options.tabWidth,
      semi: options.semi,
      printWidth: options.printWidth,
      bracketSpacing: options.bracketSpacing,
      useTabs: options.useTabs,
      arrowParens: options.arrowParens,
      quoteProps: options.quoteProps,
      requirePragma: options.requirePragma,
      insertPragma: options.insertPragma,
    }

    const configFileName = '.prettierrc'
    if (tree.exists(configFileName)) {
      tree.overwrite(configFileName, JSON.stringify(prettierOptions, null, 2))
      context.logger.info(`${configFileName} is overwritten`)
    } else {
      tree.create(configFileName, JSON.stringify(prettierOptions, null, 2))
      context.logger.info(`Created ${configFileName}`)
    }
    return tree
  }
}

The code loads the options from Schema interface and invokes Tree API to overwrite .prettierrc with the new format options.

Copy eslint prettier rule to eslint configuration file configuration file

First, we create a template, eslint-prettier.template in nest-prettier/files directory.

extends: ['plugin:prettier/recommended']
rules: {
    'prettier/prettier': ['error', {
        'singleQuote': <%= singleQuote %>, 
        'trailingComma': '<%= trailComma %>', 
        'tabWidth': <%= tabWidth %>, 
        'semi': <%= semi %>, 
        'printWidth': <%= printWidth %>, 
        'bracketSpacing': <%= bracketSpacing %>, 
        'useTabs': <%= useTabs %>,
        'arrowParens': '<%= arrowParens %>',
        'quoteProps': '<%= quoteProps %>',
        'requirePragma': <%= requirePragma %>,
        'insertPragma': <%= insertPragma %>, 
    }]
}

<% singleQuote %> is a parameterized variable that will be replaced by singleQuote property of Schema interface. It is also true to the rest of the parameterized variables.

// eslint-prettier.helper.ts

import { apply, Rule, SchematicContext, template, Tree, mergeWith, url } from '@angular-devkit/schematics'
import { strings } from '@angular-devkit/core'

export function addEslintPrettier(options: Schema): Rule {
  return (tree: Tree, context: SchematicContext) => {
    const configFileName = '.eslintrc.js'
    const buffer = tree.read(configFileName)
    if (!buffer) {
      return tree
    }

    ...
    const eslintTemplate = './eslintrc-prettier.template'
    if (tree.exists(eslintTemplate)) {
       tree.delete(eslintTemplate)
    }

    const sourceTemplate = url('./files')
    const sourceParameterizedTemplate = apply(sourceTemplate, [
        template({
          ...options,
          ...strings,
        }),
    ])
    
    return mergeWith(sourceParameterizedTemplate)(tree, context)
  }
}

url('/files') reads all the files in nest-prettier/files directory; apply replaces variables with option values and mergeWith adds the templates to the source tree of the nestjs application.

As the result, the rules have set up to achieve file format in prettier and eslint configuration files. Before publishing the library, we should test the schematic in debug mode and/or in a local nestjs application

Test in debug mode after authoring a nestjs library

npm run manual-test  

compiles the codes to dist/ directory and executes schematics .:nest-add.

We choose our prettier configuration in debug mode. Suppose the schematic is correct, the outcome is to overwrite .prettierrc and create eslintrc-prettier.template.

Debug mode enabled by default for local collections.
? Eslint file format. javascript
? What is the print width per line? 120
? Specify the number of spaces per indentation-level. 2
? Indent lines with tabs instead of spaces. No
? Print semicolons at the ends of statements. Yes
? Use single quotes instead of double quotes. No
? Change when properties in objects are quoted. as-needed
? Print trailing commas wherever possible in multi-line comma-separated syntactic structures. all
? Print spaces between brackets in object literals. Yes
? Include parentheses around a sole arrow function parameter. always
? Prettier can restrict itself to only format files that contain a special comment, called a pragma, at the top of the 
file. This is very useful when gradually transitioning large, unformatted codebases to prettier. No
? Prettier can insert a special @format marker at the top of files specifying that the file has been formatted with 
prettier. This works well when used in tandem with the --require-pragma option. No
    Found prettier@2.7.1, do not add dependency
    Found eslint-config-prettier@8.5.0, do not add dependency
    Found eslint-plugin-prettier@4.2.1, do not add dependency
    .prettierrc is overwritten
    Does not support .eslintrc.js
    Append plugin and rule from ./eslintrc-prettier.template to .eslintrc.js. Then, delete the template file.
CREATE eslintrc-prettier.template (425 bytes)
UPDATE .prettierrc (259 bytes)
Dry run enabled by default in debug mode. No files written to disk.

Test in nestjs application after authoring a nestjs library

Debug mode does not create anything; therefore, I suggest to test the library in a local nestjs application.

First, we run npm link command to link the nest-prettier dependency to nest-prettier library

npm link ../nest-prettier

Then, execute nest-add schematic of the nest-prettier library

schematics nest-prettier:nest-add
? Eslint file format. javascript
? What is the print width per line? 140
? Specify the number of spaces per indentation-level. 2
? Indent lines with tabs instead of spaces. No
? Print semicolons at the ends of statements. Yes
? Use single quotes instead of double quotes. No
? Change when properties in objects are quoted. as-needed
? Print trailing commas wherever possible in multi-line comma-separated syntactic structures. all
? Print spaces between brackets in object literals. Yes
? Include parentheses around a sole arrow function parameter. always
? Prettier can restrict itself to only format files that contain a special comment, called a pragma, at the top of the file.
 This is very useful when gradually transitioning large, unformatted codebases to prettier. No
? Prettier can insert a special @format marker at the top of files specifying that the file has been formatted with 
prettier. This works well when used in tandem with the --require-pragma option. No
    Found prettier@2.7.1, do not add dependency
    Found eslint-config-prettier@8.5.0, do not add dependency
    Found eslint-plugin-prettier@4.2.1, do not add dependency
    .prettierrc is overwritten
    Does not support .eslintrc.js
    Append plugin and rule from ./eslintrc-prettier.template to .eslintrc.js. Then, delete the template file.
CREATE eslintrc-prettier.template (425 bytes)
UPDATE .prettierrc (259 bytes)
✔ Packages installed successfully.

We verify that schematic has replaced the variables of .eslintrc-prettier.template with option values

extends: ['plugin:prettier/recommended']
rules: {
    'prettier/prettier': ['error', {
        'singleQuote': false, 
        'trailingComma': 'all', 
        'tabWidth': 2, 
        'semi': true, 
        'printWidth': 140, 
        'bracketSpacing': true, 
        'useTabs': false,
        'arrowParens': 'always',
        'quoteProps': 'as-needed',
        'requirePragma': false,
        'insertPragma': false, 
    }]
}

The manual work is to copy “prettier/prettier” and prettier plugin to .eslintrc.js

// .eslintrc.js

extends: [
  'plugin:@typescript-eslint/recommended',
  'plugin:prettier/recommended'
],

rules: {
    '@typescript-eslint/interface-name-prefix': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
    'prettier/prettier': ['error', {
      'singleQuote': false, 
      'trailingComma': 'all', 
      'tabWidth': 2, 
      'semi': true, 
      'printWidth': 140, 
      'bracketSpacing': true, 
      'useTabs': false,
      'arrowParens': 'always',
      'quoteProps': 'as-needed',
      'requirePragma': false,
      'insertPragma': false, 
    }]
},

Publish the nestjs library

My experience is to publish the library on the command line. We need to create an account in npmjs.com and enable 2FA to secure the account.

Subsequently, go back to the command line to add user on the machine

npm adduser

npm notice Log in on https://registry.npmjs.org/
Username:
Password: 
Email: (this IS public)

Then, publish the library and input OTP to deploy to npmjs.com

npm publish
Enter OTP:

Final thoughts

In this post, we have seen that writing nestjs library is made possible by Angular schematics. After testing the library locally, we can publish it to npmjs.com, execute nest add command to install the library and generates code in nestjs application

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. nest-prettier library: https://www.npmjs.com/package/nest-prettier
  2. Generating code using schematics: https://angular.io/guide/schematics

Refactor Reactive Form with components in Angular

Reading Time: 5 minutes

 68 total views

Introduction

The simple CSS grid generator has three reactive forms while these forms have material components that behave similarly. In grid form. Grid gap is a number input field and Gap unit is a drop down list. Similarly, Grid Column Gap and Grid Column Unit are number input field and drop down list respectively. We can refactor the reactive form with components to encapsulate the behavior and eliminate duplicate codes.

The component encapsulates an input field and a drop down list. It also accepts inputs from parent component and passes the values to the input controls to populate data.

In this post, we are going to create a new component consisted of two input controls and render it in the reactive form. Furthermore, we can repeat the same exercise to reuse the same component in grid template columns and grid template rows forms.

let's go

Define the interface for the new component

First, I define an interface to store the data that the input field and drop down list require.

export interface CompositeFieldDropdownConfiguration {
    controlName: string
    placeholder: string
    type: string
    min?: number | string
    max?: number | string
    unitControlName: string
    unitPlaceholder: string
    list: { value: string; text: string }[]
}
  • controlName – Control name of the input field
  • placeholder – Placeholder of the input field
  • type – Type of the input field. It could be number, text, etc
  • min – Minimum value of the input field
  • max – Maximum value of the input field
  • unitControlName – Control name of the drop down list
  • unitPlaceholder – Placeholder of the drop down list
  • list – list items of the drop down list

Build the new component for the reactive form

Next, we begin to build the new AppGridValueFieldComponent component. AppGridValueFieldComponent accepts a FormGroup input that is used to retrieve form control by form control name. Then, we can bind the form control to the HTML input element. It also has an CompositeFieldDropdownConfiguration input in order to pass the property values to the attributes of the HTML input elements.

@Component({
    selector: 'app-grid-value-field',
    ...
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppGridValueFieldComponent implements OnInit {
    @Input()
    formGroup: FormGroup

    @Input()
    fieldConfiguration: CompositeFieldDropdownConfiguration

    formControl: FormControl
    unitFormControl: FormControl

    ngOnInit() {
        const { controlName, unitControlName } = this.fieldConfiguration
        this.formControl = this.formGroup.get(controlName) as FormControl
        this.unitFormControl = this.formGroup.get(unitControlName) as FormControl
    }
}

In ngOnInit, we use controlName and unitControlName to look up the form controls in formGroup and assign them to instance members. Next, we register these instance members in the inline template.

Add inline template and style in the AppGridValueFieldComponent component

@Component({
    selector: 'app-grid-value-field',
    template: `
        <ng-container [formGroup]="formGroup">
            <mat-form-field appControlErrorContainer>
                <input
                    matInput
                    [formControl]="formControl"
                    [type]="fieldConfiguration.type || 'number'"
                    [placeholder]="fieldConfiguration.placeholder"
                    [min]="fieldConfiguration.min ?? null"
                    [max]="fieldConfiguration.max ?? null"
                />
            </mat-form-field>
            <mat-form-field>
                <mat-select [placeholder]="fieldConfiguration.unitPlaceholder" [formControl]="unitFormControl">
                    <mat-option *ngFor="let item of fieldConfiguration.list" [value]="item.value">
                        {{ item.text }}
                    </mat-option>
                </mat-select>
            </mat-form-field>
        </ng-container>
    `,
    styles: [
        `
            :host {
                display: block;
            }

            mat-form-field {
                margin-right: 0.5rem;
            }
        `,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush,
})

Then, we add inline template and inline styles to AppGridValueFieldComponent. <ng-container [formGroup]=”formGroup”> is required in order to register formControl instance. For the input field, we can optionally set type, placeholder, min value and max value. For the drop down list, we can optionally set placeholder and populate list items.

As the result, this is the complete code listing of the AppGridValueFieldComponent component

import { FormControl, FormGroup } from '@angular/forms'
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'
import { CompositeFieldDropdownConfiguration } from './appgrid-value-field.interface'

@Component({
    selector: 'app-grid-value-field',
    template: `
        <ng-container [formGroup]="formGroup">
            <mat-form-field appControlErrorContainer>
                <input
                    matInput
                    [formControl]="formControl"
                    [type]="fieldConfiguration.type || 'number'"
                    [placeholder]="fieldConfiguration.placeholder"
                    [min]="fieldConfiguration.min ?? null"
                    [max]="fieldConfiguration.max ?? null"
                />
            </mat-form-field>
            <mat-form-field>
                <mat-select [placeholder]="fieldConfiguration.unitPlaceholder" [formControl]="unitFormControl">
                    <mat-option *ngFor="let item of fieldConfiguration.list" [value]="item.value">
                        {{ item.text }}
                    </mat-option>
                </mat-select>
            </mat-form-field>
        </ng-container>
    `,
    styles: [
        `
            :host {
                display: block;
            }

            mat-form-field {
                margin-right: 0.5rem;
            }
        `,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppGridValueFieldComponent implements OnInit {
    @Input()
    formGroup: FormGroup

    @Input()
    fieldConfiguration: CompositeFieldDropdownConfiguration

    formControl: FormControl
    unitFormControl: FormControl

    ngOnInit() {
        const { controlName, unitControlName } = this.fieldConfiguration
        this.formControl = this.formGroup.get(controlName) as FormControl
        this.unitFormControl = this.formGroup.get(unitControlName) as FormControl
    }
}

Refactor reactive form to use the AppGridValueFieldComponent component

The refactoring of the reactive form begins with the declaration of new members to hold the configurations of Gap and Gap Column.

// AppgridFormComponent

gapConfiguration: CompositeFieldDropdownConfiguration
gapColConfiguration: CompositeFieldDropdownConfiguration

We continue to configure the values consumed by gap and gap column.

this.gapConfiguration = {
     controlName: 'gap',
     placeholder: 'Grid Gap',
     type: 'number',
     min: 0,
     max: 20,
     unitControlName: 'gapUnit',
     unitPlaceholder: 'Gap Unit',
     list: this.gapUnits.map((unit) => ({ text: unit, value: unit })),
}

this.gapColConfiguration = {
     controlName: 'gapCol',
     placeholder: 'Grid Column Gap',
     type: 'number',
     min: 0,
     max: 20,
     unitControlName: 'gapColUnit',
     unitPlaceholder: 'Gap Column Unit',
     list: this.gapColUnits.map((unit) => ({ text: unit, value: unit })),
}

After the setup, we are ready to refactor the reactive form in the template to use AppGridValueFieldComponent, gapConfiguration and gapColConfiguration.

<ng-container class="dimensions" [formGroup]="form">
     ... other material form fields ...
    <app-grid-value-field
        [formGroup]="form"
        [fieldConfiguration]="gapConfiguration"
    ></app-grid-value-field>
    <app-grid-value-field
        *ngIf="numGapLengths.value === 2"
        [formGroup]="form"
        [fieldConfiguration]="gapColConfiguration"
    ></app-grid-value-field>
    ... other component ...
</ng-container>

The template hides the details in components and results to shorter and maintainable markup.

Refactor reactive form of grid template rows and grid template columns forms

Finally, we apply the same process to refactor the reactive form of grid template rows and grid template column forms. The goal is to replace min value and max value with the reusable component.

Similarly, we declare and initialize new instance members in template form component.

// AppTemplateFormComponent

minWidthConfiguration: CompositeFieldDropdownConfiguration
maxWidthConfiguration: CompositeFieldDropdownConfiguration

this.minWidthConfiguration = {
    controlName: 'minWidth',
    placeholder: 'Min value',
    type: 'number',
    min: 1,
    unitControlName: 'minUnit',
    unitPlaceholder: 'Unit',
    list: [],
}

this.maxWidthConfiguration = {
     controlName: 'maxWidth',
     placeholder: 'Max value',
     type: 'number',
     min: 1,
     unitControlName: 'maxUnit',
     unitPlaceholder: 'Unit',
     list: this.units.map((unit) => ({ text: unit, value: unit })),
}

The next step is to get rid of the input fields of min value and max value and their sibling drop down lists from the template.

<ng-container [formGroup]="form">
    <fieldset class="settings">
        .. omitted for brevity ....
        <section class="section-width">
            <app-grid-value-field
                [formGroup]="form"
                [fieldConfiguration]="minWidthConfiguration"
            ></app-grid-value-field>
            <app-grid-value-field
                *ngIf="minmax.value === 'true'"
                [formGroup]="form"
                [fieldConfiguration]="maxWidthConfiguration"
            >
            </app-grid-value-field>
        </section>
    </fieldset>
</ng-container>

As the result, we have rebuilt reactive forms with customized and resusable component. When we relaunch the application, the behavior of these reactive forms should stay the same.

Final thoughts

In this post, we have seen one way of creating component to use in a reactive form. The component is reusable in any form group and we have seen it in AppgridForm, Grid Template Rows and Grid Template Columns forms.

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-simple-cssgrid-generator
  2. FormControl: https://angular.io/api/forms/FormControl

Generate Dynamic Reactive Form in Angular

Reading Time: 5 minutes

 86 total views,  1 views today

Introduction

I wrote a small Angular application to learn CSS grid a few years ago. The application has a reactive form with three form groups to input data to generate CSS grid codes. Defining similar form groups is tedious; therefore, I decide to generate dynamic reactive form based on the form group configurations.

In this post, we are going to show the configuration of form groups and the helper functions that iterate the configurations to create the reactive form.

Final result of the dynamic reactive form

The page has 3 form groups: Grid form, Grid template columns and Grid template rows. Start from scratch to this state takes the following steps

  • Create the interfaces and types of form group configuration
  • Define the value, properties and validations of each form group
  • Combine form group configurations into form configuration
  • Pass the form configuration to functions to generate reactive form using FormBuilder service and FormControl.
  • Bind reactive form to HTML template

Without further delays, we show the codes added in each step until we initialize the reactive form in ngOnInit().

let's go

Create configuration types and interfaces to generate dynamic reactive form

interface CustomFormControlOptions extends AbstractControlOptions {
    initialValue: string | number
}

export type FormGroupConfiguration = Record<string, CustomFormControlOptions>

CustomFormControlOptions has all the properties of AbstractControlOptions and an initial value. In this application, the initial value is either a number or string.

FormGroupConfiguration is a Object map that stores the name and definition of form controls.

These are the type and interface needed to generate the dynamic reactive form.

Define the value, properties and validations of each form group

Since the dynamic reactive form has 3 form groups, we are going to define 3 form group configurations.

First, we set up the initial values of Grid form group and reuse these values in the form group configuration

export const GRID_FORM_START_WITH: GridForm = {
    heightInPixel: 150,
    numDivs: 20,
    gridAutoFlow: 'row',
    numGapLengths: 1,
    gap: 0,
    gapUnit: 'px',
    gapCol: 0,
    gapColUnit: 'px',
    gridAutoRowsKeyword: 'auto',
    gridAutoRowsField: 0,
    gridAutoRowsUnit: 'px',
}

Second, we reuse the starting values in the form group configuration

const GRID_CONTROL_NAMES: FormGroupConfiguration = {
    heightInPixel: {
        initialValue: GRID_FORM_START_WITH.heightInPixel,
        updateOn: 'blur',
    },
    numDivs: {
        initialValue: GRID_FORM_START_WITH.numDivs,
        updateOn: 'blur',
    },
    gridAutoFlow: {
        initialValue: GRID_FORM_START_WITH.gridAutoFlow,
    },
    numGapLengths: {
        initialValue: GRID_FORM_START_WITH.numGapLengths,
    },
    gap: {
        initialValue: GRID_FORM_START_WITH.gap,
        updateOn: 'blur',
        validators: Validators.min(0),
    },
    gapUnit: {
        initialValue: GRID_FORM_START_WITH.gapUnit,
    },
    gapCol: {
        initialValue: GRID_FORM_START_WITH.gapCol,
        updateOn: 'blur',
        validators: Validators.min(0),
    },
    gapColUnit: {
        initialValue: GRID_FORM_START_WITH.gapColUnit,
    },
    gridAutoRowsKeyword: {
        initialValue: GRID_FORM_START_WITH.gridAutoRowsKeyword,
    },
    gridAutoRowsField: {
        initialValue: GRID_FORM_START_WITH.gridAutoRowsField,
        updateOn: 'blur',
    },
    gridAutoRowsUnit: {
        initialValue: GRID_FORM_START_WITH.gridAutoRowsUnit,
    },
}

updateOn property is the updateOn option of FormControl. Similarly, validators property defines the validation functions of FormControl.

Then, we repeat the same process to define the form group of Grid Template Column.

Grid Template Column and Grid Template Row form groups have some commont form control names and values; therefore, we share the names and values in DEFAULT_PROPERTIES map

const DEFAULT_PROPERTIES: FormGroupConfiguration = {
    repeat: {
        initialValue: 'true',
    },
    numOfTimes: {
        initialValue: 2,
        updateOn: 'blur',
    },
    minmax: {
        initialValue: 'true',
    },
    minUnit: {
        initialValue: 'px',
    },
    maxWidth: {
        initialValue: 1,
        updateOn: 'blur',
    },
    maxUnit: {
        initialValue: 'fr',
    },
}
export const TEMPLATE_COLUMNS_START_WITH: GridTemplateInfo = {
    repeat: `${DEFAULT_PROPERTIES.repeat.initialValue}`,
    numOfTimes: 5,
    minmax: `${DEFAULT_PROPERTIES.minmax.initialValue}`,
    minWidth: 10,
    minUnit: <GridUnitsType>`${DEFAULT_PROPERTIES.minUnit.initialValue}`,
    maxWidth: +DEFAULT_PROPERTIES.maxWidth.initialValue,
    maxUnit: <GridUnitsType>`${DEFAULT_PROPERTIES.maxUnit.initialValue}`,
}

The following is the initial values of Grid Template Column form group

const GRID_TEMPLATE_COLUMN_CONTROL_NAMES: FormGroupConfiguration = {
    ...DEFAULT_PROPERTIES,
    numOfTimes: {
        initialValue: TEMPLATE_COLUMNS_START_WITH.numOfTimes,
        updateOn: 'blur',
    },
    minWidth: {
        initialValue: TEMPLATE_COLUMNS_START_WITH.minWidth,
        updateOn: 'blur',
    },
}

Finally, we define the Grid Template Row form group configuration

export const TEMPLATE_ROWS_START_WITH: GridTemplateInfo = {
    repeat: `${DEFAULT_PROPERTIES.repeat.initialValue}`,
    numOfTimes: +DEFAULT_PROPERTIES.numOfTimes.initialValue,
    minmax: `${DEFAULT_PROPERTIES.minmax.initialValue}`,
    minWidth: 20,
    minUnit: <GridUnitsType>`${DEFAULT_PROPERTIES.minUnit.initialValue}`,
    maxWidth: +DEFAULT_PROPERTIES.maxWidth.initialValue,
    maxUnit: <GridUnitsType>`${DEFAULT_PROPERTIES.maxUnit.initialValue}`,
}
const GRID_TEMPLATE_ROW_COLUMN_NAMES: FormGroupConfiguration = {
    ...DEFAULT_PROPERTIES,
    minWidth: {
        initialValue: TEMPLATE_ROWS_START_WITH.minWidth,
        updateOn: 'blur',
    },
}

We have completed the tough part and the next three parts only get easier

Combine form group configurations into form configuration

export const FORM_CONFIGURATION: Record<string, FormGroupConfiguration> = {
    grid: GRID_CONTROL_NAMES,
    gridTemplateColumns: GRID_TEMPLATE_COLUMN_CONTROL_NAMES,
    gridTemplateRows: GRID_TEMPLATE_ROW_COLUMN_NAMES,
}

FORM_CONFIGURATION is a form configuration composed of grid form group, grid template column form group and grid template row form group

Generate dynamic reactive form

In order to generate the dynamic reactive form, we have to create two helper functions: createFromGroup and

private createFormGroup(controlNames: FormGroupConfiguration): Record<string, FormControl> {
        return Object.keys(controlNames).reduce((acc, field) => {
            const option = controlNames[field]
            const { initialValue: value, updateOn, validators, asyncValidators } = option
            const control = updateOn
                ? new FormControl(value, { updateOn, validators, asyncValidators })
                : new FormControl(value, validators, asyncValidators)
            acc[field] = control
            return acc
        }, {} as Record<string, FormControl>)
}
private createForm(formConfiguration: Record<string, FormGroupConfiguration>) {
        return Object.keys(formConfiguration).reduce((acc: Record<string, FormGroup>, formGroupName) => {
            const formGroupConfiguration = formConfiguration[formGroupName]
            const formGroup = this.createFormGroup(formGroupConfiguration)
            acc[formGroupName] = this.fb.group(formGroup)
            return acc
        }, {})
}

createFormGroup creates FormControl instance for each form control option and returns a form control map. createForm iterates form configuration and executes createFormGroup to return a form group map.

In ngOnit(), we put everything together to create a form group of form groups and assign to form instance member

ngOnInit() {
    this.form = this.fb.group(this.createForm(FORM_CONFIGURATION))
    ....
}

Bind reactive form to HTML template

<form [formGroup]="form" novalidate>
    <section class="dimensions">
        <app-grid-form formGroupName="grid"></app-grid-form>
    </section>
    <section class="section-form">
        <app-grid-template-form
            class="app-grid-template-form"
            formGroupName="gridTemplateColumns"
            [legend]="'Grid template columns'"
        ></app-grid-template-form>
        <app-grid-template-form
            class="app-grid-template-form"
            formGroupName="gridTemplateRows"
            [legend]="'Grid template rows'"
        ></app-grid-template-form>
    </section>
</form>

The rest is to create HTML form in the template and bind form to formGroup input. The form was refactored into components; therefore, we pass form group names (grid, gridTemplateColumns and gridTemplateRows) to the custom components to render the form controls

Bonus

Currently, we write all the configurations on the client-side, however, we can move these configurations to the server-side for enterprise scale ful-stack application. If the application uses a NoSQL database (Mongo, DynamoDB), we can persist the values as-is. If the application uses a relational database, the table can add jsonb columns to persist the configurations.

Final thoughts

In this post, we have seen one way of creating dynamic reactive form based on configuration. If we decide to add another form group, we simply define a new FormGroupConfiguration object, add it to FORM_CONFIGURATION and update the HTML template.

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-simple-cssgrid-generator
  2. FormControl: https://angular.io/api/forms/FormControl

Dynamically load components in Angular 13

Reading Time: 3 minutes

 91 total views

Introduction

Dynamically load components has simplified in Angular 13. Angular team deprecated ViewContainerRef in Angular 13 and we can load components to attached view container without it now.

Let’s recap how we load components in Angular 12. We inject ComponentFactoryResolver to map component to ComponentFactory. Then, we pass the ComponentFactory to ViewContainerRef to create an instance of ComponentRef<Component>. Through the ComponentRef, we can access the component and initialize its values to render the component in the attached view container.

In this post, we see the codes to dynamically load components in Angular 12 and Angular 13 and highlight the differences between the code snippets.

let's go

Dynamically load components in Angular 12 and 13

The use case of this post is to load FoodCardComponent in FoodShellComponent. In the html template, there is a <ng-container #viewContainerRef> with viewContainerRef.

// food-shell.component.ts  inline template

<div class="p-[1.125rem]">
  <section class="flex flex-wrap items-stretch p-2 mb-1">
     <ng-container #viewContainerRef></ng-container>
  </section>
</div>

We use ViewChild('viewContainerRef') to obtain the ViewContainerRef. Moreover, we declare ComponentRef<FoodCardComponent> array to release the memory of FoodCardComponent in ngOnDestroy to avoid memory leaks.

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

public componentRefs: ComponentRef<FoodCardComponent>[] = []

First, we show the Angular 12 codes that attaches FoodCardComponent to #viewContainerRef.

constructor(
   private componentFactoryResolver: ComponentFactoryResolver,
   private foodService: FoodService,
   private cdr: ChangeDetectorRef,
) {}

public async addDynamicFoodChoice(choice: OrderedFoodChoice): Promise<void> {
    const { FoodCardComponent } = await import('../food-card/food-card.component')
    const resolvedComponent = this.componentFactoryResolver.resolveComponentFactory(FoodCardComponent)
    const componentRef = this.orderedViewContainer.createComponent(resolvedComponent)
    const { total } = this.foodService.calculateTotal([choice])

    componentRef.instance.ordered = {
      ...choice,
    }

    componentRef.instance.total = total
    this.componentRefs.push(componentRef)

    this.orderedFood = [...this.orderedFood, choice]
    this.cdr.detectChanges()
}

Next, we show the Angular 13 codes that achieve the same result.

constructor(private foodService: FoodService, private cdr: ChangeDetectorRef) {}

public async addDynamicFoodChoice(choice: OrderedFoodChoice): Promise<void> {
    const { FoodCardComponent } = await import('../food-card/food-card.component')
    const componentRef = this.orderedViewContainer.createComponent(FoodCardComponent)
    const { total } = this.foodService.calculateTotal([choice])

    componentRef.instance.ordered = {
      ...choice,
    }

    componentRef.instance.total = total
    this.componentRefs.push(componentRef)

    this.orderedFood = [...this.orderedFood, choice]
    this.cdr.detectChanges()
 }

Compare dynamically load components between Angular 12 and 13

Lastly, we compare the new changes of load components between the two version.

The first change is the constructor does not require to inject ComponentFactoryResolver. The second change is we pass the type of the component to ViewContainerRef.createComponent() to obtain an instance of ComponentRef.

Finally, we examine the API of ViewContainerRef where createComponent is defined:

The overloaded version of createComponent accepts Type<C> as the first parameter. The second parameter is an object parameter encapsulating index, injector, ngModuleRef and prjectableNodes.

abstract createComponent<C>(componentType: Type<C>, options?: {
   index?: number;
   injector?: Injector;
   ngModuleRef?: NgModuleRef<unknown>;
   projectableNodes?: Node[][];
}): ComponentRef<C>;

Moreover, the signature of createComponent that accepts ComponentFactory is deprecated. If applications require to create a dynamic component, they should pass the component type to createComponent directly.

Final thoughts

Create dynamic components has updated in Angular 13 and ComponentFactoryResolver is deprecated since the release. When developers create dynamic components in Angular 13, they should use the new signature of createComponent and pass the component type to the method.

If existing applications are using ComponentFactoryResolver, they will have to remove all occurrences of ComponentFactoryResolver and update all arguments of ComponentFactoryResolver with arguments of component type.

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. ViewContainerRef: https://angular.io/api/core/ViewContainerRef#createcomponent

Split module into single component angular modules (SCAMs)

Reading Time: 6 minutes

 138 total views

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

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