dcr-components: Richtlijnen voor ontwikkelaars
Kernfilosofie: Waarom we doen wat we doen
Onze leidende principes:
- Duidelijkheid boven slimheid: Code moet makkelijk te lezen en te begrijpen zijn.
- Consistentie is cruciaal: Uniforme code maakt samenwerken vlotter.
- Performantie telt: Onze componenten moeten licht en snel zijn.
- Ontwikkelaarsplezier: We willen dat je graag met
dcr-componentswerkt!
Mappenstructuur
- Opbouw van bestanden voor een component:
libs/
└── dcr-components/
└── src/
└── lib/
└── button/
├── dcr-button.ts # Componentdefinitie
├── dcr-button.styles.scss # Styles
├── dcr-button.styles.ts # Automatisch gegenereerd
├── dcr-button.test.ts # Unit tests
├── index.ts # Barrelfile
└── stories/
└── dcr-button.stories.ts # Storybook stories
└── dcr-button.stories.base.ts # Automatisch gegenereerd- Gedeelde logica komt onder _shared
libs/
└── dcr-components/
└── src/
└── lib/
└── _shared/
├── aria # A11Y logica
├── const
├── docs # oa Gedeelde icons om te gebruiken in stories
├── events # Logica over event handling
├── utils
└── controllers # Gedeelde logica voor gebruik in componentenNaamgevingsconventies
Duidelijke namen besparen tijd (en debugging!).
In component ts files
| casing | prefix | Voorbeeld | |
|---|---|---|---|
| Class | PascalCase | start met Dcr | DcrButton |
| Tag | kebab-case | start met dcr- | dcr-button |
| Public Variable | camelCase | public label = '' | |
| Private variable | camelCase | start met _ | private _isLoading = false |
| Events | kebab-case | this.dispatchEvent(new CustomEvent('value-change')) |
In component scss/css files
| casing | prefix | Voorbeeld | |
|---|---|---|---|
| Class Selector | kebab-case | . | .container |
| Attribute Selector | kebab-case | [] | [range="low"] |
| Public Custom Property | kebab-case | --dcr-comp-<component-name>- | --dcr-comp-badge-bg-color |
| Private Custom Property | kebab-case | --_ | --_error-color |
Anatomie van een dcr-component
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import styles from './dcr-component-naam.styles.js';
@customElement('dcr-component-naam')
export class DcrComponentNaam extends LitElement {
static override styles = [
styles,
css`
/* Component-specifieke overrides */
`,
];
@property()
greeting = 'Hallo';
@property()
name = 'Wereld';
override render() {
return html` <p>${this.greeting}, ${this.name}!</p> `;
}
Lifecycle methods, event handlers, private methods etc.
_privateMethod() { ... }
}Volgorde properties
Om de structuur van onze componentbestanden consistent en voorspelbaar te houden, hanteren we de volgende vaste volgorde voor de declaratie van types, properties en methods:
Union types: Exporteer union types direct na de import-statements bovenaan het bestand.
staticoverrides:- Plaats
staticoverrides van LitElement-eigenschappen (bv.shadowRootOptions,styles) als eerste in de class.
typescript// Voorbeeld static override shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, delegatesFocus: true, }; static override styles = [styles]; // Waar 'styles' geïmporteerd is- Plaats
Publieke
@propertydeclaraties: Gevolgd door alle met@propertygedecoreerde publieke properties.Publieke getters/setters: Definieer publieke getters en setters na de
@propertydeclaraties.@statedeclaraties: Plaats met@stategedecoreerde private reactive properties hierna.Private instance fields: Declareer alle overige private instance fields (niet-reactive state).
Lifecycle methods: Implementeer lifecycle methods in de exacte volgorde waarin ze door Lit worden aangeroepen: Zie: Lit Lifecycle
constructor()connectedCallback()shouldUpdate(changedProperties)willUpdate(changedProperties)render()firstUpdated(changedProperties)updated(changedProperties)disconnectedCallback()
Publieke methods: Definieer alle overige publieke methods van het component.
Private methods: Implementeer alle private helper methods die geen render-logica bevatten.
Private
_render<Part>()functies: Groepeer alle private methods die delen van de template renderen (bv._renderHeader(),_renderItem(item)) aan het einde van de klasse.
Properties
- Gebruik de
@property()decorator voor reactieve properties. - Specifieer
type(bv.Number,Boolean,Array,Object). - Specifieer geen
typevoorString, die is default. - Geef string-properies altijd een expliciete
''bij de declaratie (bv.myString = '';), tenzijundefinedeen expliciet gewenste initiële staat is. - Geef boolean-properies altijd een expliciete
falsebij de declaratie (bv.myBool = false;), tenzijundefinedeen expliciet gewenste initiële staat is. - Gebruik de non-null assertion operator (
!) alleen als je absoluut zeker bent dat een property op dat punt nietnullofundefinedis en TypeScript dit niet kan afleiden - Gebruik
?in TypeScript-definities (myProp?: string;) voor properties die logischerwijs optioneel zijn voor de componentgebruiker - Gebruik
attribute: falseals de property niet als HTML-attribuut gereflecteerd moet worden. - Overweeg
reflect: truezorgvuldig – enkel voor properties die echt gereflecteerd moeten worden.
Types / Interfaces
Complexe interfaces worden zoveel mogelijk vermeden. HTML-attributen zijn by design string. Hoe minder er geconverteerd moet worden, hoe beter. De complexiteit om iets te stringifyen ligt zoveel mogelijk buiten de verantwoordelijkheid van de component. Eenvoudige types zoals
Number,BooleanenArraykunnen wel worden omgezet. Vermijd echter het gebruik van JSON-configuraties om via attributen door te geven.Voor custom types, gebruik een union type die je in dezelfde file exporteert.
De type wordt gedefinieerd vóór de klasse.
typeScriptexport type MyUnionType = 'this' | 'that'; @customElement('dcr-comp') export class DcrComp { @property() myProp: MyUnionType = 'this'; }
Events
Naamgeving:
- Gebruik kebab-case (bv.
value-change). - Gebruik voor het werkwoord in eventnamen de tegenwoordige tijd (onvoltooid tegenwoordige tijd) of de basisvorm van het werkwoord, in lijn met standaard HTML-events.
- Voorbeeld:
select,change,load,open,close,submit.
- Voorbeeld:
- Voor events die procesvoltooiing of het bereiken van een eindstaat signaleren: Gebruik een naam die voltooiing reflecteert (bv. met een voltooid deelwoord, of termen als
end,complete,loaded.- Voorbeeld:
data-loaded,animation-ended,dialog-closed).
- Voorbeeld:
- Gebruik kebab-case (bv.
Versturen:
- Gebruik
this.dispatchEvent(new CustomEvent('jouw-event-naam', options)).
- Gebruik
Data (
detail):- Gebruik de
detailproperty voor event-data. - Definieer een TypeScript-interface voor de structuur van
detail.typescript// Voorbeeld interface export interface DcrItemSelectedEventDetail { id: string; value: unknown; } // Versturen met type new CustomEvent<DcrItemSelectedEventDetail>('dcr-item-selected', { detail: { ... } })
- Gebruik de
Event Opties (
bubbles,composed,cancelable):bubbles: false(standaard): Overweegtruespaarzaam, voor events van algemeen belang voor voorouders.composed: false(standaard): Zet naartrueals het event buiten de Shadow DOM ontvangen moet worden (meestal gewenst).cancelable: false(standaard): Overweegtruealleen als er een duidelijke, annuleerbare standaardactie is.
Her-uitsturen van native events (Re-dispatching) Gebruik de
redispatchEvent(this, event)utility om een native event van een intern element opnieuw uit te sturen vanaf de host van het component, volgens deze regels:A. Wanneer
redispatchEventgebruiken:Als een intern, native event extern relevant is en niet van nature correct door de Shadow DOM-grens bubbelt en/of gecomponeerd wordt (bv.
focus,blur,load, vele media-events).Als je de propagatie van een bubbelend event specifiek wilt controleren (zoals de
redispatchEventutility doet doorstopPropagation()onder bepaalde condities aan te roepen) en een exacte kopie van het event wilt her-uitsturen.
B. Wanneer
redispatchEventNIET gebruiken: - Als het originele native event al van nature correct door de Shadow DOM-grens bubbelt én gecomponeerd wordt (bv. de meeste UI-events zoalsclickop een standaard element) en je geen specifieke propagatiecontrole of event-modificatie nodig hebt dieredispatchEventbiedt.Documentatie (JSDoc):
- Documenteer met
@fires event-naam - Beschrijving. - Beschrijf de
detailstructuur (verwijs naar de interface).
typescript/** * @fires dcr-item-selected - Item geselecteerd. Detail: `DcrItemSelectedEventDetail`. */- Documenteer met
Rendering
- Houd ze beknopt en leesbaar.
- Breek complexe templates op in kleinere, private render methods indien nodig (bv.
_renderHeader(),_renderFooter()).
Styling
BEM wordt niet gebruikt.
Shadow DOM & Scoping:
- Styles zijn standaard gescoped dankzij Shadow DOM; maak hier volledig gebruik van.
- Houd classnames zo beknopt mogelijk; ze zijn lokaal binnen het component.
Bestandsstructuur & Generatie:
- Alle component-specifieke styles worden geschreven in
*.styles.scss. - Het corresponderende TypeScript-stijlenbestand (
*.styles.ts) wordt automatisch gegenereerd.
- Alle component-specifieke styles worden geschreven in
CSS Custom Properties (Variabelen):
- Definieer component-specifieke CSS custom properties in
:hostvoor externe aanpasbaarheid, met een fallback naar een design token:var(--dcr-comp-<componentnaam>-<propertynaam>, cds.get('cds.sys.fallback')); - Alleen deze CSS custom properties zijn toegankelijk van buiten de Shadow DOM.
- Definieer component-specifieke CSS custom properties in
Design Tokens (CDS):
- Gebruik altijd design tokens via
@use '@diekeure/cds-tokens' as cds;. - Voorbeeld padding:
padding: cds.get('cds.sys.spacing.3xs'); - Voorbeeld fonts:
@include cds.font('label.large'); - Voorbeeld z-index:
@include cds.zindex('content'); - Volg de design tokens zoals gespecificeerd in de Figma-ontwerpen.
- Gebruik altijd design tokens via
Selectors & Specificiteit:
- Geef de voorkeur aan attribuutselectors op
:hostvoor state-styling (bv.:host([invalid]) { /* styles */ }). - Style de inhoud van slots spaarzaam met
::slotted(<selector>); houd de selectors binnen::slotted()eenvoudig. - Gebruik zo weinig mogelijk
::part().
- Geef de voorkeur aan attribuutselectors op
Layout & Properties:
- Gebruik altijd logical properties (bv.
padding-inline-starti.p.v.padding-left). - Definieer een
display-waarde voor:host(bv.display: block;ofdisplay: inline-flex;). - Pas
box-sizing: border-box;toe waar nodig, bij voorkeur op:host.
- Gebruik altijd logical properties (bv.
Efficiëntie & Leesbaarheid (SCSS):
- Schrijf beknopte, efficiënte SCSS. Elke byte telt.
- Structureer SCSS-bestanden met commentaarblokken voor grote secties.
- Volg de volgorde van elementen in de template waar mogelijk. Plaats uitzonderingen/overrides aan het einde.
- Gebruik
inheritvoor CSS properties waar zinvol.
Performance (Transities & Animaties):
- Test transities en animaties grondig op performantie.
- Specificeer geanimeerde properties expliciet; gebruik nooit
transition: all;. - Voor
box-shadowtransities/animaties: zorg dat het element met debox-shadowop een aparte compositing layer draait (bv. viawill-change: box-shadow;of een minimaletransform) en geen andere geanimeerde content bevat. - Ga spaarzaam om met
transform-operaties.
Focus & Borders:
- Indien een custom focus-ring wordt geïmplementeerd, zet
outline: none;op het gefocuste element. - Geef de voorkeur aan
borderbovenbox-shadowhacks voor het simuleren van randen.
- Indien een custom focus-ring wordt geïmplementeerd, zet
Theming & Interactie met andere componenten:
- Test componenten in light, dark, en high-contrast modes.
- Overschrijf styles van geneste (child)
dcr-componentsuitsluitend via hun geëxposeerde CSS custom properties.
Codekwaliteit & Standaarden:
- Gebruik nooit
!important. - Gebruik geen utility class names; geef blokken een korte, beschrijvende classnaam.
- Gebruik geen experimentele CSS-properties zonder brede browserondersteuning (verifieer via
caniuse.com). - Deel geen stylesheets tussen componenten, tenzij ze een zeer sterke functionele koppeling hebben (bv. button-varianten die een gemeenschappelijke basis delen).
- Gebruik nooit
- Semantische HTML: Gebruik de juiste HTML-tags (
<button>,<nav>,<input type="checkbox">, etc.). - ARIA-attributen: Gebruik ARIA-attributen waar nodig om extra context te bieden voor hulptechnologieën.
- Toetsenbordnavigatie: Zorg ervoor dat alle interactieve elementen focusseerbaar en bedienbaar zijn via het toetsenbord.
Testen
Hoewel unit tests momenteel nog niet volledig geïmplementeerd zijn, is het verplicht om componenten grondig te testen. We leggen een sterke nadruk op testen via Storybook voor UI, interactie, toegankelijkheid en visuele regressie.
Storybook als centrale testomgeving:
- Variantendekking: Voor elke variant gedefinieerd in Figma MOET een corresponderende Storybook story aangemaakt worden.
- Property-invloed: Elke wezenlijke UI-verandering die optreedt door het aanpassen van component-properties MOET gedemonstreerd worden in een aparte Storybook story of via controls in bestaande stories.
- Interactietests: Elke nuttige gebruikersinteractie (bv. klikken, typen, hover-effecten die state veranderen) MOET getest worden met een
playfunctie in Storybook. - Toegankelijkheidstests (A11Y): De ingebouwde A11Y-tests in Storybook MOETEN altijd slagen voor alle stories. Geen enkele A11Y-violatie wordt geaccepteerd.
- Visuele Regressietests: Alle stories worden gebruikt voor visuele regressietests via Chromatic. Zorg ervoor dat stories representatief zijn voor alle visuele staten.
Unit Tests:
- Het is een vereiste dat elk component op termijn unit tests krijgt.
- Deze tests moeten focussen op:
- Correcte rendering met verschillende properties.
- Correcte emissie van events.
- Functionele correctheid van property-updates.
- Valideren van interne logica en state-transities.
Testuitvoering:
- Voer alle relevante tests (inclusief Storybook interaction tests) uit met het commando:
npx nx test dcr-components
- Voer alle relevante tests (inclusief Storybook interaction tests) uit met het commando:
Algemene Testprincipes:
- Streef naar een zo hoog mogelijke en zinvolle testdekking. Kwaliteit en relevantie van tests primeren boven louter kwantiteit, maar een brede dekking van functionaliteit en UI-staten is cruciaal.
Formattering & linting
- Prettier & ESLint zijn je beste vrienden! We hebben voorgeconfigureerde instellingen.
- Maximale regellengte: 120 karakters
Toegankelijkheid (a11y)
Commentaar & documentatie
Heldere documentatie is cruciaal. Volg deze richtlijnen voor JSDoc en andere commentaren, geïnspireerd op de structuur van DcrBadge:
Componentklasse JSDoc (boven
@customElement):- Algemene beschrijving: Start met een duidelijke samenvatting van het doel en de functionaliteit van het component.
- Gedetailleerde secties: Gebruik Markdown headings (bv.
## Functionaliteit X) binnen JSDoc voor diepgaandere uitleg over specifieke aspecten of features (zoals## Status Types). - Belangrijke notities: Gebruik iconen ( ⚠️ Note:) voor waarschuwingen of essentiële aandachtspunten.
- Externe links: Integreer links naar Figma, Storybook, of andere relevante documentatie met
{@link https://... Figma Prototype}of{@link https://... Storybook}.
Publieke API JSDoc (Properties & Methods):
@property: Elke publieke property MOET JSDoc hebben die het doel, het effect, en eventuele mogelijke waarden of types beschrijft.typescript/** * The numerical value to display in the badge. * When undefined, no count will be shown. */ @property({ type: Number }) count?: number;- interne properties Gebruik
@internalin de JSDoc om properties als intern te beschrijven. Deze worden uit Storybook documentatie gehouden. - Publieke methods: Elke publieke methode MOET JSDoc hebben die beschrijft wat de methode doet, de parameters (
@param), en de returnwaarde (@returns).
Geëxporteerde Types (bv.
export type StatusType = ...):- Zorg voor duidelijke, zelfdocumenterende namen. De klasse-JSDoc kan hiernaar verwijzen voor context (zie
## Status Typesin het voorbeeld).
- Zorg voor duidelijke, zelfdocumenterende namen. De klasse-JSDoc kan hiernaar verwijzen voor context (zie
Inline commentaar:
- Gebruik beknopt inline commentaar binnen methodes om alleen complexe, niet-direct evidente logica of belangrijke algoritme-stappen te verduidelijken.
Code organisatie
Deze regels zorgen voor een consistente publicatie en een heldere versiegeschiedenis van dcr-components.
Component Export via Barrel Files:
- Elk nieuw component MOET via een
index.ts(barrel file) in zijn eigen directory geëxporteerd worden. - Vervolgens MOET het component vanuit de
src/index.tshoofd-barrel file geëxporteerd worden, zodat het correct wordt opgenomen in dedistoutput en als een JS-module beschikbaar is.
- Elk nieuw component MOET via een
package.jsonExports:- Voor elk component MOET een expliciete export-entry worden toegevoegd aan
package.jsononder deexportskey. Volg de onderstaande structuur:json{ "exports": { "./dcr-app-shell.js": { // Gebruik de .js extensie voor de publieke API "types": "./dist/lib/app-shell/dcr-app-shell.d.ts", "import": "./dist/lib/app-shell/dcr-app-shell.js", "default": "./dist/lib/app-shell/dcr-app-shell.js" // Kan hetzelfde zijn als import } // ... andere componenten } } - Vervang
dcr-app-shellen de paden met de correcte namen en locaties voor elk component. De mapnaam onderdist/lib/moet overeenkomen met de componentnaam.
- Voor elk component MOET een expliciete export-entry worden toegevoegd aan
Nieuwe componenten aanmaken: Het create-component script
We hebben een script om een nieuw component voor je te genereren!
npx nx run tools:create-component --name mijn-nieuwe-component- Dit script zal de basis mappenstructuur en bestanden voor je aanmaken.
Ontwikkelingsworkflow
- Maak een nieuwe branch aan vanuit
develop - Start de Storybook dev server voor componentontwikkeling en -visualisatie:
npx nx serve dcr-components. - Implementeer je component, inclusief styles, stories, en documentatie.
- Testen in
ng-client(indien van toepassing):- Bouw de
dcr-componentsbibliotheek:npx nx build dcr-components - Integreer en test het component binnen de
ng-clientapplicatie om de samenwerking met de host-applicatie te valideren. (npx nx serve ng-client)
- Bouw de
- Voer alle relevante tests uit (Storybook interactie- en A11Y-tests, visuele regressie via Chromatic):
npx nx test dcr-components - Commit je wijzigingen volgens de commit conventies (één
featvoor nieuwe componenten,fixvoor bugfixes na release). - Push je wijzigingen en maak een Pull Request.
Commits
Nieuwe componenten: Voor elk nieuw toegevoegd component mag er maximaal één
featcommit worden gebruikt voor de initiële implementatie en integratie. Link via # naar ticketnr.Bugfixes (na eerste release): Na de initiële release van een component, MOET elke bugfix in een aparte
fixcommit worden vastgelegd. Link via # naar ticketnr.Documenatie: Bij het updaten van documentatie, MOET er per logische blok een aparte
docscommit worden vastgelegd.Meer over conventional commits op Aan de slag > Gebruik van convetional commits
Releaseproces
Quick Start
De eenvoudigste manier om een release te maken is via het interactieve script:
./tools/src/release-branch.shWat dit script doet:
- Strikte Validatie: Controleert of de nieuwe branch-naam niet begint met
release/ofdevelop/. Dit voorkomt het per ongeluk overschrijven van beveiligde branches of het ongewenst triggeren van release-pipelines.- Aanbevolen format:
feature/releasing-comp-0.28.0
- Aanbevolen format:
- Project Detectie: Identificeert automatisch welke Nx-projecten wijzigingen bevatten ten opzichte van de
origin/developbranch. - Geautomatiseerd Versiebeheer: Voert
nx releaseuit voor de betreffende projecten. Dit zorgt voor:- Automatische ophoging van versienummers (
package.json). - Genereren van de changelog.
- Aanmaken van lokale Git-tags.
- Automatische ophoging van versienummers (
- Remote Publicatie: Pusht de nieuwe voorbereidings-branch inclusief alle nieuwe tags naar Azure DevOps (
--follow-tags). - Directe Review-link: Opent automatisch je browser op de Azure DevOps pagina om direct een Pull Request (PR) aan te maken van jouw nieuwe branch naar
develop.
De Workflow na het script:
- Review: Een collega controleert de PR in Azure DevOps.
- Merge naar Develop: Na goedkeuring wordt de release officieel onderdeel van de
developbranch. - Handmatige Release: De wijzigingen worden vanuit
develophandmatig gemerged naar de specifieke project-release branches (bijv.release/tokensofrelease/components) wanneer de release live mag.
Alternatief: Direct Nx Release
Je kunt ook direct nx release aanroepen:
# Release voor specifieke projecten
npx nx release --projects=dcr-components
# Release voor alle affected projects
npx nx releaseHandmatige stappen (indien nodig)
- Check de laatste develop branch uit.
- Maak een nieuwe branch
feature/release-<beschrijving>(bv.feature/release-dcr-components-0.28). - Voer het releasecommando uit:
npx nx release. - Op de vraag 'Do you want to publish these versions?' antwoord je
N(no). Daarvoor zorgt onze Azure Devops pipeline. - Nu staat er een
chore(release)commit klaar met changelog en version bump. - Controleer of de changelog en versionering kloppen. Pas ze aan indien nodig.
- Push de changes met tags:
git push --follow-tags. - Maak een PR en merge deze in develop.
- Merge naar de release branch (bv.
release/components). - De pipeline bouwt, test, maakt een npm package en update de publieke Storybook en/of documentatie website.
Release Branches
| Project | Release Branch |
|---|---|
| dcr-components | release/components |
| cds-tokens | release/tokens |
| cds-tokens-manager | release/tokens-manager |
| cds-docs | release/docs |
De grote releasechecklist
- [ ] Alle tests slagen.
- [ ] Linting en formatting zijn OK.
- [ ]
CHANGELOG.mdis automatisch bijgewerkt doornx release. - [ ] Versienummer is correct verhoogd in
package.json. - [ ] Release commit en tags zijn gepusht naar develop.
- [ ] Alle changes zijn gemerged naar release branches.
- [ ] Team werd ingelicht over de release via Slack.