【发布时间】:2015-03-27 01:22:32
【问题描述】:
这是一个关于 Web 组件的更一般的问题,但是我将在 Angular 中编写示例,因为它提供了更多处理此问题的方法(例如 replace,即使它已被弃用),而且它也更熟悉我,可能还有其他人。
更新
因为 this comment 我认为我面临的许多问题都是 Angular 特定的,因为 Angular “编译”指令的方式。 (我无法在运行时轻松添加或删除指令。)因此,我不再搜索通用解决方案,而是搜索特定于 Angular 的解决方案。很抱歉造成这种混乱!
问题
假设我想创建一个如下所示的菜单栏:
<x-menu>
<x-menu-item>Open</x-menu-item>
<x-menu-item>Edit</x-menu-item>
<x-menu-item>Create</x-menu-item>
</x-menu>
这可以翻译成这样:
<section class="menu">
<ul class="menu-list">
<li class="menu-list-item">
<button type="button" class="menu-button">Open</button>
</li>
<li class="menu-list-item">
<button type="button" class="menu-button">Edit</button>
</li>
<li class="menu-list-item">
<button type="button" class="menu-button">Create</button>
</li>
</ul>
</section>
这是相当微不足道的。 问题出现,如果我想用(现有的)指令/属性配置我的<x-menu-item>。有时属性应该引用按钮。例如。点击<x-menu-item> 可能应该代理到<button>,因为它是<x-menu-item> 中的“真实”交互元素。
<x-menu-item ng-click="foo()">Open</x-menu-item>
<!-- → possible translation -->
<li class="menu-list-item">
<button type="button" class="menu-button" ng-click="foo()">Open</button>
</li>
但其他属性应参考<li>。假设我想隐藏<x-menu-item>,我可能想隐藏所有内容,而不仅仅是<button>。
<x-menu-item ng-hide="bar">Open</x-menu-item>
<!-- → possible translation -->
<li class="menu-list-item" ng-hide="bar">
<button type="button" class="menu-button">Open</button>
</li>
当然还有影响<li> 和<button> 的属性。假设我想禁用<x-menu-item> 我可能想设置<li> 的样式并且我想禁用<button>。
<x-menu-item ng-disabled="baz">Open</x-menu-item>
<!-- → possible translation -->
<li class="menu-list-item" ng-class="{ 'is-disabled': baz }">
<button type="button" class="menu-button" ng-disabled="baz">Open</button>
</li>
这基本上是我想要实现的。我知道一些解决方案,但都有其缺点。
解决方案#1:动态生成模板并手动处理属性
我可以用完整的动态模板替换<x-menu-item> 并手动处理属性。它可能看起来像这样(功能不完整):
// directive definition
return {
restrict: 'E',
transclude: true,
template: function(tElement, tAttrs) {
var buttonAttrs = [];
var liAttrs = [];
// loop through tAttrs.$attr
// save some attributes like ng-click to buttonAttrs
// save some attributes like ng-hiden to liAttrs
// save some attributes like ng-disabled to buttonAttrs and liAttrs
// optionally alter the attr-name and -value before saving (so ng-disabled is converted to a ng-class for liAttrs)
// unknown attribute? save it to either buttonAttrs or liAttrs as a default
// generate template
var template =
'<li class="menu-list-item" ' + liAttrs.join(' ') + '>' +
'<button class="menu-button" ' + buttonAttrs.join(' ') + ' ng-transclude>' +
'</button>' +
'</li>';
return tElement.replaceWith(text);
}
}
这在某些情况下实际上效果很好。我有一个自定义的<x-checkbox>,它在内部使用<input type="checkbox">。在 95% 的情况下,我希望将所有放在 <x-checkbox> 上的属性移动到 <input type="checkbox">,并且只将一些属性放在 <input type="checkbox"> 周围的包装器上。
我实际上也在这里处理ng-disabled。如果你想知道这会是什么样子,这里有一个例子:
angular.forEach(tAttrs.$attr, function(attrHtml, attrJs) {
buttonAttrs.push(attrHtml + '="' + tAttrs[attrJs] + '"');
if (attrHtml === 'ng-disabled') {
liAttrs.push('ng-class="{ \'is-disabled\': ' + tAttrs[attrJs] + ' }"');
}
});
缺点:我需要事先决定在哪里放置我不知道的属性。它们应该放在<button> 还是<li> 上?我想我希望<button> 上的属性比<li> 上的更多,因为我的<x-menu-item> 基本上是一个包装按钮,使用它感觉就像你使用按钮一样。开发人员会期望<x-menu-item> 像<button> 一样工作。然而,not 在根元素上放置未知属性似乎很奇怪(在本例中为 <li>)。如果有必要,人们还希望<li> 上的属性会影响<button>(就像 CSS 类一样)。我也在 JavaScript 中创建我的标记,而不是纯 HTML。
替换或不替换
我知道它已被弃用,但有时我喜欢用我的模板替换我的指令。假设有人在我的指令上放置了 id,我喜欢将 id 移动到代表指令的模板中的 canonical 元素(例如,在 <x-checkbox> 上,id 将是转移到<input checkbox="type">)。因此,如果有人尝试getElementById,他将获得其背后的规范元素。如果我不替换整个指令,我需要决定应该在指令上删除哪些属性(或全部),因为它们被移动到不同的元素。这可能是错误的,如果你错过了什么(突然有两次相同的id)。
解决方案 #2:使用前缀属性
类似于#1,但用户决定是否应该在指令或某些元素上使用属性。它可能看起来像这样:
<x-menu-item li-ng-hide="bar" button-ng-click="foo()">Open</x-menu-item>
<!-- → possible translation -->
<li class="menu-list-item" ng-hide="bar">
<button type="button" class="menu-button" ng-click="foo()">Open</button>
</li>
缺点:这个变得更冗长,但提供了更大的灵活性。例如。开发人员可以为指令、li 和按钮创建自定义id。但是ng-disabled 是怎么回事?开发人员是否应该放置button-ng-disabled 和li-ng-class?这既麻烦又容易出错。所以我们可能需要再次手动处理这些情况......
解决方案 #3:使用两个指令
如果我们不能决定如何处理我们的属性,我们可以引入两个指令。这样我们就不会引入人为的前缀属性。
<x-menu-item ng-hide="bar">
<x-menu-button ng-click="foo()">Open</x-menu-button>
</x-menu-item>
<!-- → possible translation -->
<li class="menu-list-item" ng-hide="bar">
<button type="button" class="menu-button" ng-click="foo()">Open</button>
</li>
缺点:这不是很枯燥,因此容易出错。我总是在我的<x-menu-item> 中有一个<x-menu-button>。永远不会有一个空的<x-menu-item>,也不会有一个带有不同子元素的<x-menu-item>。 ng-disabled 也有与解决方案 #2 相同的问题。开发人员应该能够轻松停用我的整个<x-menu-item>。他不应该在意添加某个ng-class 以进行样式设置并自行禁用该按钮。
解决方案 #4:使用通用接口
限制你的界面。与其试图保持通用(这很好,但很麻烦),不如限制它的接口。不要对ng-disabled、ng-hide 和ng-click 进行特殊处理,而是尝试识别您的常见用例并提供更自定义的界面来使用它们。这样我们只能以特殊的方式处理显式定义的属性。
<x-menu-item hidden="bar" action="foo()" disabled="baz">Open</x-menu-item>
<!-- → possible translation -->
<li class="menu-list-item" ng-show="bar" ng-class="{ 'is-disabled': baz }">
<button type="button" class="menu-button" ng-click="foo()" ng-disabled="baz">Open</button>
</li>
缺点:这种方法不是很直观。每个 Angular 开发人员都知道ng-click。没有人知道我的action 属性。
(部分)解决方案 #5:代理 DOM 事件
与其将ng-click 从指令移动到按钮,如果指令侦听自身的点击并自动触发对按钮的点击(或相反 - 这取决于用例)。
解决方案 #6:脏查询。
有关详细信息,请参阅来自 @gulin-serge 的 answer。简短说明:使用自定义逻辑“装饰”现有指令,如 ng-click,如果它用于某个元素并防止使用默认行为。
缺点:每个ng-click 都会被检查,如果它被用于某个元素,即使不是这样。这种检查是一个很小的开销。您还必须删除ng-click 的默认行为,这可能会导致意外行为。例如。 Angulars ngTouch 模块会装饰每个 ng-click,因此它还调用了一个触摸事件。这也是<x-menu-item> 应该发生的事情,但是您现在需要检查是否手动使用了ngTouch,如果这是真的,还要监听触摸事件。这很容易出错并且无法扩展。这个“装饰步骤”目前发生在link 阶段,它可能有其自身的缺点:很难为<li> 生成ng-class,这取决于ng-disabled。您需要使用 $compile,它本身可能会产生意想不到的效果。 (例如,我在<select> 上使用它,突然间所有<options> 都被复制了。这可能很难调试。)其他指令有一个太有用的默认行为,不能松动(例如ng-class 是“动画感知”和设置像 ng-enter 这样的实用程序类 - 重建一些自定义 element.addClass(cssClass) 是不够的。
(部分)解决方案 #7:使用多个模板。
有时使用根据某些属性选择的多个模板就足够了。这可能发生在 templateUrl 函数内部。
<x-menu-item>Open</x-menu-item>
<!-- → possible translation using "template-default.html" -->
<li class="menu-list-item">
<button type="button" class="menu-button">Open</button>
</li>
或者:
<x-menu-item disabled="baz">Open</x-menu-item>
<!-- → possible translation using "template-disabled.html" -->
<li class="menu-list-item" ng-class="{ 'is-disabled': baz }">
<button type="button" class="menu-button" ng-disabled="baz">Open</button>
</li>
缺点:这不是很干燥。如果您想更改 menu-list-item 类,您需要在两个模板中执行此操作。但是很高兴最终再次用 HTML 而不是 JavaScript 字符串来编写模板。但如果你有更多的变化,它就不能很好地扩展。然而,这可能是您唯一的解决方案,如果不仅仅是一些属性发生变化,而是它背后的整个标记。
解决方案 #8:尝试使用一些默认行为初始化每个隐藏指令(即使它是一些 noop)。
也许每个特殊处理的属性都可以用一些默认值初始化,即使这个值什么都没有。可以覆盖默认行为。
<x-menu-item>Open</x-menu-item>
<!-- → possible translation -->
<!-- $scope.isDisbabled = has('ng-disabled') ? use('ng-disabled') : false -->
<!-- $scope.action = has('ng-click') ? use('ng-click') : angular.noop -->
<!-- $scope.isHidden = has('ng-hide') ? use('ng-hide') : false -->
<li class="menu-list-item" ng-hide="isHidden" ng-class="{ 'is-disabled': isDisabled }">
<button type="button" class="menu-button" ng-disabled="isDisabled" ng-click="action()">Open</button>
</li>
缺点:您初始化了有时从未使用过的指令。这可能是一个性能问题。但总而言之,这种方法是比较干净的。 这是我目前最喜欢的解决方案。
解决方案#?:???
...
【问题讨论】:
-
这被标记为 Polymer,但 Polymer/WebComponents 的工作方式与 Angular 非常不同,并且具有截然不同的术语和功能。您真的对 Polymer/WebComponent 解决方案感兴趣吗?
-
是的,当然。我只是在示例中使用 Angular,因为我更习惯它,但我认为这是所有具有类似 WebComponent 功能的框架的普遍问题。
标签: javascript angularjs polymer web-component