Lab 08: The Icon Component - andreaswissel/design-systems-workshop GitHub Wiki

Hint: if you got lost, you can always check out the corresponding branch for the lab. If you want to start over, you can reset it by typing executing git reset --hard origin/lab-7

Lab

A new component joins our design system in this lab! To illustrate our points, we'll add the FontAwesome icon component.

  • Add FontAwesome to the project

Execute the following command:

ng add @fortawesome/angular-fontawesome@^0.14.0

and press Space to select all FontAwesome 6 Free icons to be installed. After that, press Enter to continue the installation.

  • Create the IconComponent

Execute the following commands:

ng g c --standalone icon
  • Include the FontAwesome icon packs in the IconComponent

In order to make the FontAwesome icon packs available in the component scope, add the following code. This enables the IconComponent to consume the SVG icons located inside of the FontAwesome package.

src/app/icon/icon.component.ts

import { Component, Input } from '@angular/core';
+import {
+  FaIconLibrary,
+  FontAwesomeModule,
+} from '@fortawesome/angular-fontawesome';
+import { far } from '@fortawesome/free-regular-svg-icons';
+import { fas } from '@fortawesome/free-solid-svg-icons';

@Component({
  selector: 'app-icon',
  standalone: true,
-  imports: [],  
+  imports: [FontAwesomeModule],
  templateUrl: './icon.component.html',
  styleUrl: './icon.component.css',
})
export class IconComponent {
+  constructor(library: FaIconLibrary) {
+    library.addIconPacks(fas, far);
+  }
}
  • Prepare the IconComponent

Next, we want to update the icon component with some inputs to set the icon and their respective color. For this, we want to add the input iconName, as well as some linking up in the template:

src/app/icon/icon.component.ts

import { Component, Input } from '@angular/core';
import {
  FaIconLibrary,
  FontAwesomeModule,
} from '@fortawesome/angular-fontawesome';
-import { far } from '@fortawesome/free-regular-svg-icons';
+import { IconName, far } from '@fortawesome/free-regular-svg-icons';
import { fas } from '@fortawesome/free-solid-svg-icons';

@Component({
  selector: 'app-icon',
  standalone: true,
  imports: [FontAwesomeModule],
  templateUrl: './icon.component.html',
  styleUrl: './icon.component.s css',
})
export class IconComponent {
+  @Input() public iconName: IconName = 'fingerprint';

  constructor(library: FaIconLibrary) {
    library.addIconPacks(fas, far);
  }
}

src/app/icon/icon.component.html

+<fa-icon [icon]="iconName"></fa-icon>
  • Make sure to also create the stories file:
import type { Meta, StoryObj } from '@storybook/angular';

import { IconComponent } from './icon.component';

const meta: Meta<IconComponent> = {
  title: 'Components/Icon',
  component: IconComponent,
  tags: ['autodocs'],
};

export const Default: StoryObj<IconComponent> = {
  args: {
    iconName: 'coffee'
  },
};

export default meta;

If everything has worked out properly, your IconComponent story should look like this:

  • Create an icon variant of the button

Next, we want to create a new variant of the button. Navigate to the Figma workbook and add a new button variant, using the Reference Library as a specification. Following that, we want to add an input to the ButtonComponent and display an icon conditionally:

src/app/button/button.component.ts

import { Component, Input, OnInit } from '@angular/core';
+import { IconName } from '@fortawesome/fontawesome-svg-core';

export type ButtonType = 'primary' | 'secondary';

/**
 * This is the button component. It can be used to build clicky things!
 *
 * See [Figma reference](https://www.figma.com/file/X1JRLUCEp6JnoeKAxRPNeF/Angular-Architects-Design-Systems?node-id=1%3A3) for specifications.
 */
@Component({
  selector: 'app-button',
  templateUrl: './button.component.html',
  styleUrls: ['./button.component.scss'],
})
export class ButtonComponent implements OnInit {
  @Input() public label: string = 'Button Label';
  @Input() public type: ButtonType = 'primary';
+  @Input() public icon: IconName | undefined;

  constructor() {}

  ngOnInit(): void {}
}

src/app/button/button.component.html

<button [attr.class]="type">
+  <app-icon [iconName]="icon" *ngIf="icon"></app-icon>
  {{ label }}
</button>
  • Don't forget to update the imports of your component

src/app/button/button.component.ts

import { Component, Input } from '@angular/core';
import { IconName } from '@fortawesome/fontawesome-svg-core';
+import { IconComponent } from '../icon/icon.component';
+import { CommonModule } from '@angular/common';

type ButtonType = 'primary' | 'secondary';

/**
 * This is the button component. It can be used to build clicky things!
 *
 * [Figma reference](https://www.figma.com/file/X1JRLUCEp6JnoeKAxRPNeF/Angular-Architects-Design-Systems?node-id=1%3A3)
 *  |
 * [Implementation](https://github.com/andreaswissel/design-systems-workshop-latest/blob/lab-6/src/app/button/button.component.ts)
 *  |
 * [Specification](https://github.com/andreaswissel/design-systems-workshop/wiki/Lab-06:-Documentation)
 */
@Component({
  selector: 'app-button',
  standalone: true,
-  imports: [],  
+  imports: [IconComponent, CommonModule],
  templateUrl: './button.component.html',
  styleUrl: './button.component.scss',
})
export class ButtonComponent {
  @Input() public label: string = 'Button Label';
  @Input() public type: ButtonType = 'primary';
  @Input() public icon: IconName | undefined;
}
  • Add a new story for the icon button

src/app/button/button.stories.ts

import { type Meta, type StoryObj } from '@storybook/angular';

import { ButtonComponent } from './button.component';

const meta: Meta<ButtonComponent> = {
  title: 'Components/Button',
  component: ButtonComponent,
  tags: ['autodocs'],
};

export const Default: StoryObj<ButtonComponent> = {
  args: {},
};

+export const WithIcon: StoryObj<ButtonComponent> = {
+  args: {
+    label: 'Want to buy me a coffee?',
+    icon: 'coffee',
+  },
+};

export default meta;

Self check

  • The IconComponent is displayed correctly in Storybook
  • The ButtonComponent correctly displays an icon conditionally

Solutions

Icon HTML file

src/app/icon/icon.component.html

<fa-icon [icon]="iconName"></fa-icon>
Icon component file

src/app/icon/icon.component.ts

import { Component, Input } from '@angular/core';
import {
  FaIconLibrary,
  FontAwesomeModule,
} from '@fortawesome/angular-fontawesome';
import { IconName, far } from '@fortawesome/free-regular-svg-icons';
import { fas } from '@fortawesome/free-solid-svg-icons';

@Component({
  selector: 'app-icon',
  standalone: true,
  imports: [FontAwesomeModule],
  templateUrl: './icon.component.html',
  styleUrl: './icon.component.css',
})
export class IconComponent {
  @Input() public iconName: IconName = 'fingerprint';

  constructor(library: FaIconLibrary) {
    library.addIconPacks(fas, far);
  }
}
Icon stories file

src/app/icon/icon.stories.ts

import type { Meta, StoryObj } from '@storybook/angular';

import { IconComponent } from './icon.component';

const meta: Meta<IconComponent> = {
  title: 'Components/Icon',
  component: IconComponent,
  tags: ['autodocs'],
};

export const Default: StoryObj<IconComponent> = {
  args: {
    iconName: 'coffee',
  },
};

export default meta;
Button HTML file

src/app/button/button.component.html

<button [attr.class]="type">
  <app-icon
    [iconName]="icon"
    *ngIf="icon"
  ></app-icon>
  {{ label }}
</button>
Button SCSS file

src/app/button/button.component.scss

@import "../../styles/colors";

button {
  background: $blue-400;

  border: 1px solid $blue-700;
  box-sizing: border-box;
  box-shadow: inset 0px 2px 0px rgba(202, 230, 255, 0.15);
  border-radius: 5px;

  font-family: "SF Pro Display", Helvetica, Arial, sans-serif;
  font-style: normal;
  font-weight: 600;
  font-size: 13px;
  line-height: 16px;
  display: flex;
  align-items: center;
  text-align: center;

  color: #ffffff;

  padding: 11px 16px;

  &.primary {
    background: $blue-400;
    border: 1px solid $blue-700;

    app-icon {
      --color: $white;
    }
  }

  &.secondary {
    background: $gray-100;
    border: 1px solid $gray-200;
    color: $text-default;

    app-icon {
      --color: $text-default;
    }
  }

  app-icon {
    margin-right: 12px;
  }
}
Button Component file

src/app/button/button.component.ts

import { Component, Input } from '@angular/core';
import { IconName } from '@fortawesome/fontawesome-svg-core';
import { IconComponent } from '../icon/icon.component';
import { CommonModule } from '@angular/common';

type ButtonType = 'primary' | 'secondary';

/**
 * This is the button component. It can be used to build clicky things!
 *
 * [Figma reference](https://www.figma.com/file/X1JRLUCEp6JnoeKAxRPNeF/Angular-Architects-Design-Systems?node-id=1%3A3)
 *  |
 * [Implementation](https://github.com/andreaswissel/design-systems-workshop-latest/blob/lab-6/src/app/button/button.component.ts)
 *  |
 * [Specification](https://github.com/andreaswissel/design-systems-workshop/wiki/Lab-06:-Documentation)
 */
@Component({
  selector: 'app-button',
  standalone: true,
  imports: [IconComponent, CommonModule],
  templateUrl: './button.component.html',
  styleUrl: './button.component.scss',
})
export class ButtonComponent {
  @Input() public label: string = 'Button Label';
  @Input() public type: ButtonType = 'primary';
  @Input() public icon: IconName | undefined;
}
Button stories file

src/app/button/button.stories.ts

import { type Meta, type StoryObj } from '@storybook/angular';

import { ButtonComponent } from './button.component';

const meta: Meta<ButtonComponent> = {
  title: 'Components/Button',
  component: ButtonComponent,
  tags: ['autodocs'],
};

export const Default: StoryObj<ButtonComponent> = {
  args: {},
};

export const WithIcon: StoryObj<ButtonComponent> = {
  args: {
    label: 'Want to buy me a coffee?',
    icon: 'coffee',
  },
};

export default meta;
⚠️ **GitHub.com Fallback** ⚠️