0

I need to set from the main viewmodel a property inside an observable array.

I'm using the classic pre to debug and display the content of my observable array. By using types.valueHasMutated() I can see the applied changes - just only to vm.types (which wouldn't be the case otherwise).

However, I need to see this changes reflected inside my component.

In my example, when I ckick "Apples", the corresponding input shall be disabled like the one below. Sadly, this is actually not the case.

What I'm doing wrong?

ko.components.register("available-items", {
  viewModel: function(params) {
    function AvailableItems(params) {
      var self = this;
      self.params = params;
      self.location = "A";
      self.types = ko.computed(function() {
        var types = self.params.types();
        return ko.utils.arrayFilter(types, function(item) {
          return item.location == self.location;
        });
      });
      self.addItem = function(data, event) {
        self.params.items.addItem(self.location, data.type);
      };
    }
    return new AvailableItems(params);
  },
  template: '<div>' +
    '<h4>Add item</h4>' +
    '<ul data-bind="foreach: types">' +
    '<li>' +
    '<input type="text" data-bind="value: type, enable:available, event: {click: $parent.addItem}" readonly/>' +
    '</li>' +
    '</ul>' +
    '</div>',
  synchronous: true
});

var types = [{
  type: "Apples",
  location: "A",
  available: true
}, {
  type: "Bananas",
  location: "A",
  available: false
}];

function Vm(data) {
  var self = this;
  self.items = ko.observableArray();
  self.types = ko.observableArray(ko.utils.arrayMap(data, function(item) {
    return item;
  }));
  self.items.addItem = function(location, type) {
    self.items.push({
      location: location,
      type: type
    });
    if (location == "A" && type == "Apples") {
      self.types()[0].available = false;
      self.types.valueHasMutated();
    }
  };
}

ko.options.deferUpdates = true;
var vm = new Vm(types);
ko.applyBindings(vm);
pre {
  position: absolute;
  width: 300px;
  right: 0;
  top: 0;
}
<!DOCTYPE html>
<html>
  <head>
    <script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
  </head>
  <body>
    <div data-bind="component:{name:'available-items',params:vm}"></div>
    <ul data-bind="foreach: items">
      <li><span data-bind="text: location"></span> - <span data-bind="text: type"></span></li>
    </ul>
    <pre data-bind="text: ko.toJSON(vm.types, null, 2)"></pre>
  </body>
</html>
Tejas Thakar
  • 585
  • 5
  • 19
deblocker
  • 7,629
  • 2
  • 24
  • 59
  • 1
    You're trying to extend the observableArray with an addItem function? I don't think it'll work that way. Create an addItem function as a sibling for the observableArray. – Nimesco Jul 04 '17 at 12:01

5 Answers5

2

I have run this on jfiddle and even when I added a new type, I wasn't getting any update.

It seems like there was an issue with the

'<ul data-bind="foreach: types">' +

I changed it to

'<ul data-bind="foreach: $root.types">' +

https://jsfiddle.net/fabwoofer/9szbqhj7/1/

Now the type gets added but it seems like the re-rendering of the first item is not handled. People with similar problems have suggested using template rendering as described here

Knockout.js Templates Foreach - force complete re-render

Hope this helps

Fabwoofer
  • 111
  • 4
  • seems that `valueHasMutated()`doesn't affect `foreach`, maybe it will work if the properties of the objects inside the observablearray are observables too... – deblocker Jul 04 '17 at 13:48
1

Your available property isn't observable. In order to notify Knockout about changes and let it update UI - make this property observable.

ko.components.register("available-items", {
  viewModel: function(params) {
    function AvailableItems(params) {
      var self = this;
      self.params = params;
      self.location = "A";
      self.types = ko.computed(function() {
        var types = self.params.types();
        return ko.utils.arrayFilter(types, function(item) {
          return item.location == self.location;
        });
      });
      self.addItem = function(data, event) {
        self.params.items.addItem(self.location, data.type);
      };
    }
    return new AvailableItems(params);
  },
  template: '<div>' +
    '<h4>Add item</h4>' +
    '<ul data-bind="foreach: types">' +
    '<li>' +
    '<input type="text" data-bind="value: type, enable:available, event: {click: $parent.addItem}" readonly/>' +
    '</li>' +
    '</ul>' +
    '</div>',
  synchronous: true
});

var types = [{
  type: "Apples",
  location: "A",
  // Make property observable
  available: ko.observable(true)
}, {
  type: "Bananas",
  location: "A",
  // Make property observable
  available: ko.observable(false)
}];

function Vm(data) {
  var self = this;
  self.items = ko.observableArray();
  self.types = ko.observableArray(ko.utils.arrayMap(data, function(item) {
    return item;
  }));
  self.items.addItem = function(location, type) {
    self.items.push({
      location: location,
      type: type,
      available: ko.observable(false)
    });
    if (location == "A" && type == "Apples") {
      // Update property as observable.
      self.types()[0].available(false);
      self.types.valueHasMutated();
    }
  };
}

ko.options.deferUpdates = true;
var vm = new Vm(types);
ko.applyBindings(vm);
pre {
  position: absolute;
  width: 300px;
  right: 0;
  top: 0;
}
<!DOCTYPE html>
<html>
  <head>
    <script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
  </head>
  <body>
    <div data-bind="component:{name:'available-items',params:vm}"></div>
    <ul data-bind="foreach: items">
      <li><span data-bind="text: location"></span> - <span data-bind="text: type"></span></li>
    </ul>
    <pre data-bind="text: ko.toJSON(vm.types, null, 2)"></pre>
  </body>
</html>
mykhailo.romaniuk
  • 1,058
  • 11
  • 20
  • @deblocker You cannot achieve update behavior without observable properties. Knockout should be notified about property change to be able to change all HTML nodes that use this property in bindings. To make creation of your objects easier - use [mapping plugin](http://knockoutjs.com/documentation/plugins-mapping.html). To improve performance - use [rate limit extender](http://knockoutjs.com/documentation/rateLimit-observable.html). Also there are exist different performance optimizations for Knockout. For example - http://www.knockmeout.net/2012/04/knockoutjs-performance-gotcha.html – mykhailo.romaniuk Jul 04 '17 at 20:09
  • @deblocker Also I suggest you to look at build-in feature for performance optimization - http://knockoutjs.com/documentation/deferred-updates.html .However I'm not quite sure what optimization you try to achieve. Maybe it's chance to create new question )) – mykhailo.romaniuk Jul 04 '17 at 20:38
  • i appreciate your hints, really. i am aware of all the topics described in the links you posted. please note, also `deferupdates`is switched on. IMHO i believe this question is clear: the optimization described already long time ago by the great rniemeyer (updating the underlying array and theb invoke `notifysubscribers`) isn't working in combination with a `foreach` loop. this is clearly visible in my example. Is there an alternative other than create and update ~300 observable properties? – deblocker Jul 05 '17 at 07:21
  • @deblocker You could use this solution: https://stackoverflow.com/a/28150736/4065876. Here it is an example: https://codepen.io/anon/pen/RgJBrK. Hope this helps. – Jose Luis Jul 05 '17 at 10:50
  • 1
    Oh, the `
    ` is updated, but the `` keeps enabled.
    – Jose Luis Jul 05 '17 at 10:57
  • 1
    @deblocker, I updated the codepen: https://codepen.io/anon/pen/RgJBrK. I used this link, http://knockoutjs.com/documentation/deferred-updates.html, 'Forcing deferred notifications to happen early'. i added `ko.tasks.runEarly();` in the `refresh`, and now it works ok, I think. Please, if you could check it, tell me if it works for you. :-) – Jose Luis Jul 05 '17 at 11:08
  • 1
    @JoseLuis: exactly, you got the problem! `foreach`is (correctly) "listening" for insertion and removal only, and this will work also without invoking `tasks.runearly`. If you do both, insertion and removal of one array element inside the same microtask queue, the final number of array entries isn't changing at all, hence you need to split the two tasks by invoking `runearly` but you will end up by updating (and reflowing) the whole `foreach` two times. If you post an answer i'll upvote your research. – deblocker Jul 06 '17 at 07:20
  • 1
    @JoseLuis I think that observables is more optimal than remove/add items because of performance. When you remove/add item it will cause re-rendering of HTML element that binded to item (could be expensive). When property updated - updated just html elements that use this property. If you want to get rid of "ko.observable" syntax - look at this plugin http://blog.stevensanderson.com/2013/05/20/knockout-es5-a-plugin-to-simplify-your-syntax/ . Also I have posted in answers another possible optimization solution with pauseableObservable if you want to postspone update of your array. – mykhailo.romaniuk Jul 09 '17 at 02:40
  • @mykhailo.romaniuk I see that my comments are in bad place. They are in your answer, and should be below the OP question. I'm sorry very much, I confused your answer with the OP question. :-( – Jose Luis Jul 09 '17 at 09:09
1

You could use this solution given by the user JotaBe: Refresh observableArray when items are not observables.

ko.observableArray.fn.refresh = function (item) {
    var index = this['indexOf'](item);
    if (index >= 0) {
        this.splice(index, 1);
        this.splice(index, 0, item);
    }
}

Now, I need to change addItem() and add the call to refresh with the updated element:

self.items.addItem = function(location, type) {
    self.items.push({
        location: location,
        type: type
    });
    if (location == "A" && type == "Apples") {
        self.types()[0].available = false;
        self.types.refresh(self.types()[0]); // <--- New sentence
    }
};

This will refresh the <pre>, that has a list of types. But will not refresh the component, that also has a list of types.

Then I used this link, Forcing deferred notifications to happen early, and I added ko.tasks.runEarly() in the refresh, and now it works ok, I think.

ko.observableArray.fn.refresh = function (item) {
    var index = this['indexOf'](item);
    if (index >= 0) {
        this.splice(index, 1);
        ko.tasks.runEarly(); // <--- New sentence
        this.splice(index, 0, item);
    }
}

Here it is a Codepen.

Jose Luis
  • 994
  • 1
  • 8
  • 22
1

Inspired by implementation of pauseableComputed and observable withPausing I have created pauseableObservable and pauseableObservableArray that have abilities to stop notifications to subscribers and than resume when needed. Also it work recursively for all nested pauseable properties.

You can play with it HERE on Codepen (was provided example base on code from your question). Also I place code of extensions that reaching the goal:

PauseableObservable:

// PauseableObservable - it's observable that have functions to 'pause' and 'resume' notifications to subscribers (pause/resume work recursive for all pauseable child).

ko.isPauseableObservable = function(instance) {
    return ko.isObservable(instance) && instance.hasOwnProperty("pause");
}

ko.pauseableObservable = function(value) {
    var that = ko.observable(value);

    function getPauseableChildren() {
        var properties = Object.getOwnPropertyNames(that());
        var currentValue = that();
        var pauseables = properties.filter((property) => {
            return ko.isPauseableObservable(currentValue[property]);
        });
        return pauseables.map((property) => { 
            return currentValue[property];
        });
    }

    that.pauseNotifications = false;
    that.isDirty = false;

    that.notifySubscribers = function() {
        if (!that.pauseNotifications) {
            ko.subscribable.fn.notifySubscribers.apply(that, arguments);
            that.isDirty = false;
        } else {
            that.isDirty = true;
        }
    };

    that.pause = function() {    
        that.pauseNotifications = true;
        var pauseableChildren = getPauseableChildren();
        pauseableChildren.forEach((child) => { child.pause(); });
    }

    that.resume = function() {    
        that.pauseNotifications = false;

        if (that.isDirty) {
            that.valueHasMutated();
        }

        var pauseableChildren = getPauseableChildren();
        pauseableChildren.forEach((child)=> { child.resume(); });
    }

    return that;
}

PauseableObservableArray

// PauseableObservableArray - simply use pauseable functionality of his items. 
// Extension for stop notifications about added/removed items is out of scope.
ko.pauseableObservableArray = function(items) {
    var that = ko.observableArray(items);

    that.pause = function () {
        var items = that();
        items.forEach(function(item) {
            if(ko.isPauseableObservable(item)) {
                item.pause();
            }
        });
    }

    that.resume = function () {
        var items = that();
        items.forEach(function(item) {
            if(ko.isPauseableObservable(item)) {
                item.resume();
            }
        });
    }

    that.refresh = function () {
        that.resume();
        that.pause();
    }

    return that;
}

Usage example:

var firstItem = ko.pauseableObservable("Hello");
var secondItem = ko.pauseableObservable("World");
var items = [
    firstItem,
    secondItem
];
var array = ko.pauseableObservable(items);

// Stop notifications from specific observable
firstItem.pause();
// Change won't raise notification to subscribers
firstItem("Hi");
// Resume notifications
firstItem.resume();

// Stop notifications from all items of array
array.pause();
// Change won't raise notification to subscribers
array()[0]("Hi");
// Resume notifications
array.resume();

I know that implementation isn't perfect and I haven't time to test it well, however I hope that it will help you to find inspiration and improve it.

mykhailo.romaniuk
  • 1,058
  • 11
  • 20
  • 1
    that's great idea, i'm sure your pauseableArray is really useful, and moreover you posted here a high quality answer. But, sadly, this isn't useful for my current question, because with your implementation i would need anyway to create an observable for every array item. – deblocker Jul 09 '17 at 04:56
  • @deblocker Thanks for separate question for this post. I'm sorry, that it won't help you with your current problem. – mykhailo.romaniuk Jul 09 '17 at 10:20
-1

I've found the problem. It's in your html: "params: vm" should be "params: $data"

 <div data-bind="component:{name:'available-items',params:$data}"></div>
Nimesco
  • 688
  • 4
  • 13