Skip to content

Event handling in LIT components ​

Inleiding ​

Event handling is een cruciaal aspect van webcomponents en vormt de basis voor interactieve gebruikersinterfaces. In deze gids duiken we diep in de werking van events binnen LIT components, met speciale aandacht voor de complexe interacties tussen shadow DOM, event bubbling, en event compositie.

Deze kennis is essentieel voor het bouwen van robuuste, herbruikbare componenten die correct reageren op gebruikersinteracties en goed samenwerken met andere componenten in een applicatie.

Basis van DOM events ​

Event types en eigenschappen ​

Events in het DOM zijn objecten die informatie bevatten over een specifieke gebeurtenis:

typescript
interface Event {
  readonly type: string;
  readonly target: EventTarget | null;
  readonly currentTarget: EventTarget | null;
  readonly eventPhase: number;
  readonly bubbles: boolean;
  readonly cancelable: boolean;
  readonly composed: boolean;
  readonly timeStamp: number;
  readonly defaultPrevented: boolean;
  // Methoden
  stopPropagation(): void;
  stopImmediatePropagation(): void;
  preventDefault(): void;
  composedPath(): EventTarget[];
}

Event propagatie fasen ​

Events doorlopen drie fasen tijdens propagatie:

  1. Capture fase: Van het root element naar beneden naar het target element
  2. Target fase: Het event bereikt het target element
  3. Bubbling fase: Van het target element terug omhoog naar het root element
typescript
// Event listener met expliciete capture fase
element.addEventListener(
  'click',
  (event) => {
    console.log('Capture fase handler');
  },
  true
); // true activeert capture fase

// Standaard bubbling fase (impliciet false)
element.addEventListener('click', (event) => {
  console.log('Bubbling fase handler');
});

Event bubbling ​

Bubbling is het proces waarbij een event, na afhandeling op het target element, omhoog "bubbelt" door de DOM-tree:

typescript
// HTML structuur:
// <div id="outer">
//   <div id="inner">
//     <button id="button">Klik mij</button>
//   </div>
// </div>

document.getElementById('button').addEventListener('click', (e) => {
  console.log('Button event handler');
});

document.getElementById('inner').addEventListener('click', (e) => {
  console.log('Inner div event handler');
});

document.getElementById('outer').addEventListener('click', (e) => {
  console.log('Outer div event handler');
});

// Bij klik op de button, zal de console tonen:
// "Button event handler"
// "Inner div event handler"
// "Outer div event handler"

Shadow DOM en event propagatie ​

Shadow DOM encapsulatie ​

Shadow DOM biedt encapsulatie voor DOM-structuren door een afgeschermde "shadow tree" te creëren:

typescript
// LIT component met shadow DOM
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('date-picker')
class DatePicker extends LitElement {
  render() {
    return html`
      <div class="container">
        <button @click=${this.handleClick}>Selecteer datum</button>
      </div>
    `;
  }

  private handleClick(e: Event) {
    console.log('Button geklikt binnen shadow DOM');
  }
}

Shadow boundaries ​

Shadow DOM creëert een grens (boundary) tussen de "light DOM" (reguliere DOM) en de "shadow DOM" (geëncapsuleerde DOM):

<date-picker>  <!-- Light DOM -->
  #shadow-root    <!-- Shadow Boundary -->
    <div class="container">  <!-- Shadow DOM -->
      <button>Selecteer datum</button>
    </div>
</date-picker>

Deze grens beïnvloedt hoe events zich verspreiden tussen de shadow DOM en de light DOM.

Event compositie en retargeting ​

De composed eigenschap ​

De composed eigenschap van een event bepaalt of het event de shadow boundary kan oversteken:

  • composed: true - Event kan shadow boundaries oversteken
  • composed: false - Event blijft binnen de shadow DOM
typescript
// Een composed event aanmaken
const event = new CustomEvent('selection-change', {
  bubbles: true,
  composed: true,
  detail: { selectedDate: new Date() },
});

Welke events zijn composed? ​

Hier is een overzicht van veelgebruikte events en hun compositie status:

Event typeComposedOpmerkingen
click, mousedown, mouseupJaAlle UI events zijn composed
inputJaVoor directe input tracking
changeNeeBlijft binnen shadow boundary
focus, blurJaFocus events zijn composed
keydown, keyupJaToetsenbord events zijn composed
submitNeeForm submission events zijn niet composed
Custom eventsNeeStandaard niet composed, tenzij expliciet ingesteld

Event retargeting ​

Wanneer een composed event een shadow boundary oversteekt, wordt het event "geretarget". Laten we dit proces stap voor stap bekijken met een voorbeeld:

<user-profile>  <!-- Light DOM element -->
  #shadow-root
    <div class="card">
      <button class="edit">Bewerk profiel</button>
    </div>
</user-profile>

Wanneer een gebruiker op de "Bewerk profiel" knop klikt:

  1. Initiële event creatie:

    • Browser creëert een click event met event.target = de <button> binnen shadow DOM
    • Event fase: TARGET
  2. Bubbling binnen shadow DOM:

    • Event bubbelt omhoog naar <div class="card"> binnen shadow DOM
    • event.target is nog steeds de <button>
    • Event fase: BUBBLING
  3. Shadow boundary oversteken:

    • Event bereikt de shadow boundary (omdat click composed is)
    • Retargeting gebeurt hier: event.target wordt gewijzigd naar <user-profile>
    • Originele pad wordt opgeslagen in event.composedPath()
    • Event fase: BUBBLING
  4. Bubbling in light DOM:

    • Event bubbelt verder in light DOM
    • event.target blijft <user-profile> (niet meer de originele button)
    • Event fase: BUBBLING
typescript
// Light DOM code:
document.querySelector('user-profile').addEventListener('click', (event) => {
  console.log('Event target:', event.target); // <user-profile>

  // De originele target (de button binnen shadow DOM)
  const origineleTarget = event.composedPath()[0];
  console.log('Originele target:', origineleTarget); // <button class="edit">

  // Het volledige pad tonen
  console.log('Event pad:', event.composedPath());
  // Toont array: [button.edit, div.card, #shadow-root, user-profile, body, html, document, window]
});

Dit retargeting mechanisme is cruciaal voor het behouden van encapsulatie, terwijl events toch door de hele applicatie kunnen propageren. Het zorgt ervoor dat code buiten de component niet direct afhankelijk wordt van de interne DOM-structuur.

Standaard event gedrag in LIT ​

Event binding in LIT templates ​

LIT biedt een eenvoudige syntax voor event binding:

typescript
import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';

@customElement('task-counter')
class TaskCounter extends LitElement {
  @state()
  private count = 0;

  render() {
    return html`
      <button @click=${this.increment}>Taak toevoegen</button>
      <span>Aantal taken: ${this.count}</span>
    `;
  }

  private increment() {
    this.count++;
  }
}

Event listener binding en this context ​

LIT zorgt automatisch voor de juiste this binding in event handlers:

typescript
@customElement('registration-form')
class RegistrationForm extends LitElement {
  render() {
    return html`
      <form @submit=${this.handleSubmit}>
        <input type="email" name="email" />
        <button type="submit">Registreren</button>
      </form>
    `;
  }

  private handleSubmit(e: Event) {
    e.preventDefault();
    // 'this' verwijst correct naar de component instantie
    console.log('Formulier verzonden', this);

    // Toegang tot form data
    const form = e.target as HTMLFormElement;
    const formData = new FormData(form);
    console.log('Email:', formData.get('email'));
  }
}

Event modifier directives ​

LIT ondersteunt geen ingebouwde event modifiers zoals Vue of Svelte, maar je kunt helper functies gebruiken:

typescript
// Helper functie voor preventDefault
const onSubmit = (callback: (e: Event) => void) => (e: Event) => {
  e.preventDefault();
  callback(e);
};

@customElement('login-form')
class LoginForm extends LitElement {
  render() {
    return html`
      <form @submit=${onSubmit(this.handleSubmit)}>
        <!-- form inhoud -->
      </form>
    `;
  }

  private handleSubmit(e: Event) {
    // preventDefault is al aangeroepen
    console.log('Login verwerken');
  }
}

De redispatchEvent functie ​

Probleem: non-composed events ​

Niet-composed events (zoals change) kunnen niet natuurlijk door shadow boundaries bubbling:

typescript
// In een LIT component
render() {
  return html`<input @change=${this.handleChange}>`;
}

// Deze change event zal niet buiten de component hoorbaar zijn
private handleChange(e: Event) {
  console.log('Input changed');
}

De redispatchEvent oplossing ​

De redispatchEvent functie lost dit probleem op door het event opnieuw te dispatchen vanaf het host element:

typescript
/**
 * Dispatcht een event opnieuw vanaf het opgegeven element.
 *
 * Deze functie is nuttig voor het doorsturen van niet-composed events,
 * zoals `change` events.
 *
 * @param element Het element om het event vanaf te dispatchen.
 * @param event Het event om opnieuw te dispatchen.
 * @return Of het event wel of niet gedispatcht werd (indien cancelable).
 */
export function redispatchEvent(element: Element, event: Event): boolean {
  // Voor bubbling events in SSR light DOM (of composed), stop hun propagatie
  // en dispatch de kopie.
  if (event.bubbles && (!element.shadowRoot || event.composed)) {
    event.stopPropagation();
  }

  const copy = Reflect.construct(event.constructor, [event.type, event]);
  const dispatched = element.dispatchEvent(copy);
  if (!dispatched) {
    event.preventDefault();
  }

  return dispatched;
}

Gebruik van redispatchEvent in componenten ​

typescript
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { redispatchEvent } from '../utils/events';

@customElement('theme-switch')
class ThemeSwitch extends LitElement {
  render() {
    return html`
      <label>
        <input type="checkbox" @input=${this.handleInput} @change=${this.handleChange} />
        <span>Donker thema</span>
      </label>
    `;
  }

  private handleInput(event: Event) {
    // input event is al composed, hoeft niet opnieuw gedispatcht te worden
    console.log('Input event ontvangen');
  }

  private handleChange(event: Event) {
    // change event is niet composed, moet opnieuw gedispatcht worden
    redispatchEvent(this, event);
  }
}

Custom events in LIT components ​

Custom events aanmaken ​

Custom events zijn een krachtig mechanisme voor communicatie tussen componenten. Ze bieden een duidelijke, gedeclareerde API voor het doorgeven van informatie van een component naar zijn omgeving. Door custom events te gebruiken in plaats van directe methode-aanroepen, bevorder je losse koppeling en herbruikbaarheid van je componenten.

Voordelen van custom events:

  • Ze volgen het standaard DOM event model dat ontwikkelaars al kennen
  • Ze maken one-way data flow mogelijk (van child naar parent)
  • Ze maken het mogelijk om meerdere listeners te hebben voor dezelfde gebeurtenis
  • Ze kunnen bubbelen door de DOM-tree

Hier is een voorbeeld van een slider component dat custom events gebruikt:

typescript
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('range-slider')
class RangeSlider extends LitElement {
  @property({ type: Number })
  value = 0;

  render() {
    return html`
      <div class="slider">
        <input type="range" min="0" max="100" .value=${this.value.toString()} @input=${this.handleInput} />
        <span>${this.value}</span>
      </div>
    `;
  }

  private handleInput(e: Event) {
    const input = e.target as HTMLInputElement;
    this.value = Number(input.value);

    // Custom event dispatchen
    this.dispatchEvent(
      new CustomEvent('slider-change', {
        bubbles: true,
        composed: true,
        detail: { value: this.value },
      })
    );
  }
}

Custom Events naamgeving ​

  1. Wees specifiek: De eventnaam moet de actie duidelijk omschrijven.
  2. Structuur: Gebruik noun-verb in kebab-case.
    • noun: Eén zelfstandig naamwoord dat de context samenvat (bv. product, menu, order, progress).
    • verb: Eén werkwoord in de tegenwoordige tijd dat de actie beschrijft (bv. select, update, close, cancel).
  3. Betekenis: Het event signaleert de initiatie van een actie, niet de voltooiing ervan.
    • Voorbeeld: order-cancel betekent een verzoek tot annuleren wordt gedaan, niet dat de annulering al is verwerkt.

TypeScript types voor custom events ​

Het definiëren van TypeScript interfaces voor custom events verbetert de type-veiligheid en documentatie van je code. Dit maakt het voor andere ontwikkelaars duidelijker welke gegevens ze kunnen verwachten in event handlers, en helpt IDE's met autocompletion en type checking.

Voordelen van getypeerde custom events:

  • Betere code-documentatie
  • Verbeterde IDE-ondersteuning
  • Minder runtime fouten door type checking tijdens compilatie
  • Eenvoudiger refactoring
typescript
// Event type definities
export interface SliderChangeEventDetail {
  value: number;
}

export interface SliderChangeEvent extends CustomEvent {
  detail: SliderChangeEventDetail;
}

// In component
private handleInput(e: Event) {
  const input = e.target as HTMLInputElement;
  this.value = Number(input.value);

  // Getypeerde custom event
  this.dispatchEvent(new CustomEvent<SliderChangeEventDetail>('slider-change', {
    bubbles: true,
    composed: true,
    detail: { value: this.value }
  }));
}

// Bij gebruik van het component
const slider = document.querySelector('range-slider');
slider.addEventListener('slider-change', (e: SliderChangeEvent) => {
  console.log('Nieuwe waarde:', e.detail.value);
});

Best practices voor event handling ​

1. Geaggregeerde state in custom events ​

Bij complexe componenten met meerdere interne elementen is het vaak beter om een geaggregeerde state te sturen in plaats van afzonderlijke events voor elk element. Dit vereenvoudigt de API van je component en maakt het gemakkelijker om te gebruiken.

Voordelen:

  • Vereenvoudigde API voor consumenten van je component
  • Minder event handlers nodig in parent componenten
  • Betere encapsulatie van interne implementatiedetails
  • Mogelijkheid om validatie en transformatie toe te passen op de volledige dataset
typescript
@customElement('payment-form')
class PaymentForm extends LitElement {
  @state() private cardNumber = '';
  @state() private expiryDate = '';
  @state() private cvv = '';

  render() {
    return html`
      <div class="payment-inputs">
        <input
          class="card-number"
          placeholder="Kaartnummer"
          @input=${this.handleCardNumberInput}
          .value=${this.cardNumber}
        />
        <input class="expiry" placeholder="MM/JJ" @input=${this.handleExpiryInput} .value=${this.expiryDate} />
        <input class="cvv" placeholder="CVV" @input=${this.handleCvvInput} .value=${this.cvv} />
      </div>
    `;
  }

  private handleCardNumberInput(e: Event) {
    this.cardNumber = (e.target as HTMLInputElement).value;
    this.dispatchPaymentStateEvent();
  }

  private handleExpiryInput(e: Event) {
    this.expiryDate = (e.target as HTMLInputElement).value;
    this.dispatchPaymentStateEvent();
  }

  private handleCvvInput(e: Event) {
    this.cvv = (e.target as HTMLInputElement).value;
    this.dispatchPaymentStateEvent();
  }

  private dispatchPaymentStateEvent() {
    // Valideer en formatteer data
    const isValid = this.validatePaymentDetails();
    const formattedCardNumber = this.formatCardNumber(this.cardNumber);

    // Dispatch complete state event
    this.dispatchEvent(
      new CustomEvent('payment-details-change', {
        bubbles: true,
        composed: true,
        detail: {
          cardNumber: formattedCardNumber,
          expiryDate: this.expiryDate,
          cvv: this.cvv,
          isComplete: Boolean(this.cardNumber && this.expiryDate && this.cvv),
          isValid,
        },
      })
    );
  }

  private validatePaymentDetails(): boolean {
    // Validatie logica
    return true;
  }

  private formatCardNumber(number: string): string {
    // Formatteer kaartnummer (bijv. xxxx xxxx xxxx xxxx)
    return number;
  }
}

2. Duidelijke event naamgeving ​

Consistente en beschrijvende naamgeving van events maakt je code leesbaarder en onderhoudbaarder. Een goede naamgevingsconventie helpt ontwikkelaars om snel te begrijpen wat een event doet zonder de implementatie te hoeven bekijken.

Voordelen:

  • Verbeterde leesbaarheid van code
  • Eenvoudiger debuggen
  • Betere zelf-documenterende code
  • Consistentie in het codebase
typescript
// Goed
this.dispatchEvent(
  new CustomEvent('slider-change', {
    /* ... */
  })
);
this.dispatchEvent(
  new CustomEvent('selection-complete', {
    /* ... */
  })
);
this.dispatchEvent(
  new CustomEvent('form-submit', {
    /* ... */
  })
);

// Vermijd
this.dispatchEvent(
  new CustomEvent('change', {
    /* ... */
  })
);
this.dispatchEvent(
  new CustomEvent('done', {
    /* ... */
  })
);
this.dispatchEvent(
  new CustomEvent('update', {
    /* ... */
  })
);

3. Documenteer event interfaces ​

Goede documentatie van je events maakt het voor andere ontwikkelaars veel eenvoudiger om je componenten te gebruiken. Door duidelijk te documenteren welke events een component dispatcht en welke data ze bevatten, maak je je component toegankelijker voor anderen.

Voordelen:

  • Betere gebruikservaring voor andere ontwikkelaars
  • Minder tijd nodig om de component te begrijpen
  • Minder fouten bij het gebruik van de component
  • Betere onderhoudbaarheid op lange termijn
typescript
/**
 * Een formulier component voor betalingsgegevens.
 *
 * @fires payment-details-change - Wanneer een van de betalingsgegevens wijzigt
 *   detail: {
 *     cardNumber: string,
 *     expiryDate: string,
 *     cvv: string,
 *     isComplete: boolean,
 *     isValid: boolean
 *   }
 * @fires payment-submit - Wanneer het formulier wordt verzonden
 *   detail: { paymentMethod: string, amount: number }
 */
@customElement('payment-form')
class PaymentForm extends LitElement {
  // Implementatie...
}

4. Gebruik event delegation voor lijsten ​

Event delegation is een patroon waarbij je één event listener toevoegt aan een parent element in plaats van aparte listeners voor elk child element. Dit is bijzonder nuttig voor lijsten met veel items, omdat het de prestaties verbetert en de code vereenvoudigt.

Voordelen:

  • Betere prestaties, vooral bij lange lijsten
  • Minder geheugengebruik
  • Werkt automatisch voor dynamisch toegevoegde items
  • Vereenvoudigde code
typescript
@customElement('product-list')
class ProductList extends LitElement {
  @property({ type: Array })
  products = [];

  render() {
    return html`
      <ul @click=${this.handleClick}>
        ${this.products.map(
          (product, index) => html`<li data-id="${product.id}">${product.name} - €${product.price}</li>`
        )}
      </ul>
    `;
  }

  private handleClick(e: MouseEvent) {
    const target = e.target as HTMLElement;
    const li = target.closest('li');

    if (li) {
      const productId = li.dataset.id;
      const product = this.products.find((p) => p.id === productId);

      this.dispatchEvent(
        new CustomEvent('product-select', {
          bubbles: true,
          composed: true,
          detail: { product },
        })
      );
    }
  }
}

Geavanceerde patronen ​

1. Event controllers in LIT ​

Controllers in LIT bieden een krachtige manier om gedrag te hergebruiken tussen componenten. Voor event handling kunnen controllers helpen om complexe event logica te encapsuleren en te hergebruiken.

typescript
import { ReactiveController, ReactiveControllerHost } from 'lit';

class KeyboardController implements ReactiveController {
  private host: ReactiveControllerHost;
  private boundHandleKeyDown: (e: KeyboardEvent) => void;

  constructor(host: ReactiveControllerHost, private callback: (e: KeyboardEvent) => void) {
    this.host = host;
    this.boundHandleKeyDown = this.handleKeyDown.bind(this);
    host.addController(this);
  }

  hostConnected() {
    document.addEventListener('keydown', this.boundHandleKeyDown);
  }

  hostDisconnected() {
    document.removeEventListener('keydown', this.boundHandleKeyDown);
  }

  private handleKeyDown(e: KeyboardEvent) {
    this.callback(e);
  }
}

// Gebruik in component
@customElement('dialog-component')
class DialogComponent extends LitElement {
  private keyboardController = new KeyboardController(this, (e) => {
    if (e.key === 'Escape') {
      this.closeDialog();
    }
  });

  private closeDialog() {
    console.log('Dialog gesloten met Escape toets');
    this.dispatchEvent(new CustomEvent('dialog-close'));
  }

  // Rest van component...
}

2. Gecombineerde event strategieën ​

Het combineren van native en custom events biedt flexibiliteit voor verschillende use cases. Je kunt native events laten bubbling voor eenvoudige interacties, terwijl je custom events gebruikt voor complexere interacties.

typescript
@customElement('data-grid')
class DataGrid extends LitElement {
  @property({ type: Array })
  data = [];

  render() {
    return html`
      <table>
        <!-- tabel inhoud -->
        <tbody @click=${this.handleRowClick}>
          ${this.data.map(
            (row, index) => html`
              <tr data-index="${index}">
                <!-- rij cellen -->
              </tr>
            `
          )}
        </tbody>
      </table>
    `;
  }

  private handleRowClick(e: MouseEvent) {
    // Native event bubbling voor eenvoudige use cases
    const tr = (e.target as HTMLElement).closest('tr');
    if (tr) {
      const index = Number(tr.dataset.index);
      const rowData = this.data[index];

      // Custom event met complete context
      this.dispatchEvent(
        new CustomEvent('row-select', {
          bubbles: true,
          composed: true,
          detail: { index, data: rowData },
        })
      );
    }
  }
}

// Gebruik - beide benaderingen mogelijk
const grid = document.querySelector('data-grid');

// Optie 1: Native event (vereist kennis van interne structuur)
grid.addEventListener('click', (e) => {
  const target = e.composedPath()[0] as HTMLElement;
  console.log('Geklikt element:', target);
});

// Optie 2: Custom event (aanbevolen, schonere API)
grid.addEventListener('row-select', (e: CustomEvent) => {
  console.log('Geselecteerde rij:', e.detail.data);
});

3. Event filtering en transformatie ​

Het filteren en transformeren van events voordat je ze doorstuurt kan de gebruikerservaring verbeteren en de prestaties optimaliseren. Technieken zoals debouncing en throttling zijn bijzonder nuttig voor events die snel kunnen vuren, zoals scroll of input events.

typescript
@customElement('search-field')
class SearchField extends LitElement {
  @state() private searchText = '';
  @state() private debounceTimeout: number | null = null;

  render() {
    return html` <input type="text" placeholder="Zoeken..." .value=${this.searchText} @input=${this.handleInput} /> `;
  }

  private handleInput(e: Event) {
    const input = e.target as HTMLInputElement;
    this.searchText = input.value;

    // Debounce de input events
    if (this.debounceTimeout) {
      clearTimeout(this.debounceTimeout);
    }

    this.debounceTimeout = window.setTimeout(() => {
      this.dispatchEvent(
        new CustomEvent('search-request', {
          bubbles: true,
          composed: true,
          detail: { query: this.searchText },
        })
      );
      this.debounceTimeout = null;
    }, 300);
  }
}

Veelvoorkomende valkuilen ​

1. Vergeten om events composed te maken ​

Probleem: Custom events die niet buiten de component hoorbaar zijn.

typescript
// Probleem
this.dispatchEvent(
  new CustomEvent('file-upload', {
    bubbles: true,
    // composed: true is vergeten!
    detail: { fileName: 'document.pdf', size: 1024 },
  })
);

// Oplossing
this.dispatchEvent(
  new CustomEvent('file-upload', {
    bubbles: true,
    composed: true, // Zorgt ervoor dat het event shadow boundaries kan oversteken
    detail: { fileName: 'document.pdf', size: 1024 },
  })
);

2. Event listener memory leaks ​

Probleem: Event listeners die niet worden verwijderd.

typescript
// Probleem
connectedCallback() {
  super.connectedCallback();
  document.addEventListener('scroll', this.handleScroll);
}

// Oplossing
connectedCallback() {
  super.connectedCallback();
  // Bind de methode en bewaar een referentie
  this.boundHandleScroll = this.handleScroll.bind(this);
  document.addEventListener('scroll', this.boundHandleScroll);
}

disconnectedCallback() {
  super.disconnectedCallback();
  // Verwijder de listener
  document.removeEventListener('scroll', this.boundHandleScroll);
}

3. Verkeerd gebruik van event retargeting ​

Probleem: Vertrouwen op event.target in plaats van composedPath().

typescript
// Probleem
@customElement('app-container')
class AppContainer extends LitElement {
  render() {
    return html`
      <user-profile @profile-update=${this.handleProfileUpdate}></user-profile>
    `;
  }

  private handleProfileUpdate(e: Event) {
    // Dit geeft het <user-profile> element, niet het element binnen de shadow DOM
    console.log('Target:', e.target);
  }
}

// Oplossing
private handleProfileUpdate(e: Event) {
  // Dit geeft het originele element dat het event veroorzaakte
  const originalTarget = e.composedPath()[0];
  console.log('Original target:', originalTarget);
}

4. Niet herbruikbare event logica ​

Probleem: Duplicatie van event handling code.

typescript
// Probleem - duplicatie in meerdere componenten
private handleInput(e: Event) {
  const input = e.target as HTMLInputElement;
  // Dezelfde validatie logica gedupliceerd in meerdere componenten
  if (input.value.length > 0) {
    this.isValid = true;
  } else {
    this.isValid = false;
  }
}

// Oplossing - herbruikbare controller
class ValidationController implements ReactiveController {
  host: ReactiveControllerHost & { isValid: boolean };

  constructor(host: ReactiveControllerHost & { isValid: boolean }) {
    this.host = host;
    host.addController(this);
  }

  validate(value: string): boolean {
    const isValid = value.length > 0;
    this.host.isValid = isValid;
    return isValid;
  }
}

// Gebruik in component
@customElement('email-field')
class EmailField extends LitElement {
  @state() isValid = true;

  private validator = new ValidationController(this);

  private handleInput(e: Event) {
    const input = e.target as HTMLInputElement;
    this.validator.validate(input.value);
  }
}

Conclusie ​

Event handling in LIT components vereist een goed begrip van shadow DOM, event compositie, en event retargeting. Door de juiste technieken te gebruiken, kun je componenten bouwen die:

  1. Encapsulatie behouden - Door shadow DOM grenzen te respecteren
  2. Flexibel communiceren - Via goed ontworpen custom events
  3. Herbruikbaar zijn - Door consistente event patronen te implementeren
  4. Performant zijn - Door efficiënte event handling strategieën

Door de concepten in deze gids toe te passen, kun je robuuste, herbruikbare LIT componenten bouwen die effectief communiceren met hun omgeving, terwijl ze de voordelen van encapsulatie behouden.

Onthoud de belangrijkste principes:

  • Gebruik composed: true voor events die shadow boundaries moeten oversteken
  • Gebruik redispatchEvent voor het doorsturen van niet-composed events
  • Stuur geaggregeerde state in custom events voor een schonere API
  • Documenteer duidelijk welke events je component dispatcht