Build Angular app with Netlify function

 54 total views,  4 views today

Introduction

I created a container component, FoodMenuComponent, to display a menu for users to order food and drink.

There are several ways to create the menu data in JSON format:

  1. Hardcoded data in the component
  2. Create static JSON data in assets folder and use a service to retrieve the data for the component
  3. Design a backend API and use a service to call the endpoint to retrieve the data
  4. Add a serverless function to return the menu data to the component

Option 1 makes component bloated because it contains both business logic and data. Business logic and data should belong to service and external data source respectively.

Option 2 is ideal for Angular beginners such that they don’t have to worry about backend design and implementation.

Option 3 requires developers to have knowledge of one or more backend frameworks, for example, Express and NestJS. Because of the additional skills, it demands more development efforts for a simple side project.

After weighing all alernatives, I chose Netlify function because I can leverage backend capability of cloud provider instead of building my own backend server.

Result

This is the end result of FoodMenuComponent

The menu is consisted of two questions and each question has at least one choice for user to choose.

Let me show you how to create a Netlify function to return menu data that FoodMenuComponent consumes and displays on browser.

Install Netlify CLI

npm install netlify-cli -g

Create Netlify function Configuration

Create netlify.toml that is the configuration file of Netlify. Add the following sections to toml file:

[build]
  publish = "dist/ng-spanish-menu"
  command = "ng build"
  functions = "./src/assets/functions"
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

and the most important line is functions = "./assets/functions".

functions = "./assets/functions" specifies the folder of Netlify functions and all functions should save to it.

Then, start Netlify CLI with Angular in development mode

ntl dev

and open the application at http://localhost:8888

Setting up First Severless Function

Let’s add our first serverless function to return menu data. We can find the function in ./assets/functions/menu.ts.

// menu.ts 

import { Handler } from "@netlify/functions";

const data = {
  "menu": [
    {
      "id": "1",
      "question": "Which appetizer(s) do you wish to order?",
      "choices": [
        {
          "id": "a",
          "name": "Egg salad",
          "description": "Egg salad",
          "currency": "USD",
          "price": 4.99,
          "available": true
        },
        ... omitted for the sake of brevity ...
      ]
    },
    {
      "id": "2",
      "question": "Which dessert(s) do you wish to order?",
      "choices": [
        {
          "id": "a1",
          "name": "Ice cream",
          "description": "Ice cream",
          "currency": "USD",
          "price": 1.99,
          "available": true
        },
        ... omitted for the sake of brevity ...
      ]
    }
  ]
}

const handler: Handler = async () => {
  return {
    statusCode: 200,
    body: JSON.stringify(data),
  };
};

export { handler };

Testing Severless Function

Get Endpoint: http://localhost:8888/.netlify/functions/menu

If the request is successful, the response displays menu data.

Adding endpoint in Angular

Open environment.ts and add a new environment variable, menuUrl, to it. Next, we will create food service to retrieve menu data through this endpoint.

export const environment = {
  production: false,
  menuUrl: 'http://localhost:8888/.netlify/functions/menu',
}

Creating food service

@Injectable({
  providedIn: 'root',
})
export class FoodService {
  constructor(private http: HttpClient) {}

  getFood(url: string): Observable<MenuItem[] | undefined> {
    return this.http.get<Menu>(url).pipe(
      pluck('menu'),
      catchError((err: Error) => {
        console.error(err)
        return of(undefined)
      }),
      share(),
    )
  }
}

getFood function requests menu data from our serverless function and returns the result as Observable<MenuItem[] | undefined>. When request fails, we return undefined since menu does not exist.

Calling service in Angular component with RxJS

Now, FoodMenuComponent can combine RxJS operators and food service to retrieve menu data and assign to Observable<Menu[] | undefined>.

@Component({
  selector: 'app-food-menu',
  templateUrl: './food-menu.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FoodMenuComponent implements OnInit, OnDestroy {
  menuItems$: Observable<MenuItem[] | undefined>

  ngOnInit(): void {
    this.menuItems$ = this.service
       .getFood(environment.menuUrl)
       .pipe(takeUntil(this.unsubscribe$))
  }

  menumItemTrackByFn(index: number, menuItem: MenuItem): string | number {
    return menuItem ? menuItem.id : index
  }

  choiceTrackByFn(index: number, choice: Choice) {
    return choice ? choice.id : index
  }
  ... Omitted for the sake of brevity ....
}

In the template of FoodMenuComponent, async pipe resolves menuItems$ into MenuItem array (menuItems) and Angular provides ngFor directive that can iterate an array to render values in each row.

<div class="container" *ngIf="menuItems$ | async as menuItems; else notAvailable">
  <app-food-menu-card *ngFor="let menuItem of menuItems; index as i; trackBy: menumItemTrackByFn">
    <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)="handleFoodChoice.emit($event)"></app-food-choice>
    </ng-container>
  </app-food-menu-card>
</div>
<ng-template #notAvailable>No menu</ng-template>

The container div iterates menuItems and passes each menuItem to <app-food-menu-card> element. Each menuItem has one question and each question displays some options for user to select. <app-food-question> is responsible for rendering menu item question and <app-food-choice> renders all available and unavailable options of the question.

We have reached the end of the post and FoodMenuComponent is working. You should see the same component that I showed at the beginning of the post.

That’s it! I hope you find this article useful and leverage serverless function in your Angular . Good bye!

Resources:

  1. Repo: https://github.com/railsstudent/ng-spanish-menu
  2. https://www.netlify.com/blog/2021/01/25/getting-started-with-netlify-functions-for-angular/

Angular and Storybook – Component with content projection

 16 total views,  4 views today

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

 7 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

 6 total views,  2 views today

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/

Use Github Action to deploy Angular app to github page

 0 total views

  • Generate a personal access token and create ACCESS_TOKEN variable in Settings -> Secrets.
  • Keep the personal access token in a safe place and do not lose it
  • Go to Actions tab of the github repo
  • Create main.yml under .github/workflow
name: CI
on:
  push:
    branches:
      - master
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          persist-credentials: false

      - name: Install
        run: |
          npm install
          npm run github-page-deploy
      - name: Build and Deploy Repo
        uses: JamesIves/github-pages-deploy-action@releases/v3
        with:
          ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
          BASE_BRANCH: master
          BRANCH: gh-pages
          CLEAN: true
          GIT_CONFIG_NAME: <author name>
          GIT_CONFIG_EMAIL: <email address>
          FOLDER: dist/<repo name>

References:

Deploy angular app to Github Page by Travis CI

 2 total views

1) Install npm dependencies

angular-cli-ghpages is a plugin that pushes angular application to gh-pages branch.

npm install angular-cli-ghpages --save-dev 

Add prettier schematic to Angular project

npm install -g @schuchard/prettier
ng g @schuchard/prettier:add

2) Create a .travis.yml file that details the deployment steps

3) Specify language is node_js and node version is 8

language: node_js
node_js:
    - '8'

4) Cache npm dependencies and production build directory, dist.

cache:
    npm: true
    directories:
        - node_modules
        - dist/

5) Create environment variables to store github organization, repo name, github username and github email respectively. GH_TOKEN is your secret github token that is saved under Settings of your repository. You can find the link to generate github token here

env:
    global:
        - GITHUB_ORG="https://GH_TOKEN@github.com"
        - REPO_NAME="<repository name>"
        - GITHUB_NAME="<username>"
        - GITHUB_EMAIL="<email address>"

6) Add “before_script” to install npm dependencies without modifying package-lock.json file

before_script:
    - npm install --no-save

7) Add “script” to run different tasks before the final deployment

script:
    - npm audit
    - npm run prettier
    - npm run lint
    - npm run test-headless
    - npm run build-ngh

branches:
    only:
        - master

8) The above tasks are created in scripts of package.json:

  • npm audit audits npm dependencies to ensure vulnerabilities do not exist
  • npm run prettier formats files with Prettier code formatter
  • npm run lint performs linting on source files to identify any lint problem and aborts the process prematurely
  • npm run test-headless executes test cases in headless Chrome browser and terminates if any test case fails
  • npm run build-ngh compiles the source codes on master branch to production build and places the artifacts to dist/<project> directory
"scripts": {
    "prettier": "prettier --write \"**/*.{js,json,scss,ts}\"",
    "lint": "ng lint",
    "build-ngh": "ng build --prod --base-href \"https://railsstudent.github.io/<repository name>/\"",
    "test-headless": "ng test --watch=false --browsers=ChromeHeadless"
  }

9) If all script tasks finishes successfully, the task in “after_success” is executed to push the static pages to gh_pages branch

after_success:
    - ngh --repo="$GITHUB_ORG/$GITHUB_NAME/$REPO_NAME.git" --name="$GITHUB_NAME" --email="$GITHUB_EMAIL" --dir=dist/ng-rxjs-state-management-demo --no-silent

10) Add notifications configuration to receive notification email when travis build fails

notifications:
email:
recipients:
- somebody@example.come
on_success: never # default: change
on_failure: always # default: always

11) Navigate to https://<username>.github.io/<repo name>/ to view the static pages

Workaround for angular production build stuck at 92%

 0 total views

1) Add –build-optimizer false to turn off optimizer to sacrifice bundle size for space

ng build --prod --build-optimizer false

2) Add script to package.json

"scripts": {
    "build-prod": "ng build --prod --build-optimizer false",
 }

With npm,

 npm run build-prod

or with yarn,

 yarn build-prod

Build artifacts can be found in dist/ directory