I'm working with the awesome Knockout.js library on a project and am looking for a way to compose sections of my UI at run-time.
For example I have have a couple of templates (simplified, below) that are made up of child templates. Id like to pass a view model to these and render them, and then be able to append (and remove) the contents from criteria form.
<!-- used with LineGraphModel -->
<script type="text/html" name="linegraph-template">
<div id="LineGraph">
<div data-bind="contextTemplate: { name: 'series-template', data: seriesChoices, context: { selected: series } }"></div>
<div data-bind="contextTemplate: { name: 'xaxis-template', data: xAxisChoices, context: { selected: xaxis } }"></div>
<div data-bind="contextTemplate: { name: 'daterange-template', data: dateRangeChoices, context: { selected: dateRange } }"></div>
<div data-bind="template: { name: 'button-template', data: $data }"></div>
</div>
</script>
<!-- used with PieChartModel -->
<script type="text/html" name="piechart-template">
<div id="PieGraph">
<div data-bind="contextTemplate: { name: 'series-template', data: seriesChoices, context: { selected: series } }"></div>
<div data-bind="contextTemplate: { name: 'daterange-template', data: dateRangeChoices, context: { selected: dateRange } }"></div>
<div data-bind="template: { name: 'button-template', data: $data }"></div>
</div>
</script>
I've begin wandering down the path of ko.renderTemplate but I can't seem to find any good documentation on how to create a new div and append the result to an existing div. Is this possible, or is there another approach I should be trying?
After writing all this down, it dawns on me that this might exceed the scope of your question quite a bit. If that is indeed the case, I apologize; I hope that you still might get some value out of it.
This stuff here comes from a real app I have been working on for several months now. It's a quick and dirty extraction and might contain bugs or typos where I removed app-specific code or simplified it to make it easier to follow.
With it, I can
Here's a quick overview how it works.
Pretend for a second you are going to build an app that shows a list of messages. The user can click on a message to open a modal dialog and reply. We have three viewmodels:
MainMessageList that takes care of displaying the list of messagesMessageReply that is responsible for the reply functionality.All our viewmodel constructors are neatly namespaced in app.viewmodels. Let's set them up:
$(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);
});
Our markup looks something like this:
<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>
There are two custom bindings in there, childVm and modal. The former just looks up a child viewmodel ands sets it as the binding context, whereas the modal binding is responsible for rendering the template in the correct context and handing the result to a separate JS library.
Viewmodels gain the ability to nest by borrowing constructor functions, a Parent, a Child or both at the same time. Here is the source for them.
Parents
If a viewmodel should be able to have child viewmodels, it borrows the Parent constructor:
app.viewmodels.Main = function Main() {
app.viewmodels.Parent.apply(this);
this.currentUser = //.. imagine the current user being loaded here from somewhere
};
As a parent viewmodel, Main has gained three things:
.addChildVm(string): add a child viewmodel by passing its name. It's automatically looked up in the app.viewmodel namespace..getVm(name): returns the child viewmodel named 'name'._childVms: an observable list containing all the childrenChildren
Every viewmodel apart from the root Main is at least a child viewmodel. MessageList is both a child to Main, and a parent to MessageReply. Very appropriately to its name, it houses the messages to be displayed in the list.
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);
};
};
As a child viewmodel, MessageList gains:
this._parentVminit function, which is called automatically by the parent if presentSo above when we added MessageList to Main with
messageListVm = mainVm.addChildVm('MessageList');
, Main
MessageListinitThe child then set itself up by getting a reference to the current user, which is mainted by the parent Main viewmodel.
Our last viewmodel: the MessageReply
MessageReply is just a child viewmodel; like it's parent MessageList did itself, it too copies the current user when initialized. It expects to be handed a Message object from the modal binding, then creates a new Message in reply to it. That reply can be edited and submitted through the form in the modal.
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
}
};
The 'childVm' binding
Source code
<body>
<!-- context here: the Main viewmodel -->
<div data-bind="childVm: 'MessageList'">
<!-- context here: the MessageList viewmodel -->
</div>
This is merely a convenience wrapper around Knockouts own 'with:' binding. It takes a viewmodel name as its value accessor, looks up a child viewmodel of that name in the current binding context, and uses the 'with:' binding to set that child as the new context.
The 'waitForVm' binding
Source code
This isn't used in the example above, but is quite useful if you want to add viewmodels dynamically at runtime, as opposed to before ko.applyBindings. This way, you can delay initializing parts of your application until the user actually wants to interact with them.
waitForVm waits until the specified viewmodel is available before binding its child elements. It does not modify the binding context.
<div data-bind="waitForVm: 'MessageList'">
<!-- bindings in here are not executed until 'MessageList' is loaded -->
<div data-bind="childVm: 'MessageList'"> ... </div>
</div>
The 'modal' binding
Source code
This takes a Knockout template, marries it to a viewmodel, renders it and passes the result to an external JS library that handles the modal dialog.
Imagine that this modal library
</body>Let's look at the modal binding in action again:
<!-- 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 will
MessageList, found in our current binding context at $parentgetVm() for its child viewmodel instance MessageReply<p>, which when activated
setup() on MessageReply, handing it our $data - the current message the user clicked onMessageReply viewmodel, into the modals DOM containerIf you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With