| name | web-components |
| description | Web Component patterns for Oh My Brand! frontend interactivity. Custom element lifecycle, attribute observation, event handling, and accessibility. Use when writing view.ts files. |
| metadata | {"author":"Wesley Smits","version":"1.0.0"} |
Web Components
Web Component patterns for frontend interactivity in the Oh My Brand! WordPress FSE theme.
When to Use
- Adding frontend interactivity to blocks (carousels, accordions, lightboxes)
- Creating reusable interactive components
- Handling user interactions (clicks, keyboard, touch)
- Managing component state on the frontend
Reference Files
Custom Element Structure
class OmbGalleryCarousel extends HTMLElement {
static observedAttributes = ['visible-images'];
#gallery: HTMLElement | null = null;
#items: NodeListOf<HTMLElement> | null = null;
#currentIndex = 0;
connectedCallback(): void {
this.#readAttributes();
this.#queryElements();
this.#bindEvents();
this.#initialize();
}
disconnectedCallback(): void {
this.#unbindEvents();
}
attributeChangedCallback(
name: string,
oldValue: string | null,
newValue: string | null
): void {
if (oldValue === newValue) return;
}
}
if (!customElements.get('omb-gallery-carousel')) {
customElements.define('omb-gallery-carousel', OmbGalleryCarousel);
}
See OmbGalleryCarousel.ts for the complete implementation.
Lifecycle Methods
connectedCallback
Called when element is added to the DOM:
connectedCallback(): void {
this.#readAttributes();
this.#queryElements();
this.#bindEvents();
this.#initialize();
}
disconnectedCallback
Called when element is removed from the DOM:
disconnectedCallback(): void {
this.#unbindEvents();
if (this.#animationFrame) cancelAnimationFrame(this.#animationFrame);
if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
}
attributeChangedCallback
Called when observed attribute changes:
static observedAttributes = ['visible-images', 'autoplay'];
attributeChangedCallback(
name: string,
oldValue: string | null,
newValue: string | null
): void {
if (oldValue === newValue) return;
switch (name) {
case 'visible-images':
this.#visibleImages = newValue ? parseInt(newValue, 10) : 3;
this.#updateLayout();
break;
case 'autoplay':
newValue !== null ? this.#startAutoplay() : this.#stopAutoplay();
break;
}
}
Attribute Handling
Boolean Attributes
const hasAutoplay = this.hasAttribute('autoplay');
this.setAttribute('loading', '');
this.removeAttribute('loading');
this.toggleAttribute('loading');
Value Attributes
const value = this.getAttribute('visible-images');
const parsed = value ? parseInt(value, 10) : 3;
this.setAttribute('visible-images', '4');
Data Attributes
const config = JSON.parse(this.dataset.config || '{}');
this.dataset.state = 'loading';
Event Handling
Arrow Function Methods
Use arrow functions to preserve this context:
class OmbComponent extends HTMLElement {
#handleClick = (event: MouseEvent): void => {
event.preventDefault();
this.#doSomething();
};
#bindEvents(): void {
this.addEventListener('click', this.#handleClick);
}
#unbindEvents(): void {
this.removeEventListener('click', this.#handleClick);
}
}
Custom Events
this.dispatchEvent(
new CustomEvent('omb-gallery:slide-change', {
bubbles: true,
detail: { index: this.#currentIndex, total: this.#items?.length ?? 0 },
})
);
Event Delegation
#handleContainerClick = (event: MouseEvent): void => {
const target = event.target as HTMLElement;
const item = target.closest('[data-gallery-item]');
if (item) this.#handleItemClick(item as HTMLElement);
};
DOM Queries
Query and Cache Elements
#queryElements(): void {
this.#container = this.querySelector('[data-container]');
this.#items = this.querySelectorAll('[data-item]');
this.#button = this.querySelector('button') as HTMLButtonElement | null;
}
Null Safety
if (!this.#container) return;
this.#button?.click();
const count = this.#items?.length ?? 0;
Accessibility
Live Regions
#announce(): void {
if (!this.#liveRegion) return;
this.#liveRegion.textContent = `Showing image ${this.#currentIndex + 1} of ${this.#items?.length}`;
}
Keyboard Navigation
#handleKeydown = (event: KeyboardEvent): void => {
switch (event.key) {
case 'ArrowLeft':
case 'ArrowUp':
event.preventDefault();
this.#navigatePrevious();
break;
case 'ArrowRight':
case 'ArrowDown':
event.preventDefault();
this.#navigateNext();
break;
case 'Escape':
event.preventDefault();
this.#close();
break;
}
};
Focus Management
#open(): void {
this.#previousFocus = document.activeElement as HTMLElement;
this.#dialog?.showModal();
this.#dialog?.querySelector<HTMLElement>('button')?.focus();
}
#close(): void {
this.#dialog?.close();
this.#previousFocus?.focus();
}
Reduced Motion
#shouldReduceMotion(): boolean {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
#scrollToIndex(): void {
const behavior = this.#shouldReduceMotion() ? 'auto' : 'smooth';
this.#gallery?.scrollTo({ left: scrollLeft, behavior });
}
Registration
Guard Against Re-registration
if (!customElements.get('omb-gallery-carousel')) {
customElements.define('omb-gallery-carousel', OmbGalleryCarousel);
}
Naming Convention
| Pattern | Example |
|---|
| Tag name | omb-{block-name} |
| Class name | Omb{BlockName} |
Examples:
omb-gallery-carousel → OmbGalleryCarousel
omb-faq-accordion → OmbFaqAccordion
Related Skills
References