Reader small image

You're reading from  Angular for Enterprise Applications - Third Edition

Product typeBook
Published inJan 2024
Reading LevelExpert
PublisherPackt
ISBN-139781805127123
Edition3rd Edition
Languages
Right arrow
Author (1)
Doguhan Uluca
Doguhan Uluca
author image
Doguhan Uluca

Doguhan Uluca is a Principal Fellow at Excella in Washington, D.C., where he leads strategic initiatives and delivers critical systems. He has technical expertise in usability, mobility, performance, scalability, cybersecurity, and architecture. He is the author of the Angular for Enterprise Application Development books, has spoken at over 30 conferences, and is an Angular GDE Alumni. Doguhan has delivered solutions for Silicon Valley startups, Fortune 50 companies, and the U.S. Federal Government, and he is passionate about contributing to open-source projects and teaching.
Read more about Doguhan Uluca

Right arrow

Recipes – Reusability, Forms, and Caching

In the next two chapters, we will complete most of the implementation of LemonMart and round out our coverage of the router-first approach. In this chapter, I will reinforce the idea of a decoupled component architecture by creating a reusable and routable component that supports data binding. We will use Angular directives to reduce boilerplate code and leverage classes, interfaces, enums, validators, and pipes to maximize code reuse with TypeScript and ES features.

In addition, we will create a multi-step form that architecturally scales well and supports a responsive design. Then, we will differentiate between user controls and components by introducing a lemon rater and a reusable form part that encapsulates the name object.

This chapter covers a lot of ground. It is organized in a recipe format, so you can quickly refer to a particular implementation when working on your projects. I will cover the implementations’...

Technical requirements

The most up-to-date versions of the sample code for the book are on GitHub at the repository linked in the following steps. The repository contains the final and completed state of the code. You can verify your progress at the end of this chapter by looking at the end-of-chapter snapshot of the code under the projects folder.

For Chapter 8:

Be sure that lemon-mart-server is up and running. Refer to Chapter 7, Working with REST and GraphQL APIs.

  1. Clone the repo at https://github.com/duluca/lemon-mart.
  2. Execute npm install in the root folder to install the dependencies.
  3. The beginning state of the project is reflected at:
    projects/stage10
    
  4. The end state of the project is reflected at:
    projects/stage11
    
  5. Add the stage name to any ng command to act only on that stage:
    npx ng build stage11
    

Note that the dist/stage11 folder at the root of the repository...

Implementing CRUD services with caching

We need a service that can perform CRUD operations on a user so that we can implement a user profile. However, the service must be robust enough to withstand common errors. After all, it is very bad UX when users unintentionally lose the data they typed. Form data can be reset due to circumstances outside of a user’s control, like a network or validation error, or user errors, like hitting the back or refresh button by mistake. We will create a user service leveraging the CacheService we built in Chapter 5, Designing Authentication and Authorization, so keep a copy of user data in localStorage while the server processes it. The service will implement the following interface and, as always, reference the abstract IUser interface over the concrete user implementation:

export interface IUserService {
  getUser(id: string): Observable<IUser>
  updateUser(id: string, user: IUser): Observable<IUser>
  getUsers(
    pageSize...

Multi-step responsive forms

Overall, forms are a different beast than the rest of your application, and they require special architectural considerations. I don’t recommend over-engineering your form solution with dynamic templates or route-enabled components. By definition, the different parts of a form are tightly coupled. From the perspectives of maintainability and ease of implementation, creating one giant component is a better strategy than using some of the aforementioned strategies and over-engineering.

We will implement a multi-step input form to capture user profile information in a single component. I will be covering my recommended technique to split forms up into multiple components later in the chapter, in the Reusable form parts and scalability section.

Since the implementation of the form changes dramatically between this section and later in the chapter, you can find the code for the initial version on GitHub at projects/stage11/src/app/user...

Reusing repeating template behavior with directives

In the previous section, we implemented a mat-error element for every validation error for every field part of the name object. This quickly adds up to seven elements for three fields. In Chapter 5, Designing Authentication and Authorization, we implemented common/validations.ts to reuse validation rules. We can reuse the behavior we implement within mat-error, or any other div for that matter, using an attribute directive.

Attribute directives

In Chapter 1, Angular’s Architecture and Concepts, I mentioned that Angular components represent the most basic unit of an Angular app. With components, we define custom HTML elements that can reuse features and functionality represented by a template and some TypeScript code. Conversely, a directive augments the capabilities of an existing element or component. In a sense, a component is a super directive that augments basic HTML capabilities.

With this view in mind, we...

Calculated properties and DatePicker

We can display calculated properties based on user input. For example, to display a person’s age based on their date of birth, introduce class properties that calculate the age and display it as follows:

src/app/user/profile/profile.component.ts
now = new Date()
get dateOfBirth() {
  return this.formGroup.get('dateOfBirth')?.value || this.now
}
get age() {
  return this.now.getFullYear() - this.dateOfBirth.getFullYear()
}

The implementation for the age property getter is not the most performant option. To calculate the age, we call the getFullYear() function of this.now and this.dateOfBirth. As a property referenced in the template, Angular’s change detection algorithm will call age up to 60 times per second, mixed with other elements on the screen, which can lead to major performance issues. You can resolve this issue by creating a pure custom pipe so that Angular understands only to check the age property if one...

Typeahead support

In buildForm, we set a listener on address.state to support a typeahead filtering drop-down experience:

src/app/user/profile/profile.component.ts
const state = this.formGroup.get('address.state')
if (state != null) {
  this.states$ = state.valueChanges.pipe(
    startWith(''),
    map((value) => USStateFilter(value))
  )
}

On the template, implement mat-autocomplete, bound to the filtered states array with an async pipe:

src/app/user/profile/profile.component.html
...
<mat-form-field appearance="outline" fxFlex="30%">
  <mat-label>State</mat-label>
  <input type="text" aria-label="State" matInput formControlName="state"
    [matAutocomplete]="stateAuto" #state />
  <mat-autocomplete #stateAuto="matAutocomplete">
    @for (state of states$ | async; track state) {
      <mat-option [value]="state.name">
        {{ state...

Dynamic form arrays

Note that the phones property is an array, potentially allowing for many inputs. We can implement this by building a FormArray with the this.formBuilder.array function. We also define several helper functions to make it easier to build the FormArray:

  • buildPhoneFormControl helps to build FormGroup objects of individual entries.
  • buildPhoneArray creates as many FormGroup objects as needed, or if the form is empty, it creates an empty entry.
  • addPhone adds a new empty FromGroup object to the FormArray.
  • get phonesArray() is a convenient property to get the phones control from the form.

Let’s see how the implementation comes together:

src/app/user/profile/profile.component.ts
...
phones: this.formBuilder.array(this.buildPhoneArray(user?.phones || [])),
...
  private buildPhoneArray(phones: IPhone[]) {
    const groups = []
    if (phones?.length === 0) {
      groups.push(this.buildPhoneFormControl(1))
    } else {
   ...

Creating shared components

Here’s a minimal implementation of the <app-view-user> directive, a prerequisite for the Review step.

Create a new viewUser component under the user folder structure, as follows:

src/app/user/view-user/view-user.component.ts
import { AsyncPipe, DatePipe } from '@angular/common'
import {
  Component, inject, Input, OnChanges, SimpleChanges
} from '@angular/core'
import { MatButtonModule } from '@angular/material/button'
import { MatCardModule } from '@angular/material/card'
import { MatIconModule } from '@angular/material/icon'
import { Router } from '@angular/router'
import { IUser, User } from '../user/user'
@Component({
  selector: 'app-view-user',
  template: `
    @if (currentUser) {
      <div>
        <mat-card appearance="outlined">
          <mat-card-header>
            <div mat-card-avatar>
              <mat...

Reviewing and saving form data

On the last step of the multistep form, users should be able to review and then save the form data. As a good practice, a successful POST request will return the data that was saved back to the browser. We can then reload the form with the information received back from the server:

src/app/user/profile/profile.component.ts
...
async save(form: FormGroup) {
    this.userService
      .updateUser(this.currentUserId, form.value)
      .pipe(first())
      .subscribe({
        next: (res: IUser) => {
          this.patchUser(res)
          this.formGroup.patchValue(res)
          this.uiService.showToast('Updated user')
        },
        error: (err: string) => (this.userError = err),
      })
  }
...

Note that updateUser returns the saved value of the user. It is possible that the database returns a different version of user than what we had before, so we use formGroup.patchValue to update the data powering the form. The...

Scalable form architecture with reusable parts

As mentioned in the introduction to the Multi-step responsive forms section, forms are tightly coupled beasts that can grow large, and using the wrong architectural pattern to scale your implementation can cause significant issues when implementing new features or maintaining existing ones.

To demonstrate how you can break up your form into multiple parts, we will refactor it to extract the highlighted section in the following screenshot, the name FormGroup, as its own component. The technique to accomplish this is the same as you’d use when you want to put each step of your form into a separate component:

Figure 8.9: User profile’s name part highlighted

By making the name FormGroup reusable, you will also learn about how you can reuse the business logic that you build into that FormGroup in other forms. We will extract the name FormGroup logic into a new component named NameInputComponent. In doing so,...

Input masking

Masking user input is an input UX tool and also a data quality one. I’m a fan of the ngx-mask library, which makes it easy to implement input masking in Angular. We will demonstrate input masking by updating the phone number input field, ensuring that users input a valid phone number, as shown in the following screenshot:

Figure 8.11: Phone number field with input masking

Set up your input masking as follows:

  1. Install the library via npm with npm i ngx-mask.
  2. Either use the environment provider, provideEnvironmentNgxMask(), in app.config.ts or provideNgxMask() in your feature module, user.module.ts.
  3. Import the NgxMaskDirective in profile.component.html:
  4. Update the number field in ProfileComponent as follows:
    src/app/user/profile/profile.component.html
    <mat-form-field appearance="outline" fxFlex fxFlexOffset="10px">
      <mat-label>Number</mat-label>
      <input matInput type="...

Custom controls with ControlValueAccessor

So far, we’ve learned about forms using standard form controls and input controls provided by Angular Material. However, it is possible for you to create custom user controls. If you implement the ControlValueAccessor interface, then your custom controls will play nicely with forms and the ControlValueAccessor interface’s validation engine.

We will be creating the custom rater control shown in the following screenshot and will place it as a control on the first step of ProfileComponent:

Figure 8.12: The lemon rater user control

User controls are inherently highly reusable, tightly coupled, and customized components to enable rich user interactions. Let’s implement one.

Implementing a custom rating control

The Lemon Rater will dynamically highlight the number of lemons selected as the user interacts with the control in real time. As such, creating a high-quality custom control is an expensive endeavor...

Layouts using a grid list

The Flex Layout library is great for laying out content using CSS Flexbox. Angular Material provides another mechanism to lay out content, using CSS Grid with its Grid List functionality. A good way to demonstrate this functionality is by implementing a helpful list of fake login information in the LoginComponent, demonstrated here:

Figure 8.13: Login helper with the grid list

Implement your list as follows:

  1. Start by defining a roles property that is an array of all the roles:
    src/app/login/login.component.ts
    roles = Object.keys(Role)
    
  2. Import MatExpansionModule and MatGridListModule in login.component.ts.
  3. Implement a new mat-card-content below the existing one:
    src/app/login/login.component.html
    <div fxLayout="row" fxLayoutAlign="center">
      <mat-card fxFlex="400px">
        <mat-card-header>
          <mat-card-title>
            <div class="mat-headline...

Restoring cached data

At the beginning of the chapter, when implementing the updateUser method in UserService, we cached the user object in case of any errors that may wipe out user-provided data:

src/app/user/user/user.service.ts
updateUser(id: string, user: IUser): Observable<IUser> {
  ...
  This.cache.setItem('draft-user', user)
  ...
}

Consider a scenario where the user may be temporarily offline when they attempt to save their data. In this case, our updateUser function will save the data.

Let’s see how we can restore this data in ProfileComponent when loading the user profile:

  1. Start by adding functions named loadFromCache and clearCache to the ProfileComponent class:
    src/app/user/profile.component.ts
    private loadFromCache(): Observable<User | null> {
      let user = null
      try {
        const draftUser = this.cache.getItem('draft-user')
        if (draftUser != null) {
          user = User.Build(JSON.parse(draftUser)...

Exercise

Practice new concepts like signals and @defer in Angular by updating UserService and the multi-step ProfileComponent form:

  • Update UserService and its related components to use signal instead of BehaviorSubject.
  • Use @defer to delay the rendering of conditional views.
  • Implement an expansion panel in LoginComponent to communicate password complexity requirements to your users.

Summary

This chapter covered forms, directives, and user control-related functionality for LemonMart. Using data binding, we created reusable components that can be embedded within another component. We showed that you can use PUT to send data to a server and cache data input by a user. We also created a multi-step input form that is responsive to changing screen sizes. We removed the boilerplate code from our components by leveraging reusable form parts, a base form class to house common functionality, and an attribute directive to encapsulate field-level error behavior and messages.

We created dynamic form elements with a date picker, typeahead support, and form arrays. We implemented interactive controls with input masking and the lemon rater. Using the ControlValueAccessor interface, we integrated the lemon rater seamlessly with our form. We showed that we can scale the size and complexity of our forms linearly by extracting the name as its form section. Additionally, we covered...

Further reading

Questions

Answer the following questions as best as possible to ensure you’ve understood the key concepts from this chapter without googling anything. Do you know if you got all the answers right? Visit https://angularforenterprise.com/self-assessment for more:

  1. What is the difference between a component and a user control?
  2. What is an attribute directive?
  3. What is the @-syntax?
  4. What is the purpose of the ControlValueAccessor interface?
  5. What is serialization, deserialization, and hydration?
  6. What does it mean to patch values on a form?
  7. How do you associate two independent FormGroup objects with each other?
lock icon
The rest of the chapter is locked
You have been reading a chapter from
Angular for Enterprise Applications - Third Edition
Published in: Jan 2024Publisher: PacktISBN-13: 9781805127123
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime

Author (1)

author image
Doguhan Uluca

Doguhan Uluca is a Principal Fellow at Excella in Washington, D.C., where he leads strategic initiatives and delivers critical systems. He has technical expertise in usability, mobility, performance, scalability, cybersecurity, and architecture. He is the author of the Angular for Enterprise Application Development books, has spoken at over 30 conferences, and is an Angular GDE Alumni. Doguhan has delivered solutions for Silicon Valley startups, Fortune 50 companies, and the U.S. Federal Government, and he is passionate about contributing to open-source projects and teaching.
Read more about Doguhan Uluca