Schritt-für-Schritt-Anleitung basierend auf dem Skeleton Blueprint.
- Auflistungen werden immer alphabetisch sortiert
- Die ARC42.md ist die führende Architektur-Spezifikation — lies sie vollständig, bevor du eine neue Komponente erstellst
- Alle Web Components verwenden
shadow: true— Komponenten ohne Shadow DOM werden als Functional Components implementiert - Props leben in
src/internal/props/mit eigenemPropDefinitionpro Prop - Kein toter Code, keine Barrel-Files, keine
types.ts
| Schritt | Kurzbeschreibung |
|---|---|
| 0 | Projekt starten |
| 1 | Tag-Name in Stencil-Konfiguration registrieren |
| 2 | Props erstellen oder vorhandene wiederverwenden (src/internal/props/) |
| 3 | API-Definition erstellen (api.tsx) mit PropsConfigShape und ApiFromConfig |
| 4 | Controller implementieren (controller.ts) — erweitert BaseController<Api> |
| 5 | Functional Component erstellen (component.tsx) — stateless Renderer |
| 6 | Web Component erstellen (component.tsx) — erweitert BaseWebComponent<Api> |
| 7 | Tests co-lokalisiert neben component.tsx erstellen |
| 8 | Beispiel in React-Sample-App anlegen |
| 9 | Validierung: pnpm format && pnpm lint && pnpm --filter @public-ui/components test:unit |
Projekt starten, wie in Contribution beschrieben.
Den Tag-Namen der neuen Komponente in packages/components/stencil.config.ts registrieren.
Bevor die Komponente implementiert wird, müssen alle Props definiert sein.
Pro Prop eine Datei unter src/internal/props/:
// src/internal/props/name.ts
import type { SimpleProp } from './helpers/factory';
import { createPropDefinition } from './helpers/factory';
import { normalizeString } from './helpers/normalizers';
export type NameProp = SimpleProp<'name', string>;
export const nameProp = createPropDefinition<NameProp>('name', '', normalizeString);SimpleProp<K, T>wenn externer und interner Typ identisch sindProp<K, TExternal, TInternal>wenn sich die Typen unterscheiden (z.B.ColorProp)- Export in
src/internal/props/index.tshinzufügen - Bestehende Props aus
index.tswiederverwenden, wenn möglich
Details: ARC42 §4 — Schema Helper Layer
Datei: src/internal/functional-components/<component>/api.tsx
import { nameProp } from '../../props';
import type { ApiFromConfig, PropsConfigShape } from '../generic-types';
export const myComponentPropsConfig = {
required: [nameProp],
// optional: [showProp],
} as const satisfies PropsConfigShape;
export type MyComponentApi = ApiFromConfig<
typeof myComponentPropsConfig,
{
// Nur definieren, was die Komponente tatsächlich nutzt:
// Callbacks: { click: () => void };
// Emitters: { change: string };
// Methods: { focus: () => void };
// States: { count: number };
// Refs: { button: HTMLButtonElement };
// Listeners: { keydown: KeyboardEvent };
}
>;Details: ARC42 §4 — API Definition with PropsConfigShape
Datei: src/internal/functional-components/<component>/controller.ts
import { nameProp } from '../../props';
import { BaseController } from '../base-controller';
import type { ControllerInterface, ResolvedInputProps, StateAccess } from '../generic-types';
import type { MyComponentApi } from './api';
import { myComponentPropsConfig } from './api';
export class MyComponentController extends BaseController<MyComponentApi> implements ControllerInterface<MyComponentApi> {
public constructor(stateAccess: StateAccess<MyComponentApi>) {
super(stateAccess, myComponentPropsConfig);
}
public componentWillLoad(props: ResolvedInputProps<MyComponentApi>): void {
const { name } = props;
this.watchName(name);
}
public watchName(value?: string): void {
nameProp.apply(value, (v) => {
this.setRenderProp('name', v);
});
}
}- Event-Handler und Ref-Setter als Arrow-Properties (
handleClick = () => { … }) - Lifecycle- und Watcher-Methoden als Prototype-Methoden
- Details: ARC42 §4 — Controller Layer
Datei: src/internal/functional-components/<component>/component.tsx
import type { FunctionalComponent as FC } from '@stencil/core';
import { h } from '@stencil/core';
import { bem } from '../../../schema/bem-registry';
import type { FunctionalComponentProps } from '../generic-types';
import type { MyComponentApi } from './api';
const myBem = bem.forBlock('kol-my-component');
export const MyComponentFC: FC<FunctionalComponentProps<MyComponentApi>> = (props) => {
const { name } = props;
return (
<div class={myBem()}>
<span class={myBem('name')}>{name}</span>
</div>
);
};- Stateless, keine Seiteneffekte
- Details: ARC42 §4 — Functional Component Layer
Datei: src/components/<component>/component.tsx
import type { JSX } from '@stencil/core';
import { Component, h, Host, Prop, Watch } from '@stencil/core';
import { BaseWebComponent } from '../../../../internal/functional-components/base-web-component';
import type { WebComponentInterface } from '../../../../internal/functional-components/generic-types';
import type { MyComponentApi } from '../../../../internal/functional-components/my-component/api';
import { MyComponentFC } from '../../../../internal/functional-components/my-component/component';
import { MyComponentController } from '../../../../internal/functional-components/my-component/controller';
@Component({
tag: 'kol-my-component',
shadow: true,
})
export class KolMyComponent extends BaseWebComponent<MyComponentApi> implements WebComponentInterface<MyComponentApi> {
private readonly ctrl = new MyComponentController(this.stateAccess);
@Prop()
public _name!: string;
@Watch('_name')
public watchName(value?: string): void {
this.ctrl.watchName(value);
}
public componentWillLoad(): void {
this.ctrl.componentWillLoad({
name: this._name,
});
}
public render(): JSX.Element {
return (
<Host>
<MyComponentFC name={this.ctrl.getRenderProp('name')} />
</Host>
);
}
}- Immer
shadow: trueund<Host>ohne Klassen-Attribut @Watchnur auf unterstrichene Props- Details: ARC42 §4 — Web Component Layer
Wenn ein Controller garantiert kein @State benötigt, verwende den Sentinel:
private readonly ctrl = new MyComponentController(BaseWebComponent.stateLess);Tests liegen direkt neben component.tsx — kein test/-Unterordner.
Snapshot-Test (snapshot.spec.tsx):
import { executeSnapshotTests } from '../../../../utils/testing';
import { KolMyComponent } from './component';
const TAG = 'kol-my-component';
type Props = {
_name: string;
};
executeSnapshotTests<Props>(TAG, [KolMyComponent], [{ _name: 'Test' }, { _name: '' }]);Interaction-Test (interaction.e2e.ts):
import { expect } from '@playwright/test';
import { test } from '@stencil/playwright';
test.describe('kol-my-component', () => {
test.beforeEach(async ({ page }) => {
await page.setContent('<kol-my-component _name="Test"></kol-my-component>');
});
test('should render the name', async ({ page }) => {
await expect(page.locator('kol-my-component')).toBeVisible();
});
});Details: ARC42 §9 — Design Decision 11
Datei: packages/samples/react/src/scenarios/<component>.tsx
Anschließend die Route in packages/samples/react/src/scenarios/routes.ts registrieren.
Zum Testen:
cd packages/samples/react
pnpm start
# Navigiere zu http://localhost:9191pnpm format # ~10 Sekunden
pnpm lint # ~1 Minute, NICHT abbrechen
pnpm --filter @public-ui/components test:unit # ~2-3 Minuten, NICHT abbrechenAlle drei Befehle müssen fehlerfrei durchlaufen.
Die vollständige Referenzimplementierung findet sich im Skeleton Blueprint:
- Architektur:
_skeleton/ARC42.md - Agent-Instruktionen:
_skeleton/AGENTS.md - Performance-Analyse:
_skeleton/PERFORMANCE_ANALYSIS.md - Refactoring-Leitfaden:
_skeleton/REFACTORING_PROMPT.md