Building Reusable UI Components With NX Shared Library
Hey guys! Let's talk about building a reusable UI component library with NX. This is a crucial step in any large Angular project, as it allows us to create UI elements that can be used across multiple applications and features. This ensures consistency, reduces redundancy, and makes our codebase more maintainable. In this article, we'll walk through the process of creating a shared UI library using NX, focusing on building reusable components, defining proper TypeScript interfaces, and understanding how NX libraries promote code reuse.
Why Build a Shared Component Library?
Before we dive into the technical details, let's quickly discuss why building a shared component library is so important. In large projects, you'll often find yourself needing the same UI elements in multiple places. Without a shared library, you might end up copy-pasting code, which leads to inconsistencies and makes it harder to maintain your application. A shared component library solves this problem by providing a single source of truth for your UI components.
Reusability is Key
The core idea behind a shared component library is reusability. We want to create components that are flexible enough to be used in different contexts, with different data, and with different styles. This means designing components that are not tightly coupled to specific parts of your application. For example, a button component should be able to handle different click actions, display different text, and have different visual styles, all without requiring significant code changes.
Consistency Across the Board
Consistency is another major benefit. When you use the same components throughout your application, you ensure a consistent user experience. This is crucial for usability and helps users feel comfortable using your application. A shared component library makes it easy to enforce design standards and maintain a unified look and feel.
Maintainability and Efficiency
Finally, a shared component library makes your codebase more maintainable and efficient. When you need to make a change to a component, you only need to update it in one place, and the changes will be reflected everywhere the component is used. This saves time and reduces the risk of introducing bugs. Plus, with NX, we can leverage its powerful tooling to streamline the development and maintenance of our library.
Setting Up Our NX Workspace
First things first, let's make sure we have an NX workspace set up. If you don't already have one, you can create a new workspace by running the following command:
npx create-nx-workspace@latest my-org
Replace my-org
with the name of your organization or project. Follow the prompts to configure your workspace. For this article, we'll assume you've set up an Angular workspace, but the concepts apply to other frameworks as well.
Generating the Shared UI Library
Now that we have our workspace, let's generate the shared UI library. We'll use the NX CLI to do this. Run the following command:
nx g @nx/angular:library shared-ui
This command tells NX to generate a new Angular library named shared-ui
. NX will create a new folder in your workspace, typically under the libs
directory, with all the necessary files and configurations for your library.
NX Libraries Promote Code Reuse
This is where NX libraries come into play to promote code reuse. NX libraries are designed to be self-contained modules that can be easily shared and reused across different applications within your workspace. By creating our UI components in a library, we ensure that they are decoupled from any specific application and can be used anywhere we need them. NX also provides tools for managing dependencies between libraries, making it easy to share code without creating circular dependencies or other issues.
Creating Our First Components
With our library set up, let's start building some components. We'll begin with a simple button component, then move on to a card component and navigation elements.
Button Component with Variants
Let's create a button component that supports different variants, such as primary, secondary, and danger. This is a great example of how to make a component reusable by allowing it to adapt to different contexts.
First, generate the button component using the NX CLI:
nx g @nx/angular:component button --project=shared-ui
This command creates a new component in the shared-ui
library. Now, let's modify the component's code to support variants.
Component Truly Reusable: Flexibility and Adaptability
What makes a component truly reusable? It's all about flexibility and adaptability. A reusable component should be able to handle different inputs, produce different outputs, and adapt to different visual styles without requiring significant code changes. Our button component is a perfect example. By supporting variants, we make it easy to use the button in different contexts without creating separate components for each style. This reduces code duplication and makes our codebase more maintainable.
// libs/shared-ui/src/lib/button/button.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'shared-ui-button',
templateUrl: './button.component.html',
styleUrls: ['./button.component.scss'],
})
export class ButtonComponent {
@Input() variant: 'primary' | 'secondary' | 'danger' = 'primary';
@Input() disabled = false;
}
In this code, we've added an @Input()
property called variant
, which allows us to specify the button's style. We've also added a disabled
input to control whether the button is enabled or disabled. Now, let's update the template to use these inputs.
<!-- libs/shared-ui/src/lib/button/button.component.html -->
<button
[disabled]="disabled"
class="button"
[ngClass]="{
'button--primary': variant === 'primary',
'button--secondary': variant === 'secondary',
'button--danger': variant === 'danger',
}"
>
<ng-content></ng-content>
</button>
Here, we're using Angular's [ngClass]
directive to apply different CSS classes based on the variant
input. This allows us to easily style the button differently depending on the context. We're also using <ng-content>
to allow consumers of the component to pass in content, such as text or icons.
Finally, let's add some CSS styles to our button:
/* libs/shared-ui/src/lib/button/button.component.scss */
.button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&--primary {
background-color: #007bff;
color: white;
}
&--secondary {
background-color: #6c757d;
color: white;
}
&--danger {
background-color: #dc3545;
color: white;
}
}
With this code, we've created a reusable button component that supports different variants and can be easily customized. To use this component in an application, you would import the SharedUiModule
from your library and use the <shared-ui-button>
tag in your templates.
Card Component
Next, let's create a card component. Cards are a common UI pattern used to display information in a structured way. Our card component will have a header, body, and footer, and it will be flexible enough to display different types of content.
Generate the card component:
nx g @nx/angular:component card --project=shared-ui
Now, let's define the component's template:
<!-- libs/shared-ui/src/lib/card/card.component.html -->
<div class="card">
<div class="card-header">
<ng-content select=".card-header"></ng-content>
</div>
<div class="card-body">
<ng-content></ng-content>
</div>
<div class="card-footer">
<ng-content select=".card-footer"></ng-content>
</div>
</div>
Here, we're using <ng-content>
with the select
attribute to allow consumers to pass in content for the header, body, and footer of the card. This makes the component highly flexible and reusable.
Let's add some styles:
/* libs/shared-ui/src/lib/card/card.component.scss */
.card {
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 1rem;
.card-header {
padding: 0.75rem;
border-bottom: 1px solid #ccc;
}
.card-body {
padding: 1rem;
}
.card-footer {
padding: 0.75rem;
border-top: 1px solid #ccc;
text-align: right;
}
}
Navigation Components
Navigation components are essential for any application. Let's create a simple navigation component that can be used to navigate between different sections of our application.
Generate the navigation component:
nx g @nx/angular:component navigation --project=shared-ui
Here’s a basic template for our navigation component:
<!-- libs/shared-ui/src/lib/navigation/navigation.component.html -->
<nav class="navigation">
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Services</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>
And some basic styles:
/* libs/shared-ui/src/lib/navigation/navigation.component.scss */
.navigation {
ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
li {
margin-right: 1rem;
a {
text-decoration: none;
color: #333;
&:hover {
color: #007bff;
}
}
}
}
}
To make this component more reusable, we can add inputs for the navigation items. Let's define an interface for a navigation item and add an input to our component.
Adding Proper TypeScript Interfaces
Using TypeScript interfaces is crucial for creating reusable components. Interfaces allow us to define the shape of the data that our components expect, making our code more predictable and easier to maintain.
Let's define an interface for our navigation item:
// libs/shared-ui/src/lib/navigation/navigation-item.interface.ts
export interface NavigationItem {
label: string;
url: string;
}
Now, let's update our navigation component to use this interface:
// libs/shared-ui/src/lib/navigation/navigation.component.ts
import { Component, Input } from '@angular/core';
import { NavigationItem } from './navigation-item.interface';
@Component({
selector: 'shared-ui-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss'],
})
export class NavigationComponent {
@Input() items: NavigationItem[] = [];
}
We've added an @Input()
property called items
, which is an array of NavigationItem
objects. Now, let's update our template to use these items:
<!-- libs/shared-ui/src/lib/navigation/navigation.component.html -->
<nav class="navigation">
<ul>
<li *ngFor="let item of items">
<a [href]="item.url">{{ item.label }}</a>
</li>
</ul>
</nav>
By using an interface, we've made our navigation component more flexible and reusable. Consumers of the component can now pass in an array of navigation items, each with a label and a URL.
Exporting Components from the Library
To use our components in other applications, we need to export them from our library. NX makes this easy by automatically generating an index.ts
file in our library's source directory. This file is used to export the library's public API.
Let's add our components to the index.ts
file:
// libs/shared-ui/src/index.ts
export * from './lib/shared-ui.module';
export * from './lib/button/button.component';
export * from './lib/card/card.component';
export * from './lib/navigation/navigation.component';
We're exporting our SharedUiModule
and our button, card, and navigation components. Now, these components can be imported and used in other applications within our workspace.
Using the Shared Components in an Application
To use our shared components in an application, we first need to import the SharedUiModule
into our application module. For example, if we have an application named my-app
, we would update the app.module.ts
file like this:
// apps/my-app/src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { SharedUiModule } from '@my-org/shared-ui';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, SharedUiModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Note the import statement @my-org/shared-ui
. NX uses path aliases to make it easy to import libraries within your workspace. The @my-org
part comes from the name you gave your workspace when you created it.
Once we've imported the module, we can use our components in our application templates:
<!-- apps/my-app/src/app/app.component.html -->
<shared-ui-button variant="primary">Click me!</shared-ui-button>
<shared-ui-card>
<div class="card-header">Card Header</div>
<div class="card-body">Card Body</div>
<div class="card-footer">Card Footer</div>
</shared-ui-card>
<shared-ui-navigation [items]="navigationItems"></shared-ui-navigation>
And in our component class:
// apps/my-app/src/app/app.component.ts
import { Component } from '@angular/core';
import { NavigationItem } from '@my-org/shared-ui';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
navigationItems: NavigationItem[] = [
{
label: 'Home',
url: '/home',
},
{
label: 'About',
url: '/about',
},
{
label: 'Services',
url: '/services',
},
{
label: 'Contact',
url: '/contact',
},
];
}
Component Communication (Inputs/Outputs)
Let’s talk about component communication, which is primarily handled through inputs and outputs. Inputs allow us to pass data into a component, while outputs allow a component to emit events that can be handled by its parent. This is crucial for making components interactive and reusable.
Our button component already uses inputs to control its variant and disabled state. Let’s add an output to handle button clicks.
// libs/shared-ui/src/lib/button/button.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'shared-ui-button',
templateUrl: './button.component.html',
styleUrls: ['./button.component.scss'],
})
export class ButtonComponent {
@Input() variant: 'primary' | 'secondary' | 'danger' = 'primary';
@Input() disabled = false;
@Output() clicked = new EventEmitter<void>();
onClick() {
this.clicked.emit();
}
}
We’ve added an @Output()
property called clicked
, which is an EventEmitter
. When the button is clicked, we call the onClick()
method, which emits the clicked
event. Now, consumers of the component can listen for this event and take action.
Let’s update the template to call the onClick()
method:
<!-- libs/shared-ui/src/lib/button/button.component.html -->
<button
[disabled]="disabled"
class="button"
[ngClass]="{
'button--primary': variant === 'primary',
'button--secondary': variant === 'secondary',
'button--danger': variant === 'danger',
}"
(click)="onClick()"
>
<ng-content></ng-content>
</button>
To use the output in our application, we can bind to the clicked
event in our template:
<!-- apps/my-app/src/app/app.component.html -->
<shared-ui-button variant="primary" (clicked)="onButtonClick()">Click me!</shared-ui-button>
And in our component class:
// apps/my-app/src/app/app.component.ts
import { Component } from '@angular/core';
import { NavigationItem } from '@my-org/shared-ui';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
navigationItems: NavigationItem[] = [
{
label: 'Home',
url: '/home',
},
{
label: 'About',
url: '/about',
},
{
label: 'Services',
url: '/services',
},
{
label: 'Contact',
url: '/contact',
},
];
onButtonClick() {
alert('Button clicked!');
}
}
Conclusion
Building a shared component library is a powerful way to create reusable UI elements and ensure consistency across your applications. NX makes this process even easier by providing tools for generating libraries, managing dependencies, and exporting components. By following the steps outlined in this article, you can create a robust and maintainable UI component library that will save you time and effort in the long run. Remember, the key to reusable components is flexibility, adaptability, and clear communication through inputs and outputs. Keep these principles in mind, and you'll be well on your way to building a great shared UI library!