View Composition
While playing around with RecoilJS I really grew to love its compose binding. So much so that when I was back working on one of my KnockoutJS-based projects that I just HAD to come up with a new binding. Add to the fact that I’m also using Knockback and wanted to at least make concessions for the memory/release structure that framework provides, and suddenly I had motivation to make a new binding!
Where my view at?
First things first, I made a view binding. I chose to separate the compose & view bindings into 2 separate bindings because I could. It also just felt clumsy passing in an anonymous object sorta like you need to do with knockout’s template binding; I mean, the whole idea behind this was to simplify.
ko.bindingHandlers.view = { init: function (element, valueAccessor) { $(element).data("composeBinding", { view: valueAccessor() }); } }
The view binding is entirely optional. It just lets you smash down a specific view inside of your composition container, if that’s how you roll.
Compose THIS!
So then I needed to handle the actual compose binding:
ko.bindingHandlers.compose = { after: ["view"], init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { var bindingValue = ko.utils.unwrapObservable(valueAccessor()), hasValue = !!bindingValue; if (hasValue) { var viewSettings = $(element).data("composeBinding") || {}; //if no view was specified via the view binding, retrieve any available //view attribute off of the provided value if (viewSettings.view || bindingValue.view) { $(element).data("composeBinding", $.extend(viewSettings, { valid: true })); } else { $(element).data("composeBinding", undefined); throw new Error("Invalid compose binding: no view found for composition."); } } else { $(element).empty().data("composeBinding", undefined); throw new Error("Invalid compose binding: no value provided as data context."); } return { 'controlsDescendantBindings': true }; }, update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { var initData = $(element).data("composeBinding"), valueData = ko.utils.unwrapObservable(valueAccessor()); if (initData.valid) { var view = ko.utils.unwrapObservable(initData.view); if (view) { view = initData.view; } else { view = ko.utils.unwrapObservable(valueData.view); if (view) { view = valueData.view; } } kb.releaseOnNodeRemove(valueAccessor(), element); ko.bindingHandlers.template.update(element, function () { return { name: view, data: valueAccessor() }; }, allBindingsAccessor, viewModel, bindingContext); } else { $(element).empty(); } } }
So what?
This just enables some extra laziness on my part. I can now set up a view composition with data-bind=”compose: subView” where subView is a property in my current context containing a property named “view”. You can either use a straight-up string or some manner of ko.observable which returns a string, letting you chance which template is composed in place if necessary (think view/edit as one case).
If you get really persnickety you can always data-bind=”compose: subView, view: ‘my-very-specialized-circumstances’” because why not?
Oh yeah
I haven’t actually done any optimization on this thing just yet, or even really used it anywhere super heavy in my own projects. I just thought it was neat and figured I would slap it up here.