Search code examples
knockout.jsknockout-components

$component incorrect within foreach binding inside a KO component


JS Fiddle showing issue: http://jsfiddle.net/davetropeano/58vm9r6g/7/

I have a custom component that renders an observable array. List elements are readonly and I am trying to support letting a user delete an element.

Here's the template:

<template id="kv-list">
<input type="text" placeholder="key" data-bind="textInput: k">
<input type="text" placeholder="value" data-bind="textInput: v">
<button data-bind="click: add">Add</button><br>

<table>
    <thead>
        <tr>
            <th data-bind="text: keyHeading"></th>
            <th data-bind="text: valueHeading"></th>
            <th></th>
        </tr>
    </thead>
    <tbody  data-bind="foreach: items">
        <tr>
            <td data-bind="text: k"></td>
            <td data-bind="text: v"></td>
            <td><a href="#" data-bind="click: $component.delete">delete</a></td>
        </tr>
    </tbody>
</table>

and the ViewModel and registration code:


    function KV(k, v) {
        self = this;
        self.k = k;
        self.v = v;
    }

    function KVPairList(params) {
        this.items = params.items;
        this.keyHeading = params.keyHeading || 'Key';
        this.valueHeading = params.valueHeading || 'Value';
        this.k = ko.observable();
        this.v = ko.observable();   
    }

    KVPairList.prototype.add = function () {
        this.items.push(new KV(this.k(), this.v()));
    };


    KVPairList.prototype.delete = function (item) {
        this.items.remove(item);
    };


    function VM(params) {
        this.title = params && params.heading ? params.heading : 'KO Component Example';
        this.variants = ko.observableArray();
    }


    ko.components.register('kvlist', {
        viewModel: KVPairList,
        template: {
            element: 'kv-list'
        }
    });

    ko.components.register('page-main', {
        viewModel: VM,
        template: { element: 'wrapper' }
    });

    ko.applyBindings();

Adding to the observable array works fine. But if you click delete on one of the rows KO throughs an error:

Uncaught TypeError: Cannot read property 'remove' of undefined

It looks like what is happening is that $component is not the context of the component's viewmodel -- it is just the item inside the foreach binding. I tried $parent with the same effect.

Is there a way to access the component's viewmodel inside the foreach loop?

(JS Fiddle showing issue: http://jsfiddle.net/davetropeano/58vm9r6g/7/)


Solution

  • For some reason 'this' inside the remove method is not refferring to KVPairList.

    This is why i usually proffer to use a scoped variable to refer to the instance and prevent this closure issues:

    Try this:

    function KVPairList(params) {
        var self = this;
    
        self.add = function(){
             self.items.push(new KV(this.k(), this.v()));   
        };
    
        self.delete = function(item){
            self.items.remove(item);  
        }
        self.items = params.items;
        self.keyHeading = params.keyHeading || 'Key';
        self.valueHeading = params.valueHeading || 'Value';
        self.k = ko.observable();
        self.v = ko.observable();   
    }
    

    And the View Model code becomes more self-contained as well.

    Fiddle here:

    http://jsfiddle.net/luisvsilva/uzs7qhbs/1/