In an earlier post, I wrote about Angular CLI Workspaces, a new feature in Angular 6 that allows you to have more than one project in a single workspace (Angular CLI environment). In this post, I will show you how to create angular 6 Libraries. On top of that, I will show you how to pass configurations and data to your new library from your Angular Application.
Why?
If you have multiple active angular projects, you may have come to realize that there so many things they share or have in common. Sometime, as a developer you are forced to copy paste their code to new projects for features they have in common.
A good example of this is Authentication. While the UI (Sign in, sign up, password reset etc. forms in the case of Authentication) may change, the underlying code (Services, HTTP Interceptors, Guards etc.) tends to remain the same apart from some incremental modifications and bug fixes. Wouldn’t it be easier that instead of copy pasting, you could just have a private library that you can hook into your project and have access to all the features you need for your new project? This would save you lots of hours which you could utilize on the UI and other features of your new project.
Overview of our Library
We are going to work with a demo that does the following: A component to display a list of Football/Soccer National Teams and the number of World Cup trophies they have won. A service that will take a list of teams Injected to our library and sort it by descending order. The config
file will be injected using the forRoot
method through the module (Similar to the way you pass routes to the router module).
import : [ RouterModule.forRoot(routes) ]
NB: To inject our configuration, we will rely on Injection Token method. It is not the only way to pass configurations into a library or module. If you want to explore the other methods, I suggest going over this excellent post by Michele Stieven.
For our demo, we are going to rely on the following libraries: Angular Material, Angular CDK and Angular Flex Layout.
Getting Started
To get started, we are going to create a new angular project (just a regular project):
ng new application-name
Then, open that directory on terminal or PowerShell:
cd application-name
And generate a library inside our angular workspace using ng generate library
command.
ng generate library test-library --prefix=tl
A new library is generated and is placed under the /projects/my-library
directory in your angular workspace. To use your library in your current app, you need to build your library every time. To build the library, use the ng build
command followed by the name of the project (library in this case):
ng build my-library
ng build --prod my-library
The build artefacts can be found under a directory with the name of the library, inside the /dist
directory in your workspace.
Working on the library
If you open your project folder, the folder structure will look like this: When you generate a library, Angular CLI will creates 3 files: the main library module, a component and a service. For our demo, there is no need to add any extra component or service. But, if you need to add one, the ng generate command with the project flag will do the trick.
ng generate component component-name --project-name=test-library
ng generate service service-name --project-name=test-library
Building the Library
Our demo library can be broken down into 3 items: The Library Module (think of AppModule), Our Service and Component. On top of the three items, there are classes and interfaces that will play a supporting role for our library. You can get the whole source code on Github here. First, we need to define an injection token constant to use Injection Token method:
export const TestLibConfigService = new InjectionToken<TestLibConfig>(
'TestLibConfig'
);
And then, we need to define our interface(TestLibConfig
) for the object we will be passing using our forRoot
method.
export interface TestLibConfig {
title: string;
teams: {
country: string;
trophies: number;
}[];
sport: string;
}
NB: To avoid
Circular dependency
warning, you can put theTestLibConfig
andTestLibConfigService
into separate files instead of the module file. These warnings occur because the two will be imported by the service and the module, while the module will import the service.
Library Module
Since we now have an injection token, we can now go ahead and create our forRoot
method that we will use to pass the configuration object to our library.
static forRoot(config: TestLibConfig): ModuleWithProviders {
return {
ngModule: TestLibraryModule,
providers: [
TestLibraryService,
{
provide: TestLibConfigService,
useValue: config
}
]
};
}
As you can see, our forRoot
method takes a config object and provides it together with the services that are in our library. Also, we need to import Material Components we are using in our Library and declare and export our component.
@NgModule({
imports: [
BrowserModule,
FlexLayoutModule,
MatListModule,
MatToolbarModule,
MatCardModule],
declarations: [TestLibraryComponent],
exports: [TestLibraryComponent]
})
Library Service
Then, we need to inject the configuration object into our service.
constructor(@Inject(TestLibConfigService) private config: TestLibConfig) {
console.log(config);
}
Now the configuration object is ready for use in our service. The rest of the service looks like this:
export class TestLibraryService {
// inject our configuration into our service
constructor(@Inject(TestLibConfigService) private config: TestLibConfig) {
console.log(config);
}
// You can now use your configuration as you normally would
// if it was a http request, then you could return observables which you can subscribe to
getConfig(): Observable<TestLibConfig> {
return of(this.config);
}
getTitle(): Observable<string> {
return of(this.config.title);
}
getTeams(): Observable<{ country: string; trophies: number }[]> {
// just for fun, let's sort this by descending order
// we shall use, bubble sort
const sorted = this.config.teams.slice();
for (let i = 0; i < sorted.length; i++) {
for (let j = 0; j < sorted.length - 1; j++) {
if (sorted[j].trophies < sorted[j + 1].trophies) {
const swap = sorted[j];
sorted[j] = sorted[j + 1];
sorted[j + 1] = swap;
}
}
}
return of(sorted);
}
getSport(): Observable<string> {
return of(this.config.sport);
}
}
Our Component:
And finally, our service is ready to be used by our library component normally. Here is the component content:
export class TestLibraryComponent implements OnInit {
public teams$: Observable<{ country: string; trophies: number }[]>;
public title$: Observable<string>;
constructor(private testLibService: TestLibraryService) {}
ngOnInit() {
this.teams$ = this.testLibService.getTeams();
this.title$ = this.testLibService.getTitle();
}
}
And the template looks like this (Remember I am using Angular Material, you can replace everything easily with any UI Library like Bootstrap and it will still work):
<mat-toolbar color="primary"> <span>Demo App</span> </mat-toolbar>
<div gdAuto>
<div
fxLayout="column"
fxFlex.xs="100"
fxFlex="600px"
fxFlexOffset="calc(50% - 300px)"
fxFlexOffset.xs="0"
>
<mat-card>
<mat-card-header>{{ title$ | async }}</mat-card-header>
<mat-card-content>
<mat-list role="list">
<mat-list-item
*ngFor="let x of teams$ | async; let i = index"
role="listitem"
>
{{ i + 1 }}. {{ x.country }} - {{ x.trophies }}</mat-list-item
>
</mat-list>
</mat-card-content>
</mat-card>
</div>
</div>
Next, build the library:
ng build –prod test-library
Using our Library in our Angular App
After building the library is complete, go ahead and import it to your Angular application:
import { TestLibraryModule, TestLibConfig } from 'test-library';
And add it to your imports array:
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
TestLibraryModule,
TestLibraryModule.forRoot(testLibConfigs)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
Don’t forget to define the testLibConfigs
constant which we are passing to the library:
const testLibConfigs: TestLibConfig = {
title: 'World Cup Teams',
teams: [
{
country: 'France',
trophies: 2
},
{
country: 'Brazil',
trophies: 5
},
{
country: 'Germany',
trophies: 4
},
{
country: 'Argentina',
trophies: 2
},
{
country: 'Spain',
trophies: 1
},
{
country: 'England',
trophies: 1
},
{
country: 'Uruguay',
trophies: 2
},
{
country: 'Italy',
trophies: 4
}
],
sport: 'Football/Soccer'
};
And finally, you can now use the library component inside your app components as shown below:
<div gdAuto><tl-test-library></tl-test-library></div>
NB: Our demo library is using the prefix
tl
. By default, libraries will useapp
prefix just like a normal angular app. You can specify the prefix for the library when generating a library using--prefix
flag.
Deploying your Library
When you have build your library, you will want to deploy it. To do that, just navigate to the build artefacts of your library and run NPM publish.
cd dist/test-library
npm publish