In short: I'm looking for the component-equivalent of binding preprocessing.
I am trying to encapsulate complex bindings such as
<button data-bind="openModalOnClick: {template: '...', action: '...'}, css: {...}">
delete all the things
</button>
in a custom element such as
<confirmation-button>
delete all the things
</confirmation-button>
In order to do so, I want to attach behaviour to the custom element directly by adding bindings to it on the fly.
I am aware that I can have my component insert the button as a template, but the resulting markup
<confirmation-button>
<button data-bind="openModalOnClick: {template: '...', action: '...'}, css: {...}">
delete all the things
</button>
</confirmation-button>
would be redundant.
Ideally, I could use the component registration to add the required bindings dynamically to the custom element. However, (ab)using createViewModel for this does not seem to work:
ko.components.register('confirmation-button', {
viewModel: {
createViewModel: function createViewModel(params, componentInfo) {
var Vm;
$(componentInfo.element).attr('data-bind', 'click: function() { confirm("Are you sure"); }');
Vm = function Vm(params) { };
return new Vm(params);
}
},
template: '<!-- ko template: { nodes: $componentTemplateNodes } --><!-- /ko -->'
});
confirmation-button {
border: 1px solid black;
padding: 1rem;
cursor: pointer;
}
<script src="http://knockoutjs.com/downloads/knockout-3.3.0.js"></script>
<confirmation-button>do stuff</confirmation-button>
Is it possible to add dynamic bindings to custom elements themselves in some fashion?
If I understand correctly, you'd like to hook into the moment your custom component is rendered and add behavior to the top element instead of to underlying elements.
As RPN mentions in this comment, there are no hooks for lifecycle events on custom elements yet (in version 3.2). Basically, the reason your (ab)use of createViewModel doesn't work is because that code is called before any element is rendered.
So his suggestion in that comment applies for you as well. For now, the most elegant way is to have a custom binding on the top level element. If you'd like to make it generic, you could do something like this:
<custom-element data-bind="render"></custom-element>
And then in your render custom data binding's init call, you can get the name of the custom element and look up any post processing registered to be applied. Here's a (rough) example fiddle: http://jsfiddle.net/8r891g6b/ and here's the javascript just in case:
ko.components.register('confirm-button', {
viewModel: function (params) {
params = params || {};
this.text = params.text || '(no text passed in)';
},
template: '<button data-bind="text: text"></button>'
});
ko.bindingHandlers.render = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
ko.bindingHandlers.render[element.tagName.toLowerCase()](element);
}
};
ko.bindingHandlers.render['confirm-button'] = function (element) {
ko.utils.registerEventHandler(element, 'click', function (event) {
if (!confirm('Are you sure?')) {
event.preventDefault();
}
});
};
ko.applyBindings();
By the way, this example is a bit wonky because the click event on the button would hit the button first and happen regardless of the confirm handler. I was just sticking with your example, but I hope the main idea is easy enough to understand.
I have experimented with different methods to achieve the desired results, and evaluated their pro's and contra's. Without pretending to have 'the answer' this could be useful for future reference. I tested:
ko.bindingHandlers.component.preprocess: no access to: custom element, template, component & parent viewmodel. access to: bindings.loadTemplate component loader: no access to: custom element. access to: template, parent & component viewmodel (through template)ko.bindingProvider.instance.preprocessNode: no access to: component viewmodel, template access to: custom element, bindings, parent viewmodel.#3 looks like the best fit of the three. Given the following code:
ko.bindingProvider.instance.preprocessNode = function(node) {
// access to current viewmodel
var data = ko.dataFor(node),
// access to all parent viewmodels
context = ko.contextFor(node),
// useful to get current binding values
component = ko.bindingProvider.instance.getBindings(node, context);
if (node.nodeName === 'CUSTOM-BUTTON') { // only do if 'my-custom-element'
// kind of 'raw' string extraction but does the job for demo
var getMsg = node.getAttribute('params').split('msg:')[1],
msg = getMsg.slice(0,getMsg.indexOf(','));
$(node).attr('data-bind','click: function() { confirm('+ msg +'())}');
} else {
return null;
}
}
And the following fiddle to test it: http://jsfiddle.net/kevinvanlierde/7b4n9f9h/4/ (at the top of JS, set option to 1 to test #2; and 2 (default) to test #3).
(first answer) Note: although this part kind of achieves what OP requested as "making a container element useful", it attaches events after instead of before template load; keeping for reference.
Yes it is possible, although I have tried argueing from a Knockout point of view that it might not be advisable. Given that event bindings are really only statements telling Knockout "register this function to this event", you can set the click binding directly through JS like so:
function customButton(params, parent) {
var self = this;
this.msg = params.msg;
this.label = params.label;
// this is the same as the click binding
parent.addEventListener('click', function(e) {
alert(self.msg()); alert(e.target.nodeName);
}, false);
}
var myComponent = {
viewModel: { createViewModel: function(params, componentInfo) {
var parent = componentInfo.element;
return new customButton(params, parent);
}},
template: { element: 'custom-button-tmpl' }
}
For attr and css bindings, it's slightly more complicated, but given that computed observables are just functions launching everytime their observables are updated, you could, eg change the button background in our VM above like so:
//prop used as function, so name doesn't matter
this.background = ko.computed(function() {
parent.style.backgroundColor = params.bg();
});
Test it in this fiddle. (Click on the custom element's padding to see it is the custom element to which the event is bound; change the color to see 'dynamic binding on custom elements')
If 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