Angular

Die Komponenten Schritt für Schritt in eine Angular-Anwendung einbauen.

Angular braucht ein einziges Schema-Flag, um die Tags zu akzeptieren. Danach greifen seine Property- und Event-Bindings direkt auf die Komponenten, ohne Wrapper-Bibliothek und ohne Übersetzung der Event-Namen.

1. Installieren

npm i tuix-webcomponents

Die Design-Tokens in die globalen Styles aufnehmen:

angular.json
"styles": [
    "node_modules/tuix-webcomponents/dist/assets/tuix-styles.css",
    "src/styles.scss"
]

2. Elemente registrieren

Der Import der Bibliothek registriert jedes Tag. Einmal in main.ts, bevor die Anwendung bootstrappt:

src/main.ts
import { bootstrapApplication } from '@angular/platform-browser'
import 'tuix-webcomponents'
import { AppComponent } from './app/app.component'
import { appConfig } from './app/app.config'
 
bootstrapApplication(AppComponent, appConfig).catch((error) => console.error(error))

Mit Server-Side Rendering bleibt dieser Import ausserhalb des Server-Bundles, denn die Elemente erweitern HTMLElement, das der Server nicht hat:

src/app/app.component.ts
import { Component, inject, PLATFORM_ID } from '@angular/core'
import { isPlatformBrowser } from '@angular/common'
 
@Component({/* … */})
export class AppComponent {
    constructor() {
        if (isPlatformBrowser(inject(PLATFORM_ID))) {
            void import('tuix-webcomponents')
        }
    }
}

3. Tags erlauben

src/app/invoice-form.component.ts
import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core'
 
@Component({
    selector: 'app-invoice-form',
    standalone: true,
    schemas: [CUSTOM_ELEMENTS_SCHEMA],
    templateUrl: './invoice-form.component.html',
})
export class InvoiceFormComponent {}

Ohne das Schema meldet Angular 'tuix-button' is not a known element.

4. Verwenden

[prop] setzt eine DOM-Property, Objekte und Arrays funktionieren also unverändert, und (event) hört auf genau diesen Namen, auch auf camelCase:

src/app/invoice-form.component.html
<tuix-input
    title="Firmenname"
    [value]="name"
    [iconRight]="IconName.BUILDING"
    [description]="error"
    [descriptionType]="error ? DescriptionType.ERROR : DescriptionType.TEXT"
    (on-change)="onNameChange($event)"
></tuix-input>
 
<tuix-dropdown title="Land" [options]="countries" [value]="selected" (select)="onSelect($event)"></tuix-dropdown>
 
<tuix-button [variant]="MainVariant.PRIMARY" [iconLeft]="IconName.SAVE" (customClick)="save()">Speichern</tuix-button>
import { Component } from '@angular/core'
import { DescriptionType, IconName, MainVariant } from 'tuix-webcomponents'
 
@Component({/* … */})
export class InvoiceFormComponent {
    readonly DescriptionType = DescriptionType
    readonly IconName = IconName
    readonly MainVariant = MainVariant
 
    readonly countries = [
        { name: 'Schweiz', value: 'CH' },
        { name: 'Deutschland', value: 'DE' },
    ]
 
    name = ''
    country?: string
    error?: string
 
    get selected() {
        return this.countries.find((option) => option.value === this.country)
    }
 
    onNameChange(event: Event) {
        this.name = (event as CustomEvent<{ value: string }>).detail.value
        this.error = undefined
    }
 
    onSelect(event: Event) {
        this.country = String((event as CustomEvent<{ value: string }>).detail.value)
    }
 
    save() {
        if (!this.name) {
            this.error = 'Ein Firmenname ist erforderlich'
            return
        }
 
        console.log(this.name, this.country)
    }
}

Enums im Template brauchen ein Feld (readonly IconName = IconName) oder werden als einfache Strings geschrieben: iconright="building".

5. Formulare

Die Elemente sind keine Form Controls, formControlName braucht deshalb einen ControlValueAccessor. Eine kleine Direktive pro Elementtyp genügt und ist in der ganzen Anwendung wiederverwendbar:

src/app/tuix/tuix-input.value-accessor.ts
import { Directive, ElementRef, forwardRef, inject } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
 
type TuixInputElement = HTMLElement & { value: string; disabled: boolean }
 
@Directive({
    selector: 'tuix-input[formControl], tuix-input[formControlName], tuix-input[ngModel]',
    standalone: true,
    host: {
        '(on-change)': 'onInput($event)',
        '(on-blur)': 'onTouched()',
    },
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => TuixInputValueAccessor),
            multi: true,
        },
    ],
})
export class TuixInputValueAccessor implements ControlValueAccessor {
    private readonly element = inject(ElementRef<TuixInputElement>).nativeElement
    private onChange: (value: string) => void = () => {}
 
    onTouched: () => void = () => {}
 
    writeValue(value: string | null) {
        this.element.value = value ?? ''
    }
 
    registerOnChange(fn: (value: string) => void) {
        this.onChange = fn
    }
 
    registerOnTouched(fn: () => void) {
        this.onTouched = fn
    }
 
    setDisabledState(isDisabled: boolean) {
        this.element.disabled = isDisabled
    }
 
    onInput(event: Event) {
        this.onChange((event as CustomEvent<{ value: string }>).detail.value)
    }
}

Mit der Direktive in der Komponente verhält sich das Element wie jedes andere Control:

<form [formGroup]="form">
    <tuix-input title="Firmenname" formControlName="name"></tuix-input>
    <tuix-checkbox label="Per E-Mail senden" formControlName="byEmail"></tuix-checkbox>
</form>

Für Checkbox und Toggle gilt dieselbe Form der Direktive, mit (change) als Host-Listener und checked als Property.

6. Change Detection

Die Events sind gewöhnliche DOM-Events, die Zone von Angular bekommt sie also mit, und ein Template, das an das aktualisierte Feld gebunden ist, rendert von selbst neu. Mit OnPush und einem Signal oder der async-Pipe braucht es ebenfalls nichts Zusätzliches. Nur ein in ngZone.runOutsideAngular registrierter Listener muss mit ngZone.run() zurückkehren.

Fehlersuche

SymptomUrsache
'tuix-button' is not a known elementCUSTOM_ELEMENTS_SCHEMA fehlt in der Komponente oder im Modul
HTMLElement is not defined auf dem ServerDie Bibliothek wird in Code importiert, der beim SSR läuft; mit isPlatformBrowser absichern
Can't bind to 'options'…Das Schema fehlt; mit ihm werden unbekannte Bindings als DOM-Properties gesetzt
Das Control bleibt bei formControlName leerKein ControlValueAccessor; die Direktive aus Schritt 5 ergänzen
IconName is not defined im TemplateEnums müssen als Feld der Komponente bereitgestellt werden

Auf dieser Seite