Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new bindingHandler type (functions) re. #663 / #1602 #1

Closed

Conversation

brianmhunt
Copy link
Owner

re. knockout#663 / knockout#1602 ; depends on knockout#1360.

The new binding handlers have the following properties:

  1. They are function-constructors (so they can be closure-style functions, or ES6/coffeescript classes);
  2. The constructor is passed a single object-parameter, with (self-explanatory) properties:
    • value (created by defineProperty so one does not have to call an accessor)
    • $context
    • $data
    • element
    • allBindings
  3. The class is instantiated for every binding found;
  4. The .dispose for the instance, if it exists, is called when the node is removed;
  5. Instances have this.computed and this.subscribe, equivalent to the ko.* equivalent, but their owner is set to the instance and they are cleaned up when the node is removed;
  6. Are allowed in virtual elements when allowVirtual is truthy on either the constructor or prototype chain;
  7. Control descendant bindings when controlsContext is truthy on the instance or the constructor.

@brianmhunt
Copy link
Owner Author

So in ES6 parlance the following ought to work:

class Handler {
   constructor(params) {
     this.controlsBindingDescendants = true
     this.computed_value = this.computed(this.computeMethod)
     this.computed_read_only = this.computed({read: this.computeMethod})
     this.subscribe(computed_value, this.onComputedChange)
   }
   dispose() {
      /*...*/
   }
   computeMethod() {
      /*...*/
   }
   onComputedChange(newValue) {
      /*...*/
   }
}

Handler.allowVirtualElements = true

class HandlerChild extends Handler {
   computeMethod() {
     /*  some child method  */
   }
}

ko.bindingHandlers.handlerExample = Handler
ko.bindingHandlers.childHandlerExample = HandlerChild

@brianmhunt
Copy link
Owner Author

TODO:

  • twoWayBindings
  • Expose the params or parts of the params on this?
  • Error where the data-bind is present but bindings is empty
  • Add constructor to the handlerConstructorWrapper prototype

…anges

- replace the longer (and more ambiguously spelt) `allowVirtualElements` and `controlsDescendentBindings`
- better names for the binding handler functions (object or constructor)
- expose `constructor` on the handler instance prototype
- simplify the `value` accessor for the binding params
@IanYates
Copy link

Seems like a pretty elegant approach to me.
I like the computed & subscribe functions that are injected into this so they're automatically cleaned up. Should there be a pureComputed too?

Once you settle on the API a little more I'd like to try this out with TypeScript too. The only trick would be to let TypeScript know "trust me, there are two functions on the prototype called computed & subscribe". I've not tried it before, but declaring the existence of a base class and then inheriting the binding handlers from that should do the trick.

@brianmhunt
Copy link
Owner Author

Thanks @IanYates – I've added this.pureComputed (but it needs tests).

I'd be interested in how TypeScript handles this. Not knowing TS at all, if things go wonky I'd be most grateful for insight.

@ThomasMichon
Copy link

Having implemented something very similar on my team's site, I figured I'd weigh in.

Any class can already be used in ko.bindingHandlers directly if it has a 'static' method init. This means one can already assign a function as a binding handler with minimal fuss.

function TextBinding(params) {
    this.element = params.element;
    this.valueAccessor = params.valueAccessor;
    this.allBindingsAccessor = params.allBindingsAccessor;
    this.bindingContext = params.bindingContext;

    this._updateText = ko.computed(function () {
        var value = ko.unwrap(this.valueAccessor());
        this.element.textContent = value;
    }, this);
}

TextBinding.prototype.dispose = function () {
    this._updateText.dispose();
};

TextBinding.init = function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
    var binding = new (this)({
        element: element,
        valueAccessor: valueAccessor,
        allBindingsAccessor: allBindingsAccessor,
        viewModel: viewModel,
        bindingContext: bindingContext
    });

    ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
        binding.dispose();
    });
}

ko.bindingHandlers['text'] = TextBinding;

Note the use of new (this) in the init method. When you derive a class in TypeScript (and I assume ES6 as well), TypeScript copies all static methods from the super-class to the derived class. This means that if my above code were used as a BaseBinding class (with text-specific behaviors removed), then I never need to write the init method again; all derived classes can be passed to Knockout as binding handlers automatically.

There's one catch, though, and this is easy to fix:
Knockout does not use call on the init method (or update) of the binding handler to set this to the current handler object. This can be easily remedied, and makes it very easy to use classes as binding handlers.

var initResult = handlerInitFn.call(bindingKeyAndHandler.handler, node, /* ... */);
/* ... */
handlerUpdateFn.call(bindingKeyAndHandler.handler, node, /* ... */);

I think it's great to provide a base binding handler class that makes it even easier to consume this pattern, but I think we can just provide it as a optional utility and not bake it into the binding behavior.

@brianmhunt
Copy link
Owner Author

@ThomasMichon – great feedback, thank you!

@IPWright83
Copy link

I was recently reminded of a couple of issues I had with binding handlers that @brianmhunt helped with, in the end I used the approach detailed in knockout#1891. Brian, you mentioned that this might help with my issues, reading through, honestly I don't quite understand it enough to know whether it does or not - but I'd be interested if it doesn't whether it could include this somehow. The issues were essentially

  • I want to load a binding handler based on an observable
  • I want to be able to delay load a binding handler (a bit like components) and have a callback once ready
  • I want to be able to combine the two approaches above

I've got a solution at the moment which sets controlsDescendantBindings which unfortunately has caused me the odd issue now and again. It'd be really nice if I didn't have to say I want to control everything down the stack somehow.

@brianmhunt
Copy link
Owner Author

This is baked into tko now, so I'll close this PR just to clean it up.

@brianmhunt brianmhunt closed this Nov 6, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants