CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-svelte

Revolutionary JavaScript framework and compiler that builds web applications without runtime overhead by compiling components at build time.

Overview
Eval results
Files

actions.mddocs/

Actions

Element lifecycle and behavior enhancement system for reusable DOM interactions and element augmentation.

Capabilities

Action Functions

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>

TypeScript Actions

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>

Common Action Patterns

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>

Action Libraries

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" />

Action Best Practices

  1. Clean Up Resources: Always remove event listeners and clear timers in the destroy method
  2. Handle Parameter Updates: Implement update method when actions accept parameters
  3. Type Safety: Use TypeScript interfaces for better development experience
  4. Performance: Avoid creating actions in render loops or expensive operations
  5. Accessibility: Consider ARIA attributes and keyboard navigation in your actions
  6. Error Handling: Handle edge cases and provide fallbacks where appropriate

Install with Tessl CLI

npx tessl i tessl/npm-svelte@4.2.0

docs

actions.md

compiler.md

core-runtime.md

easing-functions.md

index.md

list-animations.md

motion-animation.md

store-management.md

transitions.md

tile.json