0
votes

With Knockout.js I have an observable array in my view model.

function MyViewModel() {
     var self = this;

     this.getMoreInfo = function(thing){
		var updatedSport = jQuery.extend(true, {}, thing);
		updatedThing.expanded = true;
		self.aThing.theThings.replace(thing,updatedThing);
	});
     }

     this.aThing = {
	 theThings : ko.observableArray([{
            id:1, expanded:false, anotherAttribute "someValue"
         }])
     }
}

I then have some html that will change depending on the value of an attribute called "expanded". It has a clickable icon that should toggle the value of expanded from false to true (effectively updating the icon)

<div data-bind="foreach: aThing.theThings">
    <div class="row">
    	<div class="col-md-12">
           <!-- ko ifnot: $data.expanded -->
           <i class="expander fa fa-plus-circle" data-bind="click: $parent.getMoreInfo"></i>
           <!-- /ko -->
           <!-- ko if: $data.expanded -->
           <span data-bind="text: $data.expanded"/>
           <i class="expander fa fa-minus-circle" data-bind="click: $parent.getLessInfo"></i>
           <!-- /ko -->
           <span data-bind="text: id"></span>
           (<span data-bind="text: name"></span>)
	</div>
   </div>
</div>

Look at the monstrosity I wrote in the getMoreInfo() function in order to get the html to update. I am making use of the replace() function on observableArrays in knockout, which will force a notify to all subscribed objects. replace() will only work if the two parameters are not the same object. So I use a jQuery deep clone to copy my object and update the attribute, then this reflects onto the markup. My question is ... is there a simpler way to achieve this?

I simplified my snippets somewhat for the purpose of this question. The "expanded" attribute actually does not exist until a user performs a certain action on the app. It is dynamically added and is not an observable attribute in itself. I tried to cal ko.observable() on this attribute alone, but it did not prevent the need for calling replace() on the observable array to make the UI refresh.

1
You need to explicitly subscribe to your observableArray and do stuff on the subscription callback function. Here's another SO thread about this same issue: stackoverflow.com/questions/12076886/…Marventus
The div of "theThings" is supposed to list out the the contents of MyViewModel.theThings. Subscribing explicitly would allow me to write code within a callback that is fired when I change the array. But then what? I need to see the changes reflected in the markup automatically.DrLazer
Ok, I didn't realize you were actually trying to map object properties inside your observable array against the HTML. According to knockout docs, "an observableArray tracks which objects are in the array, not the state of those objects". What you could do is turn some of your object properties into observables inside your observable array.Marventus

1 Answers

1
votes

Knockout best suits an architecture in which models that have dynamic properties and event handlers are backed by a view model.

By constructing a view model Thing, you can greatly improve the quality and readability of your code. Here's an example. Note how much clearer the template (= view) has become.

function Thing(id, expanded, name) {
  // Props that don't change are mapped
  // to the instance
  this.id = id;
  this.name = name;
  
  // You can define default props in your constructor
  // as well
  this.anotherAttribute = "someValue";

  // Props that will change are made observable
  this.expanded = ko.observable(expanded);

  // Props that rely on another property are made
  // computed
  this.iconClass = ko.pureComputed(function() {
    return this.expanded()
      ? "fa-minus-circle"
      : "fa-plus-circle";
  }, this);
};

// This is our click handler
Thing.prototype.toggleExpanded = function() {
  this.expanded(!this.expanded());
};

// This makes it easy to construct VMs from an array of data
Thing.fromData = function(opts) {
  return new Thing(opts.id, opts.expanded, "Some name");
}



function MyViewModel() {
  this.things = ko.observableArray(
    [{
      id: 1,
      expanded: false,
      anotherAttribute: "someValue"
    }].map(Thing.fromData)
  );
};

MyViewModel.prototype.addThing = function(opts) {
  this.things.push(Thing.fromData(opts));
}

MyViewModel.prototype.removeThing = function(opts) {
  var toRemove = this.things().find(function(thing) {
    return thing.id === opts.id;
  });
  
  if (toRemove) this.things.remove(toRemove);
}

var app = new MyViewModel();
ko.applyBindings(app);

// Add stuff later:
setTimeout(function() {
  app.addThing({ id: 2, expanded: true });
  app.addThing({ id: 3, expanded: false });
}, 2000);

setTimeout(function() {
  app.removeThing({ id: 2, expanded: false });
}, 4000);
.fa { width: 15px; height: 15px; display: inline-block; border-radius: 50%; background: green; }
.fa-minus-circle::after { content: "-" }
.fa-plus-circle::after { content: "+" }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

<div data-bind="foreach: things">
  <div class="row">
    <div class="col-md-12">
      <i data-bind="click: toggleExpanded, css: iconClass" class="expander fa"></i>
      <span data-bind="text: id"></span> (
      <span data-bind="text: name"></span>)
    </div>
  </div>
</div>