Skip to content
Gregor Woiwode edited this page Jan 19, 2024 · 6 revisions

Labs

Start the app

  • Open your terminal
  • Execute npm start -- --open starting your Angular application
  • Visit http://localhost:4200
  • Have a look at the beautiful start page the Angular team has prepared for you ❤️ .

Make your first change

  • Open your favourite editor and open src/app/app.component.html of your application
  • Find the span element having the content Welcome
  • Change the content from Welcome to whatever you like (e.g. Hello Porsche 2019! 😉)

🕵️‍♂️ Plesae note that the application automatically recompiles and refreshes automatically.

Enhance AppComponent

  • Add a second property to app.component.ts
  • You can name your component what ever you like (e.g. name, age, street, location, hobby, ...)
  • Bind your property in the template app.component.html using double curly braces

Create your 1st component

  • Open your terminal
  • Execute npm run ng generate component todo/todo-checker
  • ☝️ Recognize that the app.module.ts was updated too and that the TodoCheckerComponent has been registered.
  • Open src/app/todo/todo-checker.component.html and paste the following template
    <!-- todo-checker.component.html -->
    <div class="todo">
      <label class="todo__label">
        {{ "TODO: Bind the value of the todo's text" }}
        <input type="checkbox" />
        <span class="todo__checkmark"></span>
      </label>
    </div>
  • Open app.component.html
  • Replace the existing template with your component
    <!-- app.component.html -->
    <ws-todo-checker></ws-todo-checker>
  • Finally, have a look at your browser. Your component should be displayed.

Bonus

We prepared some css-Styles for you. You can check them out in the directory client/src/scss/.

Simple bindings

  • Open todo-checker.component.ts
  • Add a property todo, which is an object defining a string text and a boolean flag isDone.
  • Bind the todo to the HTML template.
  • Create an event binding for the checkbox listening for the change-Event.
  • Every time the change event is fired the method emitToggle should be executed.
  • Implement the method emitToggle that shows an alert-message for the moment.

Hints

<!-- Binding with curly braces -->
{{ todo.text }}

<!-- Property binding -->
<input [checked]="...">

<!-- Event binding -->
<input (change)="...">

Dataflow

  • Open todo-checker.component.ts
  • Make the property todo to be an @Input
  • Define an @Output called toggle and initialize it with an EventEmitter
  • Make toggle emit whenever the change-Event of the checkbox is fired
  • Open app.component.ts
  • Make the AppCompoenent pass a todo-Object (with text and isDone) to the <ws-todo-checker> using the property-binding
  • Handle the toggle-Event of ws-todo-checker by showing an alert-Window displaying the respective todo whenever the event is fired

Hints

// Imports
import { Input, Output } from '@angular/core';
 
// Input-Binding
@Input() todo;

// Output-Binding
@Output() toggle = new EventEmitter();
this.toggle.emit(/* ... */);
<!-- Property binding -->
<ws-todo-checker [todo]="...">

<!-- Event binding -->
<ws-todo-checker (toggle)="notify($event)" >

Multiply your components

  • open app.component.ts.
  • replace the single todo with a collection of at least 2 todos.
  • bind todos to the template using *ngFor.
  • adjust the event binding toggle calling the method checkOrUncheckTodo (see hints) updating the respective todo.

Hints

<ws-todo-checker
  [todo]="todo"
  (toggle)="checkOrUncheckTodo($event)"
  *ngFor="let todo of todos"
><ws-todo-checker>
// NgFo needs to be imported by AppComponent
  imports: [NgFor, TodoCheckerComponent],
// ...

todos = [/* ... */]

// ...

checkOrUncheckTodo(todoForUpdate) {
  this.todos = this.todos.map(todo => todo.text === todoForUpdate.text
    ? { ...todo, isDone: !todo.isDone }
    : todo
  );
}

Secure your coding with types

  • Generate an interface executing ng generate interface models/todo(It creates a directory model and places a file namedtodo.ts` in it)
    • define 2 properties: text: string; isDone: boolean;
  • Look up every place you are using todo (e.g. app.component.ts & todo-checker.component.ts)
  • Annotate each todo-property with your created type Todo

Hints

@Input() todo: Todo;
@Output() toggle = new EventEmitter<Todo>();

Bonus

Create your 1st form

  • Open your terminal
  • Generate a new component by running ng g c todo-quick-add
  • Open src/app/todo/todo-quick-add.component.html and paste the following template
    <!-- todo-quick-add.component.html -->
    <input
      <!-- DEFINE THE TEMPLATE REFERENCE -->
      type="text"
      class="todo__input"
      placeholder="What needs to be done?"
      (keydown.enter)="emitCreate(<!-- PASS THE TEMPLATE REFERENCE TO YOUR COMPONENT -->)"
    />
    
    <button 
      (click)="emitCreate(<!-- PASS THE TEMPLATE REFERENCE TO YOUR COMPONENT -->)"
       class="todo__button--primary">
      Add
    </button>
  • Implement the method emitCreate(input: HTMLInputElement)
  • Make emitCreate emit the event create (You need to define an @Output for that)
  • The event create should transport the text coming from the input-Field.
  • Furthermore the property isDone should initially be set to false.
  • Open app.component.html
  • Handle the create-Event and add the provided Todo to the existing colleciton todos
  • Every time you enter a new todo the list of <ws-todo-checker> should automatically grow.

Extract a feature module

  • Generate a new TodosModule
  • Generate a TodosComponent that is the root component of Todos feature
  • Move all existing components used by the todos feature into the newly created /todos folder
  • Move component declarations of those components from AppModule into TodosModule
  • Move todo markup from app.component.html into todos.component.html
  • Move todos array and methods from app.component.ts into todos.component.ts
  • Export TodosComponent from TodosModule
  • Import TodosModule in AppModule
  • Use the new TodosComponent in app.component.html

Hints

ng generate module todos

ng generate component todos

@NgModule({
  declarations: [
    TodosComponent,
    // ...
  ],
  exports: [TodosComponent]
})
export class TodosModule {}
<!-- app.component.html -->
<ws-todos></ws-todos>

Create your 1st service

  • Generate a TodosService in a /shared sub-folder of /todos (this is an Angular convention to show that this service will be used by multiple components). This service will handle all todo data operations.
  • Move the todo array from TodosComponent into your new service.
  • Create a method getAllTodos that returns the array and use it in TodosComponent.

Bonus

  • Create method create (that adds a todo) and use it in TodosComponent.
  • Create method update (that replaces a todo) and use it in TodosComponent.

Hints

ng generate service todos/shared/todos

Set up Http

  • open main.ts
  • useprovideHttpClient

Hints

import { provideHttpClient } from '@angular/common/http';

// ...

bootstrapApplication(AppComponent, {
  providers: [provideHttpClient()],
}).catch(err => console.error(err));

Load todos from API

  • Switch to the directory server/
  • Run npm start
  • Open todos.service.ts
  • Implement the method query that loads all todos from the API (URL: http://localhost:3000/todos)
  • Bind the loaded todos via subscribe
  • Checkout http://localhost:4200 if the todos from the API are displayed

Hints

  • You can find a detailed documentation of the API in the server's README
  • Make sure to subscribe in the todos.component.ts
  • Make sure to call the service in ngOnInit
// todos.service.ts
constructor(private http: HttpClient) {}

query(): Observable<Todo[]> {
  return this.http.get<Todo[]>('http://localhost:3000/todos');
}

// todos.component.ts
ngOnInit(): void {
  this.todosService.query().subscribe(todosFromApi => this.todos = todosFromApi);
}

Persist new todos

  • Enhance todos.service.ts to store a todo in the database

💡 Please note, after storing a todo you need to manually refresh the page to see the created todo in the list. We will take care about this UX-fail in the next step.

Hints

return this.http.post<Todo>('http://localhost:3000/todos', newTodo);

Bonus

  • When a todo is checked it should immediately persisted (using Put-Request)
  • When a todo is deleted it should immediately removed (using DELETE-Request)
    • If you want to add a delete button to <todo-checker> you can use the following template
      <div class="todo">
        <label class="todo__label" [ngClass]="{ 'todo--is-done': todo.isDone }">
          {{ todo.text }}
          <input type="checkbox" [checked]="todo.isDone" (change)="emitToggle()" />
          <span class="todo__checkmark"></span>
        </label>
        
        <!-- ⭐️ NEW NEW NEW ⭐️ -->
        <span class="todo__delete" (click)="emitRemove()"></span>
      </div>

Auto refresh todos

  • Open todos.component.ts.
  • Use the operator switchMap loading all todos after a todo has been created successfully.
  • Check http://localhost:4200 to see that the list of todos updates automatically.

Hints

import { switchMap } from 'rxjs/operators';

createTodo(newTodo: Todo) {
   this.todosService
     .create(newTodo)
     .pipe(switchMap(() => this.todosService.query()))
     .subscribe(todos => (this.todos = todos))
}

Clean up subscriptions

  • Open todos.component.ts.
  • Add a new property sink
  • Initialize sink with new Subscription
  • Go through the component code and add each subscribe-Block to your sink
  • Implement OnDestroy
  • Call this.sink.unsubscribe() in ngOnDestroy cleaning up all Subscriptions.

Hints

import { Subscription } from 'rxjs';
sink = new Subscription();

sink.add(/* ... */);
sink.unsubscribe();

Set up Routing

  • Create an AppRoutingModule that
    • imports RouterModule
    • configure RouterModule (using RouterModule.forRoot()) to
      • map route 'todos' to TodosComponent
      • redirect to 'todos' when url path is empty
    • Re-exports RouterModule
  • Import AppRoutingModule in AppModule
  • use router-outlet to show todos in app.component.html

💡 If your todos are shown 2 times, try removing the direct view child (ws-todos).

Hints

const routes: Routes = [
  { path: '', pathMatch: 'full', redirectTo: 'todos' },
  { path: 'todos', component: TodosComponent }
];

Create Todo Navigation

  • Create a TodosRoutingModule like you did with AppRoutingModule and import it in TodosModule
  • Configure route { path: 'todos/:query', component: TodosComponent } in TodosRoutingModule
  • Change AppRoutingModule routes to [{ path: '', pathMatch: 'full', redirectTo: 'todos/all' }]
  • Create a todos navigation component called todos-link-navigation as part of TodosModule (see hints for html content): ng generate component todos/todos-link-navigation
  • Add the <ws-todos-link-navigation></ws-todos-link-navigation> to todos.component.html
  • Extend the query() method of todos.service.ts to accept an optional string parameter for filtering the results:
query(param?: string): Observable<Todo[]> {
    return this.http.get<Todo[]>(`${todosUrl}?query=${param ? param : 'all'}`);
  }
  • Use ActivatedRoute#paramMap to read out the dynamic query url-parameter:
// todos.component.ts ngOnInit():
this.route.paramMap
 .pipe(
   switchMap(paramMap => this.todosService.query(paramMap.get('query')))
  )
 .subscribe(todos => (this.todos = todos)

Hints

<!-- todos-link-navigation.component.html -->
<ul class="todo__link-navigation">
  <li class="todo__link-navigation__link">
    <a routerLink="../all" routerLinkActive="todo__link--active">All</a>
  </li>
  <li class="todo__link-navigation__link">
    <a routerLink="../active" routerLinkActive="todo__link--active">Active</a>
  </li>
  <li class="todo__link-navigation__link">
    <a routerLink="../complete" routerLinkActive="todo__link--active"
      >Complete</a
    >
  </li>
</ul>

Lazy Loading

Watch your trainers implement lazy loading :-)