A beginner Angular workshop
ng new todo
# on the options given, choose:
# SCSS,
# Server-Side Rendering (SSR): No
cd todo
ng serve --open
# your app should open on http://localhost:4200/
Show Labs
In your freshly created project, open the file src/app/app.component.html
. You can completely remove the existing contents of this file. Now try the following bindings (one after another).
{{ 'hallo' }}
{{ 3 }}
{{ 17 + 4 }}
Which values do you see in the preview pane?
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
.
<p>{{ value }}</p>
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 and add some text(Hint:<div>My pink container</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 will 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 just fine.
Show Solution
app.component.ts
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {
title = 'todo';
public value = 'Hello';
color = 'hotpink';
public onClick() {
alert('Hello!');
}
public onMouseMove() {
console.log('Hello!');
}
}
app.component.html
{{ "hallo" }}
{{ 3 }}
{{ 17 + 4 }}
<p>{{ value }}</p>
<div [style.background-color]="color">My pink container</div>
<button (mousemove)="onMouseMove()" (click)="onClick()">Click me.</button>
styles.scss
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
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
app.component.ts
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {
public value = 'Hello';
public color = 'hotpink';
public onClick(event: MouseEvent) {
console.log(event.clientX);
}
public onMouseMove(event: MouseEvent) {
console.log(event.clientX);
}
}
app.component.html
{{ "hallo" }}
{{ 3 }}
{{ 17 + 4 }}
<p>{{ value }}</p>
<div [style.background-color]="color">My pink container</div>
<button (mousemove)="onMouseMove($event)" (click)="onClick($event)">Click me.</button>
Show Labs
In app.component.ts
, add CommonModule
to the imports
array (line 7). Now the default pipes are available.
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 | … }}
).
Generate a pipe with the name yell:
ng generate pipe 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
app.component.ts
import {CommonModule} from '@angular/common';
import {Component} from '@angular/core';
import {YellPipe} from './yell.pipe';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
imports: [CommonModule, YellPipe],
})
export class AppComponent {
public value = 'Hello';
public number = 3.14159;
}
yell.pipe.ts
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({
name: 'yell',
standalone: true,
})
export class YellPipe implements PipeTransform {
transform(value: string, args?: string) {
const suffix = args || '!!!';
return (value + suffix).toUpperCase();
}
}
app.component.html
<p>{{ value | uppercase }}</p>
<p>{{ number | percent }}</p>
<p>{{ number | currency }}</p>
<p>{{ number | number }}</p>
<p>{{ value | yell }}</p>
<p>{{ value | yell: '???' }}</p>
Show Labs
Create your first component. The new component should be named todo
.
ng generate component 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 />
to the template).
You then need to import the todo component into the app component. You can do this automatically:
If you like, you can duplicate this HTML element to see the idea of componentization in action.
Show Solution
todo.component.ts
import {Component} from '@angular/core';
@Component({
selector: 'app-todo',
standalone: true,
imports: [],
templateUrl: './todo.component.html',
styleUrl: './todo.component.scss',
})
export class TodoComponent {
}
app.component.ts
import {CommonModule} from '@angular/common';
import {Component} from '@angular/core';
import {YellPipe} from './yell.pipe';
import {TodoComponent} from './todo/todo.component';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
imports: [CommonModule, YellPipe, TodoComponent],
})
export class AppComponent {
public value = 'Hello';
public number = 3.14159;
}
app.component.html
<p>{{ value | uppercase }}</p>
<p>{{ number | percent }}</p>
<p>{{ number | currency }}</p>
<p>{{ number | number }}</p>
<p>{{ value | yell }}</p>
<p>{{ value | yell: '???' }}</p>
<app-todo/>
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 the interpolation and 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, set the tododone
property totrue
and 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
todo.component.ts
import {JsonPipe} from '@angular/common';
import {Component, EventEmitter, Input, Output} from '@angular/core';
@Component({
selector: 'app-todo',
standalone: true,
imports: [JsonPipe],
templateUrl: './todo.component.html',
styleUrl: './todo.component.scss',
})
export class TodoComponent {
@Input() todo: any;
@Output() done = new EventEmitter();
markAsDone() {
this.todo.done = true;
this.done.emit(this.todo);
}
}
todo.component.html
<p>Todo: {{ todo | json }}</p>
<button (click)="markAsDone()">Mark as done</button>
app.component.html
<app-todo [todo]="myTodo" (done)="onDoneClicked($event)"/>
app.component.ts
import {CommonModule} from '@angular/common';
import {Component} from '@angular/core';
import {TodoComponent} from './todo/todo.component';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
imports: [CommonModule, TodoComponent],
})
export class AppComponent {
public myTodo = {name: 'Wash clothes', done: false, id: 3};
onDoneClicked($event: any) {
console.log($event);
}
}
Show Labs
Create a directive:
ng generate directive color
The directive takes color
as an @Input()
binding. The directive should set the color of the host element (using a @HostBinding()
).
In the component template, declare a colorToBind
property and give it your favorite color as its value. In the component template, pass the colorToBind
property into the [color]
input binding.
Create another directive (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.
Don't forget to import ColorDirective
and ClickDirective
to the component that uses them.
Show Solution
todo.component.ts
import {JsonPipe} from '@angular/common';
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {ColorDirective} from '../color.directive';
import {ClickDirective} from '../click.directive';
@Component({
selector: 'app-todo',
standalone: true,
imports: [JsonPipe, ColorDirective, ClickDirective],
templateUrl: './todo.component.html',
styleUrl: './todo.component.scss',
})
export class TodoComponent {
@Input() todo: any;
@Output() done = new EventEmitter();
colorToBind = 'blue';
markAsDone() {
this.todo.done = true;
this.done.emit(this.todo);
}
}
todo.component.html
<p appClick appColor color="green">Todo: {{ todo | json }}</p>
<button (click)="markAsDone()">Mark as done</button>
<p appColor [color]="colorToBind">Color binding test</p>
color.directive.ts
import {Directive, Input, HostBinding} from '@angular/core';
@Directive({
selector: '[appColor]',
standalone: true,
})
export class ColorDirective {
@HostBinding('style.color')
@Input()
public color = '';
}
click.directive.ts
import {Directive, HostListener} from '@angular/core';
@Directive({
selector: '[appClick]',
standalone: true,
})
export class ClickDirective {
@HostListener('click', ['$event'])
public onClick($event: PointerEvent): void {
console.log($event);
}
}
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?
ng generate interface todo
Create a new model class called todo
and add the properties:
name
(string)done
(boolean)id
(number, optional)
ng generate service todo
In your TodoService, add the following methods:
create(todo:Todo){}
get(todoId:string){}
getAll():Todo[]{}
update(todo:Todo):void {}
delete (todoId:number):void {}
Add the following field:
public todos: Todo[] = [
{done: false, name: 'Learn Angular', id: 1},
{name: 'Wash my clothes', done: false, id: 2},
{name: 'Tidy up the room', done: true, id: 3},
{name: 'Mine bitcoin', done: false, id: 4},
];
Add a very basic, synchronous implementation for getAll returning the todos. Inject your TodoService into the AppComponent (don’t forget to update the imports on top). Log the list of todos to the console in the AppComponent.
Show Solution
app.component.ts
import {CommonModule} from '@angular/common';
import {Component, ElementRef} from '@angular/core';
import {TodoComponent} from './todo/todo.component';
import {TodoService} from './todo.service';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
imports: [CommonModule, TodoComponent],
providers: [TodoService],
})
export class AppComponent {
public myTodo = {name: 'Wash clothes', done: false, id: 3};
constructor(
private readonly elRef: ElementRef,
private readonly todoService: TodoService
) {
console.log('element ref', elRef);
console.log('service todos', todoService.getAll());
}
onDoneClicked($event: any) {
console.log($event);
}
}
todo.ts
export interface Todo {
name: string;
done: boolean;
id?: number;
}
todo.service.ts
import {Injectable} from '@angular/core';
import {Todo} from './todo';
@Injectable({providedIn: 'root'})
export class TodoService {
constructor() {
}
public todos: Todo[] = [{done: false, name: 'Learn Angular', id: 1}];
create(todo: Todo) {
}
get(todoId: string) {
}
getAll(): Todo[] {
return this.todos;
}
update(todo: Todo): void {
}
delete(todoId: number): void {
}
}
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 boolean show
field and toggle it via a new toggle()
method (Hint: this.show = !this.show;
). Your toggle button should work now.
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. You can display t he todo name via interpolation.
<ul>
<li *ngFor="let todo of todos">{{ todo.name }}, {{ todo.done }}</li>
</ul>
Now you should be able to your todo list in the browser.
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
- you can bind the markAsDone() method to the (change) Event in the checkbox
- a label to show the “name” text
todo.component.html
<label>
<input type="checkbox" [checked]="todo.done" (change)="markAsDone()">
{{ todo.name }}
</label>
Show Solution
app.component.ts
import {CommonModule} from '@angular/common';
import {Component, ElementRef} from '@angular/core';
import {TodoComponent} from './todo/todo.component';
import {TodoService} from './todo.service';
import {Todo} from './todo';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
imports: [CommonModule, TodoComponent],
providers: [TodoService],
})
export class AppComponent {
public myTodo = {name: 'Wash clothes', done: false, id: 3};
public show: boolean = false;
todos: Todo[] = [];
constructor(
private readonly elRef: ElementRef,
private readonly todoService: TodoService
) {
console.log('element ref', elRef);
console.log('service todos', todoService.getAll());
this.todos = todoService.getAll();
}
onDoneClicked($event: any) {
console.log($event);
}
toggle() {
this.show = !this.show;
}
catchDoneEvent(todo: Todo) {
console.log(todo);
}
}
app.component.html
<app-todo [todo]="myTodo" (done)="onDoneClicked($event)"/>
<button (click)="toggle()">Toggle</button>
<div *ngIf="show">
I’m visible!
</div>
<ul>
<li *ngFor="let todo of todos">{{ todo.name }}, {{ todo.done }}</li>
</ul>
<app-todo *ngFor="let todo of todos" [todo]="todo" (done)="catchDoneEvent($event)"></app-todo>
todo.service.ts
import {Injectable} from '@angular/core';
import {Todo} from './todo';
@Injectable({providedIn: 'root'})
export class TodoService {
constructor() {
}
public todos: Todo[] = [
{done: false, name: 'Learn Angular', id: 1},
{name: 'Wash my clothes', done: false, id: 2},
{name: 'Tidy up the room', done: true, id: 3},
{name: 'Mine bitcoin', done: false, id: 4},
];
create(todo: Todo) {
}
get(todoId: string) {
}
getAll(): Todo[] {
return this.todos;
}
update(todo: Todo): void {
}
delete(todoId: number): void {
}
}
todo.component.ts
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Todo} from '../todo';
@Component({
selector: 'app-todo',
standalone: true,
imports: [],
templateUrl: './todo.component.html',
styleUrl: './todo.component.scss',
})
export class TodoComponent {
@Input() todo: any;
@Output() done = new EventEmitter<Todo>();
colorToBind = 'blue';
markAsDone() {
this.todo.done = !this.todo.done;
this.done.emit(this.todo);
}
}
todo.component.html
<label>
<input type="checkbox" [checked]="todo.done" (change)="markAsDone()">
{{ todo.name }}
</label>
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: string): Observable<Todo>
getAll(): Observable<Todo[]>
update(todo: Todo): Observable<void>
delete(todoId: number): Observable<void>
In your ApplicationConfig
, provide the HttpClientModule using the provideHttpClient()
in the providers list.
Add a constructor to TodoService and request an instance of HttpClient
and use HTTP requests instead of returning synchronous data using the following URLs. Remember you need to subscribe to the
methods in the service to trigger the rest call.
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
app.config.ts
import {ApplicationConfig} from '@angular/core';
import {provideRouter} from '@angular/router';
import {routes} from './app.routes';
import {provideHttpClient} from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), provideHttpClient()],
};
todo.service.ts
import { Injectable } from '@angular/core';
import { Todo } from './todo';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class TodoService {
url = 'https://tt-todos.azurewebsites.net/todos';
constructor(private http: HttpClient) {}
public todos: Todo[] = [
{ done: false, name: 'Learn Angular', id: 1 },
{ name: 'Wash my clothes', done: false, id: 2 },
{ name: 'Tidy up the room', done: true, id: 3 },
{ name: 'Mine bitcoin', done: false, id: 4 },
];
create(todo: Todo): Observable<Todo> {
return this.http.post<Todo>(this.url, todo);
}
get(todoId: string): Observable<Todo> {
return this.http.get<Todo>(`${this.url}/${todoId}`);
}
getAll(): Observable<Todo[]> {
return this.http.get<Todo[]>(this.url);
}
update(todo: Todo): Observable<Todo> {
return this.http.put<Todo>(`${this.url}/${todo.id}`, todo);
}
delete(todoId: number): Observable<void> {
return this.http.delete<void>(`${this.url}/${todoId}`);
}
}
app.component.ts
import {CommonModule} from '@angular/common';
import {Component, ElementRef} from '@angular/core';
import {TodoComponent} from './todo/todo.component';
import {TodoService} from './todo.service';
import {Todo} from './todo';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
imports: [CommonModule, TodoComponent],
providers: [TodoService],
})
export class AppComponent {
public myTodo = {name: 'Wash clothes', done: false, id: 3};
public show: boolean = false;
todos: Todo[] = [];
constructor(
private readonly elRef: ElementRef,
private readonly todoService: TodoService
) {
console.log('element ref', elRef);
console.log('service todos', todoService.getAll());
todoService.getAll().subscribe((todos) => (this.todos = todos));
}
onDoneClicked($event: any) {
console.log($event);
}
toggle() {
this.show = !this.show;
}
catchDoneEvent(todo: Todo) {
console.log(todo);
}
}
Show Labs
Use the async
pipe instead of manually subscribing. Use the ngOnInit()
lifecycle to update the todos$
field.
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" / >
Use:
<app-todo * ngFor = "let todo of todos$ | async" [todo] = "todo" / >
Show Solution
app.component.ts
import {CommonModule} from '@angular/common';
import {Component, ElementRef} from '@angular/core';
import {TodoComponent} from './todo/todo.component';
import {TodoService} from './todo.service';
import {Todo} from './todo';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
imports: [CommonModule, TodoComponent],
providers: [TodoService],
})
export class AppComponent {
public show = false;
protected readonly todos$ = this.todoService.getAll();
constructor(
private readonly elRef: ElementRef,
private readonly todoService: TodoService
) {
console.log('element ref', elRef);
}
onDoneClicked($event: any) {
console.log($event);
}
toggle() {
this.show = !this.show;
}
catchDoneEvent(todo: Todo) {
console.log(todo);
}
}
app.component.html
<button (click)="toggle()">Toggle</button>
<div *ngIf="show">I'm visible!</div>
<ul>
<li *ngFor="let todo of todos$ | async as todos">
{{ todo.name }}, {{ todo.done }}
</li>
</ul>
<div *ngIf="todos$ | async as todos">You have {{ todos.length }} todos!</div>
<app-todo
*ngFor="let todo of todos$ | async"
[todo]="todo"
(done)="catchDoneEvent($event)"
/>
Show Labs
Add the following components:
- TodoListComponent
- TodoEditComponent
- TodoCreateComponent
- NotFoundComponent
Define/assign the following routes:
- ''
- todos
- todos/:id
- todos/new
- **
Redirect the default 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 as todos">
<a [routerLink]="[todo.id]" routerLinkActive="router-link-active">{{ todo.name }}</a>
</li>
</ul>
In AppComponent, add routerLinkActive:
<a routerLink="/todos" routerLinkActive="router-link-active">Home</a>
Add a CSS style for a.router-link-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
app.routes.ts
import {Routes} from '@angular/router';
import {TodoCreateComponent} from './todo-create/todo-create.component';
import {TodoEditComponent} from './todo-edit/todo-edit.component';
import {NotFoundComponent} from './not-found/not-found.component';
import {TodoListComponent} from './todo-list/todo-list.component';
export const routes: Routes = [
{path: '', pathMatch: 'full', redirectTo: 'todos'},
{path: 'todos', component: TodoListComponent},
{path: 'todos/new', component: TodoCreateComponent},
{path: 'todos/:id', component: TodoEditComponent},
{path: '**', component: NotFoundComponent},
];
app.component.ts
import {Component} from '@angular/core';
import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
imports: [RouterOutlet, RouterLink, RouterLinkActive],
providers: [],
})
export class AppComponent {
constructor() {
}
}
app.component.html
<div class="header">
<a [routerLink]="['todos']" routerLinkActive="router-link-active" [routerLinkActiveOptions]="{exact: true}">Home</a> <br>
<a [routerLink]="['todos', 'new']" routerLinkActive="router-link-active">Create Todo</a>
</div>
<router-outlet></router-outlet>
app.component.scss
.header {
display: flex;
gap: 1rem;
}
.router-link-active {
color: green;
}
todo-list.component.ts
import {TodoService} from '../todo.service';
import {Component} from '@angular/core';
import {TodoComponent} from '../todo/todo.component';
import {Todo} from '../todo';
import {CommonModule} from '@angular/common';
import {RouterLink} from '@angular/router';
@Component({
selector: 'app-todo-list',
standalone: true,
templateUrl: './todo-list.component.html',
styleUrl: './todo-list.component.scss',
imports: [TodoComponent, CommonModule, RouterLink],
})
export class TodoListComponent {
protected readonly todos$ = this.todoService.getAll();
constructor(private todoService: TodoService) {
}
catchDoneEvent(todo: Todo) {
console.log(todo);
}
}
todo-list.component.html
<ul>
<li *ngFor="let todo of todos$ | async as todos">
<a [routerLink]="[todo.id]" routerLinkActive="router-link-active">
{{ todo.name }}
</a>
</li>
</ul>
<div *ngIf="todos$ | async as todos">You have {{ todos.length }} todos!</div>
<app-todo
*ngFor="let todo of todos$ | async"
[todo]="todo"
(done)="catchDoneEvent($event)"
/>
todo-edit.component.ts
import {Component} from '@angular/core';
import {TodoService} from '../todo.service';
import {ActivatedRoute} from '@angular/router';
import {AsyncPipe, CommonModule} from '@angular/common';
import {map, switchMap} from 'rxjs';
@Component({
selector: 'app-todo-edit',
standalone: true,
imports: [CommonModule, AsyncPipe],
templateUrl: './todo-edit.component.html',
styleUrl: './todo-edit.component.scss',
})
export class TodoEditComponent {
protected todo$ = this.activatedRoute.params.pipe(
map((params) => params['id'] as string),
switchMap((id) => this.todoService.get(id)),
);
constructor(
private readonly todoService: TodoService,
private readonly activatedRoute: ActivatedRoute,
) {
}
}
todo-edit.component.html
<p>{{ todo$ | async | json }}</p>
todo.component.ts
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Todo} from '../todo';
import {RouterLink} from '@angular/router';
@Component({
selector: 'app-todo',
standalone: true,
imports: [RouterLink],
templateUrl: './todo.component.html',
styleUrl: './todo.component.scss',
})
export class TodoComponent {
@Input() todo: any;
@Output() done = new EventEmitter<Todo>();
markAsDone() {
this.todo.done = !this.todo.done;
this.done.emit(this.todo);
}
}
todo.component.html
<label>
<input (change)="markAsDone()" [checked]="todo.done" type="checkbox">
<a [routerLink]="todo.id">{{ todo.name }}</a>
</label>
styles.scss
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
.router-link-active {
color: green;
font-weight: bold;
}
a {
text-decoration: none;
}
Show Labs
In TodoEditComponent, update the template to contain the following form. It should have two 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
todo-edit.component.ts
import {Component, OnInit} from '@angular/core';
import {TodoService} from '../todo.service';
import {ActivatedRoute} from '@angular/router';
import {AsyncPipe, CommonModule} from '@angular/common';
import {map, switchMap} from 'rxjs';
import {Todo} from '../todo';
import {FormsModule} from '@angular/forms';
@Component({
selector: 'app-todo-edit',
standalone: true,
imports: [CommonModule, AsyncPipe, FormsModule],
templateUrl: './todo-edit.component.html',
styleUrl: './todo-edit.component.scss',
})
export class TodoEditComponent {
protected todo$ = this.activatedRoute.params.pipe(
map((params) => params['id']),
switchMap((id) => this.todoService.get(id)),
);
constructor(
private readonly todoService: TodoService,
private readonly activatedRoute: ActivatedRoute,
) {
}
onSubmit(todo: Todo) {
console.log(todo);
this.todoService.update(todo).subscribe((savedTodo) => {
console.log('saved!');
});
}
}
todo-edit.component.html
<form #form="ngForm" (ngSubmit)="onSubmit(todo)" *ngIf="todo$ | async as todo">
<input [(ngModel)]="todo.done" name="done" type="checkbox"/>
<input [(ngModel)]="todo.name" minlength="3" name="name" required="true" type="name" />
<button [disabled]="form.disabled" type="submit">Submit!</button>
</form>
Show Labs
In the class TodoCreateComponent
, inject the NonNullableFormBuilder
and the TodoService
. Then, create a new form group with a form control for setting the name
and the done
state of the
newly created todo:
private readonly fb = inject(NonNullableFormBuilder);
private readonly todoService = inject(TodoService);
protected readonly formGroup = this.fb.group({
// formControlName: ['default value']
});
Then, 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 create the
new todo item on the server using the TodoService.
<form [formGroup]="formGroup" (ngSubmit)="onSubmit(todo)">
<!-- … -->
<input type="text" formControlName="name"/>
<button>Submit!</button>
</form>
Now, add a required and minlength (5 characters) validation to the name field:
name: ['', [Validators.required, Validators.minlength(5)]]
Update the submit button to be disabled when the form is invalid:
<form [formGroup]="formGroup" (ngSubmit)="onSubmit(todo)">
<!-- … -->
<button [disabled]="formGroup.invalid">Submit!</button>
</form>
Show Solution
todo-create.component.ts
import {Component, inject} from '@angular/core';
import {NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms';
import {TodoService} from '../todo.service';
import {debounceTime} from 'rxjs';
@Component({
selector: 'app-todo-create',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './todo-create.component.html',
styleUrl: './todo-create.component.scss'
})
export class TodoCreateComponent {
private readonly fb = inject(NonNullableFormBuilder);
private readonly todoService = inject(TodoService);
protected readonly formGroup = this.fb.group({
name: ['test', [Validators.required, Validators.minLength(3)]],
done: [false],
});
constructor() {
this.formGroup.valueChanges
.pipe(debounceTime(300))
.subscribe(value => console.log(value));
}
onSubmit() {
this.todoService
.create(this.formGroup.getRawValue())
.subscribe();
}
}
todo-create.component.html
<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
<input type="text" formControlName="name">
<input type="checkbox" formControlName="done">
<button [disabled]="formGroup.invalid">Submit!</button>
</form>
A prior version of this workshop was held together with Fabian Gosebrink.