在写下所有这些之后,我突然意识到这可能超出了您的问题的范围。如果确实如此,我很抱歉;我希望您仍然可以从中获得一些价值。
这里的这些东西来自一个我已经工作了几个月的真实应用程序。这是一个快速而肮脏的提取,可能包含错误或拼写错误,我删除了特定于应用程序的代码或对其进行了简化以使其更易于理解。
有了它,我可以
- 任意嵌套视图模型
- 动态添加视图模型
- 渲染绑定到这些嵌套视图模型的 Knockout 模板,并灵活使用结果
以下是其工作原理的简要概述。
假设您要构建一个显示消息列表的应用程序。用户可以单击消息以打开模式对话框并进行回复。我们有三个视图模型:
- 名为
Main 的根视图模型
-
MessageList 负责显示消息列表
- 第三个叫
MessageReply,负责回复功能。
我们所有的视图模型构造函数都整齐地命名为app.viewmodels。让我们设置它们:
$(document).ready(function() {
var mainVm,
messageListVm,
messageReplyVm;
// we start with Main as the root viewmodel
mainVm = new app.viewmodels.Main();
// MessageList is a child of Main
messageListVm = mainVm.addChildVm('MessageList');
// and MessageReply in turn is a child of MessageList
messageReplyVm = messageListVm.addChildVm('MessageReply');
// the root is the only one that gets bound directly
ko.applyBindings(mainVm);
});
我们的标记看起来像这样:
<body>
<!-- context here: the Main viewmodel -->
<div data-bind="childVm: 'MessageList'">
<!-- context here: the MessageList viewmodel -->
<ul data-bind="foreach: messages">
<!-- context here: the individual message object -->
<li>
<p data-bind="text: body, modal: {viewmodelName: 'MessageReply', parentViewmodel: $parent, setupViewmodelWith: $data, templateName: 'message-reply-template'}">
</p>
</li>
</ul>
</div>
</body>
<script id="message-reply-template" type="text/html">
<!-- context here: the MessageReply viewmodel -->
<div>
<textarea data-bind="value: message().body"></textarea>
<input type="submit" data-bind="click: submit">
</div>
</script>
其中有两个自定义绑定,childVm 和 modal。前者只是查找子视图模型并将其设置为绑定上下文,而modal 绑定负责在正确的上下文中渲染模板并将结果交给单独的 JS 库。
视图模型通过借用构造函数、Parent、Child 或同时使用两者来获得嵌套的能力。 Here is the source for them.
父母
如果一个视图模型应该能够拥有子视图模型,它就借用Parent 构造函数:
app.viewmodels.Main = function Main() {
app.viewmodels.Parent.apply(this);
this.currentUser = //.. imagine the current user being loaded here from somewhere
};
作为父视图模型,Main 获得了三样东西:
-
.addChildVm(string):通过传递名称添加子视图模型。它会自动在 app.viewmodel 命名空间中查找。
-
.getVm(name):返回名为“name”的子视图模型
-
._childVms: 一个包含所有孩子的可观察列表
儿童
除根 Main 之外的每个视图模型至少是一个子视图模型。 MessageList 既是 Main 的子代,也是 MessageReply 的父代。非常适合它的名字,它包含要在列表中显示的消息。
app.viewmodels.MessageList = function MessageList() {
app.viewmodels.Parent.apply(this);
app.viewmodels.Child.apply(this);
// children need to set this, so we can find them by name through .getVm()
this._viewmodelName = function() { return "MessageList"; };
this.currentUser = null;
this.messages = ko.observableArray([]);
this.init = function init() {
that.currentUser = that._parentVm.currentUser;
var messages = GetMessages() // pseudocode - load our messages from somewhere
this.messages( messages);
};
};
作为子视图模型,MessageList 获得:
- 通过
this._parentVm访问其父级的能力
- 可选的
init 函数,如果存在则由父级自动调用
所以上面当我们将MessageList 添加到Main 时
messageListVm = mainVm.addChildVm('MessageList');
,Main
- 创建了
MessageList 的新实例
- 将实例添加到它自己的子节点中
- 并打电话给孩子们
init
然后子通过获取对当前用户的引用来设置自己,该引用由父 Main viewmodel 维护。
我们的最后一个视图模型:MessageReply
MessageReply 只是一个子视图模型;就像它的父 MessageList 自己做的一样,它在初始化时也会复制当前用户。它期望从模态绑定中获得一个 Message 对象,然后创建一个新的 Message 来回复它。该回复可以通过模态中的表单进行编辑和提交。
app.viewmodels.MessageReply = function MessageReply() {
app.viewmodels.Child.apply(this);
this._viewmodelName = function() { return "MessageReply"; };
var that = this;
this.currentUser = null;
// called automatically by the parent MessageList
this.init = function init() {
that.currentUser = that._parentVm.currentUser;
};
this.messageWeAreReplyingTo = ko.observable();
// our reply
this.message = ko.observable();
// called by the 'modal' binding
this.setup = function setup(messageWeAreReplyingTo) {
// the modal binding gives us the message the user clicked on
this.messageWeAreReplyingTo( messageWeAreReplyingTo );
// imagine that Message is a model object defined somewhere else
var ourReply = new Message({
sender: that.currentUser,
recipient: that.messageWeAreReplyingTo().sender();
});
this.message( ourReply );
};
// this is triggered by the form submit button in the overlay
this.submit = function submit() {
// send the message to the server
}
};
“childVm”绑定
Source code
<body>
<!-- context here: the Main viewmodel -->
<div data-bind="childVm: 'MessageList'">
<!-- context here: the MessageList viewmodel -->
</div>
这只是 Knockouts 自己的 'with:' 绑定的便捷包装。它将视图模型名称作为其值访问器,在当前绑定上下文中查找该名称的子视图模型,并使用“with:”绑定将该子视图模型设置为新上下文。
“waitForVm”绑定
Source code
这在上面的示例中没有使用,但如果您想在运行时动态添加视图模型,它非常有用,而不是之前的 ko.applyBindings。这样,您可以延迟初始化应用程序的某些部分,直到用户真正想要与它们进行交互。
waitForVm 等到指定的视图模型可用后再绑定其子元素。它不会修改绑定上下文。
<div data-bind="waitForVm: 'MessageList'">
<!-- bindings in here are not executed until 'MessageList' is loaded -->
<div data-bind="childVm: 'MessageList'"> ... </div>
</div>
“模态”绑定
Source code
这需要一个 Knockout 模板,将其与视图模型结合,渲染它并将结果传递给处理模态对话框的外部 JS 库。
想象一下这个模态库
- 初始化时,在
</body>之前创建一个DOM容器
- 当被要求显示模式时,获取此容器并将其显示在页面的其余部分上,灯箱样式
让我们再看看模态绑定的作用:
<!-- context here: the individual message object -->
<li>
<p data-bind="text: body, modal: {viewmodelName: 'MessageReply', parentViewmodel: $parent, setupViewmodelWith: $data, templateName: 'message-reply-template'}">
</p>
</li>
modal会
- 使用父视图模型
MessageList,在我们当前的绑定上下文$parent中找到
- 通过
getVm() 询问它的子视图模型实例MessageReply
- 添加一个点击绑定到
<p>,当它被激活时
- 在
MessageReply 上调用setup(),将$data 交给它——用户点击的当前消息
- 准备模态并
- 将绑定到
MessageReply 视图模型的模板“message-reply-template”呈现到模态 DOM 容器中