angular - "Återanvändbara" formulär eller åtminstone ett enklare sätt att skapa dem


Jag arbetar på ett Angular 12-projekt och jag har fått höra att sättet jag skapar formulären för CRUD-operationerna är lite överväldigande. Sättet jag använder just nu är precis som det officiella primeng-exemplet. Observera att det finns ungefär 7-8 CRUDs som är liknande, den enda skillnaden är formulärfälten.

Fråga 1

Jag tror att killen som berättade det för mig antagligen menade att ersätta den nuvarande koden med någon slags delad komponent och sedan uppräkna alla formulärfält och visa dem? Reaktiva formulär? Eller kanske finns det något fiffigt npm-paket?

Fråga 2

Jag tittade på hur den här killen gjorde det i det öppen källkodsprojektet. Kanske borde jag använda hans metod?

Kod

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>

Svar #1

Jag skulle föreslå att lägga formuläret i en separat komponent - det skulle direkt förenkla tabellkomponenten

Du kan använda Angular Reactive Forms som under huven hanterar objektet som uppdateras - vid inskickning kan du skicka ut detta objekt via en @Output eller direkt trigga CRUD-operationerna i formkomponenten med hjälp av de lämpliga tjänsterna (förmodligen en bättre metod)