0
votes

I have an observableArray on property of my view model:

self.rates = ko.observableArray([])

The contents of the array are displayed in an HTML table. There is a button to add items to the array. These are validated observables:

self.newRate = function () {
        var rate = new Rate({id: self.id});
        rate.isEditing(true);
        rate.isNew = true;
        rate = ko.validatedObservable(rate);
        self.vendor().rates.push(rate);
    };

This works fine. The item is added to the array and the view updates. There is a cancel link next to the newly added item to let the user remove the row.

self.editRateCancel = function (item) {
    if (item.isNew === true) {
        self.vendor().rates.remove(item);
    } else {
        item.cancelEdit();
        ko.utils.arrayForEach(self.unitsOfMeasure(), function (uom) {
            if(item.cacheUnitOfMeasureID === uom.value) {
                item.selectedUOM(uom);
            }
        });
    }
};

The call to the remove(item) doesn't remove the item. If I don't set the item as a validated observable, the remove succeeds. Looking at the remove function shows the item being passed in (valueOrPredicate) is of type Object, (Rate) but the value being returned from the underlying array to be of Object, (Function) so the predicate(value) returns false so the item is not removed.

KnockoutJS remove function:

ko.observableArray['fn'] = {
    'remove': function (valueOrPredicate) {
        var underlyingArray = this.peek();
        var removedValues = [];
        var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
        for (var i = 0; i < underlyingArray.length; i++) {
            var value = underlyingArray[i];
            if (predicate(value)) {
                if (removedValues.length === 0) {
                    this.valueWillMutate();
                }
                removedValues.push(value);
                underlyingArray.splice(i, 1);
                i--;
            }
        }
        if (removedValues.length) {
            this.valueHasMutated();
        }
        return removedValues;
    },

How can I remove specific validated observables from an observable array? Are there any utility functions available?

2

2 Answers

0
votes

I've encountered the same problem and these are the approaches that I've tried:

FIRST APPROACH

Create an observable array and with instances of Rate and a computed observable which will return each item from Rate as a validatedObservable. The problem with this approach is that each time you add or remove item to the array all validatedObservable are recreated which isn't efficient and causes weird UI behavior.

SECOND APPROACH

Create an additional deleted observable field for Rate and have a visible binding based on the value of this field. Then it will not be removed from the observable array but it will not be visible to the user.

THIRD APPROACH

Create an additional index field for Rate and in the parent view model (the one containing self.rates) keep a lastIndex with initial value set to 0. Then the function to add a rate would look like this:

self.newRate = function () {
    var rate = new Rate({id: self.id});
    rate.isEditing(true);
    rate.isNew = true;
    rate.index = lastIndex++;
    rate = ko.validatedObservable(rate);
    self.vendor().rates.push(rate);
};

And the function to remove the item would use a predicate function and look like this:

self.editRateCancel = function (item) {
    if (item.isNew === true) {
        self.vendor().rates.remove(function (value) {
            // remember about parenthesis after value()
            // because it's an instance of validatedObservable()
            // and not an instance of Rate()
            return value().index == item.index;
        });
    } else {
        item.cancelEdit();
        ko.utils.arrayForEach(self.unitsOfMeasure(), function (uom) {
            if(item.cacheUnitOfMeasureID === uom.value) {
                item.selectedUOM(uom);
            }
        });
    }
};

I went ahead with the third approach but the second might be acceptable for you as well.

2
votes

I just encountered this problem and my research led me here so I will jot down what I did to solve it.

The foreach binding in knockout allows access to the index of the current element using $index(). Instead of using the normal mechanism of passing the object into the remove function I passed the index.

Examples

The Usual:

function ViewModel() {
    var self = this;
    .
    .
    .

    removeItem(item) {
        // Tries to remove matching object, fails on observables
        self.myArray.remove(item);
    }
}

My Workaround:

function ViewModel() {
    var self = this;
    .
    .
    .

    removeItem(index) {
        // Removes 1 item at position of index 
        self.myArray.splice(index, 1);
    }
}

In the workaround the html would look like this

<div data-bind="foreach: myArray">
    <p data-bind="text: somePropertyOfTheObject"></p>
    // The bind call puts the index into the list of parameters of the function so it is available in the removeItems function
    <input type="button" value="remove" data-bind="click: $root.removeItem.bind(null, $index())" />
</div>

Finally here is a fiddle I made. https://jsfiddle.net/jimmypc15/yf5h0kf2/2/