Knockout JS calling ViewModel function in foreach binding


Luis Manez - Microsoft MVP

Let's consider using a culled view model like this:

var data = [{ id: 1, name: "John Doe" }, { id: 2, name: ""}, { id: 3, name: "Peter Parker"}];

var viewModel = {
    items: ko.observableArray(data)
};

viewModel.showName = function (name) {
    console.log(this);
    return name && name.length > 0;
};

viewModel.removePerson = function () {
    console.log(this); 
};

ko.applyBindings(viewModel);

Use this view:

<ul data-bind="foreach: items">
    <li><span data-bind="text: id"></span>
         <span data-bind="visible: $root.showName(name)">Yes! show the name</span>
         <a href="#" data-bind="click: $root.removePerson">Remove</a>
    </li>
</ul>

You can see it in action here : http://jsfiddle.net/SmW35/8/

In this case, when someone clicks the "delete" link, and KO calls the showName function, the object "this" inside that function is the object of the current item, for example if I click "delete" in item 2 , "this" is {id: 2, name: ""} However, when KO binds "visible" and calls the showName function, the "this" object does not contain the current item, you must pass the "name" (or you can use $data).

Therefore, I have 2 questions:

  • There is a way to call the showName function from the view without passing the name or $data (similar behavior to the Remove link)
  • If not, is there a problem? I had an interesting discussion with a colleague and thought this was incorrect because you are sending data from View($root.showName(name)), so this is not a "pure" MVVM pattern. He suggested creating a custom KO binding for functionality. Killing flies with tanks in my opinion, but I'd love to know if there is another way, or you also think I'm not using pure MVVM pattern in my code.
Jeroen

In a sense, your colleagues have a point of view. I wouldn't create a custom binding myself to handle this (in the ongoing subjective comments, custom bindings are better suited for this purpose if there is a special way of communicating between the view and the viewmodel; See this article for detailed instructions on how to use them).

On a side note, if we do explore a custom option for binding, I think you could do this with a textIfNotEmptybinding handler that combines textthe bindings visiblein one. On the other hand, if showNamethe functionality remains as-is, then you can also:

<span data-bind="visible: !!name, text: name"></span>

Anyway, I would like the following...

The underlying problem is IMO that the view model violates the single responsibility principle: showNamefunctionality should be the responsibility of the view model representing the item.

var Item = function(data) {
    var self = this;

    self.id = data.id;

    self.name = ko.observable(data.name); 
    // or plain "self.name = data.name;" if you don't need 2way binding

    self.showName = ko.computed(function() { 
         return self.name() && self.name().length > 0;
    });
}

Now you can easily bind like this:

<ul data-bind="foreach: items">
    <li><span data-bind="text: id"></span>
         <span data-bind="visible: showName">Yes! show the name</span>
         <a href="#" data-bind="click: $root.removePerson">Remove</a>
    </li>
</ul>

This also allows you to rewrite removePersonas:

viewModel.removePerson = function (person) {
    console.log(person); 
};

This does require you to do some extra work when building the observable array, but it's worth it because it clearly separates all the problems. It can be done as follows:

var viewModel = {
    items: ko.observableArray(data.map(function(item) { return new Item(item); }))
};

See this fiddle for the above example .

Related


Knockout.js foreach binding works but not binding

Gera Plant Using Knockout.js 3.2.0, I've been struggling to get the observableArray to bind successfully and narrow it down to the with binding. The foreach has no problem, but with throws the error Uncaught ReferenceError: cannot handle combining 'with: funct

Knockout.js foreach binding works but not binding

Gera Plant Using Knockout.js 3.2.0, I've been struggling to get the observableArray to bind successfully and narrow it down to the with binding. The foreach has no problem, but with throws the error Uncaught ReferenceError: cannot handle combining 'with: funct

Knockout.js data binding to ViewModel

Damon I'm trying to figure out KnockOut data binding and am struggling to get a simple form to bind to a ViewModel. I am using WebAPI to extract my JSON data. This is my ViewModel, when this "find" method is called, it creates a new WorkOrder object and popula

Knockout.js data binding to ViewModel

Damon I'm trying to figure out KnockOut data binding and am struggling to get a simple form to bind to a ViewModel. I am using WebAPI to extract my JSON data. This is my ViewModel, when this "find" method is called, it creates a new WorkOrder object and popula

Knockout.js data binding to ViewModel

Damon I'm trying to figure out KnockOut data binding and am struggling to get a simple form to bind to a ViewModel. I am using WebAPI to extract my JSON data. This is my ViewModel, when this "find" method is called, it creates a new WorkOrder object and popula

Knockout js: foreach binding to add static element

dafriskymonkey I want to do some pagination on a list of observable objects. I use bootstrap for styling and in their documentation they use an unsorted list to display links to pages. Suppose the view contains the following code: <ul class="pagination" data-b

Knockout JS: Add form in foreach binding

rookie I would like to add a "delete comment" button as a form that triggers an ajax request next to each comment generated using knockout's foreach binding. commentsis an observable array where each annotation is associated with object username, text, timesta

Knockout js foreach binding shows [object object]

Benjamin Baggins I want to add one <option>for each item in my _areas array KnockoutObservableArray<string>. However, when I do: <select multiple="multiple" class="select-multiple" data-bind="foreach: _regionGetter._areas">

Knockout js: foreach binding to add a static element

dafriskymonkey I want to do some pagination on a list of observable objects. I use bootstrap for styling and in their documentation they use an unsorted list to display links to pages. Suppose the view contains the following code: <ul class="pagination" data-b

Knockout.js: foreach binding with filter

username I'm new to Knockoutjs and I'm trying to accomplish two things: A. Hide/remove #TrueListSection or #FalseListSection if ul#TrueList is empty or ul#FalseList is empty B. print $index in each li C. Is it possible to use $index in each li to get the key v

Knockout.js uses foreach and $index with if binding

Dana I am trying to display the elements in an iterated list grouped within divs 3 by 3. I've used the value, $indexbut I don't know why it doesn't look right. <div class="row" data-bind="foreach: displaySel"> <div class="col-md-2"> <!--ko if: $

Knockout JS: Add form in foreach binding

rookie I would like to add a "delete comment" button as a form that triggers an ajax request next to each comment generated using knockout's foreach binding. commentsis an observable array where each annotation is associated with object username, text, timesta

Knockout js foreach binding shows [object object]

Benjamin Baggins I want to add one <option>for each item in my _areas array KnockoutObservableArray<string>. However, when I do: <select multiple="multiple" class="select-multiple" data-bind="foreach: _regionGetter._areas">

Knockout.js: foreach binding with filter

username I'm new to Knockoutjs and I'm trying to accomplish two things: A. Hide/remove #TrueListSection or #FalseListSection if ul#TrueList is empty or ul#FalseList is empty B. print $index in each li C. Is it possible to use $index in each li to get the key v

Knockout js: foreach binding to add a static element

dafriskymonkey I want to do some pagination on a list of observable objects. I use bootstrap for styling and in their documentation they use an unsorted list to display links to pages. Suppose the view contains the following code: <ul class="pagination" data-b