3

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?

janfoeh
  • 10,243
  • 2
  • 31
  • 56

2 Answers2

2

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:

  1. ko.bindingHandlers.component.preprocess: no access to: custom element, template, component & parent viewmodel. access to: bindings.
  2. A custom loadTemplate component loader: no access to: custom element. access to: template, parent & component viewmodel (through template)
  3. 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')

Community
  • 1
  • 1
webketje
  • 10,376
  • 3
  • 25
  • 54
  • Thank you for your answer, but I am not looking to emulate bindings piece-by-piece. I'm looking for the component-equivalent of [binding preprocessing](http://knockoutjs.com/documentation/binding-preprocessing.html) – janfoeh Feb 19 '15 at 16:43
  • Also, I'm not trying to replace the custom element — quite the opposite, I'm trying to make its existence useful above being a mere container by attaching behaviour to it. – janfoeh Feb 19 '15 at 16:48
  • @janfoeh Yeah the answer I linked to was only meant to demonstrate "preferred/ standard" use of Knockout. So if I understood you correctly now, you want something like `ko.bindingHandlers.component.preprocess`, but to have all component properties available inside of the function? – webketje Feb 19 '15 at 23:30
  • Yes; just as a `preprocess` callback for a binding handler allows me to inject further bindings, I need the same ability for a component handler. – janfoeh Feb 20 '15 at 00:21
  • Hey @janfoeh; finding this an interesting question, I tested some other methods and updated my answer.. Have a look and see whether it can be useful somehow :) – webketje Feb 22 '15 at 22:02
  • Thank you for investing quite a bit of time on this! `preprocessNode` seems to be a viable alternative indeed. While your solution has the advantage of not having to declare the delegator binding as I do with Milimetrics answer, his/her answer is a more practicable workaround for me. But while I'm going to accept Millimetrics answer as it stands now, I'll add a second bounty for yours; it'll just take a day longer, because bounties have to be open for at least that long. Thanks! – janfoeh Feb 23 '15 at 10:22
  • @janfoeh I totally agree with Millimetrics answer being more practical, especially if you have a lot of custom elements to check for :) – webketje Feb 23 '15 at 11:13
  • For some reason I cannot add another bounty to this answer without doubling it, which would be unfair to Milimetric. I've added the bounty to [this question](http://stackoverflow.com/questions/16681395/parent-inset-box-shadow-overlapped-by-children-divs) instead and will award it tomorrow as soon as SO lets me. – janfoeh Feb 24 '15 at 16:13
  • No don't worry m8, I found this question interesting before there was even a bounty awarded for answering it and I honestly don't mind. Investigating the workings of KnockoutJS was a nice and insightful exercise :) – webketje Feb 24 '15 at 19:40
2

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.

Milimetric
  • 13,411
  • 4
  • 44
  • 56
  • The generic delegating `render` binding is a nice idea! If and when KO gains component lifecycle events, it should be easy enough to transition over. Thanks! – janfoeh Feb 21 '15 at 16:03
  • No need to remind me — I've not once left a question with a useful answer marked unanswered, and I do not plan to do so in the future. The bounty's still open for a couple of days; if your workaround remains the best possible answer, I will accept it and award you the bounty. – janfoeh Feb 22 '15 at 01:49