Angular2(-cli) Villains Game! (Part 2)

Jonas Felix
letsboot
Published in
9 min readOct 6, 2016

Using CLI, Webpack and Github Pages.

The Angular2 Villains tutorial using angular-cli and webpack from the start.

Here is part 1.

If you have any inputs or improvements please fork, fix and add a pull request: https://github.com/wingsuitist/villains/

7. Components

Let’s split this up into useful components.

You can keep you’re app running ng serve and look at it’s state after every step. It should auto reload.

Final source of this part: https://github.com/wingsuitist/villains/tree/v7.4.0

7.1. Generate Edit Component

The easiest way to create a component is by using angular-cli (you may recognized, we like this tool a lot):

cd src/app/
ng generate component villain-edit

With this you get a new folder containing your component including the css, html, TypeScript class and even the testing file (spec.ts).

But if you check git status you will see it also adds your component to the app.module.ts file to make it available right away.

7.2. Naming Conventions

If you take a look at the component TypeScript you’ll se something about the naming Conventions:

@Component({
selector: 'vil-villain-edit',
templateUrl: './villain-edit.component.html',
styleUrls: ['./villain-edit.component.css']
})
export class VillainEditComponent implements OnInit {

From top down you first see the selector which refers to the HTML tag <vil-villain-edit> in this case. You may have guessed from the word selector that this allows you to use more than tags. You could also use selector: ‘.vil-villain-edit’, which would allow you to call this component by any tag with the class like <div class=”vil-villain-edit”></div>.

Now why do we have the vil- in front of the selector? That’s due to the prefix we defined at the beginning of the tutorial. Angular-cli will use this prefix for the selector to make sure that there is no other tag interfering with your module. It’s kind of a name space alternative within the template.

Then you see the file names with the hyphen separating the parts of your components name.

And last but not least you see the class name which uses upper camel case.

There are great explanations within the Style Guide on how why those conventions have been chosen. But for now it’s great to know that the angular-cli team is holding our back and makes sure everything is correct.

7.3. Prepare for Input

In our new component we will also need the Villain object to hand it over to the view.

To do this we first have to import the Villain model class into the new component:

import { Villain } from '../shared'

The AppComponent will give the Villain to the VillainEditComponent through a so called input. This Input has to be imported from the angular/core in villain-edit.component.ts within the statement already generated by angular-cli:

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

And now we can declare the Villain property as an Input property:

export class VillainEditComponent implements OnInit {  @Input()
villain: Villain;

Now this can be used as a attribute when we use the <vil-villain-edit> tag somewhere.

7.4. Move the edit form

First we move the edit form from the app.component.html to the villain-edit.component.html. Everything stays the same as we will have the Villain object as we had before.

And in the app.component.html we add our new separate edit Component with the @Input property villain:

<vil-villain-edit [villain]="villain"></vil-villain-edit>

Now if you go back to your browser you’ll see the current state, which should look the same as before.

8. Services

We are still using static villains in our app.component.ts. As we want to access them from different other components in the future, and as we may also get them from a server, we build a service.

8.1. Create and inject Service class

A service in angular 2 is a regular class with the @Injectable() decorator to prepare it for dependency injection. (Here you find a great video about how decorators work:)

But why bother, let’s angular-cli generate our service:

cd src/app/
ng generate service shared/villain

This will generate a new file villain.service.ts which contains the minimal injectable Service:

import { Injectable } from '@angular/core';@Injectable()
export class VillainService {
constructor() { }}

To make importing the service simpler we add it to the shared/index.ts:

export * from './villain.model';
export * from './villain.service';

And now we have to make it available for consumption in our app.compontent.ts. We first add it to the list of things we want to import from the ./shared folder:

import { Component } from '@angular/core';
import { Villain, VillainService } from './shared';

Then we add it ad a provider to the @Component() decorator. And finally we add a constructor method with the TypeScript feature to directly define the injected object as a property of our AppCompontent:

@Component({
selector: 'vil-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [ VillainService ]
})
export class AppComponent {
title = 'Villains unite!';
villains = VILLAINS;
villain: Villain;
constructor(private villainService: VillainService) {}

We are now able to access it via this.villainService.

8.2. Let’s get some Villains

Let’s move the array of Villains from app.component.ts to villain.service.ts and add a getVillains() function to return it. Don’t forget to import the Villain.

import { Injectable } from '@angular/core';
import { Villain } from './villain.model';
const VILLAINS: Villain[] = [
{id: 1, alias: 'Rebooter', power: 'Random Updates'},
{id: 2, alias: 'Break Changer', power: 'API crushing'},
{id: 3, alias: 'Not-Tester', power: 'Edit on Prod'},
{id: 4, alias: 'Super Spamer', power: 'Mail Fludding'},
{id: 5, alias: 'Mrs. DDOS', power: 'Service Overuse'},
{id: 6, alias: 'Trojan', power: 'Remote Control'},
{id: 7, alias: 'Randzombie', power: 'Encryptor'},
{id: 8, alias: 'Leacher', power: 'Net Overload'},
{id: 23, alias: 'Captain Spaghetticoder', power: 'Bug Creator'}
];
@Injectable()
export class VillainService {
constructor() { }
getVillains(): Villain[] {
return VILLAINS;
}
}

And now we want to use the service within the AppComponent. And the proper place to use it is in the so called OnInit life cycle hook. To make angular2 call our ngOnInit() method we import and implement it in our AppComponent:

import { Component, OnInit } from '@angular/core';
import { Villain, VillainService } from './shared';
@Component({
selector: 'vil-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [ VillainService ]
})
export class AppComponent implements OnInit {
title = 'Villains unite!';
villain: Villain;
villains: Villain[];
constructor(private villainService: VillainService) {} ngOnInit(): void {
this.villains = this.villainService.getVillains();
}
onSelect(villain: Villain): void {
this.villain = villain;
}
}

9. Routing

We want to add a separate view which trains us in knowing each villains power. For that we need the possibility to switch between views which can be solved with routing.

There will be routing support for angular-cli. It was removed due to the new router version and will be integrated later on. (Please tell me to update this as soon as it’s available in the official version.)

9.1 Separate List view

Let’s add a new component for the List of Villain, to separate it from the AppComponent:

cd src/app
ng generate component villain-list

No let’s move the code from the AppComponent to villain-list/ component.

Your app.component.ts should look like this:

import { Component, OnInit } from '@angular/core';@Component({
selector: 'vil-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
constructor() {} ngOnInit(): void {
}
}

Your villain-list.component.ts should look like this:

import { Component, OnInit } from '@angular/core';
import { Villain, VillainService } from '../shared';
@Component({
selector: 'vil-villain-list',
templateUrl: './villain-list.component.html',
styleUrls: ['./villain-list.component.css'],
providers: [ VillainService ]
})
export class VillainListComponent implements OnInit {
title = 'Villains unite!';
villain: Villain;
villains: Villain[];
constructor(private villainService: VillainService) {} ngOnInit(): void {
this.villains = this.villainService.getVillains();
}
onSelect(villain: Villain): void {
this.villain = villain;
}
}

You can cut and paste the whole html from the app component to the villain list.

For now you can add the villain-list component to your app.component.html to make sure everything works:

<vil-villain-list></vil-villain-list>

9.2 Basic Routing

We will manage our routes in a new file app.routing.ts:

import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { VillainListComponent } from './villain-list/villain-list.component';const appRoutes: Routes = [
{
path: 'villains',
component: VillainListComponent
}
];
export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);

This adds a first route for the path villains/ which will load the VillainListComponent. To make use of this route we export a routing ModuleWithProviders to use it for routing in the AppComponent later on.

Now let’s add the routing to our module in the app.module.ts:

import { routing } from './app.routing';
// ...
@NgModule({
// ...
imports: [
BrowserModule,
FormsModule,
HttpModule,
routing

And now instead of directly loading the VillainListComponent in our app.component.html we will show what the router wants to load by using the router-outlet:

<router-outlet></router-outlet>

If you look at the browser now, you’ll get a JavaScript error that no route matches and nothing is shown. If you call the URLhttp://localhost:4200/villains you’ll see the application as it was before.

9.3 The navigation

Now let’s add a navigation to our app.component.html:

<ul>
<li>
<a routerLink="villains">Villains</a>
</li>
<li>
<a routerLink="powers">Powers</a>
</li>
</ul>

And let’s add a default route to our app.routing.ts:

const appRoutes: Routes = [
{
path: 'villains',
component: VillainListComponent
},
{
path: '**',
component: VillainListComponent
}

Now we have a navigation and in any case the VillainListComponent is loaded.

9.3 Add and route the powers component

So let’s add an empty PowersComponent for now and route to it:

cd src/app
ng generate component powers

Add it to our routing configuration:

// ...
import { VillainListComponent } from './villain-list/villain-list.component';
import { PowersComponent } from './powers/powers.component';
const appRoutes: Routes = [
{
path: 'powers',
component: PowersComponent
},
// ...

Now we can navigate between our components.

9.4 Redirects

To have one clear URL we can redirect / to villains:

{
path: '',
redirectTo: 'villains',
pathMatch: 'full'
},

The router goes through the appRouters Array from top to bottom matching each case. As soon as it finds the first match it executes it as configured.

10. Learning the Power

So let’s learn the power of each villain so we are ready to defend our selves.

10.1. Show random power

First we need to inject the VillainService to fetch the villains in our powers.component.ts:

import { Component, OnInit } from '@angular/core';
import { Villain, VillainService } from '../shared';
@Component({
selector: 'vil-powers',
templateUrl: './powers.component.html',
styleUrls: ['./powers.component.css'],
providers: [ VillainService ]
})
export class PowersComponent implements OnInit {
constructor(private villainService: VillainService) {}

And then we want to get a random villain for the quiz:

export class PowersComponent implements OnInit {
villains: Villain[];
randomVillain: Villain;
constructor(private villainService: VillainService) {} ngOnInit() {
this.villains = this.villainService.getVillains();
let randomKey: number = Math.floor(Math.random() * this.villains.length);
this.randomVillain = this.villains[randomKey];
}

So now we can show this villains power in the powers.component.html:

<h2>
Guess the Villain:
</h2>
<p>
{{randomVillain.power}}
</p>

10.2. Share the Villain Service (optimize)

Angular works with so called zones and each time you add a Service to the list of providers in Component it creates a separate instance of this Service.

There is no reason to create separate instances of the VillainService for our Components.

So let’s remove providers: [ VillainService ] from the VillainListComponent and from the PowersComponent and add it to the @NgModule in app.module.ts. We also need to import it in the app.module.ts and we have to keep the imports, as well as the parameters of the constructors, in each Component.

10.3. create getRandomVillain() (optimize)

Even if finding a random item in the array is a small piece of code, we should move it out of the component. Maybe later on we change the implementation of how to receive a random villain.

So let’s add the getRandomVillain function to our villain.service.ts:

getRandomVillain(): Villain {
let villains = this.getVillains();
let randomKey: number = Math.floor(Math.random() * villains.length);
return villains[randomKey];
}

And let’s us it in the PowersComponent:

ngOnInit() {
this.villains = this.villainService.getVillains();
this.randomVillain
= this.villainService.getRandomVillain();
}

10.4. Select the matching Villain

Now let’s list the Villains and let the user select the matching one, show our score and a message depending on our success:

<h2>
Guess the Villain:
</h2>
<p>
{{randomVillain.power}}
</p>
<h3>
Which Villain has this power?
</h3>
<p *ngIf="message">{{message}}</p>
<div><label>Score: </label>{{score}}</div>
<ul>
<li *ngFor="let villain of villains">
<a (click)="chooseVillain(villain)">
{{villain.alias}}
</a>
</li>
</ul>

And create the matching method which gets a new random villain, increases or resets our score and shows a message:

export class PowersComponent implements OnInit {
villains: Villain[];
randomVillain: Villain;
score: number = 0;
message: string;
constructor(private villainService: VillainService) {} ngOnInit() {
this.villains = this.villainService.getVillains();
this.randomVillain = this.villainService.getRandomVillain();
}
chooseVillain(villain: Villain) {
if(this.randomVillain.id == villain.id) {
this.score++;
this.message = 'correct!';
} else {
this.score = 0;
this.message = 'wrong - start over.'
}
this.randomVillain = this.villainService.getRandomVillain();
}
}

Find the code for every step on github.

--

--

Jonas Felix
letsboot

Full Stack Entrepreneur - on a creative journey after first successful exit. New tech, science, OpenSource, Software Development, Space Enthusiast, Skydiver...