自定义元素提供了一种将组件注入视图的方便方法。
本节目录
- 介绍
- 例子
- 传递参数
- 父组件和子组件之间的通信
- 传递监控属性的表达式
- 将标记传递到组件中
- 控制自定义元素标记名称
- 注册自定义元素
- 备注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值以给出该表达式的结果,并将其提供给该组件。
-
总之,一般规则是:
- 如果参数的求值不涉及可观察/计算的计算,则按字面意义传递。
- 如果参数的求值涉及到计算一个或多个可观察量/计算,它将作为计算属性传递,以便您可以对参数值的更改做出反应。
将标记传递到组件中
有时,您可能需要创建接收标记并将其用作其输出的一部分的组件。例如,您可能想要构建一个“容器”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,以使其输出包括提供的标记。 下面是完整的工作示例:
Here is a special list
-
Here is another one of my special items
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。