KnockoutJS 3.X API 第六章 组件(4) 自定义元素

自定义元素提供了一种将组件注入视图的方便方法。

本节目录

  • 介绍
  • 例子
  • 传递参数
    • 父组件和子组件之间的通信
    • 传递监控属性的表达式
  • 将标记传递到组件中
  • 控制自定义元素标记名称
  • 注册自定义元素
  • 备注1:将自定义元素与常规绑定相结合
  • 备注2:自定义元素不能自行关闭
  • 备注3:自定义元素和Internet Explorer 6到8
  • 高级应用:访问$ raw参数

介绍

自定义元素是组件绑定的语法替代(实际上,自定义元素使用后台的组件绑定)。
例如,一个繁琐写法的示范:

<div data-bind='component: { name: "flight-deals", params: { from: "lhr", to: "sfo" } }'></div>

其实可以更简单:

<flight-deals params='from: "lhr", to: "sfo"'></flight-deals>

示例

这个例子声明了一个组件,然后将它的两个实例注入到一个视图中。 请参阅下面的源代码。

First instance, without parameters

Second instance, passing parameters

UI源码:

<h4>First instance, without parameters</h4>
<message-editor></message-editor>
 
<h4>Second instance, passing parameters</h4>
<message-editor params='initialText: "Hello, world!"'></message-editor>

视图模型代码:

ko.components.register('message-editor', {
    viewModel: function(params) {
        this.text = ko.observable(params.initialText || '');
    },
    template: 'Message: <input data-bind="value: text" /> '
            + '(length: <span data-bind="text: text().length"></span>)'
});
 
ko.applyBindings();

注意:在更现实的情况下,通常从外部文件加载组件视图模型和模板,而不是将它们硬编码到注册中。

传递参数

正如您在上面的示例中看到的,您可以使用params属性为组件视图模型提供参数。 params属性的内容被解释为类似于JavaScript对象字面值(就像数据绑定属性一样),因此您可以传递任何类型的任意值。 例:

<unrealistic-component
    params='stringValue: "hello",
            numericValue: 123,
            boolValue: true,
            objectValue: { a: 1, b: 2 },
            dateValue: new Date(),
            someModelProperty: myModelValue,
            observableSubproperty: someObservable().subprop'>
</unrealistic-component>

父组件和子组件之间的通信

如果在params属性中引用模型属性,那么当然是指组件外部的viewmodel(“parent”或“host”viewmodel)上的属性,因为组件本身尚未实例化。 在上面的示例中,myModelValue将是父视图模型上的一个属性,并且将被子组件viewmodel的构造函数接收为params.someModelProperty。

这是如何将属性从父视图模型传递到子组件。 如果属性本身是可观察的,则父视图模型将能够观察并对子组件插入的任何新值做出反应。

传递可观察的表达式

在以下示例中,

<some-component
    params='simpleExpression: 1 + 1,
            simpleObservable: myObservable,
            observableExpression: myObservable() + 1'>
</some-component>

...组件viewmodel params参数将包含三个值:

  • simpleExpression
    • 这将是数字值2.它不会是可观察值或计算值,因为没有涉及可观察值。

      一般来说,如果参数的求值不涉及对可观察量的求值(在这种情况下,该值不涉及可观察量),那么该值将按字面意义传递。如果值是一个对象,那么子组件可以改变它,但是由于它不可观察,所以父组件不会知道子组件已经这样做。

  • simpleObservable
    • 这将是在父viewmodel上声明为myObservable的ko.observable实例。它不是一个包装器 - 它是父母引用的实际相同的实例。因此,如果子viewmodel写入此observable,父viewmodel将接收到该更改。

      一般来说,如果一个参数的求值不涉及一个可观察值的计算(在这种情况下,观察值被简单地传递而不对其进行求值),那么这个值被字面传递。

  • observableExpression
    • 表达式本身,当被评估时,读取一个observable。该observable的值可能随时间而变化,因此表达式结果可能会随时间而变化。

      为了确保子组件能够对表达式值的更改做出反应,Knockout会自动将此参数升级为计算属性。因此,子组件将能够读取params.observableExpression()以获取当前值,或使用params.observableExpression.subscribe(...)等。

      一般来说,对于自定义元素,如果参数的求值涉及评估一个可观察量,则Knockout自动构造一个ko.computed值以给出该表达式的结果,并将其提供给该组件。

总之,一般规则是:

  1. 如果参数的求值不涉及可观察/计算的计算,则按字面意义传递。
  2. 如果参数的求值涉及到计算一个或多个可观察量/计算,它将作为计算属性传递,以便您可以对参数值的更改做出反应。

将标记传递到组件中

有时,您可能需要创建接收标记并将其用作其输出的一部分的组件。例如,您可能想要构建一个“容器”UI元素,例如网格,列表,对话框或标签集,可以接收和绑定内部的任意标记。

考虑可以如下调用的特殊列表组件:

<my-special-list params="items: someArrayOfPeople">
    <!-- Look, I'm putting markup inside a custom element -->
    The person <em data-bind="text: name"></em>
    is <em data-bind="text: age"></em> years old.
</my-special-list>

默认情况下,<my-special-list>中的DOM节点将被剥离(不绑定到任何viewmodel)并由组件的输出替换。 但是,这些DOM节点不会丢失:它们被记住,并以两种方式提供给组件:

  • 作为数组$ componentTemplateNodes,可用于组件模板中的任何绑定表达式(即作为绑定上下文属性)。 通常这是使用提供的标记的最方便的方法。 请参见下面的示例。
  • 作为一个数组,componentInfo.templateNodes,传递给它的createViewModel函数

组件可以选择使用提供的DOM节点作为其输出的一部分,但是它希望,例如通过对组件模板中的任何元素使用template:{nodes:$ componentTemplateNodes}。

例如,my-special-list组件的模板可以引用$ componentTemplateNodes,以使其输出包括提供的标记。 下面是完整的工作示例:

The person is years old.

UI源码:

<!-- This could be in a separate file -->
<template id="my-special-list-template">
    <h3>Here is a special list</h3>
 
    <ul data-bind="foreach: { data: myItems, as: 'myItem' }">
        <li>
            <h4>Here is another one of my special items</h4>
            <!-- ko template: { nodes: $componentTemplateNodes, data: myItem } --><!-- /ko -->
        </li>
    </ul>
</template>
 
<my-special-list params="items: someArrayOfPeople">
    <!-- Look, I'm putting markup inside a custom element -->
    The person <em data-bind="text: name"></em>
    is <em data-bind="text: age"></em> years old.
</my-special-list>

视图模型源码:

ko.components.register('my-special-list', {
    template: { element: 'my-special-list-template' },
    viewModel: function(params) {
        this.myItems = params.items;
    }
});
 
ko.applyBindings({
    someArrayOfPeople: ko.observableArray([
        { name: 'Lewis', age: 56 },
        { name: 'Hathaway', age: 34 }
    ])
});

这个“特殊列表”示例在每个列表项上面插入一个标题。 但是相同的技术可以用于创建复杂的网格,对话框,选项卡集等,因为这样的UI元素所需要的是常见的UI标记(例如,定义网格或对话框的标题和边框) 提供标记。

当使用没有自定义元素的组件时,即当直接使用组件绑定时传递标记,这种技术也是可能的。

控制自定义元素标记名称

默认情况下,Knockout假定您的自定义元素标记名称完全对应于使用ko.components.register注册的组件的名称。 这种约定超配置策略是大多数应用程序的理想选择。

如果你想要有不同的自定义元素标签名称,你可以覆盖getComponentNameForNode来控制这个。 例如,

ko.components.getComponentNameForNode = function(node) {
    var tagNameLower = node.tagName && node.tagName.toLowerCase();
 
    if (ko.components.isRegistered(tagNameLower)) {
        // If the element's name exactly matches a preregistered
        // component, use that component
        return tagNameLower;
    } else if (tagNameLower === "special-element") {
        // For the element <special-element>, use the component
        // "MySpecialComponent" (whether or not it was preregistered)
        return "MySpecialComponent";
    } else {
        // Treat anything else as not representing a component
        return null;
    }
}

如果要控制哪些已注册组件的子集可以用作自定义元素,则可以使用此技术。

注册自定义元素

如果你使用默认的组件加载器,因此使用ko.components.register注册你的组件,那么没有什么额外的你需要做。 以这种方式注册的组件可以立即用作自定义元素。

如果你实现了一个自定义组件加载器,并且没有使用ko.components.register,那么你需要告诉Knockout你想要用作自定义元素的任何元素名称。 为此,只需调用ko.components.register - 您不需要指定任何配置,因为您的自定义组件加载器将不会使用配置。 例如,

ko.components.register('my-custom-element', { /* No config needed */ });

或者,您可以覆盖getComponentNameForNode以动态控制哪些元素映射到哪些组件名称,而与预注册无关。

备注1:将自定义元素与常规绑定相结合

如果需要,自定义元素可以具有常规的数据绑定属性(除了任何params属性)。 例如,

<products-list params='category: chosenCategory'
               data-bind='visible: shouldShowProducts'>
</products-list>

但是,使用将修改元素内容的绑定(例如,文本或模板绑定)是没有意义的,因为它们会覆盖您的组件注入的模板。

Knockout将阻止使用任何使用controlsDescendantBindings的绑定,因为当尝试将其viewmodel绑定到注入的模板时,这也会与组件发生冲突。 因此,如果要使用if或foreach等控制流绑定,则必须将其包装在自定义元素周围,而不是直接在自定义元素上使用,例如:

<!-- ko if: someCondition -->
    <products-list></products-list>
<!-- /ko -->

或者

<ul data-bind='foreach: allProducts'>
    <product-details params='product: $data'></product-details>
</ul>

备注2:自定义元素不能自行关闭

您必须写入<my-custom-element> </ my-custom-element>,而不是<my-custom-element />。否则,您的自定义元素不会关闭,后续元素将被解析为子元素。

这是HTML规范的限制,不在Knockout可以控制的范围之内。 HTML解析器遵循HTML规范,忽略任何自闭合斜杠(除了少量的特殊“外来元素”,它们被硬编码到解析器中)。 HTML与XML不同。

注意:自定义元素和Internet Explorer 6到8

Knockout努力让开发人员处理跨浏览器兼容性问题的痛苦,特别是那些与旧版浏览器相关的问题!即使自定义元素提供了一种非常现代的web开发风格,他们仍然可以在所有常见的浏览器上工作:

  • HTML5时代的浏览器,包括Internet Explorer 9和更高版本,自动允许自定义元素没有困难。
  • Internet Explorer 6到8也支持自定义元素,但前提是它们在HTML解析器遇到任何这些元素之前注册。

IE 6-8的HTML解析器将丢弃任何无法识别的元素。为了确保不会丢弃您的自定义元素,您必须执行以下操作之一:

  • 确保在HTML解析器看到任何<your-component>元素之前调用ko.components.register('your-component')
  • 或者,至少在HTML解析器看到任何<your-component>元素之前调用document.createElement('your-component')。你可以忽略createElement调用的结果 - 所有重要的是你已经调用它。

例如,如果你像这样构造你的页面,那么一切都会OK:

<!DOCTYPE html>
<html>
    <body>
        <script src='some-script-that-registers-components.js'></script>
 
        <my-custom-element></my-custom-element>
    </body>
</html>

如果你使用AMD,那么你可能更喜欢这样的结构:

<!DOCTYPE html>
<html>
    <body>
        <script>
            // Since the components aren't registered until the AMD module
            // loads, which is asynchronous, the following prevents IE6-8's
            // parser from discarding the custom element
            document.createElement('my-custom-element');
        </script>
 
        <script src='require.js' data-main='app/startup'></script>
 
        <my-custom-element></my-custom-element>
    </body>
</html>

或者如果你真的不喜欢document.createElement调用的hackiness,那么你可以使用一个组件绑定为你的顶层组件,而不是一个自定义元素。 只要所有其他组件在您的ko.applyBindings调用之前注册,他们可以作为自定义元素在IE6-8生效:

<!DOCTYPE html>
<html>
    <body>
        <!-- The startup module registers all other KO components before calling
             ko.applyBindings(), so they are OK as custom elements on IE6-8 -->
        <script src='require.js' data-main='app/startup'></script>
 
        <div data-bind='component: "my-custom-element"'></div>
    </body>
</html>

高级应用:访问$ raw参数

考虑以下不寻常的情况,其中unObservable,observable 1和observable 2都是可观测量:

<some-component
    params='myExpr: useObservable1() ? observable1 : observable2'>
</some-component>

由于评估myExpr涉及读取observable(useObservable1),KO将向组件提供参数作为计算属性。

但是,计算属性的值本身是可观察的。这似乎导致一个尴尬的情况,其中读取其当前值将涉及双解开(即,params.myExpr()(),其中第一个括号给出表达式的值,第二个给出的值结果可见实例)。

这种双重解开将是丑陋,不方便和意想不到的,所以Knockout自动设置生成的计算属性(params.myExpr)来为你解开它的值。也就是说,组件可以读取params.myExpr()以获取已选择的可观察值(observable1或observable2)的值,而不需要双重解开。

在不太可能发生的情况下,您不想自动解包,因为您想直接访问observable1 / observable2实例,您可以从params。$raw读取值。例如,

function MyComponentViewModel(params) {
    var currentObservableInstance = params.$raw.myExpr();
     
    // Now currentObservableInstance is either observable1 or observable2
    // and you would read its value with "currentObservableInstance()"
}

这应该是一个非常不寻常的情况,所以通常你不需要使用$raw。

posted @ 2016-10-18 15:48  SmallProgram  阅读(1410)  评论(0编辑  收藏  举报