Improve Angular code with Betterer

Reading Time: 5 minutes

 283 total views,  1 views today

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

 286 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

Add value to commit message in Angular

Reading Time: 3 minutes

 168 total views

Introduction

When a team of developers works on the project, we should add tools to ensure all developers adopt the same convention of commit messages. The tools ng-spanish-menu application use to enforce commit conventions are commitlint, commitizen and husky.

show me, don't tell me

Install dependencies of commitlint and husky locally

First, we install commitlint and husky to set up automated testing of commit messages. The automation occurs when we run git commit command.

npm install --save-dev @commitlint/{cli,config-conventional}
npm install --save-dev husky

Generate commitlint configuration file

Secondly, we add a new configuration file for commitlint.

echo "module.exports = { extends: ['@commitlint/config-conventional'] }" > commitlint.config.js

Test

Before we go any further, we launch a new terminal to test comments using commitlint

Failed test

In our first example, we display a comment that does not adopt the convention and the program outputs the error messages.

echo “hello world” | ./node_modules/.bin/commitlint

Passed test

In our second example, this comment adopts the convention and the program outputs nothing when run.

echo “chore(test): hello world” | ./node_modules/.bin/commitlint

Add husky hook to lint of commit messages

Thirdly, we automate commitlint by registering a commit-msg husky hook.

# Active hooks
npx husky install

# Add hook
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'

When developers type git commit -m "{type}{scope}: subject" to commit changes, commitlint validates the format of the message before the staged files are committed.

However, reckless developers can add –no-verify to bypass husky and defeat the intention of good engineering practice.

By chance, I discovered commitzen/cz-cli that launches interactive interface to prompt user to fill in required information and it also ignores –no-verify flag

Install commitizen

Lastly, we install the required dependencies and we should all set.

npm install --save-dev commitizen
npm install --save-dev @commitlint/prompt

Generate commitizen configuration file

echo '{ "path": "@commitlint/prompt" }' > ~/.czrc

Create npm script for commitizen

"scripts: {
   "commit": "git-gz"
}

See commitizen in action

Finally, we can make changes to the codes and witness commitzen in action for the first time.

git add .
npm run commit

Type help to see all available types to choose from and I choose “feat” as an example.

Scope is optional and I type food to indicate the change is about food service.

Enter :skip when commitizen prompts body and footer to skip providing descriptions.

Git commits my new changes successfully and keeps the message in git history.

Conclusion

Commitizen is a powerful command-line utility for advocating and sharing commit convention. It is especially when multiple teams and vendors are working on a large project and we want them to write message that has the same format, descriptive and understandable.

This is the end of the post and I hope you find this article useful and consider to become upstanding commit citizens by using commitlint and commitizen in Angular application.

Good bye!

we made it to the end of the blog post

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Commitlint install guide: https://commitlint.js.org/#/guides-local-setup
  3. Commitizen: https://github.com/commitizen/cz-cli
  4. Husky: https://github.com/typicode/husky
  5. Add commitlint and commitizen to create-react-app: https://annacoding.com/article/4ynYYJEnB733BHEnbrApnX/Add-commitlint-and-commitizen-to-Create-react-app