Angular 2 (v15) beginner tutorial.
We will build a tic-tac-toe game in this tutorial. The game will have a board of 3x3 squares. Two players will take turns to mark a square with their symbol (either X or O). The player who succeeds in placing three of their marks in a horizontal, vertical, or diagonal row wins the game.
Download and install Node.js from here.
Open a terminal and run the following command to install Angular CLI globally.
npm install -g angular-cliCreate a new project using the Angular CLI.
ng new ng-beginnerWe'll be prompted to answer a few questions. The answers are as follows:
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS [ https://sass-lang.com/documentation/syntax#scss ]
Install extensions :
- Angular language service
- Angular 2 TypeScript Snippets
- Angular console
Open a terminal and navigate to the project folder. Run the following command to start the project.
ng serveOpen a browser and navigate to http://localhost:4200.
Create a new component called square using the Angular CLI.
ng generate component square --inline-template --inline-styleor
ng g c square -t -sThis component will have its HTML template and CSS styles inside. We will use this component to display a square in the board.
A neq folder called square will be created in the src/app folder. The following files will be created:
square.component.csssquare.component.html
app
├── app-routing.module.ts
├── app.component.html
├── app.component.scss
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
└── square
├── square.component.spec.ts
└── square.component.ts
Add the component in the app component template.
<app-square></app-square>
<router-outlet></router-outlet>Just as an example, we could display a random number in the square component, updated every second.
import { Component } from '@angular/core';
@Component({
selector: 'app-square',
template: `
<p>
{{ random }}
</p>
`,
styles: [
]
})
export class SquareComponent {
random = Math.random();
constructor() {
setInterval(() => {
this.random = Math.random();
}, 1000);
}
}We want to pass the value of the square to the square component.
We will use the @Input decorator to do that.
square.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-square',
template: `
<p>
{{ value }}
</p>
`,
styles: [],
})
export class SquareComponent {
@Input() value?: 'X' | 'O';
}Note: The ? after value means that the value is optional, so that TypeScript
won't complain if we don't pass a value to the component.
app.component.html
<app-square value='X'></app-square>
<app-square [value]="'O'"></app-square>
<router-outlet></router-outlet>Note: The [value] syntax is called property binding.
It is used to bind a property of a component to a value.
We just made our first UI (or dumb) component because it doesn't have any logic. Those components are called presentational components. They are used to display data, are easily testable and reusable.
Create a new component called board using the Angular CLI.
ng g c boardWhich will create a new folder called board in the src/app folder.
app/board
├── board.component.html
├── board.component.scss
├── board.component.spec.ts
└── board.component.ts
board.component.htmlis the template of the component.board.component.scssis the CSS styles of the component.board.component.spec.tsis the unit test of the component.board.component.tsis the TypeScript code of the component.
board.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-board',
templateUrl: './board.component.html',
styleUrls: ['./board.component.scss']
})
export class BoardComponent {
// representation of the nine squares on the board
squares: any[];
// help us track whose turn it is
xIsNext: boolean;
// will either be 'X', 'O', null or undefined
winner: string | null | undefined;
}We start by defining the properties of the board component.
squaresis an array of nine elements. Each element will be either 'X', 'O' or null.xIsNextis a boolean that will help us track whose turn it is.winnerwill either be'X','O',nullorundefined- If it is
'X', it means that X won the game. - If it is
'O', it means that O won the game. - If it is
null, it means that the game is not finished yet. - If it is
undefined, it means that the game is finished and there is no winner.
- If it is
We now use the onInit lifecycle hook to initialize the board.
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-board',
templateUrl: './board.component.html',
styleUrls: ['./board.component.scss']
})
export class BoardComponent implements OnInit {
// representation of the nine squares on the board
squares: any[];
// help us track whose turn it is
xIsNext: boolean;
// will either be 'X', 'O', null or undefined
winner: string | null | undefined;
ngOnInit() {
this.newGame();
}
// we initialize the game
//// we set the squares to an array of 9 nulls
//// we set the winner to null
//// we set xIsNext to true
newGame() {
this.squares = Array(9).fill(null);
this.winner = null;
// X is the first player
this.xIsNext = true;
}
}We are going to use a javascript getter to identify the player.
// identify the current player
get player() {
return this.xIsNext ? 'X' : 'O';
}We then are going to simulate a move on the board.
// simulate a move
makeMove(idx: number) {
if (!this.squares[idx]) {
// if the square is falsy, we fill it with the current player
this.squares.splice(idx, 1, this.player);
// we switch the player
this.xIsNext = !this.xIsNext;
}
}We then check if there is a winner.
makeMove(idx: number) {
if (!this.squares[idx]) {
// if the square is falsy, we fill it with the current player
this.squares.splice(idx, 1, this.player);
// we switch the player
this.xIsNext = !this.xIsNext;
}
// we check if there is a winner
this.winner = this.calculateWinner();
}
// check if there is a winner
// cf React tutorial: https://reactjs.org/tutorial/tutorial.html#declaring-a-winner
calculateWinner() {
// we define the winning combinations
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
// we loop through the winning combinations
for (let i = 0; i < lines.length; i++) {
// we destructure the winning combination
const [a, b, c] = lines[i];
// if the squares at the winning combination indexes are all equal and not falsy
if (
this.squares[a] &&
this.squares[a] === this.squares[b] &&
this.squares[a] === this.squares[c]
) {
// we return the winner
return this.squares[a];
}
}
// if there is no winner, we return null
return null;
}We first display the current player.
board.component.html
<h1>Current Player: {{ player }} </h1>We then display a button to start a new game.
<button (click)="newGame()">Start new game</button>We then display the nine squares.
<main>
<app-square
*ngFor="let square of squares; let i = index"
[value]="square"
(click)="makeMove(i)">
</app-square>
</main>*ngForis a structural directive that loops through the squares array and creates a new square component for each element.[value]="square"is an attribute directive that passes the value of the square to the square component.(click)="makeMove(i)"is an event binding that calls the makeMove method when the user clicks on a square.
We want to finally display the winner, if there is one.
<h2>
<ng-container *ngIf="winner">Player {{ winner }} wins! </ng-container>
</h2>*ngIfis a structural directive that displays the winner if there is one.<ng-container>is a structural directive that does not create a new DOM element.{{ winner }}is an interpolation that displays the winner.
The final template looks like this:
board.component.html
<h1>Current Player: {{ player }} </h1>
<button (click)="newGame()">Start new game</button>
<h2>
<ng-container *ngIf="winner">Player {{ winner }} wins! </ng-container>
</h2>
<main>
<app-square
*ngFor="let square of squares; let i = index"
[value]="square"
(click)="makeMove(i)">
</app-square>
</main>In the file board.component.scss, we add the following styles:
main {
display: grid;
grid-template-columns: 200px 200px 200px;
}
h2 {
height: 2rem;
}
app-square {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid gray;
background: lightgray;
height: 200px;
font-size: 5em;
}- The display property of the
mainelement is set togrid. This allows us to display the squares in a grid. - The
grid-template-columnsproperty of the main element is set to200px 200px 200px. This allows us to display the squares in a 3x3 grid. - The height property of the
h2element is set to2rem. This allows us to fix the size. - The display property of the
app-squareelement is set to flex. This allows us to display the value of the square in the center of the square.
hhmmm... not very pretty, is it?
Let's be smart and use a library to make it look better.
There are many third party component libraries that we can use to make our application look better like Bootstrap, Ionic, Nebular, etc. But we are going to use Angular Material.
We are going to use the Angular CLI to install Angular Material.
ng add @angular/materialThe CLI will ask us a few questions:
- Choose a prebuilt theme name, or "custom" for a custom theme:
Custom - Set up Global Angular Material typography styles?
Yes - Include the Angular animations module?
Do not include
? Choose a prebuilt theme name, or "custom" for a custom theme: Custom
? Set up global Angular Material typography styles? Yes
? Include the Angular animations module? Do not include
UPDATE package.json (1108 bytes)
✔ Packages installed successfully.
UPDATE src/styles.scss (1644 bytes)
UPDATE src/index.html (578 bytes)The CLI updated the package.json file, the src/styles.scss file and the src/index.html file.
Let's see the changes in the UI.
Not much has changed aside from the font and the margin.
src/styles.scss
// Custom Theming for Angular Material
// For more information: https://material.angular.io/guide/theming
@use '@angular/material' as mat;
// Plus imports for other components in your app.
// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
// Be sure that you only ever include this mixin once!
@include mat.core();
// Define the palettes for your theme using the Material Design palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/
$ng-beginner-primary: mat.define-palette(mat.$indigo-palette);
$ng-beginner-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
// The warn palette is optional (defaults to red).
$ng-beginner-warn: mat.define-palette(mat.$red-palette);
// Create the theme object. A theme consists of configurations for individual
// theming systems such as "color" or "typography".
$ng-beginner-theme: mat.define-light-theme((
color: (
primary: $ng-beginner-primary,
accent: $ng-beginner-accent,
warn: $ng-beginner-warn,
)
));
// Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component
// that you are using.
@include mat.all-component-themes($ng-beginner-theme);
/* You can add global styles to this file, and also import other style files */
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }The following only has impact for now:
- The
htmlandbodyelements have a height of 100%. - The
bodyelement has a margin of 0. - The
bodyelement has a font-family of Roboto.
We can see that a lot was added in terms of theming but by default not much will change unless we use material components and classes.
As for the src/index.html file, the following was added:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>NgBeginner</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>It added the following:
- The
linkelement with thehrefattribute set tohttps://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swapwas added. This allows us to use the Roboto font. - The
linkelement with thehrefattribute set tohttps://fonts.googleapis.com/icon?family=Material+Iconswas added. This allows us to use the Material Icons. - The
bodyelement has themat-typographyclass. This allows us to use the typography styles of Angular Material.
Let's start with the button.
First, we need to import the MatButtonModule in the app.module.ts file.
import {MatButtonModule} from '@angular/material/button';
@NgModule({
declarations: [
AppComponent,
SquareComponent,
BoardComponent
],
imports: [
BrowserModule,
AppRoutingModule,
MatButtonModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }Then, we can use the mat-raised-button directive in the board.component.html file
and color it with the color attribute.
<h1>Current Player: {{ player }} </h1>
<button mat-raised-button color="primary" (click)="newGame()">NEW GAME</button>
<h2>
<ng-container *ngIf="winner">Player {{ winner }} wins! </ng-container>
</h2>
<main>
<app-square
*ngFor="let square of squares; let i = index"
[value]="square"
(click)="makeMove(i)">
</app-square>
</main>Let's take a look at the UI.
Cool button, right?
We are going to use the MatCardModule to make the board look better.
<mat-card>
<mat-card-header>
<h1>Current Player : {{ player }} </h1>
</mat-card-header>
<mat-card-content>
<button mat-flat-button color="primary" (click)="newGame()">
NEW GAME
</button>
<h2>
<ng-container *ngIf="winner">Player {{ winner }} wins ! </ng-container>
</h2>
<main>
<app-square
*ngFor="let square of squares; let i = index"
[value]="square"
(click)="makeMove(i)">
</app-square>
</main>
</mat-card-content>
</mat-card>And modify the board.component.scss file to clean, center it and make it responsive.
$square-size: 8rem;
:host {
display: flex;
justify-content: center;
}
mat-card {
display: flex;
text-align: center;
flex-direction: column;
align-items: center;
margin-top: 2em;
}
main {
display: grid;
grid-template-columns: $square-size $square-size $square-size;
}
h2 {
height: 2rem;
}
app-square {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid;
height: $square-size;
}Some explanations:
- The
$square-sizevariable is used to define the dimensions of the grid. - For the grid, we changed the unit to
remto make it responsive. - The
:hostselector is used to select the host element of the component. In this case, it is theboard.component.htmlfile. - We use
display: flexandjustify-content: centerto center themat-cardelement. - Inside the
mat-cardelement, we usedisplay: flex,text-align: center,flex-direction: columnandalign-items: centerto center the content.
Also tweaked the square.component.ts file to be responsible for the font-size and change
the cursor on hover.
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-square',
template: `
{{ value }}
`,
styles: [`
:host {
font-size: 4rem;
&:hover {
cursor: pointer;
}
}
`],
})
export class SquareComponent {
@Input() value?: 'X' | 'O';
}AND Voilà!
I want to make improvement in theming:
- Define my custom palette
- Handle dark / light mode depending on the user's system preferences
After a few changes it looks like this on my System :
Definitely better!
Plus, if I were to change my system preferences to light, the theme would change automatically. You wouldn't even need to refresh the page.
I won't go into implementation details since it is not that complicated and I created a Pull Request intentionally, so you can check the code diff:
I just noticed in my code that I was repeating the same logic in the same or different templates.
In square.component.html
<h1>
Next Player :
<b [class]="player === 'X' ? 'primary' : 'accent'">{{ player }}</b>
</h1>
...
<ng-container *ngIf="winner">
Player
<b [class]="winner === 'X' ? 'primary' : 'accent'">{{ winner }}</b>
wins !
</ng-container>And in board.component.ts
template: `
<span [class]="value === 'X' ? 'primary' : 'accent'" >
{{ value }}
</span>
`,If you were to re-use some code in the same template, then it's easy, you can create a function in the component and call it in the template.
What if you want to re-use some code in different templates?
That's where the pipes come in.
Pipes are a way to transform data in the template. There are pre-defined pipes in Angular, for
example, the uppercase pipe.
{{ 'hello' | uppercase }}See https://angular.io/guide/pipes for more information.
Here, we need to create a custom pipe. Let's create a getClass pipe that will return the class
depending on the player.
You can generate a pipe with the Angular CLI:
ng generate pipe pipes/getClassAngular will generate a file getClass.pipe.ts in the src/app/pipes folder.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'getClass'
})
export class GetClassPipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]): unknown {
return null;
}
}Let's modify it to fit our needs.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'getClass'
})
export class GetClassPipe implements PipeTransform {
transform(player: 'X' | 'O' | undefined | null): string {
return player === 'X' ? 'primary' : 'accent';
}
}Then we can use it in our templates.
<h1>
Next Player :
<b class="{{ player | getClass }}">{{ player }}</b>
</h1>
<ng-container *ngIf="winner">
Player
<b class="{{ winner | getClass }}">{{ winner }}</b>
wins !
</ng-container> template: `
<span class="{{ value | getClass }}" >
{{ value }}
</span>
`,That's it!






