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
- arbitrarily nest viewmodels
- dynamically add viewmodels on the fly
- render Knockout templates bound to these nested viewmodels, and use the results flexibly
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:
- a root viewmodel called
Main
- a
MessageList
that takes care of displaying the list of messages
- a third one called
MessageReply
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 children
Children
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:
- the ability to access its parent through
this._parentVm
- an optional
init
function, which is called automatically by the parent if present
So above when we added MessageList
to Main
with
messageListVm = mainVm.addChildVm('MessageList');
, Main
- created a new instance of
MessageList
- added the instance to its own children
- and called the childs
init
The 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
- when initialized, creates a DOM container before
</body>
- when asked to display the modal, takes this container and shows it overlayed over the rest of the page, lightbox-style
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
- use the parent viewmodel
MessageList
, found in our current binding context at $parent
- ask it via
getVm()
for its child viewmodel instance MessageReply
- add a click binding to the
<p>
, which when activated
- calls
setup()
on MessageReply
, handing it our $data
- the current message the user clicked on
- prepares the modal and
- renders the template 'message-reply-template', bound to the
MessageReply
viewmodel, into the modals DOM container