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)