Revolutionary JavaScript framework and compiler that builds web applications without runtime overhead by compiling components at build time.
Element lifecycle and behavior enhancement system for reusable DOM interactions and element augmentation.
Create reusable functions that enhance DOM elements with custom behavior, event handling, or lifecycle management.
/**
* Action function that enhances a DOM element with custom behavior
* @param node - DOM element to enhance
* @param parameter - Optional parameter for configuring the action
* @returns Optional action return object with update and destroy methods
*/
interface Action<
Element = HTMLElement,
Parameter = undefined,
Attributes extends Record<string, any> = Record<never, any>
> {
(
node: Element,
parameter?: Parameter
): void | ActionReturn<Parameter, Attributes>;
}
interface ActionReturn<
Parameter = undefined,
Attributes extends Record<string, any> = Record<never, any>
> {
/** Called when the action parameter changes */
update?: (parameter: Parameter) => void;
/** Called when the element is removed from the DOM */
destroy?: () => void;
}Usage Examples:
// Basic action without parameters
function ripple(node) {
function handleClick(event) {
const rect = node.getBoundingClientRect();
const ripple = document.createElement('div');
ripple.className = 'ripple';
ripple.style.left = (event.clientX - rect.left) + 'px';
ripple.style.top = (event.clientY - rect.top) + 'px';
node.appendChild(ripple);
setTimeout(() => ripple.remove(), 600);
}
node.addEventListener('click', handleClick);
return {
destroy() {
node.removeEventListener('click', handleClick);
}
};
}
// Usage in template
<button use:ripple>Click me</button>
// Action with parameters
function tooltip(node, text) {
let tooltipElement;
function showTooltip() {
tooltipElement = document.createElement('div');
tooltipElement.className = 'tooltip';
tooltipElement.textContent = text;
document.body.appendChild(tooltipElement);
const rect = node.getBoundingClientRect();
tooltipElement.style.left = rect.left + 'px';
tooltipElement.style.top = (rect.top - tooltipElement.offsetHeight - 5) + 'px';
}
function hideTooltip() {
if (tooltipElement) {
tooltipElement.remove();
tooltipElement = null;
}
}
node.addEventListener('mouseenter', showTooltip);
node.addEventListener('mouseleave', hideTooltip);
return {
update(newText) {
text = newText;
if (tooltipElement) {
tooltipElement.textContent = text;
}
},
destroy() {
hideTooltip();
node.removeEventListener('mouseenter', showTooltip);
node.removeEventListener('mouseleave', hideTooltip);
}
};
}
// Usage with parameters
<div use:tooltip={'Hello World'}>Hover me</div>
<div use:tooltip={dynamicText}>Dynamic tooltip</div>Create type-safe actions with proper parameter and attribute typing.
Usage Examples:
interface TooltipOptions {
text: string;
position?: 'top' | 'bottom' | 'left' | 'right';
delay?: number;
}
interface TooltipAttributes {
'data-tooltip'?: string;
'aria-describedby'?: string;
}
const tooltip: Action<HTMLElement, TooltipOptions, TooltipAttributes> = (
node,
{ text, position = 'top', delay = 0 }
) => {
let timeoutId: number;
let tooltipElement: HTMLElement;
function show() {
timeoutId = setTimeout(() => {
tooltipElement = document.createElement('div');
tooltipElement.className = `tooltip tooltip-${position}`;
tooltipElement.textContent = text;
tooltipElement.id = `tooltip-${Math.random().toString(36).substr(2, 9)}`;
document.body.appendChild(tooltipElement);
node.setAttribute('aria-describedby', tooltipElement.id);
positionTooltip(tooltipElement, node, position);
}, delay);
}
function hide() {
clearTimeout(timeoutId);
if (tooltipElement) {
tooltipElement.remove();
node.removeAttribute('aria-describedby');
}
}
node.addEventListener('mouseenter', show);
node.addEventListener('mouseleave', hide);
return {
update({ text: newText, position: newPosition = 'top', delay: newDelay = 0 }) {
text = newText;
position = newPosition;
delay = newDelay;
if (tooltipElement) {
tooltipElement.textContent = text;
tooltipElement.className = `tooltip tooltip-${position}`;
positionTooltip(tooltipElement, node, position);
}
},
destroy() {
hide();
node.removeEventListener('mouseenter', show);
node.removeEventListener('mouseleave', hide);
}
};
};
// Usage in TypeScript component
<button use:tooltip={{ text: 'Save changes', position: 'bottom', delay: 500 }}>
Save
</button>Reusable patterns for building actions that handle common DOM interaction needs.
Click Outside:
function clickOutside(node, callback) {
function handleClick(event) {
if (!node.contains(event.target)) {
callback();
}
}
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
},
update(newCallback) {
callback = newCallback;
}
};
}
// Usage
let showModal = true;
<div class="modal" use:clickOutside={() => showModal = false}>
Modal content
</div>Auto-resize Textarea:
function autoresize(node) {
function resize() {
node.style.height = 'auto';
node.style.height = node.scrollHeight + 'px';
}
node.addEventListener('input', resize);
resize(); // Initial resize
return {
destroy() {
node.removeEventListener('input', resize);
}
};
}
// Usage
<textarea use:autoresize placeholder="Type here..."></textarea>Long Press:
function longpress(node, callback) {
let timeoutId;
const duration = 500; // milliseconds
function handleMouseDown() {
timeoutId = setTimeout(callback, duration);
}
function handleMouseUp() {
clearTimeout(timeoutId);
}
node.addEventListener('mousedown', handleMouseDown);
node.addEventListener('mouseup', handleMouseUp);
node.addEventListener('mouseleave', handleMouseUp);
return {
destroy() {
clearTimeout(timeoutId);
node.removeEventListener('mousedown', handleMouseDown);
node.removeEventListener('mouseup', handleMouseUp);
node.removeEventListener('mouseleave', handleMouseUp);
}
};
}
// Usage
<button use:longpress={() => alert('Long pressed!')}>
Hold me
</button>Focus Trap:
function focusTrap(node) {
const focusableElements = node.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
function handleKeyDown(event) {
if (event.key === 'Tab') {
if (event.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
event.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
event.preventDefault();
}
}
}
if (event.key === 'Escape') {
node.dispatchEvent(new CustomEvent('escape'));
}
}
// Focus first element when trap is activated
firstElement?.focus();
node.addEventListener('keydown', handleKeyDown);
return {
destroy() {
node.removeEventListener('keydown', handleKeyDown);
}
};
}
// Usage
<div class="modal" use:focusTrap on:escape={() => showModal = false}>
<input type="text" placeholder="First input" />
<input type="text" placeholder="Second input" />
<button>Close</button>
</div>Actions work well as reusable libraries for common UI patterns:
// ui-actions.js
export function ripple(node, options = {}) {
const { color = 'rgba(255, 255, 255, 0.5)', duration = 600 } = options;
function createRipple(event) {
const circle = document.createElement('span');
const rect = node.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = event.clientX - rect.left - size / 2;
const y = event.clientY - rect.top - size / 2;
circle.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
background: ${color};
border-radius: 50%;
pointer-events: none;
transform: scale(0);
animation: ripple ${duration}ms ease-out;
`;
node.appendChild(circle);
setTimeout(() => circle.remove(), duration);
}
// Ensure node is positioned
if (getComputedStyle(node).position === 'static') {
node.style.position = 'relative';
}
node.style.overflow = 'hidden';
node.addEventListener('click', createRipple);
return {
update(newOptions) {
Object.assign(options, newOptions);
},
destroy() {
node.removeEventListener('click', createRipple);
}
};
}
export function lazyload(node, src) {
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
node.src = src;
observer.disconnect();
}
});
observer.observe(node);
return {
update(newSrc) {
src = newSrc;
},
destroy() {
observer.disconnect();
}
};
} else {
// Fallback for browsers without IntersectionObserver
node.src = src;
}
}
// Usage
import { ripple, lazyload } from './ui-actions.js';
<button use:ripple={{ color: 'rgba(0, 100, 255, 0.3)' }}>
Ripple Button
</button>
<img use:lazyload={'./large-image.jpg'} alt="Lazy loaded" />Install with Tessl CLI
npx tessl i tessl/npm-svelte@4.2.0