A JavaScript framework for creating ambitious web applications
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
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;
}Install with Tessl CLI
npx tessl i tessl/npm-ember-source