Template modifier system for handling DOM events and element behavior in templates.
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>
*/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">
*/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;
}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>
*/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;
}