Guilherme Oderdenge
Guilherme Oderdenge

Reputation: 5001

Accessing external observableArray (KnockoutJS)

The goal

Access external observableArray to change one of its properties.

The problem

There is two lists in my application. One of these lists consists in store items that I have added to it — very similar to a shopping cart; the other list is to available products to buy. Each product has an "add button" and when I click on it, like a magic, its appears on the "shopping cart".

To remove this product from shopping cart we must to click in the same button that adds our product, because its state is changed to "remove". Or, we can click next to item that is added on list — there is a "remove button" there.

Until here, all works fine. The problem is: when I remove the product by clicking in the "x" of shopping cart, the button of the product doesn't back to normal. In other words, the button doesn't back to "add button".

To see an illustrative example, just click here to go to jsFiddle.

Technical language

I need to access self.products = ko.observableArray(products); that is on ProductViewModel from SummaryViewModel.

The code

If something happens with jsFiddle, the code is the following.

HTML:

<ul class="summary">
    <!-- ko foreach: Summary.items -->
        <p data-bind="text: name"></p>
        <button class="btn btn-danger btn-mini remove-item">
            <i class="icon-remove">×</i>
        </button>
    <!-- /ko -->
</ul>

<h1>What would you to buy?</h1>

<ul class="products">
    <!-- ko foreach: Product.products -->
    <li>
        <h3 data-bind="text: name"></h3>
        <p data-bind="text: desc"></p>
        <!-- ko if:isAdded -->
        <button data-bind="if: isAdded" class="btn btn-small btn-success action remove">
            <i data-bind="click: $root.Summary.remove" class="icon-ok">Remove</i>
        </button>
        <!-- /ko -->
        <!-- ko ifnot:isAdded -->
        <form data-bind="submit: function() { $root.Summary.add($data); }">
            <button data-bind="ifnot: isAdded" class="btn btn-small action add">
                <i class="icon-plus">Add</i>
            </button>
        </form>
        <!-- /ko -->
    </li>
    <!-- /ko -->
</ul>

JavaScript:

function Product(id, name, desc) {
    var self = this;

    self.id = ko.observable(id);
    self.name = ko.observable(name);
    self.desc = ko.observable(desc);
    self.isAdded = ko.observable(false);
}

function Item(id, name) {
    var self = this;

    self.id = ko.observable(id);
    self.name = ko.observable(name);
}

function SummaryViewModel() {
    var self = this;
    self.items = ko.observableArray([]);

    self.add = function (item) {
        self.items.push(new Item(item.id(), item.name()));

        console.log(item);

        item.isAdded(true);
    };

    self.remove = function (item) {
        var i = self.items().filter(function(elem){
            return elem.id() === item.id();
        })[0];
        self.items.remove(i);
        item.isAdded(false);
    };
};

function ProductViewModel(products) {
    var self = this;

    self.products = ko.observableArray(products);
};

var products = [
    new Product(1, "GTA V", "by Rockstar"), 
    new Product(2, "Watch_Dogs", "by Ubisoft")
];

ViewModel = {
    Summary: new SummaryViewModel(),
    Product: new ProductViewModel(products)
}

ko.applyBindings(ViewModel);

Upvotes: 0

Views: 158

Answers (3)

Tomalak
Tomalak

Reputation: 338208

It's best not to track the same information about an object in two different places. This will always result in syncing issues.

In your case you track whether an object is "added"...

  1. in the object itself (the isAdded observable in your Product model) and
  2. in a manual list of "added" objects (the items observable in your Summary model)

It's useful to lose one the two places.

For example you could remove the manual list and just keep track of object state in the Product model.

Then you could use a computed observable that returns a filtered view (ko.utils.arrayFilter) of the selected products and Knockout does all the rest.

function Product(id, name, desc) {
    var self = this;

    self.id = ko.observable(id);
    self.name = ko.observable(name);
    self.desc = ko.observable(desc);
    self.isAdded = ko.observable(false);

    self.addRemoveText = ko.computed(function () {
        return self.isAdded() ? "Remove" : "Add";
    });
    self.addRemove = function () {
        self.isAdded(!self.isAdded());
    };
}

function SummaryViewModel(parent) {
    var self = this;

    self.items = ko.computed(function () {
        var products = ko.utils.unwrapObservable(parent.products);
        return ko.utils.arrayFilter(products, function (product) {
            return product.isAdded();
        });
    });
}

function ProductViewModel(parent) {
    var self = this;

    self.items = ko.observableArray(parent.products);
}

function ViewModel(products) {
    var self = this;

    self.products = ko.utils.arrayMap(products, function (init) {
        return new Product(init.id, init.name, init.desc);
    });
    self.Summary = new SummaryViewModel(self);
    self.Product = new ProductViewModel(self);
}

See it live here: http://jsfiddle.net/Tomalak/Jr3Tk/4/

Also note that the base HTML got a lot simpler.

<ul class="summary" data-bind="with: Summary">
    <!-- ko foreach: items -->
    <li>
        <p data-bind="text: name"></p>
        <button class="btn btn-danger btn-mini remove-item" data-bind="click: addRemove">
            <i class="icon-remove">×</i>
        </button>
    </li>
    <!-- /ko -->
</ul>

<h1>What would you to buy?</h1>

<ul class="products" data-bind="with: Product">
    <!-- ko foreach: items -->
    <li>
        <h3 data-bind="text: name"></h3>
        <p data-bind="text: desc"></p>
        <button class="btn btn-small btn-success action remove">
            <i data-bind="click: addRemove, text: addRemoveText" class="icon-ok">Remove</i>
        </button>
    </li>
    <!-- /ko -->
</ul>

Upvotes: 1

PW Kad
PW Kad

Reputation: 14995

The reason this happens is because when you push the item into the summary list it no longer has the isAdded or desc properties. You either need to add these to your summary model or pass the item directly through, and not create a new object.

JavaScript throws the error ('has no property 'isAdded') is how I knew this. Open your console when you are running it to see the error.

Adding console.log(item); showed me the properties that it had.

self.remove = function (item) {
    var i = self.items().filter(function(elem){
        return elem.id() === item.id();
    })[0];
    self.items.remove(i);
    console.log(item);
    item.isAdded(false);
};

Give me a second and I will update your fiddle.

http://jsfiddle.net/Jr3Tk/3/

That is a working example. I removed some redundancy (such as going through and filtering for the product when you already have it)

Upvotes: 1

Andrey Nelubin
Andrey Nelubin

Reputation: 3294

ProductViewModel.products.subscribe(function(products){
    SummaryViewModel.products(products);
});

But first you need to add self.products = ko.observableArray([]); in SummaryViewModel

Did I understand correctly?

Upvotes: 0

Related Questions