Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Windows Tipps & SecurityBeyond the Build – September 2026(22.09.2026 um 00:36 Uhr)
Videos & KonferenzenGoogle for Developers: Understand the Gemma 4 model family(22.09.2026 um 01:00 Uhr)
Unix & Linux ServerSecurity: Zwei Probleme in gstreamer1-plugins-base (Red Hat)(21.09.2026 um 23:23 Uhr)
Unix & Linux ServerSecurity: Pufferüberlauf in corosync (Red Hat)(22.09.2026 um 00:49 Uhr)
Sichere ProgrammierungGitHub Enterprise adds credential inventory exports(21.09.2026 um 23:13 Uhr)
Sichere ProgrammierungYour First Factory: GtkListView and the Bind/Unbind Rhythm(22.09.2026 um 01:00 Uhr)
Windows Tipps & SecurityBeyond the Build – September 2026(22.09.2026 um 00:36 Uhr)
Videos & KonferenzenGoogle for Developers: Understand the Gemma 4 model family(22.09.2026 um 01:00 Uhr)
Unix & Linux ServerSecurity: Zwei Probleme in gstreamer1-plugins-base (Red Hat)(21.09.2026 um 23:23 Uhr)
Unix & Linux ServerSecurity: Pufferüberlauf in corosync (Red Hat)(22.09.2026 um 00:49 Uhr)
Sichere ProgrammierungGitHub Enterprise adds credential inventory exports(21.09.2026 um 23:13 Uhr)
Sichere ProgrammierungYour First Factory: GtkListView and the Bind/Unbind Rhythm(22.09.2026 um 01:00 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Angular Components: Composition-Driven Forms Over Inheritance

Angular Components: Composition-Driven Forms Over Inheritance In Object-Oriented Programming (OOP), Composition (Has-A) is a key concept, alongside Inheritance (Is-A).  In Angular, we often use composition naturally when we inject …

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

Angular Components: Composition-Driven Forms Over Inheritance

In Object-Oriented Programming (OOP), Composition (Has-A) is a key concept, alongside Inheritance (Is-A)



In Angular, we often use composition naturally when we inject services into components - services that handle API requests, caching, etc.



However, inheritance - extending a BaseComponent - is also common in Angular. One of the main strengths of Inheritance is shared behavior.



It works well in many cases, but sometimes it creates a subtle problem:



When the base component starts holding form-related logic, it forces every child component (even those that only display data, not create or update data) to inherit logic that they might not need. This can lead to tightly-coupled components.



Keeping the base component simple and minimal is a valid OOP approach.



For example, a BaseComponent might look like this:




// src/app/shared/component/base.component.ts

@Component({
selector: '',
template: '',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BaseComponent {

private errorMsg: WritableSignal<string> = signal('');
private statusMsg: WritableSignal<string> = signal('');
private ready: WritableSignal<boolean> = 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();
}
}






Reusing logic through inheritance like this is perfectly fine - when done carefully.



However, when it comes to handling forms, a cleaner and more scalable approach is to compose form functionality inside components, rather than inheriting it.



In this guide, we'll see how to:




  • Create a domain model (ChatRoom)

  • Build a reusable form (ChatRoomForm)

  • Define a component interface (HasForm similar to OnInit)

  • Create a BaseForm class that can be extended by form-related classes.



Section 1: Creating ChatRoom



Let's define a simple domain model for creating or updating a chat room.




// 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);
}
}






Section 2: CreatingBaseForm



Now, let's define the base form as follows:




// src/app/model/form/base.form.ts

export abstract class BaseForm<T> {

protected formGroup!: FormGroup;

private submitting: WritableSignal<boolean> = signal(false);
private formReady: WritableSignal<boolean> = signal(false);
private formCompleted: WritableSignal<boolean> = 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);
}

}






The formGroup contains your form.



The submitting signal tracks your form's submission state. You can use it to prevent multiple submissions until the request is processed.



The formReady signal helps you display the HTML form in the UI when initialization of the form with its control and data is completed.



The formCompleted signal is optional. It helps you display UI animations or transitions as feedback when the form submission is complete.



The control method allows you to easily retrieve a form control from a form group without repetition of syntax. For example:




  public get title(): AbstractControl | null | undefined {
return this.control('title');
}

public get description(): AbstractControl | null | undefined {
return this.control('description');
}






is simple compared to




  public get title(): AbstractControl | null | undefined {
return this.formGroup?.get('title');
}

public get description(): AbstractControl | null | undefined {
return this.formGroup?.get('description');
}






The form's variable name might change, but using the control method provides a single source of truth and the logic for retrieving a form control is encapsulated. When there is a change, you update only this method.



The getter and other methods are a way of encapsulating the logic and behavior of the form and also giving you access to its state and data.



Section 3: CreatingChatRoomForm



Now, we will create ChatRoomForm




// src/app/model/form/chat-room.form.ts

export class ChatRoomForm extends BaseForm<CreateChatRoomPayload> {

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());
}
}






The ChatRoomForm has a private constructor since it's not used in dependency injection. Instead, it's created via the static of method, following the factory pattern.



Using validators this way reduces repetition of the Validators class and supports polymorphic implementations. You can introduce custom validators, and changes apply instantly without updating the entire codebase. For example, you can define validators like this:




// src/app/shared/validators/index.ts

export const required = Validators.required;
export const minLength = Validators.minLength;
export const maxLength = Validators.maxLength;






Learn more about How to Organize Validators in an Angular app



Section 4: Creating HasForm Interface



The HasForm interface, for components requiring a user-filled form, looks like this:




// 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<any>;

get payload(): any;

get isFormReady(): boolean;

get isFormCompleted(): boolean;

get isFormValid(): boolean

get isNotSubmitting(): boolean;

get isSubmitting(): boolean;
}






These getter methods encapsulate the actual form behavior and logic and will be used in the UI and some part of your component.



These behaviors were usually implemented in the BaseComponent before and then inherited by extending components. This isn't always ideal as components like ChatRoomItem and ChatRoomList typically display data without mutating or updating it.



You can adapt this interface to work with your existing component design patterns and architecture.



The CreateChatRoom component looks like




@Component({
selector: 'app-create-chat-room',
imports: [
ValidationErrorComponent,
ReactiveFormsModule,
FaIconComponent,
],
providers: [
ChatRoomService,
],
templateUrl: './create-chat-room.html',
styleUrl: './create-chat-room.css'
})
export class CreateChatRoom extends BaseComponent implements OnInit, HasForm {

protected readonly chatRoomService: ChatRoomService = inject(ChatRoomService);
protected readonly formBuilder: FormBuilder = inject(FormBuilder);

protected readonly chatRoomForm: WritableSignal<ChatRoomForm> = signal(ChatRoomForm.empty(this.formBuilder));

public ngOnInit(): void {
this.initForm();
}

public formReady(): void {
this.formModel.openForm();
this.componentReady();
}

public completeForm(): void {
this.formModel.completeForm();
}

public initForm(): void {
const createChatRoomForm: ChatRoomForm = ChatRoomForm.of(this.formBuilder, ChatRoom.empty());
this.chatRoomForm.set(createChatRoomForm);

this.formReady();
}

public createChatRoom(): void {
if (this.isFormValid && this.isNotSubmitting) {
this.startSubmitting();

const payload: CreateChatRoomPayload = this.payload;
this.chatRoomService.create(payload)
.subscribe({
next: (response: CreateChatRoomResponse): void => { this.createChatRoomSuccess(response); },
error: (error: ErrorResponse): void => { this.createChatRoomFailure(error); },
complete: (): void => { this.createChatRoomComplete(); }
});
}
}

protected createChatRoomSuccess(result: CreateChatRoomResponse): void {
this.updateStatusMessage(result.message);
}

protected createChatRoomFailure(error: ErrorResponse): void {
this.updateErrorMessage(error.message);
this.createChatRoomComplete();
}

protected createChatRoomComplete(): void {
this.stopSubmitting();
this.completeForm();
}

public startSubmitting(): void {
this.formModel.startSubmitting();
}

public stopSubmitting(): void {
this.formModel.stopSubmitting();
}

public get createChatRoomForm(): FormGroup {
return this.formModel.form;
}

public get payload(): CreateChatRoomPayload {
return this.formModel.value;
}

public get formModel(): ChatRoomForm {
return this.chatRoomForm();
}

public get isFormReady(): boolean {
return this.formModel.isFormReady;
}

public get isFormValid(): boolean {
return this.formModel.isFormValid;
}

public get isFormCompleted(): boolean {
return this.formModel.isFormCompleted;
}

public get isSubmitting(): boolean {
return this.formModel.isSubmitting;
}

public get isNotSubmitting(): boolean {
return this.formModel.isNotSubmitting;
}

protected readonly faCheck = faCheck;
protected readonly faSpinner = faSpinner;
protected readonly faPlus = faPlus;
}






The signal(ChatRoomForm.empty()) initialize the signal with a form but whose state would not be ready, the UI and code is guarded and protected with the isFormReady check in the HTML template.




@if (isFormReady) {
<!-- .....HTML Form .......-->
}






The initForm method creates a new ChatRoomForm using static of method, initialize the form with opens the form and ready to be interacted with and then return it.



The openForm method sets the formReady signal state to true which indicates initialization is complete and the form is ready to be interacted with or completed by the user.



The payload getter method returns the form value you will use to create the chat room. Its type looks like this




export type CreateChatRoomPayload = {
title: string;
description: string;
guidelinesOrRules: string;
tags: string;
visibility: ChatRoomVisibility | string;
}






The payload getter method allows you to preprocess or add other details before the data is used. For example




public get payload(): CreateLiveStreamPayload {
const data: CreateLiveStreamPayload = this.formModel.value;
return {
...data,
startDateTime: addSecondsToDate(this.formModel.startDateTimeValue),
endDateTime: addSecondsToDate(this.formModel.endDateTimeValue),
};
}






Section 5: HTML Template




@if (isComponentReady && isFormReady) {
<form name="create-chat-room" method="post" enctype="application/x-www-form-urlencoded" autocomplete="off" [formGroup]="createChatRoomForm" (ngSubmit)="createChatRoom()">
<div>
<label for="title">Title*:</label>
<input type="text" id="title" name="title" formControlName="title" autocomplete="off"/>
<validation-error label="Title" [field]="formModel.title"></validation-error>
</div>

<div>
<label for="description">Description*:</label>
<input type="text" id="description" name="description" formControlName="description" autocomplete="off"/>
<validation-error label="Description" [field]="formModel.description"></validation-error>
</div>

<!-- Other HTML markup removed for brevity. -->

<button type="submit" [disabled]="isSubmitting">Create

@if (isFormCompleted) {<fa-icon [icon]="faCheck"/>}
@else if (isSubmitting) {<fa-icon [icon]="faSpinner" [animation]="'spin'"/>}
@else if (isNotSubmitting) {<fa-icon [icon]="faPlus"/>}
</button>
</form>
}






You use the getter methods to enhance the UI and form logic.



Addition:



What about the completeForm()?



Earlier, I mentioned how the formCompleted signal aids UI animations. The implementation is as follows:




  protected formCompleted: WritableSignal<boolean> = signal(false);






The implementation in the BaseForm will look something like this:




  public completeForm(delayToReset: number = 5000): void {
// Ensure delay is non-negative
const safeDelay: number = Math.max(0, delayToReset);

// Mark the form as completed
this.enableFormComplete();

// Set a timer to reset form completion status
timer(safeDelay).pipe(
// Reset form completion status
tap(() => this.disableFormCompleted()),
).subscribe();
}






When the user's form submission is completed and the completeForm() is called, the Font Awesome animation starts as indicated here




@if (isFormCompleted) {<fa-icon [icon]="faCheck"/>}






After the 5 seconds default delay, the status is reset to false and the check or mark icon disappears.



Note:



Of course, the methods in HasForm have already been defined in the formModel or BaseForm. It's debatable whether this introduces over-abstraction. You can decide to implement all the methods in HasForm or you can remove some and access the formModel in the Component code or directly in the HTML template.



Repository: Angular Form Composition



Using composition leads to better separation of concerns, easier testing, and more reusable components. I hope you enjoy it.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Angular Components: Composition-Driven Forms Over Inheritance

Thematisch verwandte Begriffe: Angular, Components, CompositionDriven, Forms · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-49449 | Joplin is an open source note-taking and to-do application that organise…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick