Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript wrong context this

Code:

export class ViewModel {
        public users: knockout.koObservableArrayBase;

        constructor () {
            this.users = ko.observableArray([]);
            this.removeUser = this.removeUser.bind(this);//<-- Here compiller shows error
        }

        removeUser(user: User): void {
            this.users.remove(user);
        }
}

Html:

<table>
    <thead>
        <tr>
            <th>Name</th>
            <th>Surname</th>
        </tr>
    </thead>
    <tbody data-bind="foreach: users">
        <tr>
            <td><a href="#" data-bind="click: $root.removeUser">Remove</a></td>
            <td data-bind="text: name"></td>
            <td data-bind="text: surname"></td>
        </tr>
    </tbody>
</table>

Problem is in removeUser method. By default, if I don't bind context, this == UserToDelete - not viewModel object. If I adding to constructor: this.removeUser = this.removeUser.bind(this); (manually enforce context), then context is as needed this == viewmodel, but then TypeScript complains for "Cannot convert Function to (user:User)=>void requires a call signatures, but Function lacks one."

like image 305
VikciaR Avatar asked Oct 07 '12 08:10

VikciaR


5 Answers

I'm not familiar with ko so perhaps there is a better way to solve the context switching, but your typescript compiler error is caused by 'bind' returning type 'Function' which is incompatible with the type of 'removeUser'. You should be able to solve this by casting the returned function into the original type signature as follows:

this.removeUser = <(user: User) => void> this.removeUser.bind(this);
like image 85
nxn Avatar answered Nov 09 '22 19:11

nxn


Well i had the same problem thats why i came up with the following base class to fix my issue

export class ViewModelBase {
    private prefix: string = 'On';

    public Initialize() {
        for (var methodName in this) {
            var fn = this[methodName];
            var newMethodName = methodName.substr(this.prefix.length);
            if (typeof fn === 'function' && methodName.indexOf(this.prefix) == 0 && this[newMethodName] == undefined) {
                this[newMethodName] = $.proxy(fn, this);
            }
        }
    }
}

what this does is loop all members of your class and if a method starts with On it will create a new method without On that will call the original method with the right context.

Not that $.proxy is a jquery call so jquery is needed for this to work.

like image 45
Peter Avatar answered Nov 09 '22 21:11

Peter


Well, the simplest solution and what I normally do with typescript and knockout js is that I don't declare functions called from knockout on the prototype but at the constructor. So, I would make it like this:

export class ViewModel {
        public users: knockout.koObservableArrayBase;
        removeUser:(user: User) => void;

        constructor () {
            this.users = ko.observableArray([]);
            this.removeUser = (user:User) => {
                this.users.remove(user);
            }
        }
}
like image 24
George Mavritsakis Avatar answered Nov 09 '22 19:11

George Mavritsakis


An alternative is to change the click binding to use JavaScript's bind function to force the value of this to be your view model: data-bind="click: $root.MyFunc.bind($root)".

Note that $data and the click event object will still be passed in as arguments to MyFunc from Knockout as described by the click binding specification. If you need to override the arguments being passed to MyFunc, simply pass them into the bind function after $root like so: .bind($root, param1, param2). Technically, these arguments will be prepended to the arguments being supplied by Knockout, giving arguments [param1, param2, data, event].

like image 2
Malgaur Avatar answered Nov 09 '22 21:11

Malgaur


I ran into the same problem. To get the right context you can use the parameters that are passed by the clickbinding. The clickbinding passes 2 parameters, ie the user and the jquery event of the click.

If you take the jquery event, and than use the ko.contextFor() function, you can obtain the right context.

Your function would look something like:

removeUser(user: User, clickEvent: any): void {
    var self = ko.contextFor(clickEvent.srcElement).$root;
    self.users.remove(user);
}
like image 1
Paul0515 Avatar answered Nov 09 '22 19:11

Paul0515