Attribuutwaardes in litelement: strings, nummers en meer
Inleiding
Bij het ontwikkelen van custom elements met LitElement (of webcomponenten in het algemeen) is een correct begrip van hoe attribuutwaardes worden doorgegeven en geïnterpreteerd cruciaal. Een veelvoorkomend scenario is een attribuut dat semantisch zowel een string als een numerieke waarde kan vertegenwoordigen, bijvoorbeeld een id of een value. Deze gids duikt diep in hoe HTML en LitElement omgaan met deze types, en biedt best practices voor het bouwen van robuuste en voorspelbare componenten.
Het correct afhandelen van attribuuttypes voorkomt subtiele bugs en zorgt voor een duidelijke API voor de gebruikers van je component. We bekijken de standaardgedragingen, de rol van Lit's property-declaraties met diverse types, en hoe je flexibel omgaat met gemengde en complexe data.
Basisprincipes: html attributen en litelement properties
Html attributen: altijd strings
Het fundamentele uitgangspunt in HTML is dat alle attribuutwaardes inherent strings zijn. Wanneer de browser HTML parseert, interpreteert het elke waarde die aan een attribuut wordt toegewezen als een string, ongeacht of die waarde eruitziet als een getal, JSON, of iets anders.
<!-- alle waardes hieronder worden initieel als strings door de browser gezien -->
<my-component data-value="123"></my-component>
<my-component data-value="product-xyz"></my-component>
<my-component data-options='{"color":"blue","size":"large"}'></my-component>Als je in JavaScript direct een attribuut van een DOM-element zou lezen (zonder een framework zoals Lit), zou je altijd een string terugkrijgen (of null als het attribuut niet bestaat):
const el = document.querySelector('my-component');
const attrValue = el.getAttribute('data-options');
console.log(typeof attrValue); // "string"
console.log(attrValue); // '{"color":"blue","size":"large"}'Litelement properties en typeconversie
LitElement biedt een krachtig mechanisme om HTML-attributen te koppelen aan JavaScript class properties via de @property decorator. Hierbij kun je een type specificeren. LitElement gebruikt dit type om een converter te bepalen die verantwoordelijk is voor het omzetten van de stringwaarde van een attribuut naar het gewenste property type, en vice versa als reflect: true is ingesteld.
Standaardgedrag zonder expliciet type
Als je een property declareert zonder expliciet type (of met type: String), en deze is gekoppeld aan een attribuut, zal LitElement de stringwaarde van het attribuut direct toekennen aan de property.
import { LitElement, html } from 'lit';
import { property } from 'lit/decorators.js';
class MyComponent extends LitElement {
@property({ attribute: 'data-value' }) // standaard type is String
dataValue;
render() {
return html`Waarde: ${this.dataValue} (type: ${typeof this.dataValue})`;
}
}
customElements.define('my-component', MyComponent);
// gebruik: <my-component data-value="123"></my-component>
// output in component: Waarde: 123 (type: string)Conventies voor property- en attribuutdeclaratie
Property names worden in camelCase geschreven. Indien een camelCase propertynaam een hoofdletter bevat (bv. myProp), wordt doorgaans een corresponderend attribuut in kebab-case gespecificeerd:
@property({ attribute: 'my-prop' }) myProp;Voor properties met een enkele naam zonder interne hoofdletters (bv. data) volstaat vaak @property() data;, waarbij de attribuutnaam impliciet gelijk is aan de propertynaam (dus data wordt data).
Waarom kebab-case voor html attributen (maar niet altijd)?
Wanneer je werkt met Lit-elementen, Angular-componenten of gewoon standaard HTML, kom je vroeg of laat in aanraking met de vraag: Gebruik ik nu kebab-case, camelCase, of gewoon lowercase voor mijn attributen?
Het korte antwoord: ➡️ Gebruik in HTML altijd kebab-case voor custom attributes (zoals die van webcomponents). ➡️ Gebruik in JavaScript camelCase voor properties. ➡️ Maar voor native HTML attributen zoals readonly, required of autofocus, is het juist lowercase zonder streepjes.
1. Native html attributen gebruiken lowercase
Standaard HTML attributen zoals deze:
<input type="text" readonly autofocus />... zijn niet in kebab-case, maar in simpele lowercase.
| HTML attribuut | Type | Opmerkingen |
|---|---|---|
readonly | boolean | Geen waarde nodig (readonly is genoeg) |
maxlength | string/number | Eén woord, zonder streepjes |
autofocus | boolean | Wordt herkend door browsers |
➡️ Deze komen uit HTML zelf, en volgen oude HTML-specificaties. Geen dashes nodig, geen camelCase.
2. Kebab-case wordt gebruikt voor custom elements en custom attributes
Wanneer je zelf een component maakt (bijvoorbeeld met Lit of Stencil), móét de naam van je custom element een dash bevatten:
<dcr-button label="Button"></dcr-button>Waarom? ➡️ Omdat de HTML parser custom tags alleen herkent als ze een dash bevatten.
Daarnaast gebruik je in HTML meestal kebab-case voor attributen op je component:
<dcr-task-card task-id="42" priority-label="Hoog"></dcr-task-card>Zelfs als de onderliggende JavaScript property priorityLabel heet, schrijf je het in HTML als priority-label. Lit mapt dit netjes.
3. Lit gebruikt camelcase voor properties (in js)
In je JavaScript- of TypeScript-code schrijf je properties altijd in camelCase:
@property({ type: String }) priorityLabel = 'Normaal';Lit zorgt ervoor dat priority-label in HTML automatisch wordt gekoppeld aan de priorityLabel property in JS — zolang je @property() gebruikt.
📌 Let op: HTML is niet case-sensitive, dus camelCase attributen zoals priorityLabel="..." zullen niet werken zoals je verwacht. Altijd overschakelen naar priority-label.
4. Sommige html attributen gebruiken wél een dash: aria-* en data-*
Dit zijn de uitzonderingen in native HTML:
| Attribuut | Gebruikt voor | Waarom dashes? |
|---|---|---|
aria-label | Toegankelijkheid (ARIA) | Gestandaardiseerd als kebab-case |
data-user-id | Custom data | HTML5-conventie voor data-* attribs |
Ze gedragen zich zoals gewone attributen, maar hebben een duidelijk naamgevingspatroon.
5. Properties vs attributes in lit (en interactie met frameworks zoals angular)
Als je een waarde in Angular of HTML doorgeeft aan een Lit-element, moet je weten: Gebruik ik een property binding of een attribute binding?
🔧 Property binding (via [prop]="..." in Angular, of element.prop = value in JS)
➡️ Zet direct de JavaScript-property. ➡️ Goed voor objecten, booleans, arrays. ➡️ Niet zichtbaar in de HTML markup (tenzij reflect: true is gebruikt).
<!-- Angular voorbeeld -->
<dcr-task-card [task]="myTask"></dcr-task-card>🔧 Attribute binding (via [attr.prop]="..." in Angular, of element.setAttribute('prop', value) in JS)
➡️ Zet een HTML attribuut. ➡️ Wordt automatisch geserialiseerd naar string door het framework of Lit zelf. ➡️ Blijft bestaan bij DOM-manipulatie zoals drag & drop.
<!-- Angular voorbeeld -->
<dcr-task-card [attr.task-id]="task.id"></dcr-task-card>📌 Lees je waarde terug in je component? Gebruik @property({ type: String, reflect: true }) om HTML attributen en properties met elkaar in sync te houden als de HTML attribuutwaarde de bron van waarheid moet zijn of voor styling.
Samenvatting naamgevingsconventies
| Context | Naamgeving in HTML | In code (JS/TS) | Voorbeeld |
|---|---|---|---|
| Native HTML attr | lowercase | n.v.t. | readonly, autofocus, required |
| ARIA/data attribs | kebab-case | via getAttribute() | aria-label, data-task-id |
| Custom elements | kebab-case | camelCase | priority-label → priorityLabel |
| JS properties | n.v.t. | camelCase | isActive, cardTitle |
Uitgebreide type ondersteuning in lit properties
LitElement ondersteunt diverse types voor properties, elk met hun eigen conversiegedrag.
String
- Van attribuut naar property: de stringwaarde van het attribuut wordt direct aan de property toegewezen.
- Van property naar attribuut (
reflect: true): de stringwaarde van de property wordt direct naar het attribuut geschreven. - Voorbeeld: zoals eerder getoond. Dit is het meest rechttoe rechtaan.
Number
Van attribuut naar property: Lit probeert de stringwaarde van het attribuut te converteren naar een JavaScript
number(vergelijkbaar metparseFloat). Als de string niet naar een geldig getal geconverteerd kan worden, wordt de propertyNaN.Van property naar attribuut (
reflect: true): de numerieke waarde wordt omgezet naar een string en als attribuutwaarde gezet.NaNwordt"NaN".Voorbeeld:
typescriptclass MyComponentNumber extends LitElement { @property({ type: Number, attribute: 'data-value' }) dataValue; render() { return html`Waarde: ${this.dataValue} (type: ${typeof this.dataValue})`; } } customElements.define('my-component-number', MyComponentNumber); // <my-component-number data-value="123"></my-component-number> -> dataValue is 123 (number) // <my-component-number data-value="3.14"></my-component-number> -> dataValue is 3.14 (number) // <my-component-number data-value="abc"></my-component-number> -> dataValue is NaN (number)
Boolean
Wanneer een boolean property via een attribuut configureerbaar wordt gemaakt, is het conform webstandaarden de standaardpraktijk om deze property initieel op false in te stellen.
Van attribuut naar property: een boolean attribuut wordt als
truebeschouwd als het aanwezig is op het element, enfalseals het afwezig is. De daadwerkelijke stringwaarde van het attribuut (bv.my-bool="false") speelt geen rol voor de conversie naartrue(zolang het attribuut aanwezig is).Van property naar attribuut (
reflect: true): als de propertytrueis, wordt het attribuut toegevoegd (zonder waarde, bv.<my-element my-bool>). Als de propertyfalseis, wordt het attribuut verwijderd.Voorbeeld:
typescriptclass MyComponentBoolean extends LitElement { @property({ type: Boolean, reflect: true }) active = false; // Initieel op false render() { return html`Actief: ${this.active}`; } } customElements.define('my-component-boolean', MyComponentBoolean); // <my-component-boolean active></my-component-boolean> -> active is true, attribuut 'active' is aanwezig // <my-component-boolean></my-component-boolean> -> active is false, attribuut 'active' is afwezig
Object en array
Van attribuut naar property: standaard zal LitElement een attribuut met
type: Objectoftype: Arrayautomatisch parsen metJSON.parse(attributeValue).Van property naar attribuut (
reflect: true): alsreflect: trueis ingesteld en er geen custom converter is, zal Lit een string maken metJSON.stringify(propertyValue).Aanbeveling:
- Voor complexe data via attributen, gebruik een custom converter die
JSON.parse(voorfromAttribute) enJSON.stringify(voortoAttribute) implementeert voor robuuste (de)serialisatie. - Of, geef complexe data programmatisch door aan de property in plaats van via een stringattribuut (bv.
element.config = { id: 1, name: "test" };).
- Voor complexe data via attributen, gebruik een custom converter die
Voorbeeld (met standaard JSON parsing):
typescriptclass MyDataComponent extends LitElement { @property({ type: Object, attribute: 'data-config' }) config: any; @property({ type: Array }) // attribuut naam wordt 'items' items: Array<number>; constructor() { super(); this.items = [1, 2, 3]; // Default, kan overschreven worden door attribuut } render() { return html`Config waarde: ${JSON.stringify(this.config)} <br /> Items: ${this.items?.join(', ')}`; } } customElements.define('my-data-component', MyDataComponent); // <my-data-component data-config='{"id": 1, "name": "test"}' items='[4,5,6]'></my-data-component> // output: config waarde: {"id":1,"name":"test"} // items: 4,5,6
Date
- LitElement heeft geen ingebouwd
type: Date. Properties die eenDate-object moeten bevatten, worden meestal behandeld alsObjectof met een custom converter. - Van attribuut naar property: een stringattribuut (bv. een ISO-datumstring) vereist een custom converter om het te parsen naar een
Date-object. - Van property naar attribuut (
reflect: true): eenDate-object vereist een custom converter om het te formatteren naar een geschikte string. - Directe property toewijzing:
this.myDate = new Date();werkt prima. - Aanbeveling: gebruik altijd een custom converter als je datums via attributen wilt serialiseren/deserialiseren.
Custom converters
Wanneer je specifieke logica nodig hebt voor de conversie tussen attributen en properties, kun je een converter object aan de property-declaratie meegeven. Dit is vooral nuttig voor types zoals Date of complexe JSON-structuren waar je meer controle wilt.
class MyAdvancedComponent extends LitElement {
@property({
attribute: 'user-data',
type: Object, // Kan ook Array zijn, of weggelaten als fromAttribute elk type kan retourneren
converter: {
fromAttribute: (value, type) => {
// value is de string van het HTML attribuut
if (value) {
try {
return JSON.parse(value); // Converteer string naar object
} catch (e) {
console.error('Fout bij parsen attribuut:', value, e);
return null; // Of een default waarde
}
}
return null;
},
toAttribute: (value, type) => {
// value is de property waarde
if (value) {
try {
return JSON.stringify(value); // Converteer object naar JSON string
} catch (e) {
console.error('Fout bij serialiseren property:', value, e);
return ''; // Of een default attribuut string
}
}
return '';
},
},
})
userData = { naam: 'Default', leeftijd: 0 };
render() {
return html`Gebruiker: ${this.userData?.naam}, leeftijd: ${this.userData?.leeftijd}`;
}
}
customElements.define('mijn-advanced-component', MyAdvancedComponent);
// <mijn-advanced-component user-data='{"naam":"Alice","leeftijd":30}'></mijn-advanced-component>
// userData property zal nu zijn: { naam: "Alice", leeftijd: 30 }Aanbevolen aanpak: string-first voor gemengde string/numeriek types
Als je value-attribuut bedoeld is om zowel numeriek-achtige ID's (bv. "123", "007") als pure string-ID's (bv. "user-abc", "item-xyz-789") te accepteren, is de meest robuuste en semantisch correcte aanpak de string-first benadering.
Dit houdt in:
- Declareer de Lit-property als
String(of laat het type weg, wat standaardStringis voor attributen). - Behandel de ontvangen waarde in je componentlogica als een string.
Voordelen van de string-first aanpak
| Voordeel | Uitleg |
|---|---|
| HTML-conformiteit | Sluit direct aan bij het feit dat HTML-attributen strings zijn. |
| Expliciete API | Het contract met de gebruiker is duidelijk: geef een string. De component bepaalt de interpretatie. |
| Volledige controle | De component heeft de volledige controle over parsing en validatie. |
| Robuustheid | Minder kans op NaN gerelateerde bugs als een niet-strikt numerieke string wordt doorgegeven wanneer type: Number gebruikt zou zijn. |
| Behoud van origineel | Je behoudt de originele stringwaarde (bv. "007" blijft "007", en wordt niet 7). |
- Implementeer parsing of validatie binnen je component (indien echt noodzakelijk) om te bepalen of de string numeriek is en hoe deze verder verwerkt moet worden. Let op de valkuilen hieronder: de aanpak in het volgende voorbeeld is een illustratie en niet per se de standaardimplementatie die overal nodig is.
Voorbeeld: flexibele itemid property (string-first)
import { LitElement, html } from 'lit';
import { property } from 'lit/decorators.js';
class SlimmeComponent extends LitElement {
@property({ attribute: 'item-id' }) // Impliciet type: String
itemId: string | undefined;
private _numeriekId: number | null = null;
private _stringId: string | null = null;
updated(changedProperties: Map<string | number | symbol, unknown>) {
if (changedProperties.has('itemId') && this.itemId !== undefined) {
const rawWaarde = this.itemId;
// Probeer te parsen als getal, maar alleen als het exact overeenkomt
const parsedNumber = parseFloat(rawWaarde);
if (!isNaN(parsedNumber) && String(parsedNumber) === rawWaarde) {
this._numeriekId = parsedNumber;
this._stringId = null;
} else {
// Behandel als pure string
this._stringId = rawWaarde;
this._numeriekId = null;
}
} else if (this.itemId === undefined) {
this._numeriekId = null;
this._stringId = null;
}
}
render() {
let display;
if (this._numeriekId !== null) {
display = `Numeriek ID: ${this._numeriekId}`;
} else if (this._stringId !== null) {
display = `String ID: ${this._stringId}`;
} else {
display = 'Niet ingesteld';
}
return html`<div>ID: ${display} (Oorspronkelijk ontvangen: ${this.itemId})</div>`;
}
}
customElements.define('slimme-component', SlimmeComponent);
// Gebruik:
// <slimme-component item-id="123"></slimme-component> -> Numeriek ID: 123
// <slimme-component item-id="007"></slimme-component> -> Numeriek ID: 7 (parseUnsafe) / String ID: "007" (parseSafe)
// <slimme-component item-id="product-xyz"></slimme-component> -> String ID: product-xyz(De logica in updated kan verfijnd worden afhankelijk van de exacte eisen voor het onderscheid tussen een numerieke string en een algemene string).
Valkuilen en overwegingen
Nan en type checking
Als je type: Number gebruikt, wees je ervan bewust dat typeof NaN resulteert in "number". Je moet expliciet controleren op isNaN() als je onderscheid wilt maken tussen een geldig getal en een mislukte conversie.
Verlies van originele stringopmaak (bij type: number)
Bij conversie naar number gaat de originele stringopmaak verloren. "007" wordt 7. Als de precieze stringrepresentatie belangrijk is (bv. voor ID's met voorloopnullen), is de string-first aanpak of een custom converter die de string behoudt beter.
Complexe objecten en arrays via attributen
Zoals besproken, vereist het doorgeven van complexe objecten of arrays via attributen vaak custom converters voor betrouwbare (de)serialisatie (meestal van/naar JSON). Overweeg of programmatische property-toewijzing (bv. element.mijnData = {...}) eenvoudiger en minder foutgevoelig is voor complexe data.
Datumverwerking
Herhaling: LitElement heeft geen ingebouwd type: Date. Gebruik custom converters voor het parsen van datumstrings uit attributen naar Date-objecten en vice versa.
Reflectie (reflect: true): wanneer en waarom?
Attributen moeten over het algemeen worden beschouwd als input voor het element, aangeleverd door de eigenaar ervan, en niet iets dat door het element zelf beheerd wordt. Daarom moet het reflecteren van properties naar attributen spaarzaam worden toegepast.
Een belangrijke reden om reflectie wél te gebruiken, is wanneer de property moet kunnen worden aangewend in CSS met attribuutselectors. Bijvoorbeeld, met @property({ type: Boolean, reflect: true }) invalid = false; kan men CSS-regels toepassen zoals :host([invalid]) { border-color: red; }. Het kan ook nuttig zijn voor server-side rendering (SSR) of wanneer externe tools de DOM inspecteren.
Belangrijkste principes om te onthouden
- HTML-attributen zijn fundamenteel strings. Lit's type systeem bouwt hierop voort met conversiemechanismen.
- Gebruik ingebouwde types (
String,Number,Boolean) waar ze direct passen. Wees bewust van hun conversiegedrag. - Voor speciale, complexe object of Date attributen:
- Gebruik custom converters (vaak met
JSON.parse/stringifyvoor object/array, of date parsing/formatting voorDate) voor robuuste (de)serialisatie. - Overweeg programmatische property-toewijzing als een eenvoudiger alternatief voor het doorgeven van complexe data via string-attributen.
- Gebruik custom converters (vaak met
- Voor attributen die semantisch zowel string als nummer kunnen zijn: overweeg de string-first aanpak in je componentlogica voor maximale controle en behoud van de originele waarde.
- Documenteer de verwachte input voor je component attributen duidelijk, inclusief hoe verschillende formaten en types worden geïnterpreteerd.
- Denk na over naamgevingsconventies:
kebab-casevoor HTML-attributen,camelCasevoor JavaScript/TypeScript properties. - Gebruik
reflect: truedoordacht, voornamelijk voor styling via CSS-attribuutselectors of wanneer de DOM-staat extern consistent moet zijn met de property-staat.