1
votes

When I use a foreach data-bind it seems to break the $root context for knockout.

When this happens this refers to an element (user) that is a child to the viewmodel (page) and parent to the foreach elements (requests), when it ought to refer to the viewmodel (page).

I refer to $root outside of the foreach with no issue - it calls the right method in the correct context.

HTML where it works:

<div id="home-page-view" class="view" data-bind="if: status() == AppStatus.Home">
        <button data-bind="click: $root.switchState">Switch To Friend Requests</button>
</div>

Typescript where it works:

public switchState() {
        if (this.status() == AppStatus.Home) {
            this.status(AppStatus.FriendRequests); //this refers to viewmodel (page)
        } else {
            this.status(AppStatus.Home);
        }
    }

However, when in a foreach, the buttons bound like data-bind="click: $root.acceptFriendRequest" successfully call their methods, but this refers to user, not page.

HTML where its broken:

<div id="friend-requests-view" class="view" data-bind="if: status() == AppStatus.FriendRequests">
    <h1>Friend Requests</h1>
    <p>View your pending friend requests.</p>

    <div data-bind="with: user">
        <div data-bind="foreach: requests">
            <div>
                <h2 data-bind="text: firstName"></h2>
                <p data-bind="text: lastName"></p>
            </div>
            <div>
                <input type="button" value="Accept" data-bind="click: $root.acceptFriendRequest" />
                <input type="button" value="Deny" data-bind="click: $root.rejectFriendRequest" />
            </div>
        </div>
    </div>
</div>

Typescript where its broken:

public acceptFriendRequest(newFriend: UserModel) {
        this.respondToFriendRequest(true, newFriend);
}

public rejectFriendRequest(notFriend: UserModel) {
        this.respondToFriendRequest(false, notFriend);
}

Even in the HTML where its broken, 4 lines prior status() implicitly refers to the correct $root.

What is going on here that makes this refer to page.user instead of page? Given that in other contexts this refers to page, it seems that $root ought to always refer back to page.

MCVE:

Page.html

<!DOCTYPE html>
<head>
    <meta charset="utf-8" />

    <script type="text/javascript" src="/Scripts/jquery-2.1.4.js"></script>
    <script type="text/javascript" src="/Scripts/jquery-ui-1.11.4.js"></script>
    <script type="text/javascript" src="/Scripts/knockout-3.3.0.js"></script>
    <script type="text/javascript" src="/Scripts/knockout.mapping-latest.js"></script>
    <script type="text/javascript" src="app.js"></script>
    <script type="text/javascript" src="DTO/DTOs.js"></script>
</head>

<body>
    <div id="home-page-view" class="view" data-bind="if: status() == AppStatus.Home">
        <button data-bind="click: $root.switchState">Switch To Friend Requests</button>
    </div>

    <div id="friend-requests-view" class="view" data-bind="if: status() == AppStatus.FriendRequests">
        <h1>Friend Requests</h1>
        <p>View your pending friend requests.</p>

        <div data-bind="with: user">
            <div data-bind="foreach: requests">
                <div>
                    <h2 data-bind="text: firstName"></h2>
                    <p data-bind="text: lastName"></p>
                </div>
                <div>
                    <input type="button" value="Accept" data-bind="click: $root.acceptFriendRequest" />
                    <input type="button" value="Deny" data-bind="click: $root.rejectFriendRequest" />
                </div>
            </div>
        </div>
    </div>
</body>
</html>

App.ts

/// <reference path="Scripts/typings/jquery/jquery.d.ts" />
/// <reference path="Scripts/typings/jqueryui/jqueryui.d.ts" />
/// <reference path="Scripts/typings/knockout/knockout.d.ts" />
/// <reference path="Scripts/typings/knockout.mapping/knockout.mapping.d.ts" />


var page: App;

$(document).ready(function () {
    $.ajaxSetup({ cache: false });
    page = new App();
    ko.applyBindings(page);
});


class App {

    public user: KnockoutObservable<UserModel>;
    public status: KnockoutObservable<AppStatus> = ko.observable(AppStatus.Home);

    constructor() {
        this.user = ko.observable(new UserModel());
        var us: User = {firstName: "Chip", lastName: "Dipson" };
        this.user(ko.mapping.fromJS(us));
        this.user().requests.push(new UserModel());
    }

   public switchState() {
        if (this.status() == AppStatus.Home) {
            this.status(AppStatus.FriendRequests);
        } else {
            this.status(AppStatus.Home);
        }
    }

    public acceptFriendRequest(newFriend: UserModel) {
        this.respondToFriendRequest(true, newFriend); //this is wrong
    }

    public rejectFriendRequest(notFriend: UserModel) {
        this.respondToFriendRequest(false, notFriend); //this is wrong
    }

    private respondToFriendRequest(accepted: boolean, requester: UserModel) {
      //do some ajax stuff
    }
}

enum AppStatus {
    Home, FriendRequests
}

DTOs.ts

class User {
    firstName: string;
    lastName: string;

    requests: User[];
}

class UserModel {
    firstName: KnockoutObservable<string> = ko.observable("");
    lastName: KnockoutObservable<string> = ko.observable("");

    requests: KnockoutObservableArray<UserModel> = ko.observableArray<UserModel>(null);
}
1

1 Answers

4
votes

A click binding takes a function, not a snippet of code. That means that $root isn't providing execution context, it is only helping you to locate the function. If you want the execution context to be set, you will need to bind it:

<input type="button" value="Accept" data-bind="click: $root.acceptFriendRequest.bind($root)" />