React und Next.js

Die Komponenten über die generierten Wrapper in einer React- oder Next.js-Anwendung verwenden.

Das Paket liefert für jedes Element einen React-Wrapper unter tuix-webcomponents/react, erzeugt mit @lit/react. Die Wrapper nehmen die Properties des Elements als Props, Objekte und Arrays gehen also unverändert durch, und aus jedem Custom Event wird eine on*-Prop. Nichts muss von Hand gewrappt werden.

React 18 rendert ein Custom Element, gibt aber jede Prop als String-Attribut weiter und kann keine Custom Events empfangen: genau das lösen die Wrapper. React 19 beherrscht beides von Haus aus; die Wrapper lohnen sich dort weiterhin für die typisierten Props und die on*-Event-Props.

1. Installieren

@lit/react und react sind Peer-Dependencies der Wrapper.

npm i tuix-webcomponents @lit/react

Die Design-Tokens einmal an der Wurzel der Anwendung einbinden. In Next.js ist das app/layout.tsx:

app/layout.tsx
import 'tuix-webcomponents/dist/assets/tuix-styles.css'
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html lang="de">
            <head>
                <link rel="preconnect" href="https://hccdn.tuix.ch" />
            </head>
            <body>{children}</body>
        </html>
    )
}

Dieses Stylesheet holt auch Rubik vom Tuix-CDN, die Typografie ist damit abgedeckt. Wie sich die Schrift früher anfragen lässt, steht unter Theming.

2. Verwenden

Der Import aus tuix-webcomponents/react registriert die Custom Elements, mehr ist nicht nötig. Die Props tragen die Property-Namen (iconRight, nicht iconright):

src/components/InvoiceForm.tsx
'use client'
 
import { useState } from 'react'
import { TuixButton, TuixDropdown, TuixInput } from 'tuix-webcomponents/react'
import { DescriptionType, IconName, MainVariant } from 'tuix-webcomponents'
 
const COUNTRIES = [
    { name: 'Schweiz', value: 'CH' },
    { name: 'Deutschland', value: 'DE' },
]
 
export default function InvoiceForm() {
    const [name, setName] = useState('')
    const [country, setCountry] = useState<string>()
    const [error, setError] = useState<string>()
 
    const save = () => {
        if (!name) return setError('Ein Firmenname ist erforderlich')
        console.log(name, country)
    }
 
    return (
        <>
            <TuixInput
                title="Firmenname"
                value={name}
                iconRight={IconName.BUILDING}
                description={error}
                descriptionType={error ? DescriptionType.ERROR : DescriptionType.TEXT}
                onChange={(event) => {
                    setName(event.detail.value)
                    setError(undefined)
                }}
            />
 
            <TuixDropdown
                title="Land"
                options={COUNTRIES}
                value={COUNTRIES.find((option) => option.value === country)}
                onSelect={(event) => setCountry(String(event.detail.value))}
            />
 
            <TuixButton variant={MainVariant.PRIMARY} iconLeft={IconName.SAVE} onCustomClick={save}>
                Speichern
            </TuixButton>
        </>
    )
}

Aus Sicht von React sind die Komponenten uncontrolled: sie melden eine Änderung und warten auf die neue Prop. Der Wert muss im Handler immer zurückgeschrieben werden, wie oben, sonst wirkt das Feld eingefroren.

Die Props sind aus den Properties des Elements typisiert, und diese Properties tragen die exportierten Enums als Typ. Ein String-Literal ist deshalb nicht zuweisbar: variant={MainVariant.PRIMARY} kompiliert, variant="primary" nicht. Die Enums stehen unter Enums und Typen.

Event-Props

Aus jedem Custom Event wird on plus der Name in PascalCase; ein Event, dessen Name bereits mit on beginnt, behält ein einzelnes Präfix. Der Handler bekommt das CustomEvent, typisiert mit seiner Nutzlast.

ElementPropEventevent.detail
tuix-button, tuix-icon-buttononCustomClickcustomClickKeine
tuix-inputonChange, onBluron-change, on-blur{ value: string }
tuix-inputonClickon-clickKeine
tuix-dropdownonSelectselect{ value: string | number | boolean }
tuix-dropdownonInputSingleinput-single{ value: string }
tuix-checkbox, tuix-toggle-buttononChangechange{ value: boolean, label?: string }
tuix-radio-buttononChangechange{ value: string }
tuix-tableonNextPage, onPreviousPage, onPageSizeChangenext-page, previous-page, page-size-change{ page: number, pageSize: number }
tuix-toast, tuix-modalonCloseon-closeKeine

Neun dieser Props heissen wie eine React-DOM-Prop: onChange, onBlur und onClick an TuixInput, onSelect an TuixDropdown, onChange an Checkbox, Radio-Gruppe und Toggle sowie onClose an Toast und Modal. An einem Wrapper meint die Prop immer das Custom Event aus der Tabelle oben, nie das von React. onClick an einem TuixInput ist das on-click des Elements, kein React-MouseEvent. Wo ein nativer Handler gebraucht wird, gehört er an ein umschliessendes Element.

Der Wrapper heisst wie das Element selbst: TuixButton, TuixIcon, TuixText, TuixPaper, TuixLink, TuixIconButton, TuixInput, TuixDropdown, TuixCheckbox, TuixRadioButton, TuixToggleButton, TuixTable, TuixToast, TuixModal.

3. Besonderheiten von Next.js

Die Seiten als Client-Code kennzeichnen. Custom Elements brauchen ein DOM. 'use client' gehört an den Anfang jeder Komponente, die eines rendert.

Server-Rendering ist unproblematisch. Der Server gibt <tuix-button> samt Kindern aus; das Element upgraded im Browser und schiebt die Kinder in seinen Slot. Nichts wird gemessen oder zufällig erzeugt, die Hydration passt also.

Das Theme vor dem ersten Paint setzen, wenn die Anwendung einen Dark Mode hat, damit die Property dark und die Seite von Anfang an übereinstimmen:

const THEME_INIT = `document.documentElement.setAttribute('data-theme',
    localStorage.getItem('theme') === 'dark' ? 'dark' : 'light')`
 
<script dangerouslySetInnerHTML={{ __html: THEME_INIT }} />

Statischer Export (output: 'export') funktioniert. Importiert eine Seite die Wrapper auf Modulebene und greift etwas darin auf window zu, werden sie besser verzögert geladen:

const TuixTable = dynamic(() => import('tuix-webcomponents/react').then((m) => m.TuixTable), {
    ssr: false,
})

Ohne die Wrapper

Für einen Einzelfall lässt sich das Tag direkt rendern und für TypeScript deklarieren:

src/types/tuix.d.ts
import type React from 'react'
 
declare global {
    namespace JSX {
        interface IntrinsicElements {
            'tuix-text': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
                size?: string
                color?: string
                dark?: boolean
            }
        }
    }
}
import 'tuix-webcomponents' // registriert die Elemente
 
;<tuix-text size="xl" color="primary">
    Hallo
</tuix-text>

Zu beachten: dieser Weg setzt Attribute, also nur kleingeschriebene Namen und weder Objekte noch Arrays noch Event-Handler.

Fehlersuche

SymptomUrsache
ReferenceError: HTMLElement is not definedDie Bibliothek wurde auf dem Server importiert. Import in eine Client-Komponente verschieben oder dynamic(..., { ssr: false })
Cannot find module '@lit/react'@lit/react fehlt; es ist eine Peer-Dependency und wird neben dem Paket installiert
Cannot find module 'tuix-webcomponents/react'Der Resolver liest das Feld exports des Pakets nicht; TypeScript braucht moduleResolution: "bundler"
Type '"primary"' is not assignableDie Prop ist mit einem Enum typisiert. MainVariant.PRIMARY statt des Strings übergeben
Das Feld ändert sich nieDer Wert wird im Change-Handler nicht zurückgeschrieben
onCustomClick feuert nieEs wird das Tag statt des Wrappers verwendet, React hat also keinen Listener registriert
Eine Array- oder Objekt-Prop kommt als [object Object] anEs wird das Tag statt des Wrappers verwendet, React setzt sie also als Attribut
Text erscheint in der falschen SchriftDas Stylesheet mit den Design-Tokens ist nicht eingebunden, und es holt Rubik
Es rendert gar nichtsWeder die Wrapper noch die Bibliothek wurden importiert, das Tag also nie registriert

Auf dieser Seite