У об'єктно-орієнтованому програмуванні (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