Creating re-usable select and textarea inputs

Our form only has text-inputs at the moment but for the description we would like a text-area and for the brands/types we need a select input. Just in case we need these types of inputs at a future point lets create re-usable versions of them.

Creating a re-usable Text Area input.

Create a new component for this by running the following command in the context of the client folder:

1ng g c shared/components/text-area --skip-tests

Open the new text-area.component.ts and add the following code:

1import { Component, Input, Self } from '@angular/core';
2import { NgControl, FormControl, ReactiveFormsModule } from '@angular/forms';
3import { MatFormField, MatError, MatLabel } from '@angular/material/form-field';
4import { MatInput } from '@angular/material/input';
5
6@Component({
7  selector: 'app-text-area',
8  standalone: true,
9  imports: [
10    ReactiveFormsModule,
11    MatFormField,
12    MatInput,
13    MatError,
14    MatLabel
15  ],
16  templateUrl: './text-area.component.html',
17  styleUrl: './text-area.component.scss'
18})
19export class TextAreaComponent {
20  @Input() label = '';
21
22  constructor(@Self() public controlDir: NgControl) {
23    this.controlDir.valueAccessor = this;
24  }
25  
26  writeValue(obj: any): void {
27  }
28
29  registerOnChange(fn: any): void {
30  }
31
32  registerOnTouched(fn: any): void {
33  }
34
35  get control() {
36    return this.controlDir.control as FormControl
37  }
38}

This is virtually identical to the text-input.component.ts.

Next, open this components template and add the following code:

1<mat-form-field appearance="outline" class="w-full mb-4">
2    <mat-label>{{label}}</mat-label>
3    <textarea [formControl]="control" placeholder={{label}} matInput></textarea>
4    @if (control.hasError('required')) {
5        <mat-error>{{label}} is required</mat-error>
6    }
7</mat-form-field>

Again, this is virtually identical to the text-input with just the <input> changed to the <textarea>.

Next, use this new component in the product-form.component.html for the description field:

1<app-text-area label="Description" formControlName="description" />

Also ensure you add the import for this in the product-form.component.ts imports array:

1import { TextAreaComponent } from "../../../shared/components/text-area/text-area.component";
2
3@Component({
4  selector: 'app-product-form',
5  standalone: true,
6  imports: [
7    // other imports
8    TextAreaComponent
9],

You should be able to test this now in the form when you open it:

Image

Creating the select-input re-usable component.ts

Create another new component by running the following command:

1ng g c shared/components/select-input --skip-tests

Open the new select-input.component.ts and add the following code:

1import { Component, Input, Self } from '@angular/core';
2import { ControlValueAccessor, NgControl } from '@angular/forms';
3import { MatFormFieldModule } from '@angular/material/form-field';
4import { MatSelectChange, MatSelectModule } from '@angular/material/select';
5
6@Component({
7  selector: 'app-select-input',
8  standalone: true,
9  imports: [
10    MatFormFieldModule,
11    MatSelectModule
12  ],
13  templateUrl: './select-input.component.html',
14  styleUrl: './select-input.component.scss'
15})
16export class SelectInputComponent implements ControlValueAccessor {
17  @Input() label = '';
18  @Input() options: string[] = [];
19
20  value: string = '';
21  onChange: any = () => {};
22  onTouched: any = () => {};
23
24  constructor(@Self() public controlDir: NgControl) {
25    this.controlDir.valueAccessor = this;
26  }
27
28  onSelect(event: MatSelectChange) {
29    this.value = event.value;
30    this.onChange(this.value);
31  }
32
33  writeValue(value: any): void {
34    this.value = value;
35  }
36
37  registerOnChange(fn: any): void {
38    this.onChange = fn;
39  }
40
41  registerOnTouched(fn: any): void {
42    this.onTouched = fn;
43  }
44
45}
46

This one is a bit more involved as we need to accept a string[] of options to be selected. We also create an onSelect() method that accepts the selected value which we use with the form controls onChange() method. We also specify a property for the ‘value’ which we use in 2-way binding with the component template.

Next, open the template and add the following code:

1<mat-form-field appearance="outline" class="w-full mb-4">
2    <mat-label>{{label}}</mat-label>
3    <mat-select (selectionChange)="onSelect($event)" [(value)]="value">
4        @for (option of options; track $index) {
5        <mat-option [value]="option">{{option}}</mat-option>
6        }
7    </mat-select>
8</mat-form-field>

In this code we use the <mat-select> and are using the onSelect() method with the (selectionChange) event from <mat-select>. We are also using 2 way binding here with the [(value)] being set to the value which we define in the component code.

Next, open the product-form.component.ts

We need to get a list of types and brands from the shop.service.ts so inject this into the component:

1shopService = inject(ShopService);

If we review the code in the shop.service.ts then we can see that we are subscribing to this inside the service and storing the values for these in the service too which provides client side caching functionality.

1  export class ShopService {
2	  baseUrl = environment.apiUrl;
3	  private http = inject(HttpClient);
4	  types: string[] = [];
5	  brands: string[] = [];
6	  
7	  // other code omitted
8  
9	  getBrands() {
10	    if (this.brands.length > 0) return;
11	    return this.http.get<string[]>(this.baseUrl + 'products/brands').subscribe({
12	      next: response => this.brands = response,
13	    })
14	  }
15	
16	  getTypes() {
17	    if (this.types.length > 0) return;
18	    return this.http.get<string[]>(this.baseUrl + 'products/types').subscribe({
19	      next: response => this.types = response,
20	    })
21	  }

So we can use this in the product-form.component.ts but we do need to make sure we call the getBrands() and getTypes() methods just in case they are not already cached in the service. Update the product-form.component.ts with the following code:

1  // product-form.component.ts
2  
3  ngOnInit(): void {
4    this.initializeForm();
5    setTimeout(() => {
6      this.loadBrandsAndTypes();
7    })
8  }
9  
10  // other methods and code omitted
11  
12  loadBrandsAndTypes(): void {
13    this.shopService.getBrands();
14    this.shopService.getTypes();
15  }

In this code we create a method to call the 2 shop.service methods to loadBrandsAndTypes() and then in the ngOnInit() we call this method. We wrap this in a setTimeout() as the Angular change detection will show warnings in the chrome dev tools console as we will not have these when the form first loads up. This simply prevents the warnings here.

Next, open up the product-form.component.html and update the fields for the brands and the types:

1    <div class="flex justify-between w-full gap-3">
2      <app-select-input label="Type" formControlName="type" [options]="shopService.types" class="w-full" />
3      <app-select-input label="Brand" formControlName="brand" [options]="shopService.brands" class="w-full" />
4    </div>

Ensure you import the SelectInputComponent into the product-form.component.ts imports array:

We pass the brands and types using the [options] input property. We also need to give this the ‘w-full’ class to ensure it takes up its full available width here. If you test this now you should see that you have the select input available for the brands and types:

Image

Give the form another test by populating the fields and you should see the following output in the chrome dev tools console:

Image

Next, we will test the form functionality and enable editing of existing products too.