Labs for the angular workshop at the Angular Days 2023 Munich from Christian Liebel and Sascha Lehmann.
Start: https://stackblitz.com/edit/github-uelwhb
Show Labs
In your freshly created project, open the file src/app/app.component.html
and try the following bindings (one after another). You can completely remove the existing contents of this file.
{{ 'hallo' }}
{{ 3 }}
{{ 17 + 4 }}
{{ '<div>Does this work?</div>' }}
{{ alert('boom') }}
Which values do you see in the preview pane? Are there any error messages?
Now, open the file src/app/app.component.ts
and introduce a new field called value
within the AppComponent
class:
export class AppComponent {
// …
public value = "Hello";
}
Bind the value of this field to the template file, by adding the following interpolation to src/app/app.component.html
.
{{ value }}
Then, Hello
should show up in the preview pane.
- Declare a new field called
color
on your component instance and initialize it with a CSS color value (e.g.,hotpink
) - Create a new
div
element in the AppComponent’s HTML template (Hint:<div></div>
) - Bind the value of the field to the background color of the
div
element (Hint—add the following attribute assignment to thediv
node:[style.backgroundColor]="color"
)
The square brackets are not a typo! They might look odd, but it woll work.
- Implement a new method
onClick
on the component instance that opens an alert box (Hint:public onClick() { alert('Hello!'); }
) - Create a new
button
element in the AppComponent’s HTML template (Hint:<button>Click me.</button>
) - Bind the click event of the button to the
onClick
method (Hint—add the following attribute assignment to thebutton
node:(click)="onClick()"
) - Implement a new method
onMouseMove
on the component instance that logs to the console (Hint:console.log('Hello!')
) - Bind the
mousemove
event of the button toonMouseMove
Again, the brackets are not a typo. It will work out just fine.
Show Solution
https://stackblitz.com/edit/github-uelwhb-uhxbn5
export class AppComponent {
public value = "Hello";
public color = "hotpink";
public onClick(): void {
alert('Hello!');
}
public onMouseMove(): void {
console.log('Hello!');
}
}
{{ 'hallo' }} <br/>
{{ 3 }} <br/>
{{ 17 + 4 }} <br/>
{{ '<div>Does this work?</div>' }} <br/>
<hr/>
{{ value }}
<hr/>
<div [style.backgroundColor]="color">Test</div>
<button (click)="onClick()" (mousemove)="onMouseMove()">Click me.</button>
Start: https://stackblitz.com/edit/github-uelwhb-uhxbn5
Show Labs
Adjust the implementations of onClick()
and onMouseMove()
to print the coordinates of the mouse (instead of printing Hello!
)
Hints:
(click)="onClick($event)"
public onClick(event: MouseEvent): void {}
MouseEvent documentation: https://developer.mozilla.org/de/docs/Web/API/MouseEvent
Show Solution
https://stackblitz.com/edit/github-uelwhb-ery5wz
export class AppComponent {
public value = "Hello";
public color = "hotpink";
public onClick(event: MouseEvent): void {
alert(event.clientX);
}
public onMouseMove(event: MouseEvent): void {
console.log(event.clientX);
}
}
<button (click)="onClick($event)" (mousemove)="onMouseMove($event)">Click me.</button>
Start: https://stackblitz.com/edit/github-uelwhb-ery5wz
Show Labs
Adjust your value binding from lab #1 to be printed as lowercase (Hint: {{ value | lowercase }}
).
Then, adjust it to be printed as UPPERCASE.
Add a new numeric field to your AppComponent (e.g., public number = 3.14159;
). Bind this field to the template using the pipes:
percent
currency
number
(showing five decimal places)
Please use three interpolations ({{ number | … }} {{ number | … }} {{ number | … }}
).
Right-click the app
folder and select Angular Generator, then Pipe.
The pipe should be called yell
. Open the generated file yell.pipe.ts
.
Implement the yell pipe as follows:
- The yell pipe should suffix the bound value with three exclamation marks (e.g.,
value + '!!!'
or`${value}!!!`
). - The developer can optionally pass an argument to override the suffix (
args
parameter).
Interpolation | Value |
---|---|
{{ value | yell }} |
Hello!!! |
{{ value | yell:'???' }} |
Hello??? |
Show Solution
https://stackblitz.com/edit/github-uelwhb-yr4u2r
export class AppComponent {
public value = "Hello";
public number = 3.14159;
}
@Pipe({
name: 'yell',
})
export class YellPipe implements PipeTransform {
transform(value: string, args: string): any {
const suffix = args || '!!!';
return `${value}${suffix}`;
}
}
{{ value | uppercase }} <br/>
{{ number | percent }} <br/>
{{ number | currency }} <br/>
{{ number | number:'0.5' }} <br/>
{{ value | yell }}<br/>
{{ value | yell:'???' }}
Start: https://stackblitz.com/edit/github-uelwhb-yr4u2r
Show Labs
Right-click the app
folder and select Angular Generator, then Component.
The new component should be named todo
. Which files have been created? What’s the selector of the new component (selector
property of todo.component.ts
)?
Open the AppComponent’s template (i.e., HTML file) and use the new component there by adding an HTML element with the new component’s selector name (e.g., if the selector is my-selector
, add <my-selector></my-selector>
to the template).
If you like, you can duplicate this HTML element to see the idea of componentization in action.
Show Solution
https://stackblitz.com/edit/github-uelwhb-97pzlr
todo.component.ts
@Component({
selector: 'app-todo',
templateUrl: './todo.component.html',
styleUrls: ['./todo.component.css'],
})
export class TodoComponent implements OnInit {
constructor() {}
ngOnInit() {}
}
app.component.html
<app-todo></app-todo>
Start: https://stackblitz.com/edit/github-uelwhb-97pzlr
Show Labs
- Extend your
TodoComponent
with an@Input
field calledtodo
. - Add a new
myTodo
field to the AppComponent and assign a todo object to it:{ name: "Wash clothes", done: false, id: 3 }
- Pass the
myTodo
object to thetodo
component from the AppComponent’s template by using an input binding. - In the
TodoComponent
’s template, bind the value of thetodo
field to the UI using theJSON
pipe.
- Extend your
TodoComponent
with an@Output
field calleddone
. - Add a
button
to yourTodoComponent
and an event binding for theclick
event of this button. When the button is clicked, emit thedone
event. Pass the current todo object as the event argument. - In the
AppComponent
’s template, bind to thedone
event using an event binding and log the finalized item to the console.
Show Solution
https://stackblitz.com/edit/github-uelwhb-qynuhh
todo.component.ts
import { Input, Output, EventEmitter, OnInit } from '@angular/core';
@Component({
selector: 'app-todo',
templateUrl: './todo.component.html',
styleUrls: ['./todo.component.css']
})
export class TodoComponent implements OnInit {
@Input() todo: any;
@Output() done = new EventEmitter<any>();
constructor() { }
ngOnInit() {
}
markTodoAsDone(){
this.done.emit(this.todo);
}
}
todo.component.html
<p>
inside todo-component: <br/>
{{todo | json}}
</p>
<button (click)="markTodoAsDone()">mark as done</button>
app.component.html
<app-todo [todo]="todoObject" (done)="catchDoneEvent($event)"></app-todo>
app.component.ts
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
public todoObject = { name: "Wash clothes", done: false, id: 3 }
catchDoneEvent(todo: any) {
console.log(todo)
}
}
Start: https://stackblitz.com/edit/github-uelwhb-qynuhh
Show Labs
Right-click the app
folder and select Angular Generator, then Directive. Create a directive (e.g., named color
) that takes a color as an input binding. The directive should set the color of the host element (using a host binding).
Create another directive (e.g., named click
) that adds a click handler to the elements where it’s placed on. Whenever the item is clicked, log a message to the console.
Show Solution
https://stackblitz.com/edit/github-uelwhb-kkukae
todo.component.ts
import { Input, Output, EventEmitter, OnInit } from '@angular/core';
@Component({
selector: 'app-todo',
templateUrl: './todo.component.html',
styleUrls: ['./todo.component.css']
})
export class TodoComponent implements OnInit {
@Input() todo: any;
@Output() done = new EventEmitter<any>();
colorToBind = "blue";
constructor() { }
ngOnInit() {
}
markTodoAsDone(){
this.done.emit(this.todo);
}
}
todo.component.html
<p appClick appColor color="green">
inside todo-component: <br/>
{{todo | json}}
</p>
<p>
<span appColor [color]="colorToBind">test to apply directive on</span>
</p>
<button (click)="markTodoAsDone()">mark as done</button>
color.directive.ts
import { Directive, Input, HostBinding } from '@angular/core';
@Directive({
selector: '[appColor]'
})
export class ColorDirective {
@HostBinding('style.color')
@Input() color: string;
}
click.directive.ts
import { Directive, Input, HostListener } from '@angular/core';
@Directive({
selector: '[appClick]',
})
export class ClickDirective {
@HostListener('click', ['$event'])
handleClick($event): void {
console.log('a message');
}
constructor() {}
}
Start: https://stackblitz.com/edit/github-uelwhb-kkukae
Show Labs
In your AppComponent…
import {ElementRef} from '@angular/core';
- Request an instance of
ElementRef
via constructor injection - Log the instance to the console
- Inspect it
- Is the instance provided by the root injector, a module or a component?
Right-click the app
folder and select Angular Generator, then Class.
Create a new model class called todo
and add the properties:
name
(string)done
(boolean)id
(number, optional)
Right-click the app
folder and select Angular Generator, then Service.
In your TodoService, add the following methods:
create(todo: Todo): Todo
get(todoId: number): Todo
getAll(): Todo[]
update(todo: Todo): void
delete(todoId: number): void
Add a very basic, synchronous implementation for getAll. Inject your TodoService into the AppComponent (don’t forget to update the imports on top). Log the list of todos to the console.
Show Solution
https://stackblitz.com/edit/github-uelwhb-dtv755
app.component.ts
import { ElementRef } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
public todoObject = { name: "Wash clothes", done: false, id: 3 }
constructor(private readonly elementRef: ElementRef,
private readonly todoService: TodoService){
console.log("elementRef from constructor", elementRef);
console.log(todoService.getAll());
}
catchDoneEvent(todo: any) {
console.log(todo);
}
logElementRef(){
console.log("elementRef from console as property", this.elementRef);
}
}
app.module.ts
import { NgModule, InjectionToken, Inject } from '@angular/core';
// other imports
@NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [ AppComponent,
HelloComponent,
YellPipe,
TodoComponent,
ColorDirective,
ClickDirective ],
providers: [TodoService],
bootstrap: [ AppComponent ]
})
export class AppModule {}
todo.ts
export class Todo {
name: string;
done: boolean;
id?:number;
}
todo.service.ts
@Injectable()
export class TodoService {
private todos: Todo[] = [];
constructor() { }
create(todo: Todo) { }
get(todoId: number) { }
getAll(): Todo[] {
return this.todos;
}
update(todo: Todo): void { }
delete(todoId: number): void { }
}
Start: https://stackblitz.com/edit/github-uelwhb-dtv755
Show Labs
In your AppComponent’s template, add the following snippet:
<button (click)="toggle()">Toggle</button>
<div *ngIf="show">
I’m visible!
</div>
On the component class, introduce a new show
field and toggle it via a new toggle()
method (Hint: this.show = !this.show;
).
In the AppComponent, introduce a new field todos and assign the return value of todoService.getAll() to it.
Bind this field to the view using the *ngFor
structural directive and an unordered list (ul
) with one list item (li
) for each todo:
<ul>
<li *ngFor="let todo of todos"></li>
</ul>
Next, iterate over your TodoComponent (app-todo) instead and pass the todo via the todo property binding. Adjust the template of TodoComponent to include:
- a checkbox (input) to show the “done” state
- a label to show the “name” text
<label>
<input type="checkbox" [checked]="todo.done">
{{ todo.name }}
</label>
Show Solution
https://stackblitz.com/edit/github-uelwhb-kud3vk
app.component.ts
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
show = true;
todos = [];
constructor(private readonly elementRef: ElementRef,
private readonly todoService: TodoService){
console.log("elementRef from constructor", elementRef);
this.todos = todoService.getAll();
}
catchDoneEvent(todo: any) {
console.log(todo)
}
logElementRef(){
console.log("elementRef from console as property", this.elementRef);
}
toggle() {
this.show = !this.show;
}
}
app.component.html
<button (click)="toggle()">Toggle</button>
<div *ngIf="show">
I am visible!
</div>
<ul>
<li *ngFor="let todo of todos">{{todo.name}}</li>
</ul>
<app-todo *ngFor="let todo of todos" [todo]="todo" (done)="catchDoneEvent($event)"></app-todo>
todo.service.ts
@Injectable()
export class TodoService {
private todos: Todo[] = [];
constructor() {
this.todos.push({ name: "Wash clothes", done: false, id: 3 });
}
create(todo: Todo) {
}
get(todoId: number) {}
getAll(): Todo[] {
return this.todos;
}
update(todo: Todo): void {}
delete(todoId: number): void {}
}
todo.component.ts
import { Import, Output } from '@angular/core';
@Component({
selector: 'app-todo',
templateUrl: './todo.component.html',
styleUrls: ['./todo.component.css']
})
export class TodoComponent implements OnInit {
@Input() todo: any;
@Output() done = new EventEmitter<any>();
colorToBind = "blue";
constructor() { }
ngOnInit() {
}
markTodoAsDone(todo: Todo) {
todo.done = !todo.done;
this.done.emit(todo);
}
}
todo.component.html
<label>
<input type="checkbox" [checked]="todo.done" (change)="markTodoAsDone(todo)">{{ todo.name }}
</label>
Start: https://stackblitz.com/edit/github-uelwhb-kud3vk
Show Labs
Adjust your TodoService
to now return Observables and upgrade the synchronous value in getAll()
to an Observable (via of()
).
create(todo: Todo): Observable<Todo>
get(todoId: number): Observable<Todo>
getAll(): Observable<Todo[]>
update(todo: Todo): Observable<void>
delete(todoId: number): Observable<void>
In your AppModule, add HttpClientModule to the imports array
Add a constructor to TodoService and request an instance of HttpClient and use HTTP requests instead of returning synchronous data using the following URLs:
Method | Action | URL |
---|---|---|
GET | get all | https://tt-todos.azurewebsites.net/todos |
GET | get single | https://tt-todos.azurewebsites.net/todos/1 |
POST | create | https://tt-todos.azurewebsites.net/todos |
PUT | update | https://tt-todos.azurewebsites.net/todos/1 |
DELETE | delete | https://tt-todos.azurewebsites.net/todos/1 |
Show Solution
https://stackblitz.com/edit/github-uelwhb-wxkhew
app.module.ts
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [BrowserModule, FormsModule, HttpClientModule],
declarations: [
AppComponent,
HelloComponent,
YellPipe,
TodoComponent,
ColorDirective,
ClickDirective,
],
providers: [TodoService],
bootstrap: [AppComponent],
})
export class AppModule {}
todo.service.ts
@Injectable()
export class TodoService {
private actionUrl = "https://tt-todos.azurewebsites.net/todos"
constructor(private readonly httpClient: HttpClient) { }
create(todo: Todo) {
return this.httpClient.post<Todo>(this.actionUrl, todo);
}
get(todoId: number) {
return this.httpClient.get<Todo>(`${this.actionUrl}/${todoId}`);
}
getAll(): Observable<Todo[]> {
return this.httpClient.get<Todo[]>(this.actionUrl);
}
update(todo: Todo) {
return this.httpClient.put(`${this.actionUrl}/${todo.id}`, todo);
}
delete(todoId: number) {
return this.httpClient.delete(`${this.actionUrl}/${todoId}`);
}
}
app.component.ts
import { ElementRef } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
private show = true;
todos = [];
constructor(private readonly elementRef: ElementRef,
private readonly todoService: TodoService){
console.log("elementRef from constructor", elementRef);
todoService.getAll().subscribe(todos => this.todos = todos);
}
catchDoneEvent(todo: any) {
console.log(todo)
}
logElementRef(){
console.log("elementRef from console as property", this.elementRef);
}
toggle() {
this.show = !this.show;
}
}
Start: https://stackblitz.com/edit/github-uelwhb-wxkhew
Show Labs
Use the async
pipe instead of manually subscribing.
Instead of:
public todos: Todo[];
Use:
public todos$: Observable<Todo[]>;
Instead of:
todoService.getAll().subscribe(todos => this.todos = todos);
Use:
this.todos$ = todoService.getAll();
Instead of:
<app-todo *ngFor="let todo of todos" [todo]="todo">
</app-todo>
Use:
<app-todo *ngFor="let todo of todos$ | async" [todo]="todo">
</app-todo>
Show Solution
https://stackblitz.com/edit/github-uelwhb-hp1ax1
app.component.ts
import { ElementRef } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
private show = true;
todos$: Observable<Todo[]>;
constructor(private readonly elementRef: ElementRef,
private readonly todoService: TodoService){
console.log("elementRef from constructor", elementRef);
}
ngOnInit() {
this.todos$ = this.todoService.getAll();
}
catchDoneEvent(todo: any) {
console.log(todo)
}
logElementRef(){
console.log("elementRef from console as property", this.elementRef);
}
toggle() {
this.show = !this.show;
}
}
app.component.html
<div *ngIf="todos$ | async as todos">
You have {{ todos.length }} todos!
</div>
<ul>
<li *ngFor="let todo of todos$ | async">
{{ todo.name }}
</li>
</ul>
<app-todo *ngFor="let todo of todos$ | async" [todo]="todo" (done)="catchDoneEvent($event)"></app-todo>
Start: https://stackblitz.com/edit/github-uelwhb-hp1ax1
Show Labs
Add the following components:
- TodoListComponent
- TodoEditComponent
- TodoCreateComponent
- NotFoundComponent
Define/assign the following routes:
- todos
- todos/:id
- todos/new
- **
Redirect the default (empty) route to the todo list.
Add a <router-outlet>
to your AppComponent:
<router-outlet></router-outlet>
Then try out different routes by typing them into the address bar.
- Which parts of the page change?
- Which parts stay the same?
In your AppComponent, define two links:
- Home (/todos)
- Create (/todos/new)
In TodoListComponent, request all todos and update the template:
<ul>
<li *ngFor="let todo of todos$ | async"><a [routerLink]="todo.id">{{ todo.name }}</a></li>
</ul>
In AppComponent, add routerLinkActive:
<a routerLink="/todos" routerLinkActive="my-active">Home</a>
Or, if you prefer:
<a routerLink="/todos" routerLinkActive="my-active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
Add a CSS style for a.my-active
In TodoEditComponent, listen for changes of the ActivatedRoute and retrieve the record with the given ID from the TodoService and bind it to the view as follows:
{{ todo$ | async | json }}
Show Solution
https://stackblitz.com/edit/github-uelwhb-kdonbj
app.module.ts
import { RouterModule, Routes } from '@angular/router';
const appRoutes: Routes = [
{ path: '', redirectTo: 'todos', pathMatch: 'full' },
{ path: 'todos', component: TodoListComponent },
{ path: 'todos/new', component: TodoCreateComponent },
{ path: 'todos/:id', component: TodoEditComponent },
{ path: '**', component: NotFoundComponent },
];
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpClientModule,
RouterModule.forRoot(appRoutes, { useHash: false }),
],
declarations: [
AppComponent,
HelloComponent,
YellPipe,
TodoComponent,
TodoEditComponent,
TodoListComponent,
TodoCreateComponent,
NotFoundComponent,
ColorDirective,
ClickDirective,
],
providers: [TodoService],
bootstrap: [AppComponent],
})
export class AppModule {}
app.component.ts
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {}
app.component.html
<a routerLink="/todos" routerLinkActive="my-active">Home</a> |
<a routerLink="/todos/new" routerLinkActive="my-active">Create</a>
<hr>
<br/>
<router-outlet></router-outlet>
todo.component.ts
@Component({
selector: 'app-todo',
templateUrl: './todo.component.html',
styleUrls: ['./todo.component.css']
})
export class TodoComponent implements OnInit {
@Input() todo: any;
@Output() done = new EventEmitter<any>();
constructor() { }
ngOnInit() {
}
markTodoAsDone(todo: Todo) {
todo.done = !todo.done;
this.done.emit(todo);
}
}
todo.component.html
<label >
<input type="checkbox" [checked]="todo.done" (change)="markTodoAsDone(todo)">
<a [routerLink]="todo.id">{{ todo.name }}</a>
</label>
todo-edit.component.ts
@Component({
selector: 'app-todo-edit',
templateUrl: './todo-edit.component.html',
styleUrls: ['./todo-edit.component.css']
})
export class TodoEditComponent implements OnInit {
public todo$: Observable<Todo>;
constructor(private readonly activatedRoute: ActivatedRoute,
private readonly todoService: TodoService) { }
ngOnInit() {
this.todo$ = this.activatedRoute.params.pipe(
pluck('id'),
switchMap(id => this.todoService.get(+id))
);
}
}
todo-edit.component.html
<p>
{{ todo$ | async | json }}
</p>
Start: https://stackblitz.com/edit/github-uelwhb-kdonbj
Show Labs
In TodoEditComponent, update the template to contain the following form. It should have to fields: A text field for editing the name and a checkbox for setting the done state. Implement onSubmit and send the updated todo to the server.
<form *ngIf="todo$ | async as todo" (ngSubmit)="onSubmit(todo)">
<!-- … -->
<button>Submit!</button>
</form>
Now, add a required and minlength (5 characters) validation to the name field. Update the submit button to be disabled when the form is invalid:
<form *ngIf="todo$ | async as todo" (ngSubmit)="onSubmit(todo)" #form="ngForm">
<!-- … -->
<button [disabled]="form.invalid">Submit!</button>
</form>
Show Solution
https://stackblitz.com/edit/github-uelwhb-wqvfrk
todo-edit.component.html
<form *ngIf="todo$ | async as todo" (ngSubmit)="onSubmit(todo)" #form="ngForm">
<input type="checkbox" [(ngModel)]="todo.done" name="done">
<input type="text" [(ngModel)]="todo.name" name="name" required minlength="5">
<button [disabled]="form.invalid">Submit!</button>
</form>
todo-edit.component.ts
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-todo-edit',
templateUrl: './todo-edit.component.html',
styleUrls: ['./todo-edit.component.css']
})
export class TodoEditComponent implements OnInit {
public todo$: Observable<Todo>;
constructor(private readonly activatedRoute: ActivatedRoute,
private readonly todoService: TodoService) { }
ngOnInit() {
this.todo$ = this.activatedRoute.params.pipe(
pluck('id'),
switchMap(id => this.todoService.get(+id))
);
}
onSubmit(todo: Todo) {
this.todoService.update(todo).subscribe();
}
}
A prior version of this workshop was held together with Fabian Gosebrink.