DEV Community

Cover image for Angular 10 + Storybook + npm package = NG Design System 💜 - Step-by-Step
David Lorenz
David Lorenz

Posted on • Updated on

Angular 10 + Storybook + npm package = NG Design System 💜 - Step-by-Step

! The Angular version used here is 10.x

tldr - The Task was:

Create a UI Kit with Angular that lives on its own and scales to multiple product usage.

Or in technical terms:

npm i @your-company/your-ui-kit

import { Button } from '@your-company/your-ui-kit'
Enter fullscreen mode Exit fullscreen mode

The Final Result:

  • Angular-based library published as @your-prefix/pattern-lib
  • Using Storybook to preview the library elements
  • Using and exporting global scss styles

1. Background and Decision-Making 👨🏽‍💻

Our product application is based on Angular. We wanted it to scale such that we can quickly re-use components even across other products.
Those components should always go with the Brand visuals.

To achieve that we wanted to do what the de-facto standard is: Creating a Design System that is implemented as a Pattern Library (furthermore called Pattern Lib)

1.1. A "No" to Web Components and a "Yes" to NG components 👀

If you follow me on different channels you know that I love Web Components. But:

Use the right tool for the right job 🛠

At Mercedes-Benz.io we were using Web Components together with Stencil to create the Pattern Library. This decision was based on the fact that we had React, Vue, Angular and partially even Elm in our product stack and therefore needed to find a Library approach that is framework independent.

However being framework independent comes with the downside of not getting the best from that very framework since you are mixing up different technologies - that can especially get messy once your requirement says: Enable SSR.

So at the end of the day: You have the balance the upsides and downsides based on how your business is built.

The process that led to an Angular-based PatternLib

  1. Angular does a lot of preprocessing in its compilation process. So the compiler ensures the best overall output is achieved - when using Angular. Angular cannot optimize the code you put in your Web Component.

  2. Maintaining a framework-agnostic solution makes only sense if you plan to support multiple frameworks in the long run.

  3. Small team: Focus switch avoidance between e.g. Stencil and Angular. Sticking with Angular delivers overall better output since we can apply the same guidelines.

  4. You do not necessarily benefit of awesome Editor help when mixing up 2 technologies - or you have to put in more of work to achieve that.

  5. If you still want to output Web Components from your library you will still be able to tune up your build to do so.


2. How to implement an Angular UI Kit / NG UI Library

2.1. The Pre-setup

I guess it is clear that you will have to have @angular/cli installed and ready.

Besides that: The first thing you want to do is to be able to test your library to confirm that you did a good job. So before we create the actual library we want to create the application that consumes it.

ng new my-app --prefix=ma --style=scss --legacyBrowsers=true 
Enter fullscreen mode Exit fullscreen mode

We use the prefix to be safe and isolated from any used library (that might be unprefixed).
You do not need legacyBrowsers if you do not plan to support IE.

2.2. Creating the library

Run the following command in the directory where your my-app is located (so basically in its parent directory ./my-app/../).

ng new design-system --create-application=false --prefix=ds --style=scss
Enter fullscreen mode Exit fullscreen mode

Important Note: In Angular 10.1.4 that I used for this project the --style=scss did not have any effect. This comes from the fact that we created an empty application and Angular has no option yet to predefine it (we will fix it soon in this tutorial).

We call the parent design-system to not get confused with its child module below.
Now your directory structure should look like this:

.
├── my-app
├── design-system
Enter fullscreen mode Exit fullscreen mode

Inside your design-system directory we now create the actual library that will contain our components:

ng generate library pattern-lib --prefix=pl
Enter fullscreen mode Exit fullscreen mode

First thing you want to do is to make the package scoped to easier identify it is your library.

So go to design-system/projects/pattern-lib/package.json and change the name from pattern-lib to @your-company/pattern-lib .

As said above: We also have to fix --style=scss on our own since this is not yet possible to define for a library or empty application upfront. Do the following:

  1. Go to design-system/angular.json
  2. Search for your added library in "projects": { ...}
  3. Add the schematics property to your projects/pattern-lib

In my project it then looks somewhat like this:

...
"projects": {
    "pattern-lib": {
      "projectType": "library",
      "schematics": {
        "@schematics/angular:component": {
          "style": "scss"
        }
      },
...
Enter fullscreen mode Exit fullscreen mode

2.3. Creating a button component in the library

ng generate component button --project=pattern-lib
Enter fullscreen mode Exit fullscreen mode

This command will create a component named button. Your directory structure should contain the following directories:

.
├── my-app
├── design-system
   ├── projects
      ├── pattern- lib
         ├── src
            ├── lib
               ├── button
Enter fullscreen mode Exit fullscreen mode

You might want to say: Yo David why so many subfolders? Can't I just put it in the root src?

Answer: Angular allows to control multiple libraries / components / modules etc. in one Angular project root and the @angular/cli is optimized to suppor this exact structure. If we start building a pattern lib in a default ng new my-pattern-lib application -> that will work for sure but we are leaving the safe track to a slippery path because then we have to bend the tools to do what we want to do (expose and publish library parts).


Open up button.component.ts and make sure it has 2 properties defined in the class like this:

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

@Component({
  selector: 'pl-button', // pl is our prefix
  templateUrl: './button.component.html',
  styleUrls: ['./button.component.css']
})
export class ButtonComponent implements OnInit {
  @Input('label') label: string | null;
  @Input('pink') pink: boolean;

  constructor() { }

  ngOnInit(): void {
  }

}

Enter fullscreen mode Exit fullscreen mode

Quick explainer: By setting the properties explicitly (e.g. @Input('pink') pink: boolean;) the component exposes its editable fields which will come in handy in Storybook later since Storybook can autodetect the field types.


Now open button.component.html and fill it with:

<button [attr.is-pink]="pink" [ngClass]="{'make-pink': pink}">{{label ? "😎 " + label : "No Label provided 🧐"}}</button>
Enter fullscreen mode Exit fullscreen mode

The [attr.is-pink]="pink" we use to debug if we run into errors. It is not needed though 😉

Also open button.component.scss and fill it with:

button {
  background: blue;
  padding: 1rem 2rem;
  border-radius: 3px;
  appearance: none;
  border: 0;
  -webkit-appearance: none;
  -moz-appearance: none;
  font-size: 1.5rem;
  letter-spacing: 1px;
  color: white;
  box-shadow: 0 4px 10px rgba(55, 55, 55, 0.3),
    0 6px 35px rgba(55, 55, 200, 0.7);
  cursor: pointer;

  &.make-pink {
    background: #ff00a2;
    box-shadow: 0 4px 10px rgba(55, 55, 55, 0.3), 0 6px 35px rgb(200 55 150 / 70%);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now you want to build and see the button in action - but it is just a library, not an SPA, so HOW?

2.4. Run it: Storybook to the rescue.

Adding storybook normally is quite easy as there are ready-to-go recipes. However these do not naturally apply for an Angular library.

2.4.1. Installing Storybook and binding it to our Library

  1. Run npx -p @storybook/cli sb init --type angular within your design-system Angular project.

  2. Open .storybook/tsconfig.json and change the extends line to "extends": "../projects/pattern-lib/tsconfig.lib.json" (if not already set)

2.4.2. Setting up our button story

  1. Go to design-system/stories and DELETE EVERYTHING
  2. Create a file named Intro.stories.mdx with following content:
import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Library Example/Intro" />

<style>{`
  .box {
    margin: 2rem 0;
    padding: 2rem;
    background: rgba(233,233,233,0.7);
    border-radius: 3px;
  }
`}</style>

# Hello you!

<div class="box">
  This is the Pattern Library Intro File to provide a guiding path or whatever else you want to do here. Feel free!
</div>

![](https://media.giphy.com/media/yJFeycRK2DB4c/giphy.gif)
Enter fullscreen mode Exit fullscreen mode
  1. Create a file named Button.stories.mdx with following content:
import { Story, Meta } from '@storybook/angular/types-6-0';
import { ButtonComponent } from '../projects/pattern-lib/src/lib/button/button.component';

export default {
  title: 'Atomics/Buttons',
  component: ButtonComponent,
  argTypes: {
    label: { control: 'text' }, 
    // we need to override here since in Angular it could be null as well (see button.component.ts) and therefore it would become an ambigious data type for storybook
  }
} as Meta;

const Template: Story<ButtonComponent> = (args: ButtonComponent) => ({
  component: ButtonComponent,
  props: args,
});

export const FancyBlueButton = Template.bind({});
FancyBlueButton.args = {
  label: 'Button',
};

export const FancyPinkButton = Template.bind({});
FancyPinkButton.args = {
  label: 'Pink version',
  pink: true,
};
Enter fullscreen mode Exit fullscreen mode

2.4.3. Run, run, run!

Trigger npm run storybook within your design-system project folder and wait for it to - hopefully successfully - boot up.

As a result you should see something like this:

Storybook

Cool Stuff!


Quick Summary: Up to this point we created an Angular library with a button as a component and connected Storybook to it to be able to see and structure it without running an actual Angular SPA.

2.5. Global SCSS and Component Composition

2.5.1. Global SCSS

  1. Go to design-system/projects/pattern-lib/src/
  2. Create a directory styles/
  3. Create a file named global.scss with some random content e.g. body { background: red }

Use it in Storybook

To make sure that Storybook can apply the global scss head over to .storybook/preview.js and add it as a webpack import like this:

import '!style-loader!css-loader!sass-loader!./../projects/pattern-lib/src/styles/global.scss';
Enter fullscreen mode Exit fullscreen mode

That's it. Storybook will now always use that file as a global file for every view.

Use it in Angular

Using a global SCSS in Angular shouldn't be a problem since you can always @import it in any of your SCSS files in your project e.g. like

@import '../../styles/global';
Enter fullscreen mode Exit fullscreen mode

However >> WARNING: Make sure not to import actual CSS definitions such as body { background: red } into your Angular components as those would get duplicated and would drastically decrease performance.

If you want a global CSS file for your Angular App you can (and should) surely define it in your library but you should export them with the npm package (see section 2.6.) such that your Angular SPA can then include it.

In any Angular application (!= Angular library) you can add this to the angular.json to define a global style for the App:

"styles": [
  "node_modules/@your-company/pattern-lib/styles/style.scss"
],
Enter fullscreen mode Exit fullscreen mode

2.5.2. Component Composition

Please refer to https://www.learnstorybook.com/intro-to-storybook/angular/en/composite-component/ if you wanted to combine multiple components in Storybook.

2.6. Using the pattern-lib in your Angular application my-app

Now what we want is to be able to use our patterns in any application that we create - as an npm package.

The following diagram visualizes that:

Hold up! Important!

Before we use our library we need to import the CommonModule from @angular/common as otherwise our [ngClass] binding of our button will not work (more info here: https://stackoverflow.com/a/45573086).

So in design-system/pattern-lib open up pattern-lib.module.ts and adapt it such that you do:

//... other imports
import { CommonModule } from '@angular/common';

@NgModule({
  ...
  imports: [
    CommonModule
  ],
  ...
})
Enter fullscreen mode Exit fullscreen mode

2.6.1. Export the button for usage

If we want our button to be usable outside of the library we need to export it in our pattern-lib.module.ts like this:

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


@NgModule({
  ...
  exports: [PatternLibComponent, ButtonComponent]
})

Enter fullscreen mode Exit fullscreen mode

2.6.2. Using the library package through local binding

Go to the directory design-system and run

ng build --project=pattern-lib
Enter fullscreen mode Exit fullscreen mode

Head over to design-system/dist/pattern-lib and run npm link.

Now your package should be available on your computer under its package name @your-company/pattern-lib.

No go over to your Angular SPA my-app and run npm link @your-company/pattern-lib. This basically is like a local npm install - however it will not show up in your package.json. Alternatively you can also do npm install ../design-system/dist/pattern-lib if you prefer so.

Add the linked library to our my-app

  1. Go to my-app/src/app/app.module.ts and add the library import
import { PatternLibModule } from '@your-company/pattern-lib';
Enter fullscreen mode Exit fullscreen mode
  1. Also add it to the imports field of app.module.ts:
@NgModule({
  ...
  imports: [
    ...
    PatternLibModule,
  ],
  ...
})
Enter fullscreen mode Exit fullscreen mode
  1. Go to my-app/src/app/app.component.html and delete all the contents and put the following instead:
<style>
  :host {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
    font-size: 14px;
    color: #333;
    box-sizing: border-box;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }
</style>


<div>
  <p>Hello I am the Angular SPA and following a button should be shown and it should be blue: </p>
  <dvpl-button label="hello"></dvpl-button>

  <p>And this one should be pink: </p>
  <dvpl-button label="Hello pink?" [pink]="true"></dvpl-button>
</div>

<!-- you only need this if you added a router: -->
<router-outlet></router-outlet>
Enter fullscreen mode Exit fullscreen mode

Now you should be able to run npm start in your my-app SPA and you should see that your Angular application properly makes use of our library.

Running the app

2.6.3. Using the package with an npm registry

Now that you have it already running locally this should come with ease.

  1. First things first. Choose a npm registry (e.g. Github or npmjs).

  2. Create an .npmrc file in your design-system Angular root that looks similiar to the following:

registry=https://registry.npmjs.org/
always-auth=true
email=YOUR_EMAIL_GOES_HERE
Enter fullscreen mode Exit fullscreen mode
  1. Login with npm login - It should ask you for your credentials on the registry provided in the .npmrc file.

  2. Once you are logged in you can simply run npm publish in your design-system/dist/pattern-lib directory to publish the library that you built! Make sure to NOT run it in your root directory but really in dist ! Because if you run npm publish in your root directory it will only publish all the source code and not the compiled library.

  3. Now you can use it the same way you installed it locally but from the given repository with npm install @your-company/pattern-lib 🎉

3. Testing Storybook, atomic imports and Web Components, etc.

There is way more that we could discuss - but I don't wanna excess this here too much: How to not only test the components but test them with Storybook, how to export more atomically (a single Button import instead of the whole library if needed) and publishing for other Frameworks as a Web Component.

But this might be worth another Blog Post. Let me know!

4. Conclusion

You created a Design System Repo with Angular and you are able to publish it in different versions.
Also you can use them in whatever Angular app you want 😎.

Find the source code of the sample here:

GitHub logo activenode / angular-10-storybook-library

An angular library to implement an isolated Design System







Sources:

Design References:
Packaging Design Icon made xnimrodx from flaticon.com

Top comments (21)

Collapse
 
babatos profile image
babatos • Edited

Hi,
I followed the steps to step "2.6.2. Using the library package through local binding". I installed npm link @your-company/pattern-lib but when I call selector of button component pl-button label="hello"/pl-button in app.component.html .Nothing display . I don't know why :( .Please help me to solve this issue.
my english is not good,please sympathize!

Collapse
 
activenode profile image
David Lorenz

How does your directory structure look like?

Where is your main repository and where is the library repository? Do you get any error message?

Collapse
 
babatos profile image
babatos • Edited

I think I fixed the error by adding ""preserveSymlinks": true" to "option:{ }" in Myapp/Angular.json file. It worked. But I don't understand why It work. Could you explain me, please?

I'm using :
Angular CLI: 13.0.3
Node: 16.13.1
Package Manager: npm 8.1.2
OS: win32 x64

Collapse
 
babatos profile image
babatos

I get the following error at runtime:

core.mjs:6484 ERROR TypeError: Cannot read properties of null (reading 'bindingStartIndex')
at Module.ɵɵelementStart (:51648/vendor.js:34341)
at ButtonComponent_Template (:51648/main.js:280)
at executeTemplate (:51648/vendor.js:69548)
at renderView (:51648/vendor.js:69277)
at renderComponent$1 (:51648/vendor.js:70812)
at renderChildComponents (:51648/vendor.js:69112)
at renderView (:51648/vendor.js:69311)
at renderComponent$1 (:51648/vendor.js:70812)
at renderChildComponents (:51648/vendor.js:69112)
at renderView (:51648/vendor.js:69311)

I do not see any errors while compiling

Collapse
 
babatos profile image
babatos

can I put my code to git for you review code?

Thread Thread
 
activenode profile image
David Lorenz

Yes

Thread Thread
 
babatos profile image
babatos

thanks you very much. This is my github

github.com/tiendatlgbg/LibraryAngu...

Collapse
 
messages2raja profile image
messages2raja

Hi,
I followed the steps and created a sample package and published in npm as well, But when I tried to consume the npm package I am getting the following err:
Cannot find module '@messages2raja/pattern-lib' or its corresponding type declarations.ts(2307)..Please help me to solve this issue.

Collapse
 
activenode profile image
David Lorenz

Can you provide more information where your @messages2raja /pattern-lib is located and how you installed it?

Collapse
 
messages2raja profile image
messages2raja

it is located in npmjs.org url: npmjs.com/package/@messages2raja/p...
I have installed it via the npm i command.

Thread Thread
 
activenode profile image
David Lorenz

I think i can see the problem. You were probably running npm publish in your root directory right?

That however would publish your whole angular project. What you want instead is build it and then go to dist/pattern-lib and run npm publish from there :)

Hope that solves it. I will enhance the article.

Collapse
 
ganicotte profile image
ganicotte • Edited

Hello David,

Thank you for this outstanding post it helped a lot !
There is one thing that keeps me struggling with : how do you reference your static assets in your Storybook ? Let's say you have an image in '/projects/pattern-lib/src/assets', and your button component uses it. How do you configure your webpack in order to display it on Storybook?

Cheers @activenode :)

Collapse
 
wahyufaturrizky profile image
Wahyu Fatur Rizki

hi @activenode I got this error please help me:

main.ts:7 Error: NG0203: inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with EnvironmentInjector#runInContext. Find more at angular.io/errors/NG0203
at injectInjectorOnly (core.mjs:731:15)
at Module.ɵɵinject (core.mjs:742:60)
at Object.MatCommonModule_Factory as useFactory
at Object.factory (core.mjs:8854:38)
at R3Injector.hydrate (core.mjs:8767:35)
at R3Injector.get (core.mjs:8655:33)
at injectInjectorOnly (core.mjs:738:33)
at ɵɵinject (core.mjs:742:60)
at useValue (core.mjs:8435:65)
at R3Injector.resolveInjectorInitializers (core.mjs:8704:17)

Collapse
 
wesleyvdkop profile image
Wesley van der Kop

Hi David,

I would really like to know how to publish the angular framework as web components. Could you provide any information around this?

Collapse
 
activenode profile image
David Lorenz

dev.to didn't show me this comment. Is this still an open topic for you?

Collapse
 
wesleyvdkop profile image
Wesley van der Kop

Hi David,

That's not a problem I figured it out ;)

Collapse
 
p_leppard profile image
Paul Leppard • Edited

Awesome post. Can you post on how to create a component that animates in Storybook? I have created an accordion which utilises Angular Animations to toggle it open and closing but can't make it work in storybook with the animation. It just shows it as always open. Keep up the great work!

Collapse
 
andyleeboo profile image
Andy Lee

This is awesome. Looking forward to reading more about testing in Storybook. Good job 👍

Collapse
 
millimetha profile image
Tanja Stuchels

Thank you! 🙏 This has helped me a lot. I am also looking forward to read more about testing in Storybook.

Collapse
 
weklund profile image
Wes

Great post!! Thank you @activenode !

Testing with storybook :D

Collapse
 
activenode profile image
David Lorenz

Thanks a bunch. Good hint, I should be doing the Testing with Storybook now :)