! 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'
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
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.
Maintaining a framework-agnostic solution makes only sense if you plan to support multiple frameworks in the long run.
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.
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.
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
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
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
Inside your design-system
directory we now create the actual library that will contain our components:
ng generate library pattern-lib --prefix=pl
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:
- Go to
design-system/angular.json
- Search for your added library in
"projects": { ...}
- Add the
schematics
property to yourprojects/pattern-lib
In my project it then looks somewhat like this:
...
"projects": {
"pattern-lib": {
"projectType": "library",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
...
2.3. Creating a button
component in the library
ng generate component button --project=pattern-lib
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
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 {
}
}
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>
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%);
}
}
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
Run
npx -p @storybook/cli sb init --type angular
within yourdesign-system
Angular project.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
- Go to
design-system/stories
and DELETE EVERYTHING - 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)
- 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,
};
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:
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
- Go to
design-system/projects/pattern-lib/src/
- Create a directory
styles/
- 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';
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';
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"
],
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
],
...
})
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]
})
2.6.2. Using the library package through local binding
Go to the directory design-system
and run
ng build --project=pattern-lib
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
- Go to
my-app/src/app/app.module.ts
and add the library import
import { PatternLibModule } from '@your-company/pattern-lib';
- Also add it to the
imports
field ofapp.module.ts
:
@NgModule({
...
imports: [
...
PatternLibModule,
],
...
})
- 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>
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.
2.6.3. Using the package with an npm
registry
Now that you have it already running locally this should come with ease.
First things first. Choose a npm registry (e.g. Github or npmjs).
Create an
.npmrc
file in yourdesign-system
Angular root that looks similiar to the following:
registry=https://registry.npmjs.org/
always-auth=true
email=YOUR_EMAIL_GOES_HERE
Login with
npm login
- It should ask you for your credentials on the registry provided in the.npmrc
file.Once you are logged in you can simply run
npm publish
in yourdesign-system/dist/pattern-lib
directory to publish the library that you built! Make sure to NOT run it in your root directory but really indist
! Because if you runnpm publish
in your root directory it will only publish all the source code and not the compiled library.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:
activenode / angular-10-storybook-library
An angular library to implement an isolated Design System
Angular 10.x Storybook Library
This is a sample repository that includes 2 Angular root-projects.
-
design-system
which is an angular project containing UI elements (pattern library) -
my-app
which consumes those
This is the final result of the step-by-step tutorial provided at dev.to: https://dev.to/activenode/angular-10-storybook-npm-package-ng-design-system-step-by-step-2dn2
Sources:
- [1] https://angular.io/cli/generate
- [2] https://www.blexin.com/en-US/Article/Blog/Creating-Angular-components-libraries-68
- [3] Exporting global SCSS: https://medium.com/@Dor3nz/compiling-css-in-new-angular-6-libraries-26f80274d8e5
- [4] https://medium.com/@tomsu/how-to-build-a-library-for-angular-apps-4f9b38b0ed11
Design References:
Packaging Design Icon made xnimrodx from flaticon.com
Top comments (21)
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!
How does your directory structure look like?
Where is your main repository and where is the library repository? Do you get any error message?
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
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
can I put my code to git for you review code?
Yes
thanks you very much. This is my github
github.com/tiendatlgbg/LibraryAngu...
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.
Can you provide more information where your @messages2raja /pattern-lib is located and how you installed it?
it is located in npmjs.org url: npmjs.com/package/@messages2raja/p...
I have installed it via the npm i command.
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 runnpm publish
from there :)Hope that solves it. I will enhance the article.
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 :)
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/NG0203at 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)
Hi David,
I would really like to know how to publish the angular framework as web components. Could you provide any information around this?
dev.to didn't show me this comment. Is this still an open topic for you?
Hi David,
That's not a problem I figured it out ;)
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!
This is awesome. Looking forward to reading more about testing in Storybook. Good job 👍
Thank you! 🙏 This has helped me a lot. I am also looking forward to read more about testing in Storybook.
Great post!! Thank you @activenode !
Testing with storybook :D
Thanks a bunch. Good hint, I should be doing the Testing with Storybook now :)