My first Angular Universal component
If you want to work with Angular and want to focus on server-side rendering, you have probably arrived at Angular Universal (at least from googling).
Universal comes to make server-side in Angular really handy. I decided then to try their Get Started tutorial. However, the steps described there do not seem to work as expected.
Then I saw the Universal Starter, a real bless. Now we can set up our environment to work with Angular Universal and make server-side rendering possible!
Also, I saw this awesome tutorial, which I decided to take as a base for this story.
Then, I could finally add a component of my own to the solution and check it rendered on the server.
Steps to do it:
First: create a project with Universal Starter
We have to clone the Universal starter and install the packages:
# Clone repo
git clone https://github.com/angular/universal-starter my-angular-ssr# Install modules
cd my-angular-ssr
npm install
Second: create the component
In the folder src/+app, we create a component that will display a list of music albums. The name of the new folder will be +discs
The structure of the folder will look like:
my-angular-ssr
|-- src
| |-- +app
| | |-- +about
| | |-- +discs
| | | |-- discs.component.css
| | | |-- discs.component.html
| | | |-- discs.component.ts
| | | |-- discs.module.ts
| | | |-- discs.schema.ts
| | | |-- disc-routing.module.ts
| | |-- +home
| | |-- +lazy
| | |-- +todo
| | |-- shared
...
The component will much look like the +todo one.
We can then copy/paste the following codes into the corresponding files:
- discs.component.css
.table {
padding-top: 30px;
}
.row {
display: table-row;
background: #f6f6f6;
}.row:nth-of-type(odd) {
background: #e9e9e9;
}.row.header {
color: #ffffff;
font-weight: bold;
background: #158126;
}.cell {
padding: 20px;
display: table-cell;
}
- discs.component.html
<div class="disc">Discs component
<form #f="ngForm" (ngSubmit)="addDisc()">
<label for="newTitle">Title </label><input id="newTitle" name="newDisc" [(ngModel)]="newTitleValue" />
<label for="newArtist">Artist </label><input id="newArtist" name="newDisc" [(ngModel)]="newArtistValue" />
<label for="newYear">Year </label><input id="newYear" name="newDisc" [(ngModel)]="newYearValue" />
<button>Submit</button>
</form><div class="table">
<div class="row header">
<div class="cell">Id</div>
<div class="cell">Title</div>
<div class="cell">Artist</div>
<div class="cell">Year</div>
</div>
<div class="row" *ngFor="let disc of discs">
<div class="cell">{{disc.id}}</div>
<div class="cell">{{disc.title}}</div>
<div class="cell">{{disc.artist}}</div>
<div class="cell">{{disc.year}}</div>
</div></div>
- discs.component.ts
import { Component, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';import { ModelService } from '../shared/model/model.service';
import { Disc } from './discs.schema';@Component({
changeDetection: ChangeDetectionStrategy.Default,
encapsulation: ViewEncapsulation.Emulated,
selector: 'discs',
styleUrls: [ './discs.component.css' ],
templateUrl: './discs.component.html'
})
export class DiscComponent {discs: Disc[];
newDisc: Disc;
newTitleValue = '';
newArtistValue = '';
newYearValue = '';constructor(public model: ModelService) {
// we need the data synchronously for the client to set the server response
// we create another method so we have more control for testing
this.universalInit();
}addDisc() {
this.newDisc = new Disc(
this.discs.length + 1,
this.newTitleValue,
this.newArtistValue,
parseInt(this.newYearValue, 10)
);
this.discs.push(this.newDisc);
this.newTitleValue = '';
this.newArtistValue = '';
this.newYearValue = '';
}universalInit() {
this.model
.get('/api/discs')
.subscribe(data => {
console.log(data);
this.discs = data;
});
}}
- discs.module.ts
import { NgModule } from '@angular/core';import { SharedModule } from '../shared/shared.module';
import { DiscComponent } from './discs.component';
import { DiscsRoutingModule } from './discs-routing.module';@NgModule({
imports: [
SharedModule,
DiscsRoutingModule
],
declarations: [
DiscComponent
]
})
export class DiscsModule { }
- discs.schema.ts
export class Disc {
constructor(
public id: number,
public title: string,
public artist: string,
public year: number,
) { }
}
- discs-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';import { DiscComponent } from './discs.component';@NgModule({
imports: [
RouterModule.forChild([
{ path: 'discs', component: DiscComponent }
])
]
})
export class DiscsRoutingModule { }
Third: insert the new component into the application
Now we insert the entries to the component into the application. Here are the modifications to be done into the starter to do so:
- src/+app/app.module.ts
...
import { HomeModule } from './+home/home.module';
import { AboutModule } from './+about/about.module';
import { TodoModule } from './+todo/todo.module';
import { DiscsModule } from './+discs/discs.module';
...@NgModule({
declarations: [ AppComponent, XLargeDirective ],
imports: [
...
TodoModule,
DiscsModule,
...
]
})
...
- src/+app/app.component.ts
...
@Component({
...
template: `
<h3 id="universal">Angular2 Universal</h3>
<nav>
<a routerLinkActive="router-link-active" routerLink="home">Home</a>
<a routerLinkActive="router-link-active" routerLink="about">About</a>
<a routerLinkActive="router-link-active" routerLink="todo">Todo</a>
<a routerLinkActive="router-link-active" routerLink="lazy">Lazy</a>
<a routerLinkActive="router-link-active" routerLink="discs">Discs</a>
</nav>
...
`
})
...
- src/server.routes.ts
export const routes: string[] = [
'about',
'home',
'todo',
'lazy',
'discs'
];
Now, it is just to change the backend side (if we wish to, it is only for demos!):
- src/backend/api.ts
...// disc APIvar DISCINDEX = 6;
var DISCS = [
{ title: "OK Computer", artist: "Radiohead", year: 1997, id: 1 },
{ title: "The Queen is dead", artist: "The Smiths", year: 1986, id: 2 },
{ title: "Be Here Now", artist: "Oasis", year: 1997, id: 3 },
{ title: "Appetite for Destruction", artist: "Guns N Roses", year: 1987, id: 4 },
{ title: "Back To Black", artist: "Amy Winehouse", year: 2006, id: 5 },
{ title: "Hotel California", artist: "Eagles", year: 1976, id: 6 }
];export function createDiscsApi() {var router = Router();router.route('/discs')
.get(function(req, res) {
console.log('GET');
// 70ms latency
setTimeout(function() {
res.json(DISCS);
}, 0);})
.post(function(req, res) {
console.log('POST', util.inspect(req.body, {colors: true}));
var disc = req.body;
if (disc) {
DISCS.push({
title: disc.title,
artist: disc.artist,
year: disc.year,
id: DISCINDEX++
});
return res.json(disc);
}return res.end();
});return router;
}
- src/server.ts
...
//
/////////////////////////
// ** Example API
// Notice API should be in aseparate process
import { serverApi, createDiscsApi, createTodoApi } from './backend/api';// Our API for demos only
app.get('/data.json', serverApi);
app.use('/api', createDiscsApi(), createTodoApi());
...
Run it
Now we can see the results. Run the following command:
npm start
Browse http://localhost:3000/discs:
And the preview on the server gives this:
And then, we can play with it as much as we want!
What now?
Guys from Angular Universal are working hard in order to fix the issues and provide a stable version of the great starter kit. Even though, we can say that it is possible to create and render on the server our own Angular Components with what they give us today, and that is awesome news!