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:
Main
MessageList
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:Our markup looks something like this:
There are two custom bindings in there,
childVm
andmodal
. The former just looks up a child viewmodel ands sets it as the binding context, whereas themodal
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
, aChild
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: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 theapp.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 toMain
, and a parent toMessageReply
. Very appropriately to its name, it houses the messages to be displayed in the list.As a child viewmodel,
MessageList
gains:this._parentVm
init
function, which is called automatically by the parent if presentSo above when we added
MessageList
toMain
with,
Main
MessageList
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 parentMessageList
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.The 'childVm' binding
Source code
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.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:
modal
willMessageList
, found in our current binding context at$parent
getVm()
for its child viewmodel instanceMessageReply
<p>
, which when activatedsetup()
onMessageReply
, handing it our$data
- the current message the user clicked onMessageReply
viewmodel, into the modals DOM container