Search code examples
angulartypescriptformsangular-reactive-formsangular12

"Reusable" forms or at least an easier way to create them


I'm working on an Angular 12 project and I've been told that the way I'm creating the forms for the CRUD operations is a bit overwhelming. The way I'm currently using is just like the official primeng example. Note that there are like 7-8 CRUDs that are similar, the only difference is the form fields.

Question 1

I think the guy who told me that probably meant to replace the current code with some kind of a shared component and then enumerate all form fields and display them? Reactive forms? Or maybe there is some fancy npm package?

Question 2

I was looking at how this guy did it in that open source project. Maybe I should use his approach?

Code

import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';

import { Lecturer } from '@core/types';
import { LecturerService } from './lecturer.service';
import { ConfirmationService, MessageService, PrimeNGConfig } from 'primeng/api';
import { BreadcrumbService } from '@core/services';
import { Table } from 'primeng/table';

@Component({
  selector: 'app-lecturers',
  templateUrl: './lecturers.component.html'
})
export class LecturersComponent implements OnInit, OnDestroy {
  lecturerDialog: boolean;
  lecturers: Lecturer[];
  lecturer: Lecturer;
  selectedLecturers: Lecturer[];
  submitted: boolean;
  loading: boolean = true;

  private componentDestroyed$ = new Subject<boolean>();

  @ViewChild('dt') table: Table;

  constructor(
    private lecturerService: LecturerService,
    private messageService: MessageService,
    private confirmationService: ConfirmationService,
    private breadcrumbService: BreadcrumbService,
    private primengConfig: PrimeNGConfig
  ) {
    this.breadcrumbService.setItems([{ label: 'Преподаватели' }]);
  }

  ngOnInit(): void {
    this.lecturerService
      .getAllLecturers()
      .pipe(takeUntil(this.componentDestroyed$))
      .subscribe((data) => {
        this.lecturers = data;
        this.loading = false;
      });

    this.primengConfig.ripple = true;
  }

  ngOnDestroy(): void {
    this.componentDestroyed$.next(true);
    this.componentDestroyed$.complete();
  }

  applyFilterGlobal($event: any) {
    this.table.filterGlobal(($event.target as HTMLInputElement).value, 'contains');
  }

  openNew() {
    this.lecturer = {};
    this.submitted = false;
    this.lecturerDialog = true;
  }

  deleteSelectedLecturers() {
    this.confirmationService.confirm({
      message: 'Сигурни ли сте, че искате да изтриете данните за избраните преподаватели?',
      header: 'Потвърждение',
      icon: 'pi pi-exclamation-triangle',
      acceptLabel: 'Да',
      rejectLabel: 'Не',
      accept: () => {
        this.lecturers = this.lecturers.filter((val) => !this.selectedLecturers.includes(val));
        this.selectedLecturers = [];
        this.messageService.add({
          severity: 'success',
          summary: 'Успешно',
          detail: 'Данните за преподавателите са изтрити',
          life: 3000
        });
      }
    });
  }

  editLecturer(lecturer: Lecturer) {
    this.lecturer = { ...lecturer };
    this.lecturerDialog = true;
  }

  deleteLecturer(lecturer: Lecturer) {
    this.confirmationService.confirm({
      message: 'Сигурни ли сте, че искате да изтриете данните за ' + lecturer.displayName + '?',
      header: 'Потвърждение',
      icon: 'pi pi-exclamation-triangle',
      acceptLabel: 'Да',
      rejectLabel: 'Не',
      accept: () => {
        this.lecturers = this.lecturers.filter((val) => val.id !== lecturer.id);
        this.lecturer = {};
        this.messageService.add({
          severity: 'success',
          summary: 'Успешно',
          detail: 'Данните за преподавателя са изтрити',
          life: 3000
        });
      }
    });
  }

  hideDialog() {
    this.lecturerDialog = false;
    this.submitted = false;
  }

  saveLecturer() {
    this.submitted = true;

    if (this.lecturer.fullName?.trim() && this.lecturer.displayName?.trim()) {
      if (this.lecturer.id) {
        this.lecturers[this.findIndexById(this.lecturer.id)] = this.lecturer;
        this.messageService.add({
          severity: 'success',
          summary: 'Успешно',
          detail: 'Данните за преподавателя са обновени',
          life: 3000
        });
      } else {
        this.lecturers.push(this.lecturer);

        // update to what's in db instead

        this.messageService.add({
          severity: 'success',
          summary: 'Успешно',
          detail: 'Преподавателят е добавен',
          life: 3000
        });
      }

      this.lecturers = [...this.lecturers];
      this.lecturerDialog = false;
      this.lecturer = {};
    }
  }

  findIndexById(id: number): number {
    let index = -1;
    for (let i = 0; i < this.lecturers.length; i++) {
      if (this.lecturers[i].id === id) {
        index = i;
        break;
      }
    }

    return index;
  }
}

<div class="p-grid">
  <div class="p-col-12">
    <p-toast></p-toast>

    <div class="card">
      <p-toolbar styleClass="p-mb-4">
        <ng-template pTemplate="left">
          <button
            pButton
            pRipple
            label="Добавяне"
            icon="pi pi-plus"
            class="p-button-success p-mr-2 p-mb-2"
            (click)="openNew()"
          ></button>
          <button
            pButton
            pRipple
            label="Изтриване"
            icon="pi pi-trash"
            class="p-button-danger p-mb-2"
            (click)="deleteSelectedLecturers()"
            [disabled]="!selectedLecturers || !selectedLecturers.length"
          ></button>
        </ng-template>
      </p-toolbar>

      <p-table
        #dt
        [value]="lecturers"
        [(selection)]="selectedLecturers"
        dataKey="id"
        styleClass="p-datatable-lecturers"
        [rowHover]="true"
        [rows]="10"
        [showCurrentPageReport]="true"
        [rowsPerPageOptions]="[10, 25, 50]"
        [loading]="loading"
        [paginator]="true"
        currentPageReportTemplate="Показват се от {first} до {last} от общо {totalRecords} записа"
        [globalFilterFields]="['fullName', 'displayName']"
      >
        <ng-template pTemplate="caption">
          <div class="p-d-flex p-flex-column p-flex-md-row p-jc-md-between table-header">
            <h5 class="p-m-0">Преподаватели</h5>
            <span class="p-input-icon-left">
              <i class="pi pi-search"></i>
              <input
                pInputText
                type="text"
                (input)="applyFilterGlobal($event)"
                placeholder="Търсене..."
              />
            </span>
          </div>
        </ng-template>
        <ng-template pTemplate="header">
          <tr>
            <th style="width: 3rem">
              <p-tableHeaderCheckbox></p-tableHeaderCheckbox>
            </th>
            <th pSortableColumn="fullName">Име <p-sortIcon field="fullName"></p-sortIcon></th>
            <th pSortableColumn="displayName">
              Псевдоним <p-sortIcon field="displayName"></p-sortIcon>
            </th>
            <th></th>
          </tr>
        </ng-template>
        <ng-template pTemplate="body" let-lecturer>
          <tr>
            <td>
              <p-tableCheckbox [value]="lecturer"></p-tableCheckbox>
            </td>
            <td>
              {{ lecturer.displayName }}
            </td>
            <td>
              {{ lecturer.fullName }}
            </td>
            <td>
              <button
                pButton
                pRipple
                icon="pi pi-pencil"
                class="p-button-rounded p-button-success p-mr-2"
                (click)="editLecturer(lecturer)"
              ></button>
              <button
                pButton
                pRipple
                icon="pi pi-trash"
                class="p-button-rounded p-button-warning"
                (click)="deleteLecturer(lecturer)"
              ></button>
            </td>
          </tr>
        </ng-template>
        <ng-template pTemplate="summary">
          <div class="p-d-flex p-ai-center p-jc-between">
            Общо {{ lecturers ? lecturers.length : 0 }} преподаватели.
          </div>
        </ng-template>
      </p-table>
    </div>

    <p-dialog
      [(visible)]="lecturerDialog"
      [style]="{ width: '450px' }"
      header="Данни за преподавател"
      [modal]="true"
      styleClass="p-fluid"
    >
      <ng-template pTemplate="content">
        <div class="p-field">
          <label for="fullName">Име</label>
          <input
            #fullName="ngModel"
            id="fullName"
            type="text"
            pInputText
            [(ngModel)]="lecturer.fullName"
            [ngClass]="{
              'ng-dirty': (fullName.invalid && submitted) || (fullName.dirty && fullName.invalid)
            }"
            required
            autofocus
          />
          <small
            class="p-error"
            *ngIf="(fullName.invalid && submitted) || (fullName.dirty && fullName.invalid)"
            >Името е задължително.</small
          >
        </div>
        <div class="p-field">
          <label for="displayName">Псевдоним</label>
          <input
            #displayName="ngModel"
            id="displayName"
            type="text"
            pInputText
            [(ngModel)]="lecturer.displayName"
            [ngClass]="{
              'ng-dirty':
                (displayName.invalid && submitted) || (displayName.dirty && displayName.invalid)
            }"
            required
          />
          <small
            class="p-error"
            *ngIf="(displayName.invalid && submitted) || (displayName.dirty && displayName.invalid)"
            >Псевдонимът е задължителен.</small
          >
        </div>
      </ng-template>

      <ng-template pTemplate="footer">
        <button
          pButton
          pRipple
          label="Отказ"
          icon="pi pi-times"
          class="p-button-text"
          (click)="hideDialog()"
        ></button>
        <button
          pButton
          pRipple
          label="Запази"
          icon="pi pi-check"
          class="p-button-text"
          (click)="saveLecturer()"
        ></button>
      </ng-template>
    </p-dialog>

    <p-confirmDialog [style]="{ width: '450px' }"></p-confirmDialog>
  </div>
</div>


Solution

  • I'd suggest putting the form in a separate component - straight away that would simplify the table component

    You could use Angular Reactive Forms which under the hood manage the object that's being updated - on submit you could emit this object via an @Output or directly trigger the CRUD operations in the form component using the appropriate services (probably a better approach)