KendoUI data- attribute event handlers and 'this' scope
Asked Answered
E

4

7

It seems that kendo's unobtrusive-javascript style event calls break this in my method context.

Say I have an object Foo, instantiated as bar = new Foo()

function Foo(){};

Foo.prototype.name = "Herring";
Foo.prototype.doSomething = function(e) {
   alert(this.name);
};

bar = new Foo();

And attach the event using data-click for example

<a data-role="button" data-click="bar.doSomething">Click Me</a>

Object context to bar is replaced (not sure why, since we have the convenient element container.) and so this.name is undefined.

I've tried the old var self = this; in the object constructor, but it's not working, does anyone know what the best way to solve this is?

Update : Hacky Workaround

Since I really don't want to lose the benefits of wrapping up my modules as classes, I've created event call functions wrappers, which then call the methods on the appropriate object.

For example, connect the markup to a wrapper-function.

<a data-role="button" data-click="doSomething">Click Me</a>

and the wrapper function just calls the object.method.

function doSomething(e){ bar.doSomething(e) };  

Now, this achieves the intended result, but it's quite horrible, each and every event called from markup must have a proxy function like the one above. So just imagine a scenario where you have 300 events... and you'll instantly see why this is horrible.

If there's no other solution, and I dearly hope there is. I'll post this workaround as an answer, but as far as I'm concerned, it's far from desirable.

Footnote

I'll be completely honest this seems like major architectural flaw in Kendo, since this method of calling events from markup is "the kendo way." Obviously it can't be patched out, because there's probably a fair bit of code already dealing with this as a reference to the html element.

Being able to override it, or being able to route these event calls through a generic handler which can pass the call on, essentially a generic proxy function, are possible ways this could be dealt with. It could also be a simple configurable value on the kendo. object.

Theoretical Solution

I'll post follow-up if this works, in theory it's possible to throw events at a generic proxy, and have it call the properly scoped function.

Say we use the event attribute to call the proxy and then create a separate attribute to convey the object/method call. For example.

<a data-role="button" data-click="prox" data-prox="o.eventHandler">Click Me</a>

The proxy function would pull prox from the attribute dataset:

method - using eval

Not because I'm evil, but needs must.

// sitting in global namespace 
function prox(e){
    var p = e.sender.element.data['prox'];
    // make sure our delegate is a function.
    if("function" == eval("typeof "+p)) { 
        eval(p + "(e)");
    }
}

Obviously I'd like a better way to do this but, at least it's DRY.

(I'll cook a non-eval method in a moment...)

Begone Eval...

let's use the window context to locate the object / method.

function prox(e) {
   var p = e.sender.element.data['prox'];
   if(p.indexOf(".") == -1){
      // global function : where *this* is window.
      // check you've got the function if not ditch it.
      if("function" == typeof window[p]) window[p](e);
   } else {
      // object/method (one level deep only.)
      var s = p.split(".");
      var o = s[0], m = s[1];
      // check the object/method is a function before executing it.
      if("function" == typeof window[o][p]) window[o][p](e);
   }
}

Of course for global (window) scoped functions, this as the element is probably more useful, but in that case, you have a choice, I'd leave out the

Earthenware answered 29/12, 2012 at 13:14 Comment(0)
E
0

version in use.

// dynamic proxy for retaining object context on methods called by
// data- attributes in Kendo.
// 
// e.g.
//
//     data-click="o.method" 
//
// Would lose context with `o` - context would be set in the same
// way as JQuery handlers, which is an inconvenience.
// 
// Alternatively we use the prox method
//
//     data-click="prox"
// 
// We'd then set `data-prox` on the same element, to the
// object.method pair.
//
//     data-prox="o.method"
//
// This is read by prox, transformed into a method call, type
// checked and executed if it's a valid method.
//
// A `data-prox` value in any form other than `object.method` will
// be ignored, for example, `object.child.method` will fail. If
// you're doing that sort of thing, feel free to hack it.
// 
// There's a backup eval() to locate the object if window doesn't
// own it. It should be possible to remove it under most
// circumstances, it's here for compatability with
// JSFiddle. (JSBin works without it.)
function prox(e) {
    var p = this.element.data().prox;
    if(p.indexOf(".") > -1){
        var s = p.split("."); if(s.length > 2) return; 
        var o = s[0], m = s[1];
        if("object" == typeof window[o]) {
            o = window[o];
        } 
        if("function" == typeof o[m]) o[m](e);
        // comment this out in production:
        l( "prox called " + s[0] + "::" + s[1] );
    }
}

function l(s) { console.log(s); }

Caveats

If you have multiple handlers on the same element, prox() is unsuitable, for example, if you have data-init, data-show, etc. prox cannot differentiate, and will fail.

I'll probably update this, especially if this becomes a prevalent use-case for me.

Earthenware answered 31/12, 2012 at 9:11 Comment(0)
E
0

I temporarily tried a third method, with a non-generic technique, which works like this.

Pseudo code:

MyObject {

   method : function(e) {
      if (this instanceof MyObject) {
        // Do something with this
      } else {
        myInstance.method(e); // otherwise re-call the method to set this properly.
      }
   }
}
myInstance = new MyObject();

Not as flexible as the prox method, but suitable for my use case, and at least doesn't require a separate function proxy away from the method we want to use. We could make this more terse by doing the type check & re-call up front.

e.g.

MyObject = {
   method : function(e) {
      if (! this instanceof MyObject) myInstance.method(e); // re-call

      // Method body... 

   }
}
myInstance = new MyObject();

It also meant I didn't need custom data- attributes in my markup.

Note: this method is problematic for objects which will have multiple instances, however, the objects I was applying to were single instances.

If you have handlers which need to be instance specific (which is the main reason I raised this question) the prox method is a much better fit than this, which is just a neater way of doing one-per-event proxy functions.

Earthenware answered 5/1, 2013 at 8:38 Comment(0)
F
0

You may use jQuery Proxy (http://api.jquery.com/jQuery.proxy/).

function Foo(){};

Foo.prototype.name = "Herring";
Foo.prototype.doSomething = function(e) {
   alert(this.name);
};

bar = new Foo();

$("btn").click($.proxy(bar.doSomething), bar);

or for inside using

$("btn").click($.proxy(this.doSomething), this);
Fulsome answered 19/3, 2013 at 11:53 Comment(1)
The question is how to do this with kendo's data- attributes.Earthenware
S
0

I developed a proxy method using the JS Proxy Polyfill that simplify calling custom logic via parameters in a custon html data-* attribute.

Include https://raw.githubusercontent.com/GoogleChrome/proxy-polyfill/master/proxy.js

function makeGridTemplateEventProxy(o) {
    return new Proxy(o, {
        get(target, eventName) {
            return function (options) {
                return templateEventProxy(options, eventName);
            }
        }
    });
}

templateEventProxy: function (options, attribute) {
    if (!options.sender.element.attr('data-proxy-' + attribute)) {
        throw new Error('Cannot find attribute data-proxy-' + attribute + ' on ' + options.sender.name + ' widget');
    }
    var proxyParams = JSON.parse(options.sender.element.attr('data-proxy-' + attribute));

    method = $("#" + proxyParams.id).data(proxyParams.widget).element.data(proxyParams.method);

    if (method && typeof method == 'function') {
        return $.proxy(method, this)(options);
    }

    return null;

}


var eventproxy = makeGridTemplateEventProxy({});

for example for upload component

<input type=file ... 
       data-success="eventproxy.customsuccesshandler"
       data-proxy-customsuccesshandler='{widget:"kendoGrid",method:"<myJqueryDataDefinedMethod>",id:"<gridId>"}'
       ....
/>

substitute myJqueryDataDefinedMethod and gridId with your parameters

as you see you can define in data-success an eventproxy with dynamic name

data-success="eventproxy.CUSTOMKEY"

and after define a custom attribute

data-proxy-CUSTOMKEY

data-proxy-CUSTOMKEY contains parameters ( JSON encoded ) you can use to implement a custom logic,

I suggested custom logic which can retrieve JS method stored on kendo widget grid via $.data

$("#" + proxyParams.id).data(proxyParams.widget).element.data(proxyParams.method)

You can bind method to grid for example with this

$('#my-grid-id').data("kendoGrid").element.data('methodName',function(e){ 
    // my implementation 
});
Scrutinize answered 21/6, 2017 at 12:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.