Fri. Apr 19th, 2024

Angular Material offers a powerful set of features for building your Angular application on. It’s time to take them to the next level. A project I’m working on needed to be able to edit, view, and delete data. Not wanting to reinvent the wheel. I had an idea, what about reusable field components that combined the ability to Edit, Delete, and View a field.

My solution began with a base abstract class:

import {Component, EventEmitter, Input, OnInit, Output} from "@angular/core";
import {LanguageService} from "../../../service/language.service";
import {Subscription} from "rxjs";


@Component({
  template: ''
})
export abstract class AbstractFieldComponent {
  @Input("edit") allowEdit: boolean = false;
  @Input("delete") allowDelete: boolean = false;
  @Input("label") label: string = "";
  @Input("placeholder") placeHolder: string = "";
  @Input("hint") hint: string = "";
  @Input("enableClear") enableClear:boolean = false;


  @Output("onSave") onSave: EventEmitter<null> = new EventEmitter<null>();
  @Output("onDelete") onDelete: EventEmitter<null> = new EventEmitter<null>();
  @Output("onClear") onClear: EventEmitter<null> = new EventEmitter<null>();

  DISPLAY_MODE:string = 'DISPLAY';
  EDIT_MODE:string = "EDIT";

  mode:string = this.DISPLAY_MODE;

  editLabel: string = "edit";
  deleteLabel: string = "delete";
  undoLabel: string = "undo";
  clearLabel: string = "clear";
  saveLabel: string = "save";

  editSub: Subscription;
  deleteSub: Subscription;
  undoSub: Subscription;
  clearSub: Subscription;
  saveSub: Subscription;


  constructor(public languageService:LanguageService) {
    this.editLabel = this.languageService.editLabel;
    this.deleteLabel = this.languageService.deleteLabel;
    this.undoLabel = this.languageService.undoLabel;
    this.clearLabel = this.languageService.clearLabel;
    this.saveLabel = this.languageService.saveLabel;

    this.editSub = this.languageService.editSub.subscribe( value => this.editLabel = value);
    this.deleteSub = this.languageService.deleteSub.subscribe( value => this.deleteLabel = value);
    this.undoSub = this.languageService.undoSub.subscribe( value => this.undoLabel = value);
    this.clearSub = this.languageService.clearSub.subscribe( value => this.clearLabel = value);
    this.saveSub = this.languageService.saveSub.subscribe( value => this.saveLabel = value);
  }

  public changeMode(mode:string) {
    this.mode = mode;
  }

  public save() {
    this.onSave.emit();
    this.mode = this.DISPLAY_MODE;
  }

  public delete() {
    this.onDelete.emit();
  }

  public clear() {
    this.onClear.emit();
  }

  public edit() {
    this.mode = this.EDIT_MODE;
    this.backupValue();
  }

  public undo() {
    this.restoreValue();
    this.mode = this.DISPLAY_MODE;
  }

  abstract restoreValue():null;
  abstract backupValue():null;
}

Two inputs [edit] and [delete] that accept a boolean to indicate if the user should have the ability Edit or Delete the value. Simple enough to work in most if not all Role/Permission management methods to provide the answer.

Next came three fields that I found would be extremely common to most of these fields [label], [placeholder], and [hint] to provide the text for these field that are common to mat-form-field.

Lastly came three outputs that would emit when an action should occur. (onSave), (onDelete), and (onClear) were the needed emitters. I wrapped these into aptly named methods to easily be called.

Two abstract methods to backupValue() to store the value in case the user canceled the edit action. Then a restoreValue() method to call when an edit was cancelled to restore the value.

In addition, you can see that I am getting my label values from the LanguageService I described here in Translations in a Angular Library. You can see that I use the getter to get the current value and subscribe to get the latest value.

StringFieldComponent

So now we need to create a component that extends the abstract class, to make use of our base inputs, outputs, and methods. To begin with we will create a String field:

import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {AbstractFieldComponent} from "../abstract-field/abstract-field.component";
import {LanguageService} from "../../../service/language.service";

@Component({
  selector: 'mymiller-string-field',
  templateUrl: './string-field.component.html',
  styleUrls: ['./string-field.component.scss']
})
export class StringFieldComponent extends AbstractFieldComponent {
  componentValue: string  = '';

  @Output() valueChange: EventEmitter<string> = new EventEmitter<string>();
  @Input()

  get value() {
    return this.componentValue;
  }

  set value(val) {
    this.componentValue = val;
    this.valueChange.emit(this.componentValue);
  }

  private backup: string = '';

  constructor(public languageService:LanguageService) {
    super(languageService);
  }

  restoreValue(): null {
    this.value = this.backup;
    return null;
  }
  backupValue(): null {
    this.backup = this.value;
    return null;
  }
}

We implement a [(value)] to pass in the data for this field, in this case a string. Next we implement the restoreValue(), and backupValue() from the base component. Can you get any simpler in your component file?

Now we need to create the HTML to actually display the field.

<ng-container *ngIf="mode ==='DISPLAY'">
  <mat-form-field appearance="legacy" class="full-width">
    <mat-label *ngIf="label !== ''">{{label}}</mat-label>
    <input matInput [placeholder]="placeHolder" [(ngModel)]="value" disabled>
    <mymiller-icon-button matSuffix icon="edit" (click)="edit()" *ngIf="allowEdit" [tooltip]="editLabel"></mymiller-icon-button>
    <mymiller-icon-button matSuffix icon="delete" (click)="delete()" *ngIf="allowDelete" color="warn" [tooltip]="deleteLabel"></mymiller-icon-button>

  </mat-form-field>
</ng-container>

<ng-container *ngIf="mode===EDIT_MODE">
  <mat-form-field appearance="legacy" class="full-width">
    <mat-label *ngIf="label !== ''">{{label}}</mat-label>
    <input matInput [placeholder]="placeHolder" [(ngModel)]="value" >
    <mymiller-icon-button matSuffix icon="save" (click)="save()" [tooltip]="saveLabel"></mymiller-icon-button>
    <mymiller-icon-button matSuffix icon="clear" (click)="clear()" [tooltip]="clearLabel"  *ngIf="enableClear"></mymiller-icon-button>
    <mymiller-icon-button matSuffix icon="undo" (click)="undo()" [tooltip]="undoLabel" color="warn"></mymiller-icon-button>
    <mat-hint *ngIf="hint !== ''">{{hint}}</mat-hint>
  </mat-form-field>
</ng-container>

We switch based on the mode whether we are DISPLAY, or EDIT. Now we enter in the elements to show our field. You can see I just “disabled” the DISPLAY. Each has their own set of icons that will appear, to switch between Edit/Delete/Save/Clear/Cancel actions. Those have *ngIf=”…” to determine if they should appear.

Last thing I would recommend having a full-width CSS/SCSS class to maximize the field to it’s container.

.full-width {
  width: 100%;
}

By Jeffery Miller

I am known for being able to quickly decipher difficult problems to assist development teams in producing a solution. I have been called upon to be the Team Lead for multiple large-scale projects. I have a keen interest in learning new technologies, always ready for a new challenge.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.