【问题标题】:What is a good way for a Angular directive to act as an facade to other elements?Angular 指令充当其他元素的外观的好方法是什么?
【发布时间】: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>

这是相当微不足道的。 问题出现,如果我想用(现有的)指令/属性配置我的&lt;x-menu-item&gt;。有时属性应该引用按钮。例如。点击&lt;x-menu-item&gt; 可能应该代理到&lt;button&gt;,因为它是&lt;x-menu-item&gt; 中的“真实”交互元素。

<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>

但其他属性应参考&lt;li&gt;。假设我想隐藏&lt;x-menu-item&gt;,我可能想隐藏所有内容,而不仅仅是&lt;button&gt;

<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>

当然还有影响&lt;li&gt;&lt;button&gt; 的属性。假设我想禁用&lt;x-menu-item&gt; 我可能想设置&lt;li&gt; 的样式并且我想禁用&lt;button&gt;

<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:动态生成模板并手动处理属性

我可以用完整的动态模板替换&lt;x-menu-item&gt; 并手动处理属性。它可能看起来像这样(功能不完整):

// 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);
  }
}

这在某些情况下实际上效果很好。我有一个自定义的&lt;x-checkbox&gt;,它在内部使用&lt;input type="checkbox"&gt;。在 95% 的情况下,我希望将所有放在 &lt;x-checkbox&gt; 上的属性移动到 &lt;input type="checkbox"&gt;,并且只将一些属性放在 &lt;input type="checkbox"&gt; 周围的包装器上。

我实际上也在这里处理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] + ' }"');
  }
});

缺点:我需要事先决定在哪里放置我不知道的属性。它们应该放在&lt;button&gt; 还是&lt;li&gt; 上?我想我希望&lt;button&gt; 上的属性比&lt;li&gt; 上的更多,因为我的&lt;x-menu-item&gt; 基本上是一个包装按钮,使用它感觉就像你使用按钮一样。开发人员会期望&lt;x-menu-item&gt;&lt;button&gt; 一样工作。然而,not 在根元素上放置未知属性似乎很奇怪(在本例中为 &lt;li&gt;)。如果有必要,人们还希望&lt;li&gt; 上的属性会影响&lt;button&gt;(就像 CSS 类一样)。我也在 JavaScript 中创建我的标记,而不是纯 HTML。

替换或不替换

我知道它已被弃用,但有时我喜欢用我的模板替换我的指令。假设有人在我的指令上放置了 id,我喜欢将 id 移动到代表指令的模板中的 canonical 元素(例如,在 &lt;x-checkbox&gt; 上,id 将是转移到&lt;input checkbox="type"&gt;)。因此,如果有人尝试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-disabledli-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>

缺点:这不是很枯燥,因此容易出错。我总是在我的&lt;x-menu-item&gt; 中有一个&lt;x-menu-button&gt;。永远不会有一个空的&lt;x-menu-item&gt;,也不会有一个带有不同子元素的&lt;x-menu-item&gt;ng-disabled 也有与解决方案 #2 相同的问题。开发人员应该能够轻松停用我的整个&lt;x-menu-item&gt;。他不应该在意添加某个ng-class 以进行样式设置并自行禁用该按钮。

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

限制你的界面。与其试图保持通用(这很好,但很麻烦),不如限制它的接口。不要对ng-disabledng-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>

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

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

与其将ng-click 从指令移动到按钮,如果指令侦听自身的点击并自动触发对按钮的点击(或相反 - 这取决于用例)。

解决方案 #6:脏查询。

有关详细信息,请参阅来自 @gulin-serge 的 answer。简短说明:使用自定义逻辑“装饰”现有指令,如 ng-click,如果它用于某个元素并防止使用默认行为。

缺点:每个ng-click 都会被检查,如果它被用于某个元素,即使不是这样。这种检查是一个很小的开销。您还必须删除ng-click 的默认行为,这可能会导致意外行为。例如。 Angulars ngTouch 模块会装饰每个 ng-click,因此它还调用了一个触摸事件。这也是&lt;x-menu-item&gt; 应该发生的事情,但是您现在需要检查是否手动使用了ngTouch,如果这是真的,还要监听触摸事件。这很容易出错并且无法扩展。这个“装饰步骤”目前发生在link 阶段,它可能有其自身的缺点:很难为&lt;li&gt; 生成ng-class,这取决于ng-disabled。您需要使用 $compile,它本身可能会产生意想不到的效果。 (例如,我在&lt;select&gt; 上使用它,突然间所有&lt;options&gt; 都被复制了。这可能很难调试。)其他指令有一个太有用的默认行为,不能松动(例如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


【解决方案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;
    }
  }})

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

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


P.S. 根据您的列表,我更喜欢 #2,但使用自定义指令命名空间。比如:

<app-menu-item app-click="..." app-hide="..."/>

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

【讨论】:

  • 您好,谢谢您的回答。我在可能的解决方案中添加了一些额外的缺点。我不“接受”您的回答,因为我认为这只有一个真正的好处:其他开发人员不需要学习新接口。他们可以重用他们关于ng-click 的知识。与其他解决方案一样,这有太多缺点。
  • @Pipo,比较返回非常快。通常,当您覆盖 $attrs 值时,它可以很好地适用于没有自己范围的指令。几乎所有看起来像外观的内置指令都可以在没有自己范围的情况下工作。当然 ng-if 和相同的东西不适合使用这种方法。使用这个方法的时候也可以使用transclusion + template/templateUrl
【解决方案2】:

更新:根据评论重写

这在很大程度上是根据 cmets 中的说明重写的。作为一个纯粹的基于组件的框架,Polymer Elements 背后的理念是为开发人员提供一种完成两项任务的简单方法。首先是能够使用以前未编程的功能制作简单的元素。第二个是使用简单的自定义标记制作复杂控件的能力。

这与 Angular 有很大不同,后者将功能整合到指令中,将自定义标记转换为本地标记,并将功能与翻译后的标记耦合。

关于聚合物的注意事项:

在 Web Components、Shadow DOM、Scoped CSS 和 HTML Imports 标准化和实现之前,Polymer 主要是一个框架 polyfill。 HTML 模板已经在现代浏览器中很好地实现了,值得一读。也就是说,Polymer 不会在这些标准成为标准时消失,因为它提供了方便的功能和属性,以及一组好的组件。 polyfill 被认为比原生支持慢,但这是意料之中的。

Polymer 和 Angular 的区别:

与 Angular 不同,当您创建一个元素时,您并不是将它从一个标记集“翻译”到另一个标记集。您实际上是在定义它的功能和表示。即使它可能在其 Template(s) 和 Shadow DOM 中提供额外的标记,这些额外的标记都是功能元素(无论是自定义的还是原生的)。

这也意味着您的元素可以拥有自己的 CSS 类,并且 ID 由浏览器处理,从而可以很容易地进行不同的呈现。选择器引擎将获取实际的自定义元素,并可以获取与其关联的属性和方法。

简单演示:

<polymer-element name="x-menu">
    <template>
        <style>
        /* Scoped Style rules */
        </style>
        <content></content>
    </template>
    <script>
    /* Registers the Element with the browser. */
    Polymer('x-menu', {
        // Additional Element properties and methods
    });
    </script>
</polymer-element>

<polymer-element name="x-menuitem">
    <template>
        <style>
        /* Scoped style rules */
        </style>
        <button type="button" class="{{parentElement.classList}}">
            <content></content>
        </button>
    </template>
    <script>
    /* Registers the element with the browser */
    Polymer('x-menuitem', {
        // Additional Element properties and methods
    });
    </script>
</polymer-element>

实际使用情况

<x-menu class="cool">
    <x-menuitem>Open</x-menuitem>
    <x-menuitem>Edit</x-menuitem>
    <x-menuitem>Create</x-menuitem>
</x-menu>

当您实际运行它时,您会看到该按钮实际上从 parentElement 复制了 classList。换句话说:{{parentElement.classList}} 实际上是this.parentElement.classList 的简写。了解这一点,您实际上可以构建许多基于父标记的函数和属性。相反,也可以反过来。你也可以使用document.querySelector('x-menu'),你会得到&lt;x-menu&gt;

此外,由于这些属性仅适用于您自己的自定义元素,因此您不必担心属性的命名空间。没有其他元素会理解您的属性,并且浏览器不会尝试对它们做任何有趣的事情。

更新:将聚合物应用于您的需求

首先,使用上面的示例,如果您有两个元素,那么您将有两个自定义元素。它们的耦合程度在很大程度上取决于它们的编程方式。需要理解的是,如果您需要多种类型的功能或演示,则仅需要在您的自定义元素中使用本机元素。首先,我建议不要使用原生元素,除非你需要样式。使用 Polymer,通常最好从小处着手...

对于您的功能示例,除了内容之外没有任何实际需要,除非您需要 a) 多个样式块或 b) 集成另一个元素或组件的功能。由于您需要单击功能和聚焦/样式的能力,因此最应该添加的是一个按钮。其余的可以很容易地在 CSS 中处理。

摆脱组件应该处理多个元素的思维方式很重要,因为只有当您需要多个元素时才会如此。有一个简单的元素可以根据您的需要扩展 HTML 元素集。当您需要高级组件时,只需担心管理其他元素(自定义或原生)。

更新:立面元素

Polymer 提供了多种机制,使一个元素具有基于属性的多个表示或功能。最简单的机制是模板绑定,它根据条件更改相关文档片段。

<polymer-element name="x-menuitem">
    <template>
        <template if="{{condition}}">
            <style>Uses this set of styles</style>
        </template>
        <template if="{{condition2 OR !condition}}">
            <style>Use this stylesheet instead</style>
        </template>
        <content></content>
    </template>
    <script>Polymer Registration</script>
</polymer-element>

可能有任意数量的条件,由于&lt;template&gt; 只是一个元素,您实际上可以在其中放置任何元素,包括更多&lt;template&gt; 绑定。

更新:禁用的两个示例

在第一个示例中,我们将简单地创建一个&lt;x-menu-item&gt; 并具有禁用它的能力。我们只使用核心元素,没有&lt;li&gt;&lt;button&gt;。要禁用该项目,您可以直接设置属性(当您标记它时,或者通过选择器查询获取元素并设置禁用属性。

<polymer-element name="x-menuitem">
    <template>
        <style>
            :host { color:blue; }
            :host([disabled]) { color:red; }
        </style>
        <content><content>
    </template>
    <script> 
        Polymer('x-menu-item', {
            publish: {
                disabled: {
                    value: false,
                    reflect: true
                }
            },
            method: function() {
                if (this.disabled) return;
            }
        });
    </script>
</polymer-element>

在我们的第二个示例中,我们将使用&lt;x-menu-item&gt; 与子&lt;li&gt;&lt;button&gt;。这将有一些条件模板绑定,以便它使用不同的类呈现。还是可以和上面一样禁用的。

<polymer-element name="x-menuitem">
    <template>
        <style>
            :host #item {
                /* styles */
            }
            :host([disabled]) #item { 
                /* styles */
            }
        </style>
        <template if="{{disabled}}">
            <li id="item" class="disabled_class1">
                <button class="disabled_class2">
                    <content><content>
                </button>
            </li>
        </template>
        <template if="{{!disabled}}">
            <li id="item" class="enabled_class1">
                <button class="enabled_class2">
                    <content><content>
                </button>
            </li>
        </template>
    </template>
    <script> 
        Polymer('x-menu-item', {
            publish: {
                disabled: {
                    value: false,
                    reflect: true
                }
            },
            method: function() {
                if (this.disabled) return;
            }
        });
    </script>
</polymer-element>

最后说明更新:

请记住,上述示例只是完成您需要的两种声明方式。还有其他声明性技术可以实现相同的目标。此外,您还可以始终使用 ES/JS 来强制定义所有内容,从而提供更多选择。

Polymer 有很多与 HTML 元素的默认浏览器行为相匹配的地方。这并不意味着没有太多要知道的。这个答案只是为了展示您问题的一个特定的期望功能。由于开发过程和术语如此不同,我不确定还应该解决什么问题。任何可能丢失的东西都可以添加到更新的答案中;只需让我知道您想要添加的具体内容。

【讨论】:

  • 您好,感谢您的深入评论,但我的问题略有不同。 “如果我正确理解您的问题,您基本上希望有两个自定义元素,但定义父元素可能会影响子元素的属性。”不太对。我的“问题”是我想要 one 需要管理多个元素的自定义元素。在这个例子中,我想要一个&lt;x-menu-item&gt; 来管理&lt;li&gt;&lt;button/&gt;&lt;/li&gt;。例如。如果我想禁用&lt;x-menu-item&gt;,则应将一个类添加到&lt;li&gt;,并将disabled 属性添加到&lt;button&gt;
  • (更复杂:我不只是想自己添加&lt;li&gt; 的类和&lt;button&gt; 的属性,如果可能的话,我想重用现有的Web 组件或在在 Angular 的情况下,指令 ng-classng-disabled 为我执行此操作。)
  • 这里的问题,我认为是一个误解。没有理由在 Polymer 上使用 li 或 button。我只是将按钮放在那里进行演示,对于您的任何功能都是完全不需要的。只是一个菜单项......这改变了整个动态,因此我对你的根本问题感到困惑。
  • 让一个自定义管理多个元素非常容易。我会相应地调整答案。我不知道您所说的重用现有 Web 组件是什么意思。这就是制作 Web 组件的原因......在 Polymer 中,您只需将组件导入到您想要使用的页面或组件中。
  • 希望这个答案更完整....如果您需要其他技术或解决问题,请告诉我。同时,我会为您获取文档的链接。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-07
  • 2017-02-08
  • 1970-01-01
  • 2016-05-13
  • 2014-03-30
相关资源
最近更新 更多