Angular Exercises

This slides contain 8 exercises building on each other. The learning objectives focus on Angular basics and features.

  • Use the right arrow key to navigate to the next exercise
  • Use the down arrow key to step through the exercise steps

Simple Component

Learning objectives:

  1. Create a new module
  2. Create a new component
  3. Import the component into the module

Application requirements:

  • Display movie dummy data

Initial branch: exercise/1-project-seed
Git Cheat Sheet

Simple Component (1/4)

  1. Create a new module Movies (ng g m movies).
  2. Create MovieDetails component in movies directory (ng g c movies/movie-details).
  3. If exists, remove standalone: true and imports: [] from MovieDetailsComponent decorator
  4. Export the MovieDetailsComponent from the MoviesModule - both of them you just created.
  5. Import the MoviesModule in the AppModule

Simple Component (2/4)

  1. In the AppComponent class remove the title property.
  2. In the AppComponent template change the title to "My Movie DB"
  3. In the AppComponent template add <app-movie-details>.
    movie-details works! appears on the page.

Simple Component (3/4)

  1. Create an interface Movie in the movies directory (ng g interface movies/movie). Add the following properties:
    id: number, directors: string, title: string, description: string, year: number.
  2. Add a public currentMovie property of type Movie to the MovieDetailsComponent
  3. Add OnInit Interface and method ngOnInit() to MovieDetailsComponent as a lifecycle hook
  4. In the OnInit lifecycle hook assign a newly created Movie object with the properties of your choice. {id:1, title: 'a movie',...}

Simple Component (4/4)

  1. in the file movie-details.component.html
    • Create a <div> and display the movies properties inside
    • For now don't use <input>. Just display labels and values of the director and title properties (using the interpolation {{currentMovie.title}}).
  2. Add some component's styling (to the file movie-details.component.scss) , e.g. make the labels' font weight bold. Make use of the :host pseudo selector.

Master/Details Pattern @Input

Learning objectives:

  1. Create a component containing another one
  2. Use structural directives
  3. Handle a click events
  4. Bind input from overview component to the details component from previous exercise

Application requirements:

  • Display an overview of a list of movies
  • Show details of a movie from the list next to the list when selected by clicking on it

Fallback branch: exercise/2-simple-component
Git Cheat Sheet

Master/Details Pattern @Input (1/6)

  1. Create MovieOverview component (ng g c movies/movie-overview) and export it from the MoviesModule.
  2. Remove MovieDetails component from the exports of MoviesModule.
  3. Replace <app-movie-details> with <app-movie-overview> in the AppComponent's template. movie-overview works! should appear on the page.

Master/Details Pattern @Input (2/6)

  1. Add properties to the MovieOverviewComponent class:
    • the array movies which keeps available movies,
    • the selectedMovie which keeps the currently selected movie of the available ones.
  2. Initialize movies with an empty array.
  3. Add some movie entries to the movies array (In the OnInit lifecycle hook).

Master/Details Pattern @Input (3/6)

  1. Add two <div> elements to the overview component.
  2. To the first <div> add a table with rows. Generate the table's rows iterating over the movies. Make use of the *ngFor directive. For each movie element show its directors and title.

Master/Details Pattern @Input (4/6)

  1. Extend the MovieOverviewComponent class: Implement the selectMovie(movie: Movie): void method which sets the passed movie as the selected one.
  2. In the template add the (click) event binding to table rows to call the selectMovie(movie:Movie) method

Master/Details Pattern @Input (5/6)

  1. Go to the MovieDetailsComponent class and remove all its content except for the currentMovie property. Rename it to movie.
  2. Add @Input() decorator to movie property.
  3. Go to the MovieDetailsComponent's template. The <div> element containing the movie details should only be displayed if the movie property is set. Use the *ngIf directive.

Master/Details Pattern @Input (6/6)

  1. Go to the MovieOverviewComponent template
  2. Put the <app-movie-details> element in a second <div>
  3. Bind the selectedMovie to the <app-movie-details> element with the [movie] property binding.

Master/Details Pattern @Input (Optional 1/3)

  1. Implement the isMovieSelected(movie: Movie): boolean method which checks if the movie passed is the selected one.
  2. Add the class binding [class.row-active] to the isMovieSelected(movie: Movie): boolean method.
  3. Set a style for the row-active CSS-class

Master/Details Pattern @Input (Optional 2/3)

  1. Show the row number in the list as well
  2. Give the <body> a light gray background
  3. Go to the MovieOverviewComponent. Give the <div>s
    1. a white background
    2. box shadow
    3. 16px of padding and margin

Master/Details Pattern @Input (Optional 3/3)

  1. Use Flex Box Styling to create a responsive design. The <div>'s should be next to each other or below each other depending on the screen width

Master/Details Pattern @Output

Learning objectives:

  1. Add HTML input fields with two-way binding
  2. Emit output events from the details component
  3. Bind events to a method in the overview component

Application requirements:

  • Movie Details are editable
  • An "apply" button updates the list of movies
  • A "create" button clears input fields (optional)

Fallback branch: exercise/3-master-details-input
Git Cheat Sheet

Master/Details Pattern @Output (1/7)

  1. Import the FormsModule in the MoviesModule.
  2. In the MovieDetailsComponent's template replace the interpolation of movies's title, directors etc. with text input fields. Add two-way binding ([(ngModel)]) to the input fields.
  3. Move the *ngIf from the movie-details template to the <app-movie-details> in the movie-overview template

Master/Details Pattern @Output (2/7)

Check the result. Every change in an input immediatly changes the corresponding entry in the list. Nice feature, but that's not exactly what we want here

Master/Details Pattern @Output (3/7)

  1. Go to the MovieDetailsComponent class and add a setter and a getter of the movie property
    (get movie(){..}). You need to rename the movie property for that. Recommendation: _movie
  2. Move the @Input() decorator from the property to the setter.
  3. Change the _movie property's visibility to private

Master/Details Pattern @Output (4/7)

  1. Change the setter implementation so that instead of a direct assignment a copy of the passed object is made. Use the Spread Operator
    this._movie = {...movie}

Master/Details Pattern @Output (5/7)

  1. Add the movieUpdate property of type EventEmitter<Movie>.
  2. Put the decorator @Output() on the movieUpdate property.
  3. Implement the apply(): void method which uses the movieUpdate EventEmitter and emits the _movie property.
  4. Add a button to the template and assign the apply() method to the (click) event binding

Master/Details Pattern @Output (6/7)

  1. Go to the MovieOverviewComponent class and implement the onMovieUpdated(updatedMovie: Movie): void method.
  2. The onMovieUpdated(updatedMovie: Movie): void method should find a movie (among movies in the movies array) with the same ID as the passed movie (consider using Array.prototype.find function) and update the found movie accordingly.

Master/Details Pattern @Output (7/7)

  1. Go to the MovieOverviewComponent's template and add the (movieUpdate) event binding to the <app-movie-details> element and assign the onMovieUpdated($event) method call

Master/Details Pattern @Output (optional 1/3)

  1. In the movie-details class add a movieCreate EventEmitter
  2. Add a create(): void method that triggers the movieCreate EventEmitter
  3. Add a button to the template to use the create() function

Master/Details Pattern @Output (optional 2/3)

  1. In the movie overview class add a onCreateMovie() method that initializes the selectedMovie with a new object with all properties set to null
  2. Bind the onCreateMovie() to the movieCreate event of <app-movie-details>
  3. Add a private property movieIdSequence

Master/Details Pattern @Output (optional 3/3)

  1. Modify the onMovieUpdated()function. If no movie with the id of the updated movie is found, set the id to the next from movieIdSequence and push the updated movie to the array.
  2. Style the input fields and the button

Service

Learning objectives:

  1. Create a service
  2. Refactor the application by moving the handling of the movie list to the service
  3. Inject service into a component

Application requirements:

  • After refactoring application behaves as before

Fallback branch: exercise/4-master-details-output
Git Cheat Sheet

Service (1/4)

  1. Create a new Movie service (ng g service movies/movie) with the private movies property.
  2. Move the code initializing the movies array from MovieOverviewComponent.ngOnInit() into the service.

Service (2/4)

  1. Implement findAll(): Movie[] method which returns the movie array.
  2. Implement save(movieToSave: Movie): Movie. Move in the logic for updating/saving movies and return the created/updated movie.

Service (3/4)

  1. Inject MovieService in MovieOverviewComponent's constructor.
  2. In MovieOverviewComponent.ngOnInit() call the MovieService.findAll() method and initialize the movie list
  3. Use the save(movieToSave: Movie): Movie in the MovieOverviewComponent.onMovieUpdated and re-assign the correct movies after the update.

Service (4/4)

    Extend the MovieService:
  1. Implement the findOne(id: number): Movie method which finds a movie by the ID passed and returns its copy.

Note: This method will only be called by a component in a later exercise, but should already be implemented here.

Service (extra)

    Extend the MovieService:
  1. Implement unit tests using the Jasmine framework.

Http

Learning objectives:

  1. Use Observables from RxJS library in a service and in a component
  2. Use HttpClient and its Angular Module

Application requirements:

  • It retrieves movie data from a REST backend
  • It posts changes of movie details to the REST backend

Fallback branch: exercise/5-services
Git Cheat Sheet

Http (1/3)

We prepared a server for you.
Use the following command to start the Angular server with reverse proxy and a server with the REST service: npm run start

Possible REST calls:

  • GET /services/rest/movies
  • GET /services/rest/movies/:id
  • POST /services/rest/movies

Http (2/3)

In the next steps the return types of the service methods will change from Movie to Observable<Movie>.
You will need to refactor the overview component accordingly and use one of the following:
  • subscribe(...) - Easier for now
  • The async pipe - Better solution for most use cases

Http (3/3)

We want to get rid of the movies array in the service and get the data from the server instead

  1. Import the HttpClientModule into the AppModule, add it to the imports array
  2. Refactor the findAll() movie method - use http.get
  3. Refactor the findOne(id: Number) method - use http.get
  4. Refactor the save(movie: Movie) method - use http.post

Routing

Learning objectives:

  1. Setup Angular Router for Routes to existing and new components
  2. Create a navigation bar
  3. Load movie details according to url parameter

Application requirements:

  • Open webpages using URLs or routerLinks
  • Highlight active navigation-buttons
  • Display an error-page for invalid URLs
  • Change shown movie details using its ID as part of URL

Fallback branch: exercise/6-http
Git Cheat Sheet

Routing (1/4)

  • Create a new Module Login (ng g m login)
    • containing a Register component (ng g c login/register)
  • Create a new Module Static-Pages (ng g m static-pages)
    • containing Page-Not-Found component
    • and an About component
  • Import both modules in the AppModule

Routing (2/4)

Import the RouterModule in AppModule. Use the RouterModule.forRoot defining following routes:

  • redirection to /register on application startup
  • /register shows the register component
  • /movies shows movie-overview component
  • /movies/2 shows movie-overview component (with Details of Movie with id=2)
  • /about shows about component
  • other paths -> show page-not-found component

Routing (3/4)

In your app component:

  • Add a navigationbar to your App component template with three links on it (register, movies, about).
  • Use the router-outlet instead of the movie-overview selector
  • Use the routerLink directive on menu-items to enable navigation.
  • Add css-class "active" for currently active menu-item and style it using some css

Routing (4/4)

change movie-overview component so that

  • The movie shown in the movie-detail-component changes whenever the movie-id in the URL-path changes. Hint: there is an unused method in your movie-service you could use here.
  • When the user clicks on any of the movies, the app navigates to the according app-url (e.g. movies/2). Change this in selectMovie()

Forms

Learning objectives:

  1. Create a Model driven form (reactive)
  2. Use standard validators

Application requirements:

  • Collect basic registration information via form
  • If valid, submit button logs data to console
  • Display error messages to guide the user

Fallback branch: exercise/7-router
Full solution branch: exercise/8-forms
Git Cheat Sheet

Forms (1/4)

  • Import the ReactiveFormsModule in your LoginModule.
  • In RegisterComponent, create a FormGroup registrationForm with following structure by using FormGroup and FormControl or (preferred) the formBuilder:
{ 
firstname: string, 
lastname: string, 
email: string,
passwords: {
  password: string, 
  passwordRepeat: string}
}

Forms (2/4)

  • Email should contain the default value "example@mail.com"
  • Create an according form in the template of your RegisterComponent and bind it to your FormGroup.
  • Add a submit button to your form. When the user submits the form, log the formGroups content (value) to the console.

Forms (3/4)

  • add following validations:
    • Both names are validated for a minimum length of 4 characters
    • Email has a valid e-mail format
    • Password is a required field

Forms (4/4)

  • Show textual descriptions on each field validated to give the user guidance on how to fix the errror. Use *ngIf to hide messages (controls.fieldname.getError('minlength')) to check for different validation-errors.

Forms (optional)

  • Show errors for required fields only after the field was touched.
  • Repeat-password-field is validated for exact match with the password-field. (use a custom validator)
  • You already used the template-driven form in your movie-details-component. Try to add some validations and validation error messages for movies by your own.
  • Make the form group type safe. (use a TypeScript interface)