Компоненти Angular: Форми на основі композиції замість наслідування

У об'єктно-орієнтованому програмуванні (OOP) композиція та наслідування — це важливі концепції. В Angular композиція часто застосовується, коли інжектуються сервіси в компоненти, що обробляють запити до API або кешування. Однак наслідування також є популярним, особливо коли потрібно розширити базові компоненти, такі як BaseComponent. Проблема може виникнути, коли базовий компонент містить логіку для роботи з формами, що змушує дочірні компоненти успадковувати її, навіть якщо вони лише відображають дані. Це може призвести до сильно зв'язаних компонентів. Тому важливо тримати базові компоненти простими і мінімалістичними.

Наприклад, BaseComponent може виглядати наступним чином:

// src/app/shared/component/base.component.ts  
@Component({  
 selector: '',  
 template: '',  
 changeDetection: ChangeDetectionStrategy.OnPush  
})  
export class BaseComponent {  
 private errorMsg: WritableSignal = signal('');  
 private statusMsg: WritableSignal = signal('');  
 private ready: WritableSignal = signal(false);  
 protected componentReady(): void {  
 this.ready.set(true);  
 }  
 protected resetErrorMessage(): void {  
 this.errorMsg.set('');  
 }  
 protected resetStatusMessage(): void {  
 this.statusMsg.set('');  
 }  
 protected updateStatusMessage(message: string = ''): void {  
 this.statusMsg.set(message);  
 }  
 protected updateErrorMessage(message: string = ''): void {  
 this.errorMsg.set(message);  
 }  
 public get isComponentReady(): boolean {  
 return this.ready();  
 }  
 public get errorMessage(): string {  
 return this.errorMsg();  
 }  
 public get statusMessage(): string {  
 return this.statusMsg();  
 }  
}  

Перезасвоєння логіки через наслідування є правильним, але для обробки форм доцільніше використовувати складання функціональності форм в компонентах, а не успадковувати її.

Давайте розглянемо, як створити доменну модель для чат-кімнати (ChatRoom), форму для чат-кімнати (ChatRoomForm), і інтерфейс компонента (HasForm).

Розділ 1: Створення ChatRoom

Для створення або оновлення чат-кімнати ми можемо визначити просту доменну модель, що містить усі необхідні поля, як от id, title, description тощо:

// src/app/model/domain/chat-room.ts  
export class ChatRoom {  
 public readonly id: number | string;  
 public readonly title: string;  
 public readonly description: string;  
 public readonly tags: string;  
 public readonly guidelinesOrRules: string;  
 public readonly visibility: ChatRoomVisibility;  
 public constructor(data: ChatRoom) {  
 this.id = data.id ?? 0;  
 this.title = data.title ?? '';  
 this.description = data.description ?? '';  
 this.tags = data.tags ?? '';  
 this.guidelinesOrRules = data.guidelinesOrRules ?? '';  
 this.visibility = data.visibility ?? '';  
 }  
 public static of(data: ChatRoom): ChatRoom {  
 return new ChatRoom(data);  
 }  
 public static empty(): ChatRoom {  
 return new ChatRoom({} as ChatRoom);  
 }  
}  

Розділ 2: Створення BaseForm

Далі ми створюємо базову форму, яка буде розширена для обробки різних форм:

// src/app/model/form/base.form.ts  
export abstract class BaseForm {  
 protected formGroup!: FormGroup;  
 private submitting: WritableSignal = signal(false);  
 private formReady: WritableSignal = signal(false);  
 private formCompleted: WritableSignal = signal(false);  
 protected constructor() {}  
 protected initForm(): void {}  
 protected control(name: string): AbstractControl | null | undefined {  
 return this.form?.get(name);  
 }  
 public enableFormComplete(): void {  
 this.formCompleted.set(true);  
 }  
 public disableFormCompleted(): void {  
 this.formCompleted.set(false);  
 }  
 public openForm(): void {  
 this.formReady.set(true);  
 }  
 public startSubmitting(): void {  
 this.submitting.set(true);  
 }  
 public stopSubmitting(): void {  
 this.submitting.set(false);  
 }  
 public get form(): FormGroup {  
 return this.formGroup;  
 }  
 public get value(): T {  
 return this.form.value as T;  
 }  
 public get isFormValid(): boolean {  
 return this.formGroup.valid;  
 }  
 public get isFormCompleted(): boolean {  
 return this.formCompleted();  
 }  
 public get isFormReady(): boolean {  
 return this.formReady();  
 }  
 public get isSubmitting(): boolean {  
 return this.submitting();  
 }  
 public get isNotSubmitting(): boolean {  
 return !(this.isSubmitting);  
 }  
}  

Розділ 3: Створення ChatRoomForm

Тепер визначимо ChatRoomForm, який розширює BaseForm і містить усі специфічні валідатори та поля для чат-кімнати:

// src/app/model/form/chat-room.form.ts  
export class ChatRoomForm extends BaseForm {  
 private constructor(  
 private formBuilder: FormBuilder,  
 private chatRoom: ChatRoom) {  
 super();  
 }  
 public get title(): AbstractControl | null | undefined {  
 return this.control('title');  
 }  
 public get description(): AbstractControl | null | undefined {  
 return this.control('description');  
 }  
 public get guidelines(): AbstractControl | null | undefined {  
 return this.control('guidelinesOrRules');  
 }  
 public get tags(): AbstractControl | null | undefined {  
 return this.control('tags');  
 }  
 public get visibility(): AbstractControl | null | undefined {  
 return this.control('visibility');  
 }  
 public get visibilities(): string[] {  
 return Object.values(ChatRoomVisibility);  
 }  
 protected override initForm(): void {  
 this.formGroup = this.formBuilder.group({  
 title: [this.chatRoom.title, [  
 required,  
 minLength(10),  
 maxLength(500),  
 ]],  
 description: [this.chatRoom.description, [  
 required,  
 maxLength(1000),  
 ]],  
 tags: [this.chatRoom.tags, [  
 required,  
 minLength(10),  
 maxLength(500),  
 ]],  
 guidelinesOrRules: [this.chatRoom.guidelinesOrRules, [  
 required,  
 maxLength(1500),  
 ]],  
 visibility: [this.chatRoom.visibility, [  
 required,  
 oneOf(ChatRoomVisibility)  
 ]],  
 });  
 }  
 public static of(formBuilder: FormBuilder, chatRoom: ChatRoom): ChatRoomForm {  
 const chatRoomForm: ChatRoomForm = new ChatRoomForm(formBuilder, chatRoom);  
 chatRoomForm.initForm();  
 return chatRoomForm;  
 }  
 public static empty(formBuilder: FormBuilder): ChatRoomForm {  
 return new ChatRoomForm(formBuilder, ChatRoom.empty());  
 }  
}  

Розділ 4: Створення інтерфейсу HasForm

Інтерфейс HasForm дозволяє компонентам, що мають форму, зберігати логіку для ініціалізації форми, перевірки її стану та подачі даних.

// src/app/model/interface/form/has-form.interface.ts  
export interface HasForm {  
 formReady(): void;  
 initForm(data?: any): void;  
 startSubmitting(): void;  
 stopSubmitting(): void;  
 completeForm(): void;  
 get formModel(): BaseForm;  
 get payload(): any;  
 get isFormReady(): boolean;  
 get isFormCompleted(): boolean;  
 get isFormValid(): boolean  
 get isNotSubmitting(): boolean;  
 get isSubmitting(): boolean;  
}  

Цей інтерфейс дозволяє інкапсулювати логіку форми та її поведінку для використання в UI і компоненті.

У підсумку, ми застосовуємо композицію замість наслідування, що дозволяє досягти кращого розділення обов'язків, полегшує тестування та забезпечує повторне використання компонентів.

Перекладено з: Angular Components: Composition-Driven Forms Over Inheritance