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

Angular and Storybook – Mock Data in Component

Reading Time: 5 minutes

 277 total views

Introduction

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

This is the component tree of food menu component.

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

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

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

Create Food menu component in food module

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

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

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

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

  qtyMap: Record<string, number> | undefined

  constructor(private service: FoodService) {}

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

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

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

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

  .. omit trivial logic ...

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

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

Set up mock service and data

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

// mock.ts

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

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

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

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

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

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

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

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

// constants.ts

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

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

Add Storybook for Food Menu Component

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

// food-menu.storeis.ts

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

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

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

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

export const Menu = Template.bind({})

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

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

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

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

Start storybook application

npm run storybook

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

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

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

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

Final thought

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

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

Resources:

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

Angular and Storybook – Publish to Chromatic

Reading Time: 3 minutes

 196 total views

Introduction

In the ng spanish menu repo, I created some Angular components and storybooks for the application. I thought it would be an excellent idea to deploy the storybooks to Chromatic to share with other Angular developers for feedback.

Install Chromatic dependencies

npm i --save-dev chromatic

Create Chromatic app for Github repository

  • Sign in with Github account
  • Choose a project to add to Chromatic and set up npm script in package.json for publication
"scripts": {
    "chromatic": "npx chromatic --project-token=<Chromatic project token>",
}
  • Run "npm run chromatic" to manually publish build 1

Whenever we finish the design of components and test thoroughly in Storybook, we can publish them to Chromatic with the script.

However, the process is manual and interrupts developers’ concentration on coding. Developers’ time is valuable and developers should focus on building application rather than manual devOps. Therefore, we should leverage the Github action of Chromatic to automate it’s publish process.

Automate Chromatic with Github action

First, add a new repository secret to store the Chromatic project token.

Go to Settings -> Secrets -> Actions -> New repository secret,

Name: CHROMATIC_PROJECT_TOKEN

Value: <the project token seen in chromatic script>

Second, in .github/workflow directory, create a main.yml that uses the Chromatic action

// main.yml

name: CI
on:
  push:
    branches:
      - main
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          persist-credentials: false
          fetch-depth: 0 # 👈 Required to retrieve git history

      - name: Setup node 14
        uses: actions/setup-node@v1
        with:
          node-version: 14.x

      - name: Publish to Chromatic
        uses: chromaui/action@v1
        # Chromatic GitHub Action options
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          exitZeroOnChanges: true 
          autoAcceptChanges: true

exitZeroOnChanges and autoAcceptChanges are optional properties and can be included depending on use cases.

exitZeroOnChanges: Option to prevent the workflow from failing

autoAcceptChanges: Option to accept all changes

Publish Storybook

Next, Make changes in the project and push codes to Github repository to trigger Github action to run.

Build passed and new changes were auto-accepted.

Finally, Select storybooks on the side menu to display components and interact with input fields to confirm outputs and events are emitted.

This is the end of the blog post and I hope the contents explain the necessary procedure to publish storybooks to Chromatic and share the static app in team for review and collaboration.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Chromatic Application: https://613821be390968003af8c529-lzttnlkywp.chromatic.com
  3. Deploy Storybook: https://storybook.js.org/tutorials/intro-to-storybook/angular/en/deploy/
  4. Automate Chromatic with Github Action: https://www.chromatic.com/docs/github-actions

Angular and Storybook – Component with content projection

Reading Time: 3 minutes

 180 total views

In Bonnie’s visual DOM course, I learnt the technique of applying multiple content projections in a component.

I created a FoodMenuCardComponent with two content projections; i.e., a component with two <ng-content> elements. The first <ng-content> has selector ‘head’ that projects FoodQuestion component and the second <ng-content> has selector ‘body’ that projects a list of FoodChoice components.

The same result can be achieved without content projections but I want to practice the new concept and create a reusable card component that has head and body sections.

Create a food menu card component in food module

ng g c foodMenuCard --module=food
// food-menu-card.component.ts 
import { Component } from '@angular/core'

@Component({
  selector: 'app-food-menu-card',
  template: `
  <div>
    <ng-content select="[head]"></ng-content>
    <ng-content select="[body]"></ng-content>
  </div>`,
})
export class FoodMenuCardComponent {}

Create Storybook for Food Menu Card Component

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

// food-menu-card.storeis.ts

import { moduleMetadata } from '@storybook/angular'
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
import { action } from '@storybook/addon-actions'
import { Story, Meta } from '@storybook/angular/types-6-0'
import { FoodChoiceComponent, FoodMenuCardComponent, FoodQuestionComponent } from '@/food'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'

export default {
  title: 'Food Menu Card',
  component: FoodMenuCardComponent,
  decorators: [
    moduleMetadata({
      imports: [ReactiveFormsModule, FormsModule],
      declarations: [FoodQuestionComponent, FoodChoiceComponent],
    }),
  ],
} as Meta

const FoodMenuCardTemplate: Story<FoodMenuCardComponent> = (args: FoodMenuCardComponent) => ({
  props: {
    ...args,
    foodChoiceAdded: action('foodChoiceAdded'),
  },
  template: `<app-food-menu-card>
    <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" (foodChoiceAdded)="foodChoiceAdded($event)"></app-food-choice>
    </ng-container>
  </app-food-menu-card>`,
})

export const Primary = FoodMenuCardTemplate.bind({})
Primary.args = {
  menuItem: {
    id: '1',
    question: 'Do you wish to order dessert?',
    choices: [
      {
        id: 'd',
        name: 'Buffalo Chicken Wings',
        description: 'Spicy chicken wings',
        price: 8.99,
        available: true,
      },
    ],
  },
}

export const Soldout = FoodMenuCardTemplate.bind({})
Soldout.args = {
  menuItem: {
    id: '1',
    question: 'Do you wish to order dessert?',
    choices: [
      {
        id: 'a',
        name: 'Egg salad',
        description: 'Egg salad',
        price: 4.99,
        available: false,
      },
    ],
  },
}

To order to create story for component with content projection, my solution is to create template component for FoodChoiceMenuComponent

template: <app-food-menu-card>
    <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" (foodChoiceAdded)="foodChoiceAdded($event)"></app-food-choice>
    </ng-container>
</app-food-menu-card>

<app-food-menu-card> is root element containing <app-food-question> and <ng-container> children. <app-root-question> has head attribute and replaces <ng-content select='[head]’> element whereas <ng-container> has body attribute and replaces <ng-content select='[body]’> element.

Two stories are created for FoodMenuCardComponent: Primary and Soldout. Primary renders question and available food choices whereas Soldout renders question and a sold out message.

Unfortunately, Primary story did not log action when Submit button was clicked. After googling, I found the solution in Github issue and Storybook examples repo. I had to pass addon action to props to listen to foodChoiceAdded event emitter of FoodChoiceComponent

props: {
    ...args,
    foodChoiceAdded: action('foodChoiceAdded'),
}

Property name is foodChoiceAdded that is the name of the event emitter. Action name is ‘foodChoiceAdded’ but it can be arbitrary text such as action(‘log’). When button is clicked, Storybook Actions tab logs ‘foodChoiceAdded’ name and its data.

Start storybook application

npm run storybook

Click the title Food Menu Card -> Primary and the component is rendered with a question and once choice

When quantity is updated and submitted, event is emitted and logged on Actions tab

Click the title Food Menu Card -> Soldout and the component is rendered with a question and a sold out message.

This is the end of the blog post and we will keep you posted after progress is made, thanks.

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
  3. Angular Storybook Netlify App: https://storybooks-angular.netlify.app/?path=/story/custom-style–default
  4. Angular Storybook Examples: https://github.com/storybookjs/storybook/blob/next/examples/angular-cli/src/stories/core/styles/story-styles.stories.ts
  5. Action Logger does not handle event with template prop: https://github.com/storybookjs/storybook/issues/4820

Angular and Storybook – Simple Component with inputs and actions

Reading Time: 4 minutes

 163 total views

Previously, I created a story book for simple component with inputs only (link) . In this post, I want to add new stories for a simple components with both input and action.

The component is FoodChoiceComponent that has two states: choice available to order and choice that has sold out.

If choice is available, user can input quantity and submit the reactive form. Otherwise, the component hides the form and displays a message, “Sorry, this choice is sold out”.

Create a food choice component in food module

ng g c foodChoice --module=food

Food choice component is a simple presentational component that is consisted of name, description, price, quantity and a reactive form.

// food-choice.component.ts 

// for the sakes of brevity, import statements are omitted

@Component({
  selector: 'app-food-choice',
  templateUrl: './food-choice.component.html',
  styleUrls: ['./food-choice.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FoodChoiceComponent implements OnInit, OnDestroy {
  @Input()
  choice: Choice

  @Output()
  foodChoiceAdded = new EventEmitter<OrderedFoodChoice>()

  submitChoice$ = new Subject<Event>()
  unsubscribe$ = new Subject<boolean>()
  processing = false

  form = this.fb.group({
    quantity: new FormControl(1, [Validators.required, Validators.min(1)]),
  })

  constructor(private fb: FormBuilder) {}

  ngOnInit(): void {
    this.submitChoice$
      .pipe(
        tap(($event) => {
          $event.preventDefault()
          $event.stopPropagation()
          this.processing = true
        }),
        delay(1000),
        map(() => ({
          ...this.form.value,
          name: this.choice.name,
          description: this.choice.description,
          price: this.choice.price,
        })),
        tap(() => (this.processing = false)),
        takeUntil(this.unsubscribe$),
      )
      .subscribe((value) => this.foodChoiceAdded.emit(value))
  }

  get quantity() {
    return this.form.get('quantity') as FormControl
  }

  ngOnDestroy(): void {
    this.unsubscribe$.next(true)
    this.unsubscribe$.complete()
  }
}
// food-choice.component.html

<div class="container">
  ...
  <ng-container [ngSwitch]="choice.available">
    <form [formGroup]="form" (ngSubmit)="submitChoice$.next($event)" *ngSwitchCase="true">
      <label name="quantity" class="item">
        <span class="field">Quantity: </span>
        <input type="number" formControlName="quantity" />
      </label>
      <div *ngIf="quantity.invalid && (quantity.touched || quantity.dirty)" class="alert alert-danger">
        <div *ngIf="quantity.errors?.min" class="error">Quantity must be at least {{ quantity.errors?.min?.min }}.</div>
      </div>
      <div *ngIf="processing" class="alert">
        <div class="processing">Loading...</div>
      </div>
      <button type="submit" [disabled]="processing || form.value.quantity <= 0">Submit</button>
    </form>
    <ng-container *ngSwitchDefault>
      <ng-container *ngTemplateOutlet="soldout; context: { choice: choice }"> </ng-container>
    </ng-container>
  </ng-container>
  <ng-template #soldout let-choice="choice">
    <span class="item sold-out">Sorry, {{ choice.name }} is sold out</span>
  </ng-template>
</div>

Input is a Choice interface that has several properties

export interface Choice {
  id: string
  name: string
  description: string
  price: number
  available: boolean
}
If available is true, reactive form is rendered.
If available is false, a message is displayed to indicate that the choice is sold out.

The food choice component has an event emitter that emits the user’s choice to container component. When user submits the reactive form, the submit event notifies RxJS subject to process the data and to emit the result to container component.

<form [formGroup]="form" (ngSubmit)="submitChoice$.next($event)" *ngIf="choice.available; else soldout">
....
</form>
submitChoice$ = new Subject<Event>()

@Output()
foodChoiceAdded = new EventEmitter<OrderedFoodChoice>()

form = this.fb.group({
  quantity: new FormControl(1, [Validators.required, Validators.min(1)]),
})

ngOnInit(): void {
    this.submitChoice$
      .pipe(
        tap(($event) => {
          $event.preventDefault()
          $event.stopPropagation()
          this.processing = true
        }),
        delay(1000),
        map(() => ({
          ...this.form.value,
          name: this.choice.name,
          description: this.choice.description,
          price: this.choice.price,
        })),
        tap(() => (this.processing = false)),
        takeUntil(this.unsubscribe$),
      )
      .subscribe((value) => this.foodChoiceAdded.emit(value))
}

Create Storybook for Food Choice Component

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

// food-choice.storeis.ts
import { moduleMetadata } from '@storybook/angular'
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
import { Story, Meta } from '@storybook/angular/types-6-0'
import { FoodChoiceComponent } from '@/food'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'

export default {
  title: 'Food Choice',
  component: FoodChoiceComponent,
  decorators: [
    moduleMetadata({
      imports: [ReactiveFormsModule, FormsModule],
    }),
  ],
  argTypes: { onClick: { action: 'clicked' } },
} as Meta

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

export const Primary = Template.bind({})
Primary.args = {
  choice: {
    id: '1',
    name: 'Vino tinto',
    description: 'Red wine',
    price: 12.99,
    available: true,
  },
}

export const Soldout = Template.bind({})
Soldout.args = {
  choice: {
    id: '1',
    name: 'Vino tinto',
    description: 'Red wine',
    price: 12.99,
    available: false,
  },
}

Two stories are created for FoodChoiceComponent, Primary and Soldout. Primary renders that state of an available food choice while Soldout renders the state of a sold out food choice.

Start storybook application

npm run storybook

Click the title Food Choice -> Primary and the component is rendered with reactive form

When quantity is updated and submitted, event is emitted and logged on Actions tab

Click the title Food Choice -> Soldout and the component is rendered with a sold out message.

This is the end of the blog post and we will keep you posted after progress is made, thanks.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Storybook: Build a simple component: https://storybook.js.org/tutorials/intro-to-storybook/angular/en/simple-component/
  3. Actions: https://storybook.js.org/docs/angular/essentials/actions

Angular and Storybook – Simple Component

Reading Time: 3 minutes

 170 total views

I recently completed Visual DOM course at Angular Nation and the course covers advanced materials and is beneficial to Angular developers of any level. I personally struggle with the concept of view container ref (vcr) and would like to build a side project about it with my mentor, Nati.

The idea of the application is to displays a restaurant menu in Spanish (I am also learning Spanish), user can order food and beverages from the menu and the selection is added to the page dynamically through vcr. The project is at the early stage and we use storybook to visualize the components as the application comes along the way.

Create an angular application

ng new ng-spanish-menu

Install storybook dependencies

# Add Storybook:
npx sb init

The command adds storybook script in package.json and developer can run npm run storybook to launch storybook site at http://localhost:6006.

Create a food module

ng g module food

This module keeps food-related components and services.

Create a food card component in food module

ng g c foodCard --module=food

Food card component is a simple presentational component that displays name, description, price, quantity and total amount of a food/beverage.

// food-card.component.ts 

import { ChangeDetectionStrategy, Component, Input } from '@angular/core'

@Component({
  selector: 'app-food-card',
  templateUrl: './food-card.component.html',
  styleUrls: ['./food-card.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FoodCardComponent {
  @Input()
  name: string

  @Input()
  description: string

  @Input()
  price: number

  @Input()
  quantity: number

  get total() {
    return Math.round(this.price * this.quantity * 100) / 100
  }
}
// food-card.component.html

<div class="container">
  <label name="name" class="item">
    <span class="field">Name:</span>
    <span>{{ name }}</span>
  </label>
  <label class="item" name="description">
    <span class="field">Description:</span>
    <span>{{ description }}</span>
  </label>
  <label class="item" name="price">
    <span class="field">Price:</span>
    <span>{{ price }}</span>
  </label>
  <label class="item" name="quantity">
    <span class="field">Quantity:</span>
    <span>{{ quantity }}</span>
  </label>
  <label class="item" name="total">
    <span class="field">Total:</span>
    <span>USD {{ total }}</span>
  </label>
</div>

Create Storybook for Food Card Component

Create a new food folder under stories.

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

// food-card.storeis.ts
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
import { Story, Meta } from '@storybook/angular/types-6-0'
import { FoodCardComponent } from '@/food'

export default {
  title: 'Food Card',
  component: FoodCardComponent,
} as Meta

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

export const Primary = Template.bind({})
Primary.args = {
  name: 'Vino tinto',
  description: 'Red wine',
  price: 12.99,
  quantity: 3,
}

Then, input values are supplied to Primary.args and subsequently passed to FoodCardComponent to be displayed.

Start storybook application

npm run storybook

Click the title Food Card -> Primary and the component is rendered with initial input values.

Input values can be updated and then the component re-renders with new values.

CSS is very simple currently but Nati and I can work on the styling later after functionality is completed.

I am very excited of this project because Nati is very talented Angular developer and we can learn from each other while we work it together and she teaches me Spanish.

This is the end of the blog post and we will keep you posted after progress is made, thanks.

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. Install Storybook: https://storybook.js.org/docs/react/get-started/install
  3. Storybook: Build a simple component: https://storybook.js.org/tutorials/intro-to-storybook/angular/en/simple-component/