Angular指令作为其他元素的外观有什么好方法?

Pip*_*ipo 5 javascript web-component angularjs polymer

这是一个关于Web组件的通用问题,但是我会在Angular中编写示例,因为它提供了一些处理这些问题的方法(比如replace即使它已被弃用),而且对我和其他人来说也更熟悉.

更新

由于这个评论,我认为我面临的许多问题都是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>
Run Code Online (Sandbox Code Playgroud)

这可以转化为:

<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>
Run Code Online (Sandbox Code Playgroud)

这是相当微不足道的.在出现的问题,如果我想我的配置<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>
Run Code Online (Sandbox Code Playgroud)

但是其他属性应该引用<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>
Run Code Online (Sandbox Code Playgroud)

并且当然有属性影响<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>
Run Code Online (Sandbox Code Playgroud)

这基本上就是我想要实现的目标.我知道一些解决方案,但都有它们的缺点.

解决方案#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);
  }
}
Run Code Online (Sandbox Code Playgroud)

在某些情况下,这实际上非常有效.我有<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] + ' }"');
  }
});
Run Code Online (Sandbox Code Playgroud)

缺点:我需要决定在哪里放置我不知道的属性.他们应该放在<button><li>?我想我要上更多的属性<button>比对<li>,因为我<x-menu-item>基本上是一个包裹按钮,使用它的感觉就像你会使用按钮.开发人员期望<x-menu-item>像工作一样工作<button>.但是我觉得奇怪,把(在这种情况下,根元素未知属性<li>).如果需要,人们也会期望属性<li>会影响<button>(就像CSS类一样).我还用JavaScript创建标记,而不是纯HTML.

更换或不更换

我知道它已被弃用,但有时我喜欢用我的模板替换我的指令.有人说,一个放置id在我的指导,我想移动id经典元素代表指令(.eg在模板<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>
Run Code Online (Sandbox Code Playgroud)

缺点:这一点变得更加冗长,但提供了更多的灵活性.例如,开发人员可以id为指令,li和按钮创建自定义.但是有什么用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>
Run Code Online (Sandbox Code Playgroud)

缺点:这不是很干,因此容易出错.我一直有一个<x-menu-button>在我的<x-menu-item>.永远不会有空,<x-menu-item>也不<x-menu-item>会有不同的子元素.我也遇到与ng-disabled解决方案#2 相同的问题.开发人员应该能够轻松地停用我的整体<x-menu-item>.他不应该ng-class为了样式目的添加一些东西并且自己禁用按钮.

解决方案#4:使用通用接口

限制您的界面.应该限制​​其界面,而不是试图保持通用(这很好,但很麻烦).而不是特殊处理ng-disabled,ng-hideng-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>
Run Code Online (Sandbox Code Playgroud)

缺点:这种方法不是很直观.每个Angular开发者都知道ng-click.没有人知道我的action属性.

(部分)解决方案#5:代理DOM事件

ng-click如果指令侦听自身的点击并自动触发按钮上的点击(或者反过来 - 它取决于用例),而不是将指令从指令移动到按钮,它有时不会有用.

解决方案#6:脏查询.

有关详细信息,请参阅@ gulin-serge 的答案.简短说明:"装饰"现有指令,如ng-click自定义逻辑,如果它在某个元素上使用并阻止使用默认行为.

缺点:ng-click如果在某个元素上使用它,即使不是这种情况,也会检查每个元素.这种检查是一个很小的开销.您还必须删除ng-click可能导致意外行为的默认行为.例如,Angulars ngTouch模块每一个都会进行装饰,ng-click所以它也会调用触摸事件.这也是应该发生的事情<x-menu-item>,但你现在需要检查,如果ngTouch是手动使用,如果这是真的,也要监听触摸事件.这很容易出错并且无法扩展.这种"装饰一步"目前发生的link,可以有自己的缺点阶段:这将是很难产生ng-class<li>依赖于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>
Run Code Online (Sandbox Code Playgroud)

要么:

<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>
Run Code Online (Sandbox Code Playgroud)

缺点:这不是很干.如果要更改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>
Run Code Online (Sandbox Code Playgroud)

缺点:您初始化有时从未使用过的指令.这可能是性能问题.但总而言之,这种方法相对干净.这是目前我最喜欢的解决方案.

解决方案#?:???

...

Gul*_*rge 1

可能#6。肮脏的查询。

如果我们在应该关闭默认实现的情况下使用指令定义作为查询表达式会怎么样?

你写这样的东西:

  .directive('ngClick', function() {
  return {
    restrict: 'A',
    priority: 100, // higher than default
    link: function(scope, element, attr) {

      // don't do that magic in other cases
      if (element[0].nodeName !== 'X-MENU-ITEM') return; 

      element.bind('click', function() {
        // passthrough attr value to a controller/scope/etc
      })

      // switch off default implementation based on attr value
      delete attr.ngClick;
    }
  }})
Run Code Online (Sandbox Code Playgroud)

这将关闭标签上 ng-click 的默认实现。ng-hide/ng-show/等的工作相同。

是的,从感觉上来说它看起来很糟糕,但结果更接近你的想法。当然,它会稍微减慢编译的链接过程。


PS根据您的列表,我更喜欢#2,但具有自定义指令名称空间。就像是:

<app-menu-item app-click="..." app-hide="..."/>
Run Code Online (Sandbox Code Playgroud)

并向文档添加约定,以app对所有自定义事物和行为使用前缀。通常app是项目名称的缩写。