Skip to content

DOM elementen zoeken en doorlopen ​

Inleiding ​

Het efficiΓ«nt zoeken en doorlopen van DOM elementen is een fundamenteel onderdeel van webcomponent ontwikkeling. Of je nu focusbare elementen moet vinden voor toegankelijkheid, specifieke child nodes moet localiseren, of door een complexe DOM-structuur moet navigeren - de juiste techniek maakt een groot verschil in prestaties en onderhoudbaarheid.

In deze gids ontdek je de verschillende methoden voor DOM querying en traversal, leer je wanneer je welke technieken moet gebruiken, en krijg je diepgaand inzicht in hoe browsers deze operaties efficiΓ«nt uitvoeren.

Overzicht van technieken ​

Er zijn verschillende benaderingen voor het zoeken en doorlopen van DOM elementen:

1. Query selectors ​

De meest gangbare methoden voor het selecteren van elementen op basis van CSS selectors:

typescript
// Enkel element
const button = document.querySelector('button.primary');
const header = this.shadowRoot.querySelector('.header');

// Meerdere elementen
const allButtons = document.querySelectorAll('button');
const inputs = this.shadowRoot.querySelectorAll('input');

2. Manual traversal ​

Handmatige navigatie door de DOM boom met native properties:

typescript
// Parent-child relaties
const parent = element.parentNode;
const children = Array.from(element.children);
const firstChild = element.firstElementChild;
const lastChild = element.lastElementChild;

// Sibling navigatie
const nextSibling = element.nextElementSibling;
const previousSibling = element.previousElementSibling;

3. TreeWalker API ​

Een geoptimaliseerde, native browser API voor het doorlopen van DOM structuren:

typescript
const walker = document.createTreeWalker(
  root, // Start node
  NodeFilter.SHOW_ELEMENT, // Wat tonen (alleen elements)
  {
    acceptNode: (node) => {
      // Optionele filter functie
      return NodeFilter.FILTER_ACCEPT;
    },
  },
);

while (walker.nextNode()) {
  const element = walker.currentNode;
  // Verwerk element
}

Wanneer welke techniek gebruiken? ​

De keuze van de juiste techniek hangt af van je use case:

Use CaseAanbevolen TechniekReden
Enkel specifiek elementquerySelector()Direct, eenvoudig, goed genoeg
Lijst van bekende elementenquerySelectorAll()CSS selector is expressief
Directe childrenelement.childrenSnelste optie, geen query nodig
Parent elementelement.parentElementDirect, geen overhead
Dynamische filteringTreeWalkerFlexibel, performant voor complexe logica
Grote DOM treesTreeWalkerOptimaal geheugengebruik
Runtime property checksTreeWalkerKan niet met CSS selectors

Deep dive: querySelector vs querySelectorAll ​

Hoe werken ze? ​

Query selectors werken door CSS selector strings te parsen en te matchen tegen de DOM:

typescript
// Browser proces:
// 1. Parse CSS selector string
// 2. Compileer selector naar intern formaat
// 3. Walk de DOM tree en match elementen
// 4. Return resultaat(en)

const buttons = element.querySelectorAll('button:not(:disabled)');

Voordelen ​

βœ… Bekend en leesbaar - CSS selectors zijn vertrouwd
βœ… Expressief - Complexe selectors mΓΆglich
βœ… Statische filtering - Simpele use cases

Nadelen ​

❌ Parsing overhead - Selector moet eerst geparsed worden
❌ Beperkte logica - Alleen wat CSS selectors kunnen uitdrukken
❌ Browser compatibiliteit - Nieuwere pseudo-classes (:is(), :has()) werken niet overal
❌ Geen runtime properties - Kan geen JavaScript properties checken (zoals element.disabled vs disabled attribute)

Performance karakteristieken ​

typescript
// Voorbeeld met 100 elementen in de DOM:

// querySelectorAll timing breakdown:
// - Parse selector: ~0.05ms
// - DOM traversal: ~0.2ms
// - Array allocatie: ~0.05ms
// Totaal: ~0.3ms

const result = element.querySelectorAll('button:not(:disabled)');

Wanneer gebruiken? ​

Gebruik querySelector / querySelectorAll wanneer:

  • Je een eenvoudige, statische selector hebt
  • De structuur bekend is op voorhand
  • Je weinig elementen moet vinden (< 50)
  • De selector simpel is (geen :is(), geen complexe combinators)

Voorbeeld use case:

typescript
@customElement('app-header')
class AppHeader extends LitElement {
  private _openMenu(): void {
    // Direct, bekend element - perfect voor querySelector
    const menu = this.shadowRoot.querySelector('.menu');
    menu?.classList.add('open');
  }
}

Deep dive: TreeWalker API ​

Wat is TreeWalker? ​

TreeWalker is een native browser API (onderdeel van de DOM spec) voor het efficiΓ«nt doorlopen van een document tree. Het is geΓ―mplementeerd in C++ in de browser engine, niet in JavaScript.

Hoe werkt TreeWalker? ​

1. Creatie ​

typescript
const walker = document.createTreeWalker(
  rootNode, // Startpunt van de traversal
  whatToShow, // Bitmask van node types
  filter, // Optionele filter functie
);

whatToShow opties:

typescript
NodeFilter.SHOW_ALL; // Alle nodes
NodeFilter.SHOW_ELEMENT; // Alleen element nodes
NodeFilter.SHOW_TEXT; // Alleen text nodes
NodeFilter.SHOW_COMMENT; // Alleen comment nodes
NodeFilter.SHOW_DOCUMENT; // Alleen document nodes
// ... en meer

2. Navigatie methoden ​

typescript
walker.nextNode(); // Volgende node in tree order
walker.previousNode(); // Vorige node in tree order
walker.firstChild(); // Eerste child
walker.lastChild(); // Laatste child
walker.parentNode(); // Parent node
walker.nextSibling(); // Volgende sibling
walker.previousSibling(); // Vorige sibling

// Huidige positie
walker.currentNode; // Actuele node

3. Depth-first traversal ​

TreeWalker gebruikt depth-first traversal (diepte-eerst):

         root
        /    \
      A        B
     / \      / \
    C   D    E   F

Volgorde: root β†’ A β†’ C β†’ D β†’ B β†’ E β†’ F

Voorbeeld:

typescript
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);

while (walker.nextNode()) {
  console.log(walker.currentNode.nodeName);
}

// Output: DIV β†’ HEADER β†’ H1 β†’ NAV β†’ UL β†’ LI β†’ LI β†’ MAIN β†’ ...

Architectuur: Wat draait in C++ en wat in JavaScript? ​

Het is belangrijk om te begrijpen welke delen van TreeWalker in C++ draaien en welke in JavaScript:

1. C++ Implementatie (Native Browser Code) ​

Wat draait in C++:

  • βœ… Tree traversal zelf - Het navigeren door de DOM boom
  • βœ… whatToShow filtering - Filteren op node types (ELEMENT, TEXT, etc.)
  • βœ… Pointer management - Bijhouden van huidige positie
  • βœ… Memory operations - Direct memory access
JavaScript                    Browser Engine (C++)
──────────                    ────────────────────
walker.nextNode() ──────────► Traverse to next node
                              Check if node matches whatToShow
                              Update internal pointer
                              ◄──── Return node reference

Voorbeeld van pure C++ filtering:

typescript
// Deze filtering gebeurt volledig in C++
const walker = document.createTreeWalker(
  root,
  NodeFilter.SHOW_ELEMENT, // ← C++ check: is het een Element node?
);

// nextNode() traverseert in C++, returnt alleen Element nodes
while (walker.nextNode()) {
  // Alleen Element nodes komen hier
}

2. JavaScript Filter Callbacks ​

BELANGRIJK: Custom filter functies draaien NIET in C++, maar in JavaScript!

typescript
const walker = document.createTreeWalker(
  root,
  NodeFilter.SHOW_ELEMENT, // ← C++ filtering
  {
    acceptNode: (node) => {
      // ← JavaScript callback!
      // Deze code draait in JAVASCRIPT, niet C++
      return this._isFocusable(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
    },
  },
);

Wat er echt gebeurt:

C++ TreeWalker                JavaScript Runtime
──────────────                ──────────────────
1. Traverse tree
2. Find next node
3. Check whatToShow (C++)
4. Call filter ──────────────► acceptNode(node)
                                β”‚ Execute JS function
                                β”‚ Call _isFocusable()
                                └─ Return FILTER_ACCEPT/SKIP
5. Use return value ◄──────────
6. Continue or skip node

3. Performance Implicaties ​

De C++/JavaScript boundary crossing heeft overhead:

typescript
// Voor 100 elementen:

// Met filter callback:
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
  acceptNode: (node) => {
    // 100x C++ β†’ JS boundary crossing
    // 100x closure context setup
    return someCheck(node) ? FILTER_ACCEPT : FILTER_SKIP;
  },
});
// Tijd: ~0.32ms (boundary overhead: ~0.02ms)

// Zonder filter callback:
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
  // 100x directe JS function call
  if (someCheck(walker.currentNode)) {
  }
}
// Tijd: ~0.30ms (geen boundary overhead)

Conclusie: Voor JavaScript checks (zoals _isFocusable), filter callbacks bieden geen performance voordeel.

4. Wanneer Filter Callbacks WEL zinvol zijn ​

Filter callbacks zijn nuttig voor structurele optimalisaties met FILTER_REJECT:

typescript
// βœ… GOED: Skip hele subtrees in C++
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
  acceptNode: (node) => {
    const el = node as HTMLElement;

    // Als container hidden is, skip alle children in C++!
    if (el.classList.contains('hidden-container')) {
      return NodeFilter.FILTER_REJECT; // ← C++ skips subtree
    }

    return NodeFilter.FILTER_ACCEPT;
  },
});

// TreeWalker zal de hele .hidden-container subtree skippen
// zonder elk child element te bezoeken - dat is wel sneller!

Filter return values en hun effect:

Return ValueEffectC++ Optimalisatie
FILTER_ACCEPTNode accepterenNee
FILTER_SKIPNode skippen, children wel checkenNee
FILTER_REJECTNode EN children skippenJa - subtree skip in C++

Waarom TreeWalker Toch Snel Is ​

Ondanks dat filter callbacks in JavaScript draaien, is TreeWalker nog steeds sneller dan manual traversal:

Vergelijking met Manual Traversal ​

Manual Recursive (Langzaam):

typescript
function traverse(node: Node): void {
  processNode(node); // JavaScript

  for (const child of node.children) {
    // JavaScript array iteration
    traverse(child); // Function call stack overhead
  }
}

// Kosten:
// - Stack frame per recursie
// - Array.from() allocatie
// - Iterator allocatie
// - Closure creation

**Manual Iterative (Medium):

typescript
const stack = [root]; // Array allocatie

while (stack.length) {
  const node = stack.pop()!;
  processNode(node);

  stack.push(...node.children); // Spread allocatie!
}

// Kosten:
// - Array allocaties per node
// - Push/pop overhead
// - Spread operations

TreeWalker (Snel):

typescript
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);

while (walker.nextNode()) {
  // C++ traversal
  processNode(walker.currentNode); // JavaScript
}

// Kosten:
// - GEEN allocaties
// - GEEN stack overhead
// - Alleen property access

De winst van TreeWalker komt van:

  1. βœ… Zero allocations - Geen arrays, geen closures
  2. βœ… C++ tree navigation - EfficiΓ«nte pointer operations
  3. βœ… Reusable state - Walker hergebruikt interne state
  4. βœ… Cache-friendly - Sequential memory access in C++

Niet van:

  • ❌ Filter callbacks in C++ (die draaien in JS!)

Praktisch Advies voor Focus Detection ​

Voor het detecteren van focusbare elementen:

typescript
// ❌ NIET optimaler: Filter callback heeft overhead
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
  acceptNode: (node) => {
    // Boundary crossing overhead + closure overhead
    return this._isFocusable(node) ? FILTER_ACCEPT : FILTER_SKIP;
  },
});

// βœ… BETER: Direct check in loop (zelfde snelheid, leesbaarder)
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);

while (walker.nextNode()) {
  const el = walker.currentNode as Element;

  // Directe function call, geen boundary overhead
  if (this._isFocusable(el)) {
    collection.push(el as HTMLElement);
  }
}

Reden: De _isFocusable check moet toch in JavaScript draaien (runtime property checks), dus de filter callback geeft geen voordeel. De while-loop variant is duidelijker en heeft iets minder overhead.

Performance deep dive ​

Vergelijking: Manual vs TreeWalker ​

Optie 1: Manual recursive traversal

typescript
// ❌ LANGZAAM - Function call stack overhead
function traverse(node: Node): void {
  // Verwerk node
  processNode(node);

  // Recursief voor elk child
  for (const child of node.children) {
    traverse(child); // Function call overhead!
  }
}

// Kosten per node:
// - Function call allocation
// - Stack frame creation
// - Closure context
// - Garbage collection

Optie 2: Manual iterative traversal

typescript
// ❌ LANGZAAM - Array allocaties
function traverse(root: Node): void {
  const stack = [root]; // Array allocatie

  while (stack.length) {
    const node = stack.pop()!;
    processNode(node);

    // Nieuwe array per iteratie!
    stack.push(...Array.from(node.children));
  }
}

// Kosten per node:
// - Array allocation
// - Array spread operation
// - Push/pop overhead
// - Garbage collection pressure

Optie 3: TreeWalker (Optimaal)

typescript
// βœ… SNEL - Zero allocations
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);

while (walker.nextNode()) {
  processNode(walker.currentNode);
}

// Kosten per node:
// - Enkel C++ pointer movement
// - Geen allocaties
// - Direct memory access
// - Cache-friendly

Benchmark resultaten ​

Test: 100 elementen doorlopen

MethodeTijd (ms)AllocatiesMemory Druk
Manual Recursive2.5100+ closuresHoog
Manual Iterative1.8100+ arraysMiddel
TreeWalker0.30Nul

TreeWalker is 5-8x sneller! πŸš€

TreeWalker met custom filters ​

Een van de krachtigste features van TreeWalker is de mogelijkheid om runtime filtering toe te passen:

typescript
// Voorbeeld: Vind alle focusbare elementen
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
  acceptNode: (node) => {
    const el = node as HTMLElement;

    // Runtime property checks (niet mogelijk met CSS!)
    if (el.disabled) return NodeFilter.FILTER_REJECT;
    if (el.offsetParent === null) return NodeFilter.FILTER_REJECT;
    if (el.shadowRoot?.delegatesFocus) return NodeFilter.FILTER_ACCEPT;
    if (el.tabIndex >= 0) return NodeFilter.FILTER_ACCEPT;

    return NodeFilter.FILTER_SKIP;
  },
});

const focusableElements: HTMLElement[] = [];
while (walker.nextNode()) {
  focusableElements.push(walker.currentNode as HTMLElement);
}

Filter return values:

  • NodeFilter.FILTER_ACCEPT - Node accepteren
  • NodeFilter.FILTER_REJECT - Node en alle descendants rejecten
  • NodeFilter.FILTER_SKIP - Node skippen maar descendants wel checken

Praktisch voorbeeld: Focus detection ​

Laten we een real-world voorbeeld bekijken uit de dcr-dialog component:

typescript
/**
 * Verzamel alle focusbare elementen uit een element tree
 */
private _collectFocusableFromTree(
  root: Element,
  collection: HTMLElement[],
  seen: Set<HTMLElement>
): void {
  // Check root element zelf
  if (this._isFocusable(root) && !seen.has(root as HTMLElement)) {
    collection.push(root as HTMLElement);
    seen.add(root as HTMLElement);
  }

  // TreeWalker voor efficiΓ«nte traversal
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);

  while (walker.nextNode()) {
    const el = walker.currentNode as Element;

    if (this._isFocusable(el) && !seen.has(el as HTMLElement)) {
      collection.push(el as HTMLElement);
      seen.add(el as HTMLElement);
    }
  }
}

/**
 * Runtime check voor focusbare elementen
 * Dit kan NIET met CSS selectors!
 */
private _isFocusable(element: Element): element is HTMLElement {
  const el = element as HTMLElement;

  // Check JavaScript properties (geen attributes!)
  if (el.disabled === true) return false;

  // Shadow DOM delegatesFocus - alleen via runtime check
  if (el.shadowRoot?.delegatesFocus) return true;

  // Visibility check - alleen via runtime check
  if (el.offsetParent === null && getComputedStyle(el).position !== 'fixed') {
    return false;
  }

  // TabIndex property (niet attribute!)
  if (el.tabIndex >= 0) return true;

  return false;
}

Waarom TreeWalker hier essentieel is:

  1. βœ… Runtime property checks - element.disabled, element.offsetParent, element.shadowRoot
  2. βœ… Zero allocations - Geen arrays, geen recursie
  3. βœ… Dynamische logica - Complexe beslissingen per element
  4. βœ… Performance - Kan 100+ elementen checken in <1ms

TreeWalker voor slots en Shadow DOM ​

TreeWalker werkt perfect met Shadow DOM, maar je moet expliciet slotted content afhandelen:

typescript
private _getFirstAndLastFocusableChildren(): [HTMLElement, HTMLElement] | [null, null] {
  const focusableElements: HTMLElement[] = [];

  if (this.shadowRoot) {
    const walker = document.createTreeWalker(
      this.shadowRoot,
      NodeFilter.SHOW_ELEMENT
    );

    while (walker.nextNode()) {
      const el = walker.currentNode as Element;

      // Speciale afhandeling voor <slot> elementen
      if (el.tagName === 'SLOT') {
        const slot = el as HTMLSlotElement;
        const assigned = slot.assignedElements({ flatten: true });

        // Verwerk slotted content
        for (const assignedEl of assigned) {
          this._collectFocusableFromTree(assignedEl, focusableElements, new Set());
        }
      }
      // Normale shadow DOM elementen
      else if (this._isFocusable(el)) {
        focusableElements.push(el as HTMLElement);
      }
    }
  }

  return focusableElements.length
    ? [focusableElements[0], focusableElements[focusableElements.length - 1]]
    : [null, null];
}

Belangrijke aandachtspunten:

  • TreeWalker ziet alleen de <slot> tag, niet de assigned content
  • Je moet slot.assignedElements() gebruiken om slotted content te krijgen
  • Slotted content zit in de light DOM, niet shadow DOM
  • Gebruik recursive calls voor slotted content trees

Best practices ​

1. Kies de juiste tool voor de job ​

typescript
// βœ… GOED: querySelector voor simpele, bekende selectors
const header = this.shadowRoot.querySelector('.header');
const closeBtn = this.shadowRoot.querySelector('[data-action="close"]');

// βœ… GOED: Direct property access voor parent/children
const parent = element.parentElement;
const children = Array.from(element.children);

// βœ… GOED: TreeWalker voor dynamische, complexe logica
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
  if (complexRuntimeCheck(walker.currentNode)) {
    // ...
  }
}

// ❌ VERMIJD: TreeWalker voor simpele selectors
// Overkill - querySelector is hier beter
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
  if (walker.currentNode.matches('.button')) {
    // Gebruik gewoon querySelector('.button')!
  }
}

2. Cache waar zinvol, maar pas op voor premature optimization ​

βœ… Cache wanneer:

  • Traversal is duur (>5ms gemeten)
  • Wordt vaak herhaald (bijv. elke keypress)
  • DOM is stabiel (weinig changes)

❌ Vermijd caching wanneer:

  • Traversal is goedkoop (<1ms)
  • Cache invalidatie complex is
  • DOM vaak muteert

Voorbeeld: Cache met invalidation

typescript
private _focusableCache: HTMLElement[] | null = null;
private _cacheInvalidated = true;

private _getFocusableElements(): HTMLElement[] {
  // Return cached result als nog valid
  if (!this._cacheInvalidated && this._focusableCache) {
    return this._focusableCache;
  }

  // Traversal uitvoeren
  const walker = document.createTreeWalker(this, NodeFilter.SHOW_ELEMENT);
  const result: HTMLElement[] = [];

  while (walker.nextNode()) {
    if (this._isFocusable(walker.currentNode)) {
      result.push(walker.currentNode as HTMLElement);
    }
  }

  // Cache updaten
  this._focusableCache = result;
  this._cacheInvalidated = false;

  return result;
}

// Invalideer cache bij slot changes
private _handleSlotChange(): void {
  this._cacheInvalidated = true;
  this._focusableCache = null;
}

3. Gebruik type guards voor type safety ​

typescript
// Type guard voor focusbare elementen
function isFocusable(element: Element): element is HTMLElement {
  const el = element as HTMLElement;

  // Checks...
  if (el.shadowRoot?.delegatesFocus) return true;
  if (el.tabIndex >= 0) return true;

  return false;
}

// Gebruik in TreeWalker
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
const focusable: HTMLElement[] = [];

while (walker.nextNode()) {
  const el = walker.currentNode as Element;

  // Type guard geeft correcte type
  if (isFocusable(el)) {
    focusable.push(el); // el is nu HTMLElement, niet Element
    el.focus(); // Type-safe!
  }
}

4. Clean up TreeWalker instanties ​

TreeWalker instanties worden automatisch opgeruimd door de garbage collector, maar het is goed om dit expliciet te doen in loops:

typescript
// βœ… GOED: Walker in function scope (automatic cleanup)
private _findElements(): HTMLElement[] {
  const walker = document.createTreeWalker(this, NodeFilter.SHOW_ELEMENT);
  const result: HTMLElement[] = [];

  while (walker.nextNode()) {
    if (someCondition(walker.currentNode)) {
      result.push(walker.currentNode as HTMLElement);
    }
  }

  return result;
  // Walker wordt hier automatisch GC'd
}

// ⚠️ OPGELET: Walker als class member
private readonly walker = document.createTreeWalker(
  this,
  NodeFilter.SHOW_ELEMENT
);

// Moet expliciet opgeruimd worden in disconnectedCallback
disconnectedCallback() {
  super.disconnectedCallback();
  // Walker cleanup (technisch niet nodig, maar goed voor duidelijkheid)
  this.walker = null;
}

5. Documenteer complexe traversal logica ​

typescript
/**
 * Verzamelt focusbare elementen in de juiste volgorde:
 * 1. Content slot (formulier velden, etc.)
 * 2. Actions slot (custom buttons)
 * 3. Shadow DOM footer (prebuilt buttons)
 *
 * Headers worden bewust overgeslagen voor autofocus.
 *
 * @returns Tuple van [eerste focusbare, laatste focusbare] of [null, null]
 */
private _getFirstAndLastFocusableChildren(): [HTMLElement, HTMLElement] | [null, null] {
  // Implementatie met TreeWalker...
}

Geavanceerde patronen ​

1. Herbruikbare traversal utility ​

CreΓ«er herbruikbare utilities voor veelvoorkomende traversal patterns:

typescript
// _shared/utils/dom-traversal.ts

/**
 * Verzamel alle elementen die aan een predicate voldoen
 */
export function findElements(root: Element, predicate: (el: Element) => boolean): HTMLElement[] {
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
  const result: HTMLElement[] = [];

  while (walker.nextNode()) {
    const el = walker.currentNode as Element;
    if (predicate(el)) {
      result.push(el as HTMLElement);
    }
  }

  return result;
}

/**
 * Vind het eerste element dat aan predicate voldoet
 */
export function findFirstElement(root: Element, predicate: (el: Element) => boolean): HTMLElement | null {
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);

  while (walker.nextNode()) {
    const el = walker.currentNode as Element;
    if (predicate(el)) {
      return el as HTMLElement;
    }
  }

  return null;
}

// Gebruik in component
import { findElements, findFirstElement } from '../_shared/utils/dom-traversal';

const focusableElements = findElements(this, (el) => {
  return el.tabIndex >= 0 && !(el as HTMLElement).disabled;
});

const firstButton = findFirstElement(this, (el) => {
  return el.tagName === 'BUTTON' && !el.hasAttribute('disabled');
});

2. Focused traversal met early exit ​

Voor betere performance, stop de traversal zodra je hebt wat je nodig hebt:

typescript
/**
 * Vind eerste focusbaar element (met early exit)
 */
private _findFirstFocusable(): HTMLElement | null {
  const walker = document.createTreeWalker(
    this.shadowRoot,
    NodeFilter.SHOW_ELEMENT
  );

  while (walker.nextNode()) {
    const el = walker.currentNode as Element;

    if (el.tagName === 'SLOT') {
      const slot = el as HTMLSlotElement;
      const assigned = slot.assignedElements({ flatten: true });

      for (const assignedEl of assigned) {
        const focusable = this._findFirstFocusableInTree(assignedEl);
        if (focusable) return focusable; // Early exit!
      }
    }
    else if (this._isFocusable(el)) {
      return el as HTMLElement; // Early exit!
    }
  }

  return null;
}

3. Traversal met context tracking ​

Track extra context tijdens traversal voor complexe use cases:

typescript
interface TraversalContext {
  depth: number;
  parent: Element | null;
  index: number;
}

function traverseWithContext(root: Element, callback: (el: Element, context: TraversalContext) => void): void {
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);

  let depth = 0;
  let parent: Element | null = root;
  let index = 0;

  while (walker.nextNode()) {
    const el = walker.currentNode as Element;

    // Track depth changes
    if (el.parentElement !== parent) {
      if (el.parentElement === walker.currentNode) {
        depth++;
      } else {
        depth--;
      }
      parent = el.parentElement;
      index = 0;
    }

    callback(el, { depth, parent, index });
    index++;
  }
}

// Gebruik
traverseWithContext(this, (el, { depth, index }) => {
  console.log(`Element ${el.tagName} op depth ${depth}, index ${index}`);
});

Performance tips ​

1. Batch DOM reads en writes ​

typescript
// ❌ SLECHT: Interleaved reads en writes (causes layout thrashing)
elements.forEach((el) => {
  const height = el.offsetHeight; // READ (forces layout)
  el.style.top = height + 'px'; // WRITE
});

// βœ… GOED: Batch reads, dan writes
const heights = elements.map((el) => el.offsetHeight); // Alle READS
heights.forEach((height, i) => {
  // Alle WRITES
  elements[i].style.top = height + 'px';
});

2. Gebruik requestAnimationFrame voor visual updates ​

typescript
private _updateFocusStyles(): void {
  requestAnimationFrame(() => {
    const walker = document.createTreeWalker(this, NodeFilter.SHOW_ELEMENT);

    while (walker.nextNode()) {
      const el = walker.currentNode as HTMLElement;
      if (this._isFocusable(el)) {
        el.classList.add('focusable-highlight');
      }
    }
  });
}

3. Limiteer diepte voor grote trees ​

typescript
function traverseWithMaxDepth(root: Element, maxDepth: number, callback: (el: Element) => void): void {
  let depth = 0;
  let currentParent = root;

  const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);

  while (walker.nextNode()) {
    const el = walker.currentNode as Element;

    // Track depth
    if (el.parentElement !== currentParent) {
      if (el.parentElement?.contains(currentParent)) {
        depth++;
      } else {
        depth--;
      }
      currentParent = el.parentElement;
    }

    // Stop bij max depth
    if (depth > maxDepth) break;

    callback(el);
  }
}

Samenvatting ​

Beslissingsboom ​

Moet je DOM elementen vinden?
β”‚
β”œβ”€ Simpel, bekend element?
β”‚  └─ βœ… Gebruik querySelector/querySelectorAll
β”‚
β”œβ”€ Direct parent/child?
β”‚  └─ βœ… Gebruik element.parentElement / element.children
β”‚
β”œβ”€ Runtime property checks nodig?
β”‚  └─ βœ… Gebruik TreeWalker met custom filter
β”‚
β”œβ”€ Grote tree (>50 elementen) doorlopen?
β”‚  └─ βœ… Gebruik TreeWalker
β”‚
└─ Complexe dynamische logica?
   └─ βœ… Gebruik TreeWalker met predicate functie

Key takeaways ​

  1. querySelector/querySelectorAll

    • Perfect voor simpele, statische selectors
    • Vertrouwde CSS syntax
    • Beperkt tot wat CSS kan uitdrukken
  2. Manual traversal

    • Goed voor directe relaties (parent/children)
    • Eenvoudig en direct
    • Niet geschikt voor grote trees
  3. TreeWalker

    • Beste performance voor complexe traversal
    • Runtime property checks mogelijk
    • Zero allocations, C++ geoptimaliseerd
    • Vereist meer code maar biedt maximale flexibiliteit

De keuze hangt af van je specifieke use case - gebruik de eenvoudigste tool die het werk doet, maar schakel over naar TreeWalker wanneer je complexe logica of optimale performance nodig hebt.