I have a ui component :
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatRippleModule } from '@angular/material/core';
import { TranslocoModule } from '@ngneat/transloco';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
/**
* Todo - Add description
* Todo - Add a basic button and use them in dialogs, now we use outline buttons without hover shadow
* Also remove no-shadow class
* https://material.angular.io/components/button/overview
*
* form: for when the button is of type submit and it's outside of the form tag. Add an id to the form with the same name
*
* @description
* This component is used to render a button.
*s
* @example
* <vetfysio-button
* [label]="'Button label'"
* [disabled]="false"
* (action)="action()">
* </vetfysio-button>
*/
@Component({
selector: 'vh-button',
templateUrl: './button.component.html',
styleUrls: ['./button.component.scss'],
standalone: true,
imports: [MatRippleModule, CommonModule, TranslocoModule, FontAwesomeModule],
})
export class ButtonComponent {
@Input() label!: string;
@Input() disabled?: boolean;
@Input() color: 'basic' | 'primary' | 'secondary' | 'warning' | 'dark' =
'basic';
@Input() size: 'extra-small' | 'small' | 'base' | 'large' | 'extra-large' =
'base';
@Input() type: string = 'button';
@Input() rounded: boolean = false;
@Input() outline: boolean = false;
@Input() prefixIcon?: IconProp;
@Input() suffixIcon?: IconProp;
@Input() shadow: boolean = true;
@Input() cdkFocusInitial: boolean = false;
@Input() form?: string;
@Output() action = new EventEmitter<void>();
}
button.component.stories.ts:
import {
Meta,
StoryObj,
moduleMetadata,
argsToTemplate,
} from '@storybook/angular';
import { provideHttpClient } from '@angular/common/http';
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
import { applicationConfig } from '@storybook/angular';
import { provideTransloco } from '@ngneat/transloco';
import { TranslocoHttpLoader, translocoConf } from '@vh-components/transloco';
import { ButtonComponent } from './button.component';
import {
FaIconLibrary,
FontAwesomeModule,
} from '@fortawesome/angular-fontawesome';
import { APP_INITIALIZER, importProvidersFrom } from '@angular/core';
import { faCoffee } from '@fortawesome/free-solid-svg-icons';
const meta: Meta<ButtonComponent> = {
component: ButtonComponent,
title: 'Design system/Buttons/Button',
tags: ['autodocs'],
decorators: [
moduleMetadata({
imports: [FontAwesomeModule],
providers: [
{
provide: APP_INITIALIZER,
useFactory: (iconLibrary: FaIconLibrary) => async () => {
// Add any icons needed here:
iconLibrary.addIcons(faCoffee);
},
// When using a factory provider you need to explicitly specify its dependencies.
deps: [FaIconLibrary],
multi: true,
},
],
}),
applicationConfig({
providers: [
provideHttpClient(),
provideTransloco({
config: translocoConf,
loader: TranslocoHttpLoader,
}),
// importProvidersFrom(TranslocoSharedModule), // TODO Why does this not work?
],
}),
],
render: (args: ButtonComponent) => ({
props: {
...args,
},
template: `
<vh-button ${argsToTemplate(args)}></vh-button>
`,
}),
};
export default meta;
type Story = StoryObj<ButtonComponent>;
export const Default: Story = {
args: {
label: 'text1',
disabled: false,
color: 'primary',
prefixIcon: 'coffee',
},
argTypes: {
color: {
control: {
type: 'select',
},
options: ['basic', 'primary', 'secondary', 'warning', 'dark'],
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText(/button works!/gi)).toBeTruthy();
},
};
export const disabled: Story = {
args: {
...Default.args,
disabled: true,
},
};
but fontawesome is not working in storybook.
It is working in my angular app:
import { Component } from '@angular/core';
import { FaConfig, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faCoffee } from '@fortawesome/free-solid-svg-icons';
import { NxWelcomeComponent } from './nx-welcome.component';
@Component({
standalone: true,
imports: [NxWelcomeComponent],
selector: 'vh-components-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'vh-components';
constructor(library: FaIconLibrary, faConfig: FaConfig) {
faConfig.fixedWidth = true;
library.addIcons(faCoffee);
}
}
Package.json:
{
"name": "@vh-components/source",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"start": "nx serve",
"build": "nx build",
"test": "nx test",
"storybook": "nx run vh-components:storybook"
},
"private": true,
"dependencies": {
"@angular/animations": "~16.2.0",
"@angular/cdk": "^16.2.12",
"@angular/common": "~16.2.0",
"@angular/compiler": "~16.2.0",
"@angular/core": "~16.2.0",
"@angular/forms": "~16.2.0",
"@angular/material": "^16.2.12",
"@angular/platform-browser": "~16.2.0",
"@angular/platform-browser-dynamic": "~16.2.0",
"@angular/router": "~16.2.0",
"@fortawesome/angular-fontawesome": "^0.13.0",
"@fortawesome/fontawesome-svg-core": "^6.5.0",
"@fortawesome/free-solid-svg-icons": "^6.5.0",
"@ngneat/transloco": "^6.0.0",
"@nx/angular": "17.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.13.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "~16.2.0",
"@angular-devkit/core": "~16.2.0",
"@angular-devkit/schematics": "~16.2.0",
"@angular-eslint/eslint-plugin": "~16.0.0",
"@angular-eslint/eslint-plugin-template": "~16.0.0",
"@angular-eslint/template-parser": "~16.0.0",
"@angular/cli": "~16.2.0",
"@angular/compiler-cli": "~16.2.0",
"@angular/language-service": "~16.2.0",
"@nx/eslint": "17.0.3",
"@nx/eslint-plugin": "17.0.3",
"@nx/jest": "17.0.3",
"@nx/js": "17.0.3",
"@nx/storybook": "17.0.3",
"@nx/web": "17.0.3",
"@nx/workspace": "17.0.3",
"@schematics/angular": "~16.2.0",
"@storybook/addon-essentials": "^7.5.1",
"@storybook/addon-interactions": "^7.5.1",
"@storybook/addon-storysource": "^7.5.1",
"@storybook/angular": "^7.5.1",
"@storybook/core-server": "^7.5.1",
"@storybook/jest": "~0.1.0",
"@storybook/test-runner": "^0.13.0",
"@storybook/testing-library": "~0.2.0",
"@swc-node/register": "~1.6.7",
"@swc/core": "~1.3.85",
"@tailwindcss/typography": "^0.5.10",
"@types/jest": "^29.4.0",
"@types/node": "16.11.7",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"autoprefixer": "^10.4.0",
"eslint": "~8.46.0",
"eslint-config-prettier": "^9.0.0",
"jest": "^29.4.1",
"jest-environment-jsdom": "^29.4.1",
"jest-preset-angular": "~13.1.0",
"jsonc-eslint-parser": "^2.1.0",
"ng-packagr": "~16.2.0",
"nx": "17.0.3",
"postcss": "^8.4.5",
"postcss-import": "~14.1.0",
"postcss-preset-env": "~7.5.0",
"postcss-url": "~10.1.3",
"prettier": "^2.6.2",
"storybook-addon-pseudo-states": "^2.1.2",
"tailwindcss": "^3.3.5",
"ts-jest": "^29.1.0",
"ts-node": "10.9.1",
"typescript": "~5.1.3"
}
}
project.json
{
"name": "vh-components",
"$schema": "node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"prefix": "vh-components",
"sourceRoot": "./src",
"tags": [],
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:browser",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/vh-components",
"index": "./src/index.html",
"main": "./src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "./tsconfig.app.json",
"assets": ["./src/favicon.ico", "./src/assets"],
"styles": ["./src/styles.scss"],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"executor": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "vh-components:build:production"
},
"development": {
"browserTarget": "vh-components:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "vh-components:build"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["./src/**/*.ts", "./src/**/*.html"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectName}"],
"options": {
"jestConfig": "jest.config.app.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"storybook": {
"executor": "@storybook/angular:start-storybook",
"options": {
"port": 4400,
"configDir": "./.storybook",
"browserTarget": "vh-components:build",
"compodoc": false,
"assets": [
"src/favicon.ico",
"src/assets"
]
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-storybook": {
"executor": "@storybook/angular:build-storybook",
"outputs": ["{options.outputDir}"],
"options": {
"outputDir": "dist/storybook/vh-components",
"configDir": "./.storybook",
"browserTarget": "vh-components:build",
"compodoc": false,
"assets": [
"src/favicon.ico",
"src/assets"
]
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"test-storybook": {
"executor": "nx:run-commands",
"options": {
"command": "test-storybook -c ./.storybook --url=http://localhost:4400"
}
},
"static-storybook": {
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "vh-components:build-storybook",
"staticFilePath": "dist/storybook/vh-components"
},
"configurations": {
"ci": {
"buildTarget": "vh-components:build-storybook:ci"
}
}
}
}
}
The issue is that you have APP_INITIALIZER
in the module definition, whereas it's not the one being bootstrapped. If you place a log
there, you'll see that it never fires.
To fix the thing, you need to move it to the applicationConfig
and it starts working. There is no need even for the moduleMetadata
. It just works.
import {
Meta,
StoryObj,
moduleMetadata,
argsToTemplate,
} from '@storybook/angular';
import { provideHttpClient } from '@angular/common/http';
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
import { applicationConfig } from '@storybook/angular';
import { provideTransloco } from '@ngneat/transloco';
import { TranslocoHttpLoader, translocoConf } from '@vh-components/transloco';
import { ButtonComponent } from './button.component';
import {
FaIconLibrary,
FontAwesomeModule,
} from '@fortawesome/angular-fontawesome';
import { APP_INITIALIZER, importProvidersFrom } from '@angular/core';
import { faCoffee } from '@fortawesome/free-solid-svg-icons';
const meta: Meta<ButtonComponent> = {
component: ButtonComponent,
title: 'Design system/Buttons/Button',
tags: ['autodocs'],
decorators: [
applicationConfig({
providers: [
{
provide: APP_INITIALIZER,
useFactory: (iconLibrary: FaIconLibrary) => async () => {
// Add any icons needed here:
iconLibrary.addIcons(faCoffee);
},
// When using a factory provider you need to explicitly specify its dependencies.
deps: [FaIconLibrary],
multi: true,
},
provideHttpClient(),
provideTransloco({
config: translocoConf,
loader: TranslocoHttpLoader,
}),
// importProvidersFrom(TranslocoSharedModule), // TODO Why does this not work?
],
}),
],
render: (args: ButtonComponent) => ({
props: {
...args,
},
template: `
<vh-button ${argsToTemplate(args)}></vh-button>
`,
}),
};
export default meta;
type Story = StoryObj<ButtonComponent>;
export const Default: Story = {
args: {
label: 'text1',
disabled: false,
color: 'primary',
prefixIcon: 'coffee',
},
argTypes: {
color: {
control: {
type: 'select',
},
options: ['basic', 'primary', 'secondary', 'warning', 'dark'],
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText(/button works!/gi)).toBeTruthy();
},
};
export const disabled: Story = {
args: {
...Default.args,
disabled: true,
},
};
P.S. You might want to consider the approach by the Angular Material team, where the component blends in with the native button. It's easier to work with, and you have the full access to the underlying button. No need for label
prop :)