Ember Modifier修饰符 #

一、Modifier概述 #

Modifier是Ember中用于直接操作DOM元素的工具。它们类似于Vue的指令或React的ref,用于:

  • DOM属性操作
  • 事件监听
  • 第三方库集成
  • 焦点管理
  • 动画效果

1.1 生成Modifier #

bash
ember generate modifier autofocus

生成文件:

text
app/modifiers/autofocus.js
tests/integration/modifiers/autofocus-test.js

二、基本Modifier #

2.1 函数式Modifier #

javascript
// app/modifiers/autofocus.js
import { modifier } from 'ember-modifier';

export default modifier(function autofocus(element, positional, { delay = 0 }) {
  setTimeout(() => {
    element.focus();
  }, delay);
});
handlebars
{{! 使用Modifier}}
<input {{autofocus}} />

{{! 带参数}}
<input {{autofocus delay=100}} />

2.2 Class-based Modifier #

javascript
// app/modifiers/tooltip.js
import Modifier from 'ember-modifier';
import { tracked } from '@glimmer/tracking';

export default class TooltipModifier extends Modifier {
  @tracked tooltip;

  modify(element, [text], { position = 'top' }) {
    this.tooltip = this.createTooltip(element, text, position);
  }

  createTooltip(element, text, position) {
    const tooltip = document.createElement('div');
    tooltip.className = `tooltip tooltip-${position}`;
    tooltip.textContent = text;
    tooltip.style.display = 'none';

    element.appendChild(tooltip);

    element.addEventListener('mouseenter', () => {
      tooltip.style.display = 'block';
    });

    element.addEventListener('mouseleave', () => {
      tooltip.style.display = 'none';
    });

    return tooltip;
  }

  remove() {
    if (this.tooltip) {
      this.tooltip.remove();
    }
  }
}
handlebars
<button {{tooltip "点击提交表单" position="bottom"}}>
  提交
</button>

三、常用Modifier示例 #

3.1 自动聚焦 #

javascript
// app/modifiers/autofocus.js
import { modifier } from 'ember-modifier';

export default modifier(function autofocus(element, [condition = true], { delay = 0 }) {
  if (!condition) return;

  const timeoutId = setTimeout(() => {
    element.focus();
  }, delay);

  return () => clearTimeout(timeoutId);
});
handlebars
{{! 始终聚焦}}
<input {{autofocus}} />

{{! 条件聚焦}}
<input {{autofocus @isOpen}} />

{{! 延迟聚焦}}
<input {{autofocus delay=500}} />

3.2 点击外部 #

javascript
// app/modifiers/click-outside.js
import { modifier } from 'ember-modifier';

export default modifier(function clickOutside(element, [callback]) {
  function handleClick(event) {
    if (!element.contains(event.target)) {
      callback(event);
    }
  }

  document.addEventListener('click', handleClick);

  return () => {
    document.removeEventListener('click', handleClick);
  };
});
handlebars
<div {{click-outside this.closeDropdown}}>
  {{! 下拉菜单内容}}
</div>

3.3 滚动到元素 #

javascript
// app/modifiers/scroll-into-view.js
import { modifier } from 'ember-modifier';

export default modifier(function scrollIntoView(element, [shouldScroll], { behavior = 'smooth', block = 'start' }) {
  if (shouldScroll) {
    element.scrollIntoView({ behavior, block });
  }
});
handlebars
<div {{scroll-into-view @shouldScroll behavior="smooth" block="center"}}>
  目标内容
</div>

3.4 事件监听 #

javascript
// app/modifiers/on-key.js
import { modifier } from 'ember-modifier';

export default modifier(function onKey(element, [key, callback], { event = 'keydown' }) {
  function handleKey(e) {
    if (e.key === key) {
      callback(e);
    }
  }

  document.addEventListener(event, handleKey);

  return () => {
    document.removeEventListener(event, handleKey);
  };
});
handlebars
<div {{on-key "Escape" this.closeModal}}>
  {{! 模态框内容}}
</div>

3.5 样式绑定 #

javascript
// app/modifiers/style.js
import { modifier } from 'ember-modifier';

export default modifier(function style(element, [], styles) {
  Object.entries(styles).forEach(([property, value]) => {
    element.style[property] = value;
  });
});
handlebars
<div {{style color=this.textColor backgroundColor=this.bgColor}}>
  内容
</div>

3.6 类名绑定 #

javascript
// app/modifiers/class.js
import { modifier } from 'ember-modifier';

export default modifier(function classModifier(element, [], classMap) {
  Object.entries(classMap).forEach(([className, condition]) => {
    if (condition) {
      element.classList.add(className);
    } else {
      element.classList.remove(className);
    }
  });
});
handlebars
<div {{class active=this.isActive disabled=this.isDisabled}}>
  内容
</div>

四、第三方库集成 #

4.1 集成Chart.js #

javascript
// app/modifiers/chart.js
import Modifier from 'ember-modifier';
import Chart from 'chart.js/auto';

export default class ChartModifier extends Modifier {
  chart;

  modify(element, [config], { type = 'line' }) {
    this.destroyChart();

    this.chart = new Chart(element, {
      type,
      data: config.data,
      options: config.options,
    });
  }

  destroyChart() {
    if (this.chart) {
      this.chart.destroy();
    }
  }

  remove() {
    this.destroyChart();
  }
}
handlebars
<canvas {{chart this.chartConfig type="bar"}}></canvas>

4.2 集成Trix编辑器 #

javascript
// app/modifiers/trix-editor.js
import Modifier from 'ember-modifier';
import 'trix';
import 'trix/dist/trix.css';

export default class TrixEditorModifier extends Modifier {
  modify(element, [], { value, onChange }) {
    element.value = value || '';

    if (onChange) {
      element.addEventListener('trix-change', (e) => {
        onChange(e.target.value);
      });
    }
  }
}
handlebars
<trix-editor {{trix-editor value=@content onChange=this.handleContentChange}}></trix-editor>

4.3 集成IntersectionObserver #

javascript
// app/modifiers/in-viewport.js
import Modifier from 'ember-modifier';

export default class InViewportModifier extends Modifier {
  observer;

  modify(element, [callback], { threshold = 0.5, rootMargin = '0px' }) {
    this.observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          callback(entry.isIntersecting, entry);
        });
      },
      { threshold, rootMargin }
    );

    this.observer.observe(element);
  }

  remove() {
    if (this.observer) {
      this.observer.disconnect();
    }
  }
}
handlebars
<div {{in-viewport this.onVisibilityChange threshold=0.5}}>
  {{! 懒加载内容}}
</div>

五、内置修饰符 #

5.1 on修饰符 #

handlebars
{{! 事件监听}}
<button {{on "click" this.handleClick}}>
  点击
</button>

{{! 带选项}}
<input {{on "input" this.handleInput capture=true passive=true}} />

{{! 多个事件}}
<div
  {{on "mouseenter" this.handleMouseEnter}}
  {{on "mouseleave" this.handleMouseLeave}}
>
  悬停区域
</div>

5.2 fn修饰符 #

handlebars
{{! 函数绑定}}
<button {{on "click" (fn this.delete @item.id)}}>
  删除
</button>

{{! 部分应用}}
<button {{on "click" (fn this.update "status" "active")}}>
  激活
</button>

5.3 prevent-default修饰符 #

handlebars
{{! 阻止默认行为}}
<form {{on "submit" (prevent-default this.handleSubmit)}}>
  <button type="submit">提交</button>
</form>

5.4 stop-propagation修饰符 #

handlebars
{{! 阻止事件冒泡}}
<div {{on "click" this.handleOuterClick}}>
  <button {{on "click" (stop-propagation this.handleInnerClick)}}>
    内部按钮
  </button>
</div>

六、Modifier最佳实践 #

6.1 清理资源 #

javascript
// 好的做法 - 清理事件监听
export default modifier(function resize(element, [callback]) {
  window.addEventListener('resize', callback);
  return () => window.removeEventListener('resize', callback);
});

// 避免 - 不清理
export default modifier(function resize(element, [callback]) {
  window.addEventListener('resize', callback);
  // 没有清理,可能导致内存泄漏
});

6.2 参数验证 #

javascript
export default modifier(function tooltip(element, [text], { position = 'top' }) {
  if (!text) {
    console.warn('tooltip modifier requires text');
    return;
  }

  // 创建tooltip...
});

6.3 响应式更新 #

javascript
export default class ActiveModifier extends Modifier {
  modify(element, [isActive]) {
    if (isActive) {
      element.classList.add('active');
    } else {
      element.classList.remove('active');
    }
  }
}

七、测试Modifier #

7.1 集成测试 #

javascript
// tests/integration/modifiers/autofocus-test.js
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, focus } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';

module('Integration | Modifier | autofocus', function (hooks) {
  setupRenderingTest(hooks);

  test('it focuses the element', async function (assert) {
    await render(hbs`
      <input id="first" />
      <input id="second" {{autofocus}} />
    `);

    assert.dom('#second').isFocused('second input should be focused');
  });

  test('it respects delay option', async function (assert) {
    await render(hbs`
      <input {{autofocus delay=50}} />
    `);

    // 等待延迟
    await new Promise((resolve) => setTimeout(resolve, 100));

    assert.dom('input').isFocused();
  });
});

八、常用Modifier集合 #

8.1 表单相关 #

javascript
// app/modifiers/autofocus.js - 自动聚焦
// app/modifiers/autoselect.js - 自动选择文本
export default modifier(function autoselect(element) {
  element.select();
});

// app/modifiers/submit-on-enter.js - 回车提交
export default modifier(function submitOnEnter(element, [callback]) {
  function handleKeydown(e) {
    if (e.key === 'Enter') {
      e.preventDefault();
      callback();
    }
  }
  element.addEventListener('keydown', handleKeydown);
  return () => element.removeEventListener('keydown', handleKeydown);
});

8.2 UI交互 #

javascript
// app/modifiers/draggable.js - 拖拽
// app/modifiers/resizable.js - 调整大小
// app/modifiers/sticky.js - 粘性定位

8.3 动画 #

javascript
// app/modifiers/fade-in.js - 淡入
export default modifier(function fadeIn(element, [duration = 300]) {
  element.style.opacity = '0';
  element.style.transition = `opacity ${duration}ms`;

  requestAnimationFrame(() => {
    element.style.opacity = '1';
  });
});

九、总结 #

Modifier是Ember中操作DOM的推荐方式:

类型 用途 示例
函数式 简单操作 {{autofocus}}
Class-based 复杂逻辑 {{tooltip "提示"}}
内置 常用功能 {{on "click" fn}}

合理使用Modifier可以让DOM操作更加模块化和可复用。

最后更新:2026-03-28