or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

application-engine.mdcomponents.mdcontrollers.mddata-structures.mddebugging-development.mddestroyables-cleanup.mdindex.mdmodifiers.mdobject-model.mdreactivity-tracking.mdrouting.mdservices.mdtemplates-rendering.mdtesting.mdutilities.md
tile.json

modifiers.mddocs/

Modifiers

Template modifier system for handling DOM events and element behavior in templates.

Capabilities

Built-in Modifiers

Core modifiers provided by Ember for common DOM interactions.

/**
 * Event handling modifier for attaching event listeners to DOM elements
 * Usage in templates: {{on "click" this.handleClick}}
 */
interface OnModifier {
  /**
   * Attach event listener to element
   * @param element - DOM element to attach listener to
   * @param args - [eventName, handler, ...options]
   */
  (element: Element, args: [string, Function, ...any[]]): void;
}

Usage Examples:

// Template usage of on modifier
export default class ButtonComponent extends Component {
  @action
  handleClick(event) {
    console.log('Button clicked:', event);
    this.args.onClick?.(event);
  }
  
  @action
  handleMouseEnter(event) {
    event.target.classList.add('hover');
  }
  
  @action
  handleMouseLeave(event) {
    event.target.classList.remove('hover');
  }
  
  @action
  handleKeyDown(event) {
    if (event.key === 'Enter' || event.key === ' ') {
      this.handleClick(event);
    }
  }
}

/* Template:
<button 
  {{on "click" this.handleClick}}
  {{on "mouseenter" this.handleMouseEnter}}
  {{on "mouseleave" this.handleMouseLeave}}
  {{on "keydown" this.handleKeyDown}}
  type="button"
>
  {{@label}}
</button>

<!-- With event options -->
<div {{on "scroll" this.handleScroll passive=true}}>
  Scrollable content
</div>

<!-- With once option -->
<div {{on "click" this.handleOneTimeClick once=true}}>
  Click me once
</div>

<!-- Preventing default -->
<form {{on "submit" this.handleSubmit preventDefault=true}}>
  <input type="submit" value="Submit">
</form>
*/

Custom Modifier Creation

System for creating custom modifiers for reusable DOM behavior.

/**
 * Set custom modifier manager for a modifier definition
 * @param manager - Modifier manager factory function
 * @param modifierDefinition - Modifier definition to manage
 * @returns Modifier definition with manager
 */
function setModifierManager(
  manager: (owner: Owner) => ModifierManager, 
  modifierDefinition: ModifierDefinition
): ModifierDefinition;

/**
 * Define capabilities for a modifier manager
 * @param version - Manager API version
 * @param options - Capability options
 * @returns Capabilities object
 */
function capabilities(version: string, options?: ModifierCapabilityOptions): ModifierCapabilities;

Usage Examples:

import { setModifierManager, capabilities } from "@ember/modifier";

// Simple functional modifier
function autofocus(element) {
  element.focus();
}

// Class-based modifier with lifecycle
class TooltipModifier {
  element = null;
  tooltip = null;
  
  modify(element, [text], { placement = 'top' }) {
    this.element = element;
    this.setupTooltip(text, placement);
  }
  
  setupTooltip(text, placement) {
    // Remove existing tooltip
    this.cleanup();
    
    // Create new tooltip
    this.tooltip = document.createElement('div');
    this.tooltip.className = 'tooltip';
    this.tooltip.textContent = text;
    this.tooltip.setAttribute('data-placement', placement);
    
    // Position tooltip
    this.positionTooltip();
    
    // Add event listeners
    this.element.addEventListener('mouseenter', this.showTooltip);
    this.element.addEventListener('mouseleave', this.hideTooltip);
    this.element.addEventListener('focus', this.showTooltip);
    this.element.addEventListener('blur', this.hideTooltip);
    
    // Append to DOM
    document.body.appendChild(this.tooltip);
  }
  
  positionTooltip() {
    const rect = this.element.getBoundingClientRect();
    const tooltipRect = this.tooltip.getBoundingClientRect();
    
    let top, left;
    
    switch (this.tooltip.getAttribute('data-placement')) {
      case 'top':
        top = rect.top - tooltipRect.height - 8;
        left = rect.left + (rect.width - tooltipRect.width) / 2;
        break;
      case 'bottom':
        top = rect.bottom + 8;
        left = rect.left + (rect.width - tooltipRect.width) / 2;
        break;
      case 'left':
        top = rect.top + (rect.height - tooltipRect.height) / 2;
        left = rect.left - tooltipRect.width - 8;
        break;
      case 'right':
        top = rect.top + (rect.height - tooltipRect.height) / 2;
        left = rect.right + 8;
        break;
    }
    
    this.tooltip.style.position = 'fixed';
    this.tooltip.style.top = `${top}px`;
    this.tooltip.style.left = `${left}px`;
    this.tooltip.style.opacity = '0';
    this.tooltip.style.pointerEvents = 'none';
  }
  
  showTooltip = () => {
    if (this.tooltip) {
      this.tooltip.style.opacity = '1';
    }
  };
  
  hideTooltip = () => {
    if (this.tooltip) {
      this.tooltip.style.opacity = '0';
    }
  };
  
  cleanup() {
    if (this.tooltip) {
      this.tooltip.remove();
      this.tooltip = null;
    }
    
    if (this.element) {
      this.element.removeEventListener('mouseenter', this.showTooltip);
      this.element.removeEventListener('mouseleave', this.hideTooltip);
      this.element.removeEventListener('focus', this.showTooltip);
      this.element.removeEventListener('blur', this.hideTooltip);
    }
  }
  
  destroy() {
    this.cleanup();
    this.element = null;
  }
}

// Register modifier with manager
const TooltipModifierManager = {
  capabilities: capabilities('3.22'),
  
  createModifier(factory, args) {
    return new factory();
  },
  
  installModifier(instance, element, args) {
    const [text] = args.positional;
    const options = args.named;
    instance.modify(element, [text], options);
  },
  
  updateModifier(instance, args) {
    const [text] = args.positional;
    const options = args.named;
    instance.modify(instance.element, [text], options);
  },
  
  destroyModifier(instance) {
    instance.destroy();
  }
};

setModifierManager(() => TooltipModifierManager, TooltipModifier);

/* Template usage:
<button {{tooltip "Click me!" placement="top"}}>
  Hover for tooltip
</button>

<input {{autofocus}} placeholder="Auto-focused input">
*/

Modifier Manager API

Interface for creating custom modifier managers.

/**
 * Modifier manager interface for handling modifier lifecycle
 */
interface ModifierManager {
  /**
   * Create modifier instance
   * @param factory - Modifier factory
   * @param args - Modifier arguments
   * @returns Modifier instance
   */
  createModifier(factory: ModifierFactory, args: ModifierArgs): any;
  
  /**
   * Install modifier on element
   * @param instance - Modifier instance
   * @param element - DOM element
   * @param args - Modifier arguments
   */
  installModifier(instance: any, element: Element, args: ModifierArgs): void;
  
  /**
   * Update modifier when arguments change
   * @param instance - Modifier instance
   * @param args - Updated modifier arguments
   */
  updateModifier(instance: any, args: ModifierArgs): void;
  
  /**
   * Destroy modifier and clean up resources
   * @param instance - Modifier instance
   */
  destroyModifier(instance: any): void;
  
  /** Modifier capabilities */
  capabilities: ModifierCapabilities;
}

Advanced Modifier Patterns

Complex modifier examples for common use cases.

/**
 * Resize observer modifier for tracking element size changes
 */
class ResizeObserverModifier {
  observer = null;
  
  modify(element, [callback], options = {}) {
    this.cleanup();
    
    this.observer = new ResizeObserver((entries) => {
      for (const entry of entries) {
        callback(entry.contentRect, entry.target);
      }
    });
    
    this.observer.observe(element, options);
  }
  
  cleanup() {
    if (this.observer) {
      this.observer.disconnect();
      this.observer = null;
    }
  }
  
  destroy() {
    this.cleanup();
  }
}

/**
 * Intersection observer modifier for visibility tracking
 */
class IntersectionObserverModifier {
  observer = null;
  
  modify(element, [callback], { threshold = 0, rootMargin = '0px' } = {}) {
    this.cleanup();
    
    this.observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        callback(entry.isIntersecting, entry);
      });
    }, { threshold, rootMargin });
    
    this.observer.observe(element);
  }
  
  cleanup() {
    if (this.observer) {
      this.observer.disconnect();
      this.observer = null;
    }
  }
  
  destroy() {
    this.cleanup();
  }
}

/**
 * Click outside modifier for closing dropdowns/modals
 */
class ClickOutsideModifier {
  element = null;
  handler = null;
  
  modify(element, [callback]) {
    this.element = element;
    this.handler = (event) => {
      if (!element.contains(event.target)) {
        callback(event);
      }
    };
    
    // Use capture phase to handle before other events
    document.addEventListener('click', this.handler, true);
  }
  
  destroy() {
    if (this.handler) {
      document.removeEventListener('click', this.handler, true);
      this.handler = null;
      this.element = null;
    }
  }
}

Usage Examples:

// Register advanced modifiers
setModifierManager(() => new SimpleModifierManager(), ResizeObserverModifier);
setModifierManager(() => new SimpleModifierManager(), IntersectionObserverModifier);
setModifierManager(() => new SimpleModifierManager(), ClickOutsideModifier);

// Component using advanced modifiers
export default class DashboardWidget extends Component {
  @tracked isVisible = false;
  @tracked dimensions = { width: 0, height: 0 };
  @tracked showDropdown = false;
  
  @action
  handleResize(rect) {
    this.dimensions = {
      width: rect.width,
      height: rect.height
    };
    
    // Adapt layout based on size
    if (rect.width < 300) {
      this.compactMode = true;
    }
  }
  
  @action
  handleVisibilityChange(isVisible) {
    this.isVisible = isVisible;
    
    if (isVisible && !this.dataLoaded) {
      this.loadData();
    }
  }
  
  @action
  handleClickOutside() {
    this.showDropdown = false;
  }
  
  @action
  toggleDropdown() {
    this.showDropdown = !this.showDropdown;
  }
}

/* Template:
<div 
  class="dashboard-widget"
  {{resize-observer this.handleResize}}
  {{intersection-observer this.handleVisibilityChange threshold=0.5}}
>
  <div class="widget-header">
    <h3>{{@title}}</h3>
    <div 
      class="dropdown-container"
      {{click-outside this.handleClickOutside}}
    >
      <button {{on "click" this.toggleDropdown}}>
        Options ▼
      </button>
      {{#if this.showDropdown}}
        <div class="dropdown-menu">
          <button>Edit</button>
          <button>Delete</button>
        </div>
      {{/if}}
    </div>
  </div>
  
  <div class="widget-content">
    {{#if this.isVisible}}
      <!-- Widget content loads when visible -->
      {{yield}}
    {{else}}
      <div class="placeholder">Scroll to load content</div>
    {{/if}}
  </div>
</div>
*/

Types

interface ModifierDefinition {
  /** Modifier implementation */
  [Symbol.toStringTag]: 'Modifier';
}

interface ModifierFactory {
  /** Create modifier instance */
  new (): any;
}

interface ModifierArgs {
  /** Positional arguments */
  positional: any[];
  
  /** Named arguments */
  named: Record<string, any>;
}

interface ModifierCapabilities {
  /** Whether modifier can receive arguments */
  canReceiveArguments?: boolean;
  
  /** Whether modifier has destructor */
  hasDestructor?: boolean;
  
  /** Whether modifier schedules effects */
  hasScheduledEffect?: boolean;
}

interface ModifierCapabilityOptions {
  /** Async lifecycle support */
  asyncLifecycleCallbacks?: boolean;
  
  /** Destructor support */
  destructor?: boolean;
}

interface SimpleModifierManager {
  /** Standard capabilities */
  capabilities: ModifierCapabilities;
  
  /** Create modifier */
  createModifier(factory: ModifierFactory): any;
  
  /** Install modifier */
  installModifier(instance: any, element: Element, args: ModifierArgs): void;
  
  /** Update modifier */
  updateModifier(instance: any, args: ModifierArgs): void;
  
  /** Destroy modifier */
  destroyModifier(instance: any): void;
}

interface EventOptions {
  /** Prevent default behavior */
  preventDefault?: boolean;
  
  /** Stop event propagation */
  stopPropagation?: boolean;
  
  /** Stop immediate propagation */
  stopImmediatePropagation?: boolean;
  
  /** Use passive listener */
  passive?: boolean;
  
  /** Listen once only */
  once?: boolean;
  
  /** Use capture phase */
  capture?: boolean;
}