1

I'm just starting learning knockout.js and am a bit stuck on managing observables and updating them correctly. See the jsfiddle for the code.

 self.addItem = function(item, event){
    ptitle = $(event.currentTarget).prev('h3').text();

    self.items.push({
      productQty: self.productQty(),
      productClip: self.productClip(),
      productTitle: ptitle
    });
  }

http://jsfiddle.net/L61qhrc1/

What I have is a list of existing html elements. I want to create another list from that list with some input fields that can be set. It's mostly working but I cannot figure out from the examples around the net I've been looking at.

when one field updates all the fields in the list update but I only want the field I'm currently updating to be updated not the whole list.

Can any kind person point me in the right direction? Cheers.

Roy J
  • 42,522
  • 10
  • 78
  • 102
digital-pollution
  • 1,054
  • 7
  • 15
  • 3
    *"What I have is a list of existing html elements"* - What you ought to have is a list of data items. The HTML (the view) is supposed to be a function of your data, not the other way around. – Tomalak Nov 26 '15 at 13:51

1 Answers1

2

As I said in my comment, your user interface has to be a logical consequence of your data and viewmodel. Your viewmodel must never be concerned with the details of the view.

Also, your fiddle looks pretty over-engineered to me.

The following is what I have gathered from your sample, but in a less byzantine way.

  • Make separate self-contained viewmodels. Ideally make them so that they can bootstrap themselves from the data you pass to the constructor.
  • Working with templates keeps the HTML of the view clean and improves modularity and reusability.
  • For more complex data, consider the the mapping plugin to bootstrap your models.
  • Consult Unique ids in knockout.js templates for a way to create working <input> / <label> pairs.

function ListItem(data, parent) {
    var self = this;
    data = data || {};

    self.productQty = ko.observable(data.productQty);
    self.productClip = ko.observable(!!data.productClip);
}

function Product(data) {
    var self = this;
    data = data || {};

    self.title = ko.observable(data.title);
    self.id = ko.observable(data.id);
    self.items = ko.observableArray();
    self.newItem = new ListItem();

    self.addItem = function () {
        self.items.push(new ListItem(ko.toJS(self.newItem), self));
    };
    self.removeItem = function (item) {
        self.items.remove(item);
    };
}

function ProductList(data) {
    var self = this;
    data = data || {};

    self.products = ko.observableArray(ko.utils.arrayMap(data.products, function (p) {
        return new Product(p);
    }));
}

var vm = new ProductList({
    products: [
        {title: "ProductName 1", id: "ProductId 1"},
        {title: "ProductName 2", id: "ProductId 2"}
    ]
});

ko.applyBindings(vm);
ul {
    list-style-type: none;
    padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<div class="container">
    <div class="m-col-6">
        <ul data-bind="foreach: products">
            <li data-bind="template: 'productTemplate'"></li>
        </ul>
    </div>
</div>

<hr />
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>

<script type="text/html" id="productTemplate">
    <h1 data-bind="text: title"></h1>
    <p data-bind="text: id"></p>
    <div data-bind="with: newItem">
        <input type="checkbox" data-bind="checked: productClip" />
        <label>has pump clips</label>
        <input type="number" data-bind="value: productQty" />
        <button data-bind="click: $parent.addItem">add to list</button>
    </div>
    <ul data-bind="foreach: items">
        <li>
            <!-- ko template: 'itemsTemplate' --><!-- /ko -->
            <button data-bind="click: $parent.removeItem">Remove</button>
        </li>
    </ul>
</script>

<script type="text/html" id="itemsTemplate">
    <b data-bind="text: $parent.title"></b>
    <span data-bind="text: productClip() ? 'has' : 'does not have'"></span> pump clips
    (<span data-bind="text: productQty"></span>)
</script>
Community
  • 1
  • 1
Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • OK but here you've drastically changed the functionality. The product list is pre-rendered it comes from a cms. the output should be in a single element but here there is an output element for each product. The uqestion is just about how to stop the all the input fields in the prerendered list from updating on change. – digital-pollution Nov 26 '15 at 14:55
  • 2
    Well, it's how I understood your sample. My code solves a superset of your problem, you can always take functionality away from it. Your two lists keep updating simultaneously because you actually only have a single list. You must create two separate viewmodels instead of binding two HTML sections to one and the same model. – Tomalak Nov 26 '15 at 15:08
  • Thanks I'm accepting your answer as the principle is correct. Using knockout observable was not required for the pre-rendered input, as you commented 'over engineered'. But inside only for the submit button to retrieve the input values and push to the observable list in the basket where most of the dynamic updating is happening (will be happening outside of the scope of this example). Thanks for pointing me in the right Direction @Tomalak – digital-pollution Nov 27 '15 at 15:41