Skip to content

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-components werkt!

Mappenstructuur

  • Opbouw van bestanden voor een component:
bash

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
bash

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 componenten

Naamgevingsconventies

Duidelijke namen besparen tijd (en debugging!).

In component ts files

casingprefixVoorbeeld
ClassPascalCasestart met Dcr DcrButton
Tagkebab-casestart met dcr-dcr-button
Public VariablecamelCasepublic label = ''
Private variablecamelCasestart met _private _isLoading = false
Eventskebab-casethis.dispatchEvent(new CustomEvent('value-change'))

In component scss/css files

casingprefixVoorbeeld
Class Selectorkebab-case..container
Attribute Selectorkebab-case[][range="low"]
Public Custom Propertykebab-case--dcr-comp-<component-name>---dcr-comp-badge-bg-color
Private Custom Propertykebab-case--_--_error-color

Anatomie van een dcr-component

typescript
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:

  1. Union types: Exporteer union types direct na de import-statements bovenaan het bestand.

  2. static overrides:

    • Plaats static overrides 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
  3. Publieke @property declaraties: Gevolgd door alle met @property gedecoreerde publieke properties.

  4. Publieke getters/setters: Definieer publieke getters en setters na de @property declaraties.

  5. @state declaraties: Plaats met @state gedecoreerde private reactive properties hierna.

  6. Private instance fields: Declareer alle overige private instance fields (niet-reactive state).

  7. Lifecycle methods: Implementeer lifecycle methods in de exacte volgorde waarin ze door Lit worden aangeroepen: Zie: Lit Lifecycle

    1. constructor()
    2. connectedCallback()
    3. shouldUpdate(changedProperties)
    4. willUpdate(changedProperties)
    5. render()
    6. firstUpdated(changedProperties)
    7. updated(changedProperties)
    8. disconnectedCallback()
  8. Publieke methods: Definieer alle overige publieke methods van het component.

  9. Private methods: Implementeer alle private helper methods die geen render-logica bevatten.

  10. 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 type voor String, die is default.
  • Geef string-properies altijd een expliciete '' bij de declaratie (bv. myString = '';), tenzij undefined een expliciet gewenste initiële staat is.
  • Geef boolean-properies altijd een expliciete false bij de declaratie (bv. myBool = false;), tenzij undefined een expliciet gewenste initiële staat is.
  • Gebruik de non-null assertion operator (!) alleen als je absoluut zeker bent dat een property op dat punt niet null of undefined is en TypeScript dit niet kan afleiden
  • Gebruik ? in TypeScript-definities (myProp?: string;) voor properties die logischerwijs optioneel zijn voor de componentgebruiker
  • Gebruik attribute: false als de property niet als HTML-attribuut gereflecteerd moet worden.
  • Overweeg reflect: true zorgvuldig – 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, Boolean en Array kunnen 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.

    typeScript
    export type MyUnionType =  'this' | 'that';
    
    
    @customElement('dcr-comp')
    export class DcrComp {
      @property() myProp: MyUnionType = 'this';
    }

Events

  1. 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.
    • 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).
  2. Versturen:

    • Gebruik this.dispatchEvent(new CustomEvent('jouw-event-naam', options)).
  3. Data (detail):

    • Gebruik de detail property 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: { ... } })
  4. Event Opties (bubbles, composed, cancelable):

    • bubbles: false (standaard): Overweeg true spaarzaam, voor events van algemeen belang voor voorouders.

    • composed: false (standaard): Zet naar true als het event buiten de Shadow DOM ontvangen moet worden (meestal gewenst).

    • cancelable: false (standaard): Overweeg true alleen als er een duidelijke, annuleerbare standaardactie is.

    • zie Event compositie en retargeting

  5. 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 redispatchEvent gebruiken:

    • 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 redispatchEvent utility doet door stopPropagation() onder bepaalde condities aan te roepen) en een exacte kopie van het event wilt her-uitsturen.

    • zie Redispatch functie

    B. Wanneer redispatchEvent NIET gebruiken: - Als het originele native event al van nature correct door de Shadow DOM-grens bubbelt én gecomponeerd wordt (bv. de meeste UI-events zoals click op een standaard element) en je geen specifieke propagatiecontrole of event-modificatie nodig hebt die redispatchEvent biedt.

  6. Documentatie (JSDoc):

    • Documenteer met @fires event-naam - Beschrijving.
    • Beschrijf de detail structuur (verwijs naar de interface).
    typescript
    /**
     * @fires dcr-item-selected - Item geselecteerd. Detail: `DcrItemSelectedEventDetail`.
     */

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.

  1. 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.
  2. Bestandsstructuur & Generatie:

    • Alle component-specifieke styles worden geschreven in *.styles.scss.
    • Het corresponderende TypeScript-stijlenbestand (*.styles.ts) wordt automatisch gegenereerd.
  3. CSS Custom Properties (Variabelen):

    • Definieer component-specifieke CSS custom properties in :host voor 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.
  4. 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.
  5. Selectors & Specificiteit:

    • Geef de voorkeur aan attribuutselectors op :host voor 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().
  6. Layout & Properties:

    • Gebruik altijd logical properties (bv. padding-inline-start i.p.v. padding-left).
    • Definieer een display-waarde voor :host (bv. display: block; of display: inline-flex;).
    • Pas box-sizing: border-box; toe waar nodig, bij voorkeur op :host.
  7. 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 inherit voor CSS properties waar zinvol.
  8. Performance (Transities & Animaties):

    • Test transities en animaties grondig op performantie.
    • Specificeer geanimeerde properties expliciet; gebruik nooit transition: all;.
    • Voor box-shadow transities/animaties: zorg dat het element met de box-shadow op een aparte compositing layer draait (bv. via will-change: box-shadow; of een minimale transform) en geen andere geanimeerde content bevat.
    • Ga spaarzaam om met transform-operaties.
  9. Focus & Borders:

    • Indien een custom focus-ring wordt geïmplementeerd, zet outline: none; op het gefocuste element.
    • Geef de voorkeur aan border boven box-shadow hacks voor het simuleren van randen.
  10. Theming & Interactie met andere componenten:

    • Test componenten in light, dark, en high-contrast modes.
    • Overschrijf styles van geneste (child) dcr-components uitsluitend via hun geëxposeerde CSS custom properties.
  11. 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).
  • 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.

  1. 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 play functie 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.
  2. 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.
  3. Testuitvoering:

    • Voer alle relevante tests (inclusief Storybook interaction tests) uit met het commando: npx nx test dcr-components
  4. 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:

  1. 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}.
  2. 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 @internal in 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).
  3. Geëxporteerde Types (bv. export type StatusType = ...):

    • Zorg voor duidelijke, zelfdocumenterende namen. De klasse-JSDoc kan hiernaar verwijzen voor context (zie ## Status Types in het voorbeeld).
  4. 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.

  1. 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.ts hoofd-barrel file geëxporteerd worden, zodat het correct wordt opgenomen in de dist output en als een JS-module beschikbaar is.
  2. package.json Exports:

    • Voor elk component MOET een expliciete export-entry worden toegevoegd aan package.json onder de exports key. 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-shell en de paden met de correcte namen en locaties voor elk component. De mapnaam onder dist/lib/ moet overeenkomen met de componentnaam.

Nieuwe componenten aanmaken: Het create-component script

We hebben een script om een nieuw component voor je te genereren!

bash
npx nx run tools:create-component --name mijn-nieuwe-component
  • Dit script zal de basis mappenstructuur en bestanden voor je aanmaken.

Ontwikkelingsworkflow

  1. Maak een nieuwe branch aan vanuit develop
  2. Start de Storybook dev server voor componentontwikkeling en -visualisatie: npx nx serve dcr-components.
  3. Implementeer je component, inclusief styles, stories, en documentatie.
  4. Testen in ng-client (indien van toepassing):
    • Bouw de dcr-components bibliotheek: npx nx build dcr-components
    • Integreer en test het component binnen de ng-client applicatie om de samenwerking met de host-applicatie te valideren. (npx nx serve ng-client)
  5. Voer alle relevante tests uit (Storybook interactie- en A11Y-tests, visuele regressie via Chromatic): npx nx test dcr-components
  6. Commit je wijzigingen volgens de commit conventies (één feat voor nieuwe componenten, fix voor bugfixes na release).
  7. Push je wijzigingen en maak een Pull Request.

Commits

  • Nieuwe componenten: Voor elk nieuw toegevoegd component mag er maximaal één feat commit 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 fix commit worden vastgelegd. Link via # naar ticketnr.

  • Documenatie: Bij het updaten van documentatie, MOET er per logische blok een aparte docs commit 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:

bash
./tools/src/release-branch.sh

Wat dit script doet:

  1. Strikte Validatie: Controleert of de nieuwe branch-naam niet begint met release/ of develop/. Dit voorkomt het per ongeluk overschrijven van beveiligde branches of het ongewenst triggeren van release-pipelines.
    • Aanbevolen format: feature/releasing-comp-0.28.0
  2. Project Detectie: Identificeert automatisch welke Nx-projecten wijzigingen bevatten ten opzichte van de origin/develop branch.
  3. Geautomatiseerd Versiebeheer: Voert nx release uit voor de betreffende projecten. Dit zorgt voor:
    • Automatische ophoging van versienummers (package.json).
    • Genereren van de changelog.
    • Aanmaken van lokale Git-tags.
  4. Remote Publicatie: Pusht de nieuwe voorbereidings-branch inclusief alle nieuwe tags naar Azure DevOps (--follow-tags).
  5. 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 develop branch.
  • Handmatige Release: De wijzigingen worden vanuit develop handmatig gemerged naar de specifieke project-release branches (bijv. release/tokens of release/components) wanneer de release live mag.

Alternatief: Direct Nx Release

Je kunt ook direct nx release aanroepen:

bash
# Release voor specifieke projecten
npx nx release --projects=dcr-components

# Release voor alle affected projects
npx nx release

Handmatige stappen (indien nodig)

  1. Check de laatste develop branch uit.
  2. Maak een nieuwe branch feature/release-<beschrijving> (bv. feature/release-dcr-components-0.28).
  3. Voer het releasecommando uit: npx nx release.
  4. Op de vraag 'Do you want to publish these versions?' antwoord je N(no). Daarvoor zorgt onze Azure Devops pipeline.
  5. Nu staat er een chore(release) commit klaar met changelog en version bump.
  6. Controleer of de changelog en versionering kloppen. Pas ze aan indien nodig.
  7. Push de changes met tags: git push --follow-tags.
  8. Maak een PR en merge deze in develop.
  9. Merge naar de release branch (bv. release/components).
  10. De pipeline bouwt, test, maakt een npm package en update de publieke Storybook en/of documentatie website.

Release Branches

ProjectRelease Branch
dcr-componentsrelease/components
cds-tokensrelease/tokens
cds-tokens-managerrelease/tokens-manager
cds-docsrelease/docs

De grote releasechecklist

  1. [ ] Alle tests slagen.
  2. [ ] Linting en formatting zijn OK.
  3. [ ] CHANGELOG.md is automatisch bijgewerkt door nx release.
  4. [ ] Versienummer is correct verhoogd in package.json.
  5. [ ] Release commit en tags zijn gepusht naar develop.
  6. [ ] Alle changes zijn gemerged naar release branches.
  7. [ ] Team werd ingelicht over de release via Slack.