🍽️ 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:
- Basic Example:
MySlider
- Learn how to create a slider component with prev/next buttons and a dot indicator, demonstrating single-component reactivity. - Basic Composition:
TabList
andAccordionPanel
- See how a parent component can control the visibility of multiple child components, showcasing state sharing and communication between components. - Syntax Highlighting: - See how wrapping content in a
CodeBlock
component enables syntax highlighting on the client, demonstrating integration with third-party libraries. - Fetching Data Example:
LazyLoad
- Learn how to fetch content only when needed, handling asynchronous operations and updating state reactively as data is loaded. - Form Validation Example:
InputField
with Client-Side & Server-Side Validation - Validate input fields based on requirements passed from the server and dynamically check the validity of entries, such as checking the availability of usernames via server requests. - Simple Application: TodoMVC-like Example - Build an interactive to-do list app that uses multiple coordinated components, covering signals, event handling, and state management in a more complex structure.
- Context Example:
MediaContext
- Discover how to share state globally across components using context, with practical use cases like adapting to media queries and responsive design.
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
MySlider Source Code
Loading...
TabList and AccordionPanel Example
Tab 1
Content for Tab 1
Tab 2
Content for Tab 2
Tab 3
Content for Tab 3
TabList Source Code
Loading...
AccordionPanel Source Code
Loading...
CodeBlock Example
<code-block collapsed language="html" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="file">code-block.html</span>
<span class="language">html</span>
</p>
<pre><code class="language-html"><code-block collapsed language="html" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="file">code-block.html</span>
<span class="language">html</span>
</p>
<pre><code class="language-html"></code></pre>
<input-button class="copy">
<button type="button" class="secondary small">Copy</button>
</input-button>
<button type="button" class="overlay">Expand</button>
</code-block></code></pre>
<input-button class="copy">
<button type="button" class="secondary small">Copy</button>
</input-button>
<button type="button" class="overlay">Expand</button>
</code-block>
CodeBlock Source Code
Loading...
InputButton Source Code
Loading...
LazyLoad Example
Loading...
LazyLoad Source Code
Loading...
InputField Example
Max. 20 characters
InputField Source Code
Loading...
TodoApp Example
TodoApp Source Code
Loading...
TodoForm Source Code
Loading...
TodoList Source Code
Loading...
InputField Source Code
Loading...
InputButton Source Code
Loading...
InputCheckbox Source Code
Loading...
InputRadiogroup Source Code
Loading...
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:
- Media Motion (
media-motion
): Indicates whether the user prefers reduced motion based on the(prefers-reduced-motion)
media query. - *Media Theme (
media-theme
): Provides the user's preferred color scheme, such as dark mode, based on the(prefers-color-scheme)
media query. - Media Viewport (
media-viewport
): Indicates the current viewport size and is classified into different sizes (e.g.,xs
,sm
,md
,lg
,xl
). Custom breakpoints can be configured by setting attributes on the<media-context>
element. - Media Orientation (
media-orientation
): Tracks the device's screen orientation, switching betweenlandscape
andportrait
.
Configuring Breakpoints
The viewport sizes can be customized by providing attributes on the <media-context>
element:
sm
: Small screen breakpoint (default:32em
).md
: Medium screen breakpoint (default:48em
).lg
: Large screen breakpoint (default:72em
).xl
: Extra large screen breakpoint (default:108em
).
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 '@zeix/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
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 '@zeix/ui-element';
class ThemedComponent extends UIElement {
static consumedContexts = ['media-theme'];
connectedCallback() {
// Toggle the class based on 'media-theme' signal
this.self.sync(toggleClass('dark', () => this.get('media-theme')));
this.self.sync(toggleClass('light', () => !this.get('media-theme')));
}
}
ThemedComponent.define('themed-component');
AnimatedComponent Example
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 '@zeix/ui-element';
class AnimatedComponent extends UIElement {
static consumedContexts = ['media-motion'];
connectedCallback() {
// Toggle classes based on 'media-motion' context
this.self.sync(toggleClass('motion', () => !this.get('media-motion')));
this.self.sync(toggleClass('no-motion', 'media-motion'));
}
}
AnimatedComponent.define('animated-component');
Responsive TabList Example
Source Code
HTML
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, toggleClass } from '@zeix/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.sync(toggleClass('accordion'));
// Pass 'collapsible' state to tab-panels based on 'accordion' state
this.all('tab-panel').pass({
collapsible: 'accordion'
});
// Handle tab clicks in normal tabbed mode
this.all('.tab-button').on('click', () => this.set('activeIndex', index))
// Set active tab-panel based on 'activeIndex'
this.all('tab-panel').sync((host, target, index) =>
this.self.sync(toggleClass('active', () => index === this.get('activeIndex'))(host, target))
);
}
}
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.sync(toggleClass('collapsible', 'collapsible'));
if (this.get('collapsible')) {
const header = this.querySelector('.panel-header');
header.addEventListener('click', () => {
this.set('expanded', !this.get('expanded'));
});
this.self.sync(toggleClass('active', 'expanded'));
}
}
}
TabPanel.define('tab-panel');
Responsive Image Gallery Example
![Image 1](image1.jpg)
![Image 2](image2.jpg)
![Image 3](image3.jpg)
![Image 4](image4.jpg)
![Image 5](image5.jpg)
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, enqueue } from '@zeix/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.sync(toggleClass('landscape', () => this.get('media-orientation') === 'landscape'));
this.self.sync(toggleClass('portrait', () => this.get('media-orientation') === 'portrait'));
// Dynamically wrap images in <my-slider> for portrait mode
effect(() => {
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.appendChild(this.slider), [this, 'add-slider']);
}
} else {
// Remove <my-slider> and display images as a grid in landscape mode
if (this.slider) enqueue(() => this.slider.remove(), [this.slider, 'remove-slider']);
}
});
}
}
ResponsiveImageGallery.define('responsive-image-gallery');