UIElement Docs Version 0.8.5

๐Ÿงช Examples & Recipes

Discover practical examples and patterns for building reactive, modular components with UIElement. Each example focuses on showcasing a specific feature or best practice, guiding you through real-world use cases.

What You'll Learn

This collection of examples demonstrates a range of scenarios, from simple state updates in a single component to managing complex interactions across multiple components. Here's an overview of what you'll find:

Whether you're getting started with a basic component or building a full-featured application, these examples will help you understand how to use UIElement effectively to build reactive Web Components.

MySlider Example

Slide 1
Slide 2
Slide 3
Source Code

HTML


			<my-slider>
			<button class="prev">Previous</button>
			<div class="slides">
				<div class="slide">Slide 1</div>
				<div class="slide">Slide 2</div>
				<div class="slide">Slide 3</div>
			</div>
			<button class="next">Next</button>
			<div class="dots"></div>
			</my-slider>
					

CSS


			my-slider {
			display: flex;
			align-items: center;
			
			.slides {
				display: flex;
				overflow: hidden;
			}
			
			.slide {
				min-width: 100%;
				transition: transform 0.3s ease;
			}
			
			.slide:not(.active) {
				display: none;
			}
			
			.dots {
				display: flex;
				gap: 5px;
				margin-top: 10px;
			}
			
			.dot {
				width: 10px;
				height: 10px;
				border-radius: 50%;
				background-color: gray;
			}
			
			.dot.active {
				background-color: black;
			}
			
			button {
				margin: 0 10px;
			}
			}
					

JavaScript


			import { UIElement, on, toggleClass } from '@efflore/ui-element';
			
			class MySlider extends UIElement {
			connectedCallback() {
				super.connectedCallback();
			
				// Initialize state for the active slide index
				this.set('activeIndex', 0);
				const slides = this.querySelectorAll('.slide');
				this.set('totalSlides', slides.length);
			
				// Generate dots based on totalSlides
				const dotsContainer = this.first('.dots').target;
				slides.forEach(() => {
				const dot = document.createElement('span');
				dot.className = 'dot';
				dotsContainer.appendChild(dot);
				});
			
				// Event listeners for navigation
				const getNewIndex = (prev, direction) => (prev + direction + slides.length) % slides.length;
				this.first('.prev').map(on('click', () => this.set('activeIndex', (prev) => getNewIndex(prev, -1))));
				this.first('.next').map(on('click', () => this.set('activeIndex', (prev) => getNewIndex(prev, 1))));
			
				// Auto-effects for updating slides and dots
				this.all('.slide').map((el, idx) => toggleClass('active', () => idx === this.get('activeIndex')));
				this.all('.dot').map((el, idx) => toggleClass('active', () => idx === this.get('activeIndex')));
			}
			}
			
			MySlider.define('my-slider');
					

TabList and TabPanel Example

Content for Tab 1 Content for Tab 2 Content for Tab 3
Source Code

HTML


			  <tab-list>
				<button class="tab-button">Tab 1</button>
				<button class="tab-button">Tab 2</button>
				<button class="tab-button">Tab 3</button>
			  
				<tab-panel>Content for Tab 1</tab-panel>
				<tab-panel>Content for Tab 2</tab-panel>
				<tab-panel>Content for Tab 3</tab-panel>
			  </tab-list>
					  

CSS


			  tab-list {
				display: flex;
				flex-direction: column;
			  
				.tab-button {
				  cursor: pointer;
				  padding: 10px;
				  border: none;
				  background: lightgray;
				  transition: background-color 0.2s ease;
				}
			  
				.tab-button:hover {
				  background-color: darkgray;
				}
			  }
			  
			  tab-panel {
				display: none;
			  
				&.active {
				  display: block;
				}
			  }
					  

JavaScript


			  import { UIElement, on, pass, toggleClass } from '@efflore/ui-element';
			  
			  class TabList extends UIElement {
				connectedCallback() {
				  // Initialize state for active tab index
				  this.set('activeIndex', 0);
			  
				  // Event listeners for tab buttons
				  this.all('.tab-button').map((el, idx) =>
					on('click', () => this.set('activeIndex', idx))(el)
				  );
			  
				  // Pass active state to TabPanels
				  this.all('tab-panel').forEach((el, idx) =>
					pass({ active: () => idx === this.get('activeIndex') })(el)
				  );
				}
			  }
			  
			  class TabPanel extends UIElement {
				connectedCallback() {
				  // Toggle visibility based on 'active' state
				  this.self.map(toggleClass('active'));
				}
			  }
			  
			  TabList.define('tab-list');
			  TabPanel.define('tab-panel');
					  

TodoApp Example

Todo List

    Well done, all done!

    tasks left

    Filter
    Source Code

    HTML

    
    			  <todo-app>
    				<todo-form>
    				  <form action="#">
    					<input-field>
    					  <label for="add-todo">What needs to be done?</label>
    					  <input id="add-todo" type="text" value="" required />
    					</input-field>
    					<input-button class="submit">
    					  <button type="submit" disabled>Add Todo</button>
    					</input-button>
    				  </form>
    				</todo-form>
    			  
    				<todo-list filter="all">
    				  <ul></ul>
    				  <template id="todo-list-item">
    					<li>
    					  <todo-item>
    						<label>
    						  <input type="checkbox" />
    						  <span></span>
    						</label>
    						<button type="button">Delete</button>
    					  </todo-item>
    					</li>
    				  </template>
    				</todo-list>
    			  
    				<todo-count>
    				  <p class="all-done">Well done, all done!</p>
    				  <p class="remaining"><span></span> tasks left</p>
    				</todo-count>
    			  
    				<todo-filter>
    				  <fieldset>
    					<legend>Filter</legend>
    					<input type="radio" id="filter-all" name="filter" value="all" checked />
    					<label for="filter-all">All</label>
    					<input type="radio" id="filter-active" name="filter" value="active" />
    					<label for="filter-active">Active</label>
    					<input type="radio" id="filter-completed" name="filter" value="completed" />
    					<label for="filter-completed">Completed</label>
    				  </fieldset>
    				</todo-filter>
    			  
    				<input-button class="clear-completed">
    				  <button type="button">Clear Completed</button>
    				</input-button>
    			  </todo-app>
    					  

    CSS

    
    			  /* Styles for todo-list */
    			  todo-list {
    			  
    				&[filter="completed"] {
    				  li:not(:has(.completed)) {
    					display: none;
    				  }
    				}
    			  
    				&[filter="active"] {
    				  li:has(.completed) {
    					display: none;
    				  }
    				}
    			  }
    			  
    			  /* Styles for todo-item */
    			  todo-item {
    				&.completed {
    				  span {
    					text-decoration: line-through;
    					opacity: 0.6;
    				  }
    				}
    			  }
    			  
    			  /* Styles for todo-filter */
    			  todo-filter {
    				> fieldset {
    				  border: none;
    				  margin: 0;
    				  padding: 0.5rem 0 1rem;
    				}
    			  }
    					  

    JavaScript

    
    						import { UIElement, on, pass, toggleClass, toggleAttribute, setText, setProperty } from '@efflore/ui-element';
    						
    						// TodoApp - coordinator of all components
    						class TodoApp extends UIElement {
    						  connectedCallback() {
    							// Pass filter state from todo-filter to todo-list
    							this.first('todo-filter').map(pass({ selected: 'filter' }, 'todo-list'));
    						
    							// Count remaining tasks from todo-list and pass to todo-count
    							this.first('todo-list').map(pass({ remaining: 'count' }, 'todo-count'));
    						
    							// Clear completed tasks when the "Clear Completed" button is clicked
    							this.first('.clear-completed').map(on('click', () => this.first('todo-list').target.clearCompleted()));
    							this.first('.clear-completed').map(toggleAttribute('disabled', 'none-completed'));
    						
    							// Listen for "add-task" event and add the task to the todo-list
    							this.on('add-task', ({ detail }) => this.first('todo-list').target.addItem(detail));
    						  }
    						}
    						TodoApp.define('todo-app');
    						
    						// TodoForm - handles adding new tasks
    						class TodoForm extends UIElement {
    						  connectedCallback() {
    							// Listen for changes in input fields and pass the valid state to enable/disable the submit button
    							this.all('input-field').map(pass({ valid: 'valid' }, '.submit'));
    						
    							// Prevent form submission on enter key
    							this.self.map(on('keydown', e => e.key === 'Enter' && e.preventDefault()));
    						
    							// Handle form submission
    							this.self.map(on('submit', e => {
    							  e.preventDefault();
    							  this.emit('add-task', { detail: this.first('input-field').target.value });
    							  this.first('input-field').target.clearField();
    							}));
    						  }
    						}
    						TodoForm.define('todo-form');
    						
    						// InputField - handles form input
    						class InputField extends UIElement {
    						  connectedCallback() {
    							// Synchronize input value and validity
    							this.self.map(on('change', 'value'));
    							this.self.map(on('input', 'value'));
    							this.self.map(setProperty('valid', el => el.checkValidity()));
    						  }
    						
    						  // Clear the input field
    						  clearField() {
    							this.set('value', '');
    						  }
    						}
    						InputField.define('input-field');
    						
    						// InputButton - submit button logic
    						class InputButton extends UIElement {
    						  connectedCallback() {
    							// Toggle disabled state based on 'disabled' signal
    							this.self.map(toggleAttribute('disabled'));
    						  }
    						}
    						InputButton.define('input-button');
    						
    						// TodoList - manages task list
    						class TodoList extends UIElement {
    						  connectedCallback() {
    							// Get template for new todo items
    							const template = this.querySelector('template').content;
    						
    							// Count remaining and completed tasks
    							this.effect(() => {
    							  const remaining = this.querySelectorAll('todo-item:not(.completed)').length;
    							  const completed = this.querySelectorAll('todo-item.completed').length;
    							  this.set('remaining', remaining);
    							  this.set('none-completed', completed === 0);
    							});
    						
    							// Handle filter state
    							this.set('filter', 'all');
    						
    							// Add new item to the list
    							this.on('add-item', ({ detail }) => this.addItem(detail));
    						  }
    						
    						  addItem(task) {
    							const listItem = document.importNode(this.querySelector('template').content, true);
    							listItem.querySelector('span').textContent = task;
    							this.querySelector('ul').appendChild(listItem);
    						  }
    						
    						  clearCompleted() {
    							this.querySelectorAll('todo-item.completed').forEach(item => item.remove());
    						  }
    						}
    						TodoList.define('todo-list');
    						
    						// TodoItem - represents individual task
    						class TodoItem extends UIElement {
    						  connectedCallback() {
    							// Toggle 'completed' state and class based on checkbox state
    							this.first('input[type="checkbox"]').map(on('change', () => this.toggleCompleted()));
    							this.self.map(toggleClass('completed', 'completed'));
    						  }
    						
    						  toggleCompleted() {
    							this.set('completed', !this.get('completed'));
    						  }
    						}
    						TodoItem.define('todo-item');
    						
    						// TodoCount - displays count of active tasks
    						class TodoCount extends UIElement {
    						  connectedCallback() {
    							// Show a message when all tasks are completed
    							this.self.map(toggleClass('none', () => this.get('count') === 0));
    							
    							// Display remaining tasks
    							this.first('.remaining span').map(setText('count'));
    						  }
    						}
    						TodoCount.define('todo-count');
    						
    						// TodoFilter - provides filtering options
    						class TodoFilter extends UIElement {
    						  connectedCallback() {
    							// Track selected filter
    							this.self.map(on('change', 'selected'));
    						  }
    						}
    						TodoFilter.define('todo-filter');
    						

    MediaContext Example

    The MediaContext component provides global state to any sub-components in its DOM tree by exposing context for responsive and adaptive features. It tracks the following:

    Configuring Breakpoints

    The viewport sizes can be customized by providing attributes on the media-context element:

    For example, to set a small breakpoint at 40em and a medium breakpoint at 60em, use:

    
    			  <media-context sm="40em" md="60em"></media-context>
    				
    Source Code
    
    			  import { UIElement, maybe } from '@efflore/ui-element'
    			  
    			  const VIEWPORT_XS = 'xs';
    			  const VIEWPORT_SM = 'sm';
    			  const VIEWPORT_MD = 'md';
    			  const VIEWPORT_LG = 'lg';
    			  const VIEWPORT_XL = 'xl';
    			  const ORIENTATION_LANDSCAPE = 'landscape';
    			  const ORIENTATION_PORTRAIT = 'portrait';
    			  
    			  class MediaContext extends UIElement {
    				static providedContexts = ['media-motion', 'media-theme', 'media-viewport', 'media-orientation'];
    			  
    				connectedCallback() {
    				  const getBreakpoints = () => {
    					const parseBreakpoint = (breakpoint) => {
    					  const attr = this.getAttribute(breakpoint)?.trim();
    					  if (!attr) return null;
    					  const unit = attr.match(/em$/) ? 'em' : 'px';
    					  const value = maybe(parseFloat(attr)).filter(Number.isFinite)[0];
    					  return value ? value + unit : null;
    					};
    			  
    					const sm = parseBreakpoint(VIEWPORT_SM) || '32em';
    					const md = parseBreakpoint(VIEWPORT_MD) || '48em';
    					const lg = parseBreakpoint(VIEWPORT_LG) || '72em';
    					const xl = parseBreakpoint(VIEWPORT_XL) || '108em';
    					return { sm, md, lg, xl };
    				  };
    				  
    				  const breakpoints = getBreakpoints();
    			  
    				  const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');
    				  const darkMode = matchMedia('(prefers-color-scheme: dark)');
    				  const screenSmall = matchMedia(`(min-width: ${breakpoints.sm})`);
    				  const screenMedium = matchMedia(`(min-width: ${breakpoints.md})`);
    				  const screenLarge = matchMedia(`(min-width: ${breakpoints.lg})`);
    				  const screenXLarge = matchMedia(`(min-width: ${breakpoints.xl})`);
    				  const screenOrientation = matchMedia('(orientation: landscape)');
    			  
    				  const getViewport = () => {
    					if (screenXLarge.matches) return VIEWPORT_XL;
    					if (screenLarge.matches) return VIEWPORT_LG;
    					if (screenMedium.matches) return VIEWPORT_MD;
    					if (screenSmall.matches) return VIEWPORT_SM;
    					return VIEWPORT_XS;
    				  };
    			  
    				  this.set('media-motion', reducedMotion.matches);
    				  this.set('media-theme', darkMode.matches);
    				  this.set('media-viewport', getViewport());
    				  this.set('media-orientation', screenOrientation.matches ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT);
    			  
    				  reducedMotion.onchange = (e) => this.set('media-motion', e.matches);
    				  darkMode.onchange = (e) => this.set('media-theme', e.matches);
    				  screenSmall.onchange = () => this.set('media-viewport', getViewport());
    				  screenMedium.onchange = () => this.set('media-viewport', getViewport());
    				  screenLarge.onchange = () => this.set('media-viewport', getViewport());
    				  screenXLarge.onchange = () => this.set('media-viewport', getViewport());
    				  screenOrientation.onchange = (e) => this.set('media-orientation', e.matches ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT);
    				}
    			  }
    			  
    			  MediaContext.define('media-context');
    				  

    ThemedComponent Example

    This component changes its background based on the theme!
    Source Code

    HTML

    
    			  <media-context>
    				<themed-component>
    				  This component changes its background based on the theme!
    				</themed-component>
    			  </media-context>
    					  

    CSS

    
    			  themed-component {
    				display: block;
    				padding: 20px;
    				color: white;
    				transition: background-color 0.3s ease;
    			  
    				&.dark {
    				  background-color: black;
    				}
    			  
    				&.light {
    				  background-color: lightgray;
    				}
    			  }
    					  

    JavaScript

    
    			  import { UIElement, toggleClass } from '@efflore/ui-element';
    			  
    			  class ThemedComponent extends UIElement {
    				static consumedContexts = ['media-theme'];
    			  
    				connectedCallback() {
    				  // Toggle the class based on 'media-theme' signal
    				  this.self.map(toggleClass('dark', () => this.get('media-theme')));
    				  this.self.map(toggleClass('light', () => !this.get('media-theme')));
    				}
    			  }
    			  
    			  ThemedComponent.define('themed-component');
    					  

    AnimatedComponent Example

    Box 1
    Box 2
    Box 3
    Source Code

    HTML

    
    			  <media-context>
    				<animated-component>
    				  <div class="animated-box">Box 1</div>
    				  <div class="animated-box">Box 2</div>
    				  <div class="animated-box">Box 3</div>
    				</animated-component>
    			  </media-context>
    					  

    CSS

    
    			  animated-component {
    				display: block;
    				padding: 20px;
    				overflow: hidden;
    			  
    				.animated-box {
    				  width: 50px;
    				  height: 50px;
    				  margin: 10px;
    				  background-color: lightblue;
    				  text-align: center;
    				  line-height: 50px;
    				  font-weight: bold;
    				  color: white;
    				}
    			  
    				&.no-motion .animated-box {
    				  opacity: 0;
    				  transition: opacity 1s ease-in;
    				}
    			  
    				&.motion .animated-box {
    				  animation: moveAndFlash 2s infinite ease-in-out alternate;
    				}
    			  
    				@keyframes moveAndFlash {
    				  0% {
    					transform: translateX(0);
    					background-color: lightblue;
    				  }
    				  100% {
    					transform: translateX(100px);
    					background-color: lightcoral;
    				  }
    				}
    			  }
    					  

    JavaScript

    
    			  import { UIElement, toggleClass } from '@efflore/ui-element';
    			  
    			  class AnimatedComponent extends UIElement {
    				static consumedContexts = ['media-motion'];
    			  
    				connectedCallback() {
    				  // Toggle classes based on 'media-motion' context
    				  this.self.map(toggleClass('motion', () => !this.get('media-motion')));
    				  this.self.map(toggleClass('no-motion', 'media-motion'));
    				}
    			  }
    			  
    			  AnimatedComponent.define('animated-component');
    					  

    Responsive TabList Example

    Content for Tab 1
    Content for Tab 2
    Content for Tab 3
    Source Code

    HTML

    
    			  <media-context>
    				<tab-list>
    				  <button class="tab-button">Tab 1</button>
    				  <button class="tab-button">Tab 2</button>
    				  <button class="tab-button">Tab 3</button>
    			  
    				  <tab-panel>
    					<button class="panel-header">Tab 1</button>
    					<div class="panel-content">Content for Tab 1</div>
    				  </tab-panel>
    				  <tab-panel>
    					<button class="panel-header">Tab 2</button>
    					<div class="panel-content">Content for Tab 2</div>
    				  </tab-panel>
    				  <tab-panel>
    					<button class="panel-header">Tab 3</button>
    					<div class="panel-content">Content for Tab 3</div>
    				  </tab-panel>
    				</tab-list>
    			  </media-context>
    					  

    CSS

    
    			  tab-list {
    				display: flex;
    				flex-direction: column;
    			  
    				&.accordion .tab-button {
    				  display: none; /* Hide tab buttons in accordion mode */
    				}
    			  
    				.tab-button {
    				  cursor: pointer;
    				  padding: 10px;
    				  border: none;
    				  background: lightgray;
    				  transition: background-color 0.2s ease;
    				}
    			  
    				.tab-button.active {
    				  background-color: gray;
    				}
    			  }
    			  
    			  tab-panel {
    				display: none;
    			  
    				&.active {
    				  display: block;
    				}
    			  
    				&.collapsible {
    				  .panel-header {
    					cursor: pointer;
    					padding: 10px;
    					background-color: lightgray;
    					border: none;
    					outline: none;
    				  }
    			  
    				  .panel-header:hover {
    					background-color: darkgray;
    				  }
    			  
    				  .panel-header.active {
    					background-color: gray;
    				  }
    			  
    				  .panel-content {
    					display: none;
    					padding: 10px;
    				  }
    			  
    				  &.active .panel-content {
    					display: block;
    				  }
    				}
    			  }
    					  

    JavaScript

    
    			  import { UIElement, on, pass, toggleClass } from '@efflore/ui-element';
    			  
    			  // TabList Component
    			  class TabList extends UIElement {
    				static consumedContexts = ['media-viewport'];
    			  
    				connectedCallback() {
    				  super.connectedCallback(); // Necessary to consume context
    			  
    				  // Set 'accordion' signal based on viewport size
    				  this.set('accordion', () => ['xs', 'sm'].includes(this.get('media-viewport')));
    			  
    				  // Toggle 'accordion' class based on the signal
    				  this.self.map(toggleClass('accordion'));
    			  
    				  // Pass 'collapsible' state to tab-panels based on 'accordion' state
    				  this.all('tab-panel').forEach(pass({ collapsible: 'accordion' }));
    			  
    				  // Handle tab clicks in normal tabbed mode
    				  this.all('.tab-button').map((el, idx) =>
    					on('click', () => this.set('activeIndex', idx))(el)
    				  );
    			  
    				  // Set active tab-panel based on 'activeIndex'
    				  this.all('tab-panel').map((el, idx) =>
    					this.self.map(toggleClass('active', () => idx === this.get('activeIndex'))(el))
    				  );
    				}
    			  }
    			  
    			  TabList.define('tab-list');
    			  
    			  // TabPanel Component
    			  class TabPanel extends UIElement {
    				static observedAttributes = ['collapsible'];
    			  
    				connectedCallback() {
    				  super.connectedCallback(); // Ensure correct setup with context
    			  
    				  // Handle expanding/collapsing if 'collapsible' is true
    				  this.self.map(toggleClass('collapsible', 'collapsible'));
    			  
    				  if (this.get('collapsible')) {
    					const header = this.querySelector('.panel-header');
    					header.addEventListener('click', () => {
    					  this.set('expanded', !this.get('expanded'));
    					});
    			  
    					this.self.map(toggleClass('active', 'expanded'));
    				  }
    				}
    			  }
    			  
    			  TabPanel.define('tab-panel');
    					  

    Responsive Image Gallery Example

    Image 1 Image 2 Image 3 Image 4 Image 5
    Source Code

    HTML

    
    			  <media-context>
    				<responsive-image-gallery>
    				  <img src="image1.jpg" alt="Image 1">
    				  <img src="image2.jpg" alt="Image 2">
    				  <img src="image3.jpg" alt="Image 3">
    				  <img src="image4.jpg" alt="Image 4">
    				  <img src="image5.jpg" alt="Image 5">
    				</responsive-image-gallery>
    			  </media-context>
    					  

    CSS

    
    			  responsive-image-gallery {
    				display: flex;
    				flex-wrap: wrap;
    				gap: 10px;
    				padding: 10px;
    				transition: all 0.3s ease;
    			  
    				&.landscape {
    				  flex-direction: row;
    				  justify-content: space-between;
    				}
    			  
    				&.portrait {
    				  flex-direction: column;
    				}
    			  
    				img {
    				  flex: 1 1 calc(20% - 10px); /* Creates a grid with up to 5 images per row */
    				  max-width: calc(20% - 10px);
    				  height: auto;
    				  border: 2px solid transparent;
    				  border-radius: 5px;
    				  cursor: pointer;
    				}
    			  
    				&.portrait img {
    				  flex: 0 0 100%; /* Each image takes full width in slider mode */
    				  max-width: 100%;
    				  margin-bottom: 10px;
    				}
    			  }
    					  

    JavaScript

    
    			  import { UIElement, toggleClass, effect } from '@efflore/ui-element';
    			  import { MySlider } from './my-slider.js'; // Assume this is the existing MySlider component
    			  
    			  class ResponsiveImageGallery extends UIElement {
    				static consumedContexts = ['media-orientation'];
    			  
    				connectedCallback() {
    				  super.connectedCallback(); // Ensure correct setup with context
    			  
    				  // Toggle classes based on orientation
    				  this.self.map(toggleClass('landscape', () => this.get('media-orientation') === 'landscape'));
    				  this.self.map(toggleClass('portrait', () => this.get('media-orientation') === 'portrait'));
    			  
    				  // Dynamically wrap images in  for portrait mode
    				  effect(enqueue => {
    					if (this.get('media-orientation') === 'portrait') {
    					  if (!this.slider) {
    						this.slider = document.createElement('my-slider');
    						while (this.firstChild) {
    						  this.slider.appendChild(this.firstChild);
    						}
    						enqueue(this, 'add-slider', el => () => el.appendChild(this.slider));
    					  }
    					} else {
    					  // Remove  and display images as a grid in landscape mode
    					  if (this.slider) enqueue(this.slider, 'remove-slider', el => () => el.remove());
    					}
    				  });
    				}
    			  }
    			  
    			  ResponsiveImageGallery.define('responsive-image-gallery');
    					  

    CodeBlock Example

    
    			  import { UIElement, effect, asBoolean } from '@efflore/ui-element';
    			  import Prism from 'prismjs';
    			  
    			  class CodeBlock extends UIElement {
    				static observedAttributes = ['collapsed'];
    				
    				attributeMap = {
    				  collapsed: asBoolean,
    				};
    			  
    				connectedCallback() {
    				  super.connectedCallback();
    				  
    				  // Synchronize code content
    				  this.set('code', this.innerHTML.trim());
    			  
    				  // Effect to highlight code using Prism.js
    				  this.effect(() => {
    					const highlightedCode = Prism.highlight(this.get('code'), Prism.languages[this.get('language') || 'html'], this.get('language') || 'html');
    					this.querySelector('code').innerHTML = highlightedCode;
    				  });
    			  
    				  // Copy-to-clipboard functionality
    				  this.first('.copy').map(on('click', () => {
    					navigator.clipboard.writeText(this.get('code')).then(() => {
    					  this.set('copying', true);
    					  setTimeout(() => this.set('copying', false), 2000);
    					});
    				  }));
    				  
    				  // Toggle collapse state
    				  this.first('.overlay').map(on('click', () => this.set('collapsed', false)));
    				}
    			  }
    			  
    			  CodeBlock.define('code-block');
    				  
    Source Code

    HTML

    
    			  <code-block language="javascript" collapsed>
    				<pre><code>
    				  // Your code snippet goes here
    				</code></pre>
    			  </code-block>
    					  

    CSS

    
    			  code-block {
    				display: block;
    				position: relative;
    				margin: 1rem 0;
    				padding: var(--padding, 1rem);
    				background-color: var(--background-color, #2d2d2d);
    				color: var(--text-color, #ccc);
    				border-radius: 5px;
    				overflow: hidden;
    			  
    				.meta {
    				  display: flex;
    				  justify-content: space-between;
    				  font-size: 0.8rem;
    				  margin-bottom: 0.5rem;
    				}
    			  
    				.copy {
    				  cursor: pointer;
    				}
    			  
    				.overlay {
    				  position: absolute;
    				  bottom: 0;
    				  left: 0;
    				  right: 0;
    				  height: 3rem;
    				  background: linear-gradient(to bottom, transparent, #2d2d2d);
    				  cursor: pointer;
    				  text-align: center;
    				  padding: 1rem 0;
    				}
    			  
    				&.collapsed pre {
    				  max-height: 12rem;
    				  overflow: hidden;
    				}
    			  
    				&.collapsed .copy {
    				  display: none;
    				}
    			  
    				&.collapsed .overlay {
    				  display: block;
    				}
    			  }
    					  

    JavaScript

    
    			  import { UIElement, effect, asBoolean } from '@efflore/ui-element';
    			  import Prism from 'prismjs';
    			  
    			  class CodeBlock extends UIElement {
    				static observedAttributes = ['collapsed'];
    				
    				attributeMap = {
    				  collapsed: asBoolean,
    				};
    			  
    				connectedCallback() {
    				  super.connectedCallback();
    				  
    				  // Synchronize code content
    				  this.set('code', this.innerHTML.trim());
    			  
    				  // Effect to highlight code using Prism.js
    				  this.effect(() => {
    					const highlightedCode = Prism.highlight(this.get('code'), Prism.languages[this.get('language') || 'html'], this.get('language') || 'html');
    					this.querySelector('code').innerHTML = highlightedCode;
    				  });
    			  
    				  // Copy-to-clipboard functionality
    				  this.first('.copy').map(on('click', () => {
    					navigator.clipboard.writeText(this.get('code')).then(() => {
    					  this.set('copying', true);
    					  setTimeout(() => this.set('copying', false), 2000);
    					});
    				  }));
    				  
    				  // Toggle collapse state
    				  this.first('.overlay').map(on('click', () => this.set('collapsed', false)));
    				}
    			  }
    			  
    			  CodeBlock.define('code-block');
    					  

    LazyLoad Component Example

    Source Code

    HTML

    
    			  <lazy-load src="https://example.com/content.html"></lazy-load>
    					  

    CSS

    
    			  /* No specific styles are necessary, but the content fetched can have its own styles */
    			  lazy-load {
    				display: block;
    			  }
    					  

    JavaScript

    
    			  import { UIElement, effect } from '@efflore/ui-element';
    			  
    			  class LazyLoad extends UIElement {
    				static observedAttributes = ['src'];
    			  
    				connectedCallback() {
    				  effect(async () => {
    					await fetch(this.get('src'))
    					  .then(async response => {
    						const html = await response.text();
    						const shadow = this.shadowRoot || this.attachShadow({ mode: 'open' });
    						shadow.innerHTML = html;
    						shadow.querySelectorAll('script').forEach(script => {
    						  const newScript = document.createElement('script');
    						  const scriptText = document.createTextNode(script.textContent);
    						  newScript.appendChild(scriptText);
    						  shadow.appendChild(newScript);
    						  script.remove();
    						});
    					  })
    					  .catch(error => console.error(error));
    				  });
    				}
    			  }
    			  
    			  LazyLoad.define('lazy-load');
    					  

    InputField Component Example

    3 to 20 characters left

    Age must be between 0 and 100

    Choose a unique username (3 to 20 characters)

    Source Code

    HTML

    
    			  <!-- Text input with remaining count -->
    			  <input-field>
    				<label for="name-input">Your Name</label>
    				<div class="row">
    				  <div class="group">
    					<input
    					  type="text"
    					  id="name-input"
    					  name="name"
    					  placeholder="Enter your name"
    					  aria-describedby="name-description"
    					  minlength="3"
    					  maxlength="20"
    					  required
    					/>
    				  </div>
    				</div>
    				<p id="name-description" class="description" aria-live="polite">3 to 20 characters left</p>
    			  </input-field>
    			  
    			  <!-- Numeric input with spin buttons -->
    			  <input-field>
    				<label for="age-input">Your Age</label>
    				<div class="row">
    				  <div class="group">
    					<input
    					  type="number"
    					  id="age-input"
    					  name="age"
    					  value="42"
    					  min="0"
    					  max="100"
    					  step="1"
    					  aria-describedby="age-description"
    					/>
    				  </div>
    				  <div class="spinbutton" data-step="1">
    					<button type="button" class="decrement" aria-label="Decrement Age">โˆ’</button>
    					<button type="button" class="increment" aria-label="Increment Age">+</button>
    				  </div>
    				</div>
    				<p id="age-description" class="description" aria-live="polite">Age must be between 0 and 100</p>
    			  </input-field>
    			  
    			  <!-- Username input with server-side validation -->
    			  <input-field validate="/validate-username">
    				<label for="username-input">Username</label>
    				<div class="row">
    				  <div class="group">
    					<input
    					  type="text"
    					  id="username-input"
    					  name="username"
    					  placeholder="Choose a username"
    					  aria-describedby="username-description"
    					  minlength="3"
    					  maxlength="20"
    					  required
    					/>
    				  </div>
    				</div>
    				<p id="username-description" class="description" aria-live="polite">Choose a unique username (3 to 20 characters)</p>
    			  </input-field>
    					  

    CSS

    
    			  input-field {
    				display: block;
    				margin: 1rem 0;
    				--padding: 0.5rem;
    			  }
    			  
    			  input-field input {
    				padding: var(--padding);
    				border: 1px solid #ccc;
    				border-radius: 4px;
    				width: 100%;
    				box-sizing: border-box;
    			  }
    			  
    			  input-field .spinbutton button {
    				cursor: pointer;
    				padding: var(--padding);
    				border: none;
    				background: #007bff;
    				color: #fff;
    				border-radius: 4px;
    				transition: background-color 0.2s;
    			  }
    			  
    			  input-field .spinbutton button:hover {
    				background-color: #0056b3;
    			  }
    			  
    			  input-field .description,
    			  input-field .error-message {
    				font-size: 0.875rem;
    				margin-top: 0.25rem;
    			  }
    			  
    			  input-field .description {
    				color: #6c757d;
    			  }
    			  
    			  input-field .error-message {
    				color: #dc3545;
    			  }
    					  

    JavaScript

    
    			  import { UIElement, effect, asBoolean, asInteger } from '@efflore/ui-element';
    			  
    			  class InputField extends UIElement {
    				static observedAttributes = ['value', 'description'];
    			  
    				attributeMap = {
    				  value: el => (el.type === 'number' ? asInteger : String),
    				  description: String,
    				};
    			  
    				connectedCallback() {
    				  super.connectedCallback();
    			  
    				  // Track input value changes
    				  this.self.map(on('change', 'value'));
    				  this.self.map(on('input', 'value'));
    			  
    				  // Track length of input value and empty state
    				  this.set('length', () => this.get('value').length || 0);
    				  this.set('empty', () => !this.get('value'));
    			  
    				  // Handle input validity and error messages
    				  this.effect(() => {
    					const errorMessage = this.checkValidity()
    					  ? ''
    					  : this.validationMessage;
    					this.set('error', errorMessage);
    				  });
    			  
    				  // Server-side validation
    				  this.effect(async () => {
    					if (this.hasAttribute('validate')) {
    					  const response = await fetch(this.get('validate'));
    					  if (!response.ok) {
    						this.set('error', 'Invalid input');
    					  }
    					}
    				  });
    			  
    				  // Increment/Decrement for numeric inputs
    				  if (this.type === 'number') {
    					this.first('.step-up').map(on('click', () => this.stepUp()));
    					this.first('.step-down').map(on('click', () => this.stepDown()));
    				  }
    			  
    				  // Clear input button
    				  this.first('.clear-button').map(on('click', () => this.set('value', '')));
    				}
    			  }
    			  
    			  InputField.define('input-field');
    					  

    Wrap-Up

    The examples we've explored showcase the versatility and power of UIElement in building interactive and responsive Web Components. UIElement provides:

    The wide range of use cases from context-aware components to enhanced input handling demonstrates how UIElement can simplify state management and reactive programming in your Web Components.

    What's Next?

    Now that you've seen UIElement in action, here are some recommended next steps:

    Whether you are developing simple UI components or building complex, data-driven applications, UIElement offers a solid foundation for building fast, modular, and adaptive web experiences.