简体   繁体   中英

Knockout computed observable not firing 'write'

I have a fairly simple array of objects that can be edited in KO

Here's a test case. Try clicking on the items and editing them down below. It works.

However...

The data loaded into the array comes from a JSON string:

 self.text = ko.observable('[{ "value": "1", "text": "Low" }, ..... ]');

This must be parsed and converted into a JS object. This is done in the computed function like this:

 self.ssArray = ko.computed({
    read: function() {

        // Convert text into JS object
        // Not using ko.utils because I want to use try/catch to detect bad JS later

        var arrayJS = JSON.parse(ko.utils.unwrapObservable(self.text));

        // Make an array of observables
        // Not using ko.mapping in order to get back to basics
        // Also mapping function throws an error re: iterations or something

        var obsArrayJS = ko.utils.arrayMap(arrayJS, function(i) {
            return {
                "value": ko.observable(i.value),
                "text": ko.observable(i.text)
            };
        });

        // return array of objects with observable properties.
        return obsArrayJS;

        // Tried this but made no difference:
        //return ko.observableArray(obsArrayJS);
    },

Now what I want is for the original text string to be updated whenever the model is updated. It should be a simple case of ko.toJSON on the model:

 write: function(value) {
        self.text(ko.toJSON(this.ssArray));
    },

As you can see from the fiddle, self.text is not updated.

Why is this?

I have tried the following:

  • returning an observableArray from the read function - makes no difference
  • return an observableArray of observable objects each with observable properties
  • using the mapping plugin to make everything possible observable

I guess it boils down to how KO knows to fire the write function. Surely if the contents of ssArray change then write is fired? But not in my case...

Possible further complication is that this will be a KO component. The text input will actually come from a parameter passed from the widget. So I guess it will already be an observable? So it will need to update the parent viewmodel too.

In addition to this I'm trying to use the sortable plugin to allow reordering of these items - but I've removed that from my test case.

The 'write' function of your computed is not firing, because you are not writing to the computed — that would mean calling ssArray(some_value) somewhere.

This is an alternative solution that works:

  1. We create an observableArray named items for our individual text/value pairs
  2. This observableArray is populated by calling loadJSON manually.
  3. We create a computed that establishes subscriptions to the items observableArray, as well as to all the items text and value observables by iterating over them. Whenever either items are added or removed or change, we serialize the whole array back to JSON

You could certainly subscribe to self.text and trigger loadJSON automatically, but then you will have to take care of the circle of 'text' triggering 'loadJSON', triggering our computed, writing back to text .

(I have hidden the code snippets in order to get rid of the HTML and CSS code blocks. Click "Show code snippet" to run the examples.)

    function MyViewModel() {

        var self = this;

        this.selectedItemSS = ko.observable();
        this.setSelectedSS = function(item) {
            self.selectedItemSS(item);
        };

        // Data in text form. Passed in here as a parameter from parent component
        this.text = ko.observable('[{"value": "1", "text": "Low"}, {"value": "2", "text": "Medium"}, {"value": "3", "text": "High"} ]');

        this.items = ko.observableArray([]);

        this.loadJSON = function loadJSON(json) {
            var arrayOfObjects = JSON.parse(json),
                arrayOfObservables;

            // clear out everything, or otherwise we'll end
            // up with duplicated objects when we update
            self.items.removeAll();

            arrayOfObservables = ko.utils.arrayMap(arrayOfObjects, function(object) {
                return {
                    text:  ko.observable(object.text),
                    value: ko.observable(object.value)
                };
            });

            self.items(arrayOfObservables);
        };

        this.loadJSON( this.text() );

        ko.computed(function() {
            var items = this.items();

            // iterate over all observables in order
            // for our computed to get a subscription to them
            ko.utils.arrayForEach(items, function(item) {
                item.text();
                item.value();
            });

            this.text(ko.toJSON(items));

        }, this);
    }

    ko.applyBindings(new MyViewModel());

  function MyViewModel() { var self = this; this.selectedItemSS = ko.observable(); this.setSelectedSS = function(item) { self.selectedItemSS(item); }; // Data in text form. Passed in here as a parameter from parent component this.text = ko.observable('[ \\ {\\ "value": "1",\\ "text": "Low"\\ },\\ { \\ "value": "2",\\ "text": "Medium"\\ },\\ {\\ "value": "3",\\ "text": "High"\\ } ]'); this.items = ko.observableArray([]); this.loadJSON = function loadJSON(json) { var arrayOfObjects = JSON.parse(json), arrayOfObservables; // clear out everything, or otherwise we'll end // up with duplicated objects when we update self.items.removeAll(); arrayOfObservables = ko.utils.arrayMap(arrayOfObjects, function(object) { return { text: ko.observable(object.text), value: ko.observable(object.value) }; }); self.items(arrayOfObservables); }; this.loadJSON( this.text() ); ko.computed(function() { var items = this.items(); // iterate over all observables in order // for our computed to get a subscription to them ko.utils.arrayForEach(items, function(item) { item.text(); item.value(); }); this.text(ko.toJSON(items)); }, this); } ko.applyBindings(new MyViewModel()); 
 body { font-family: arial; font-size: 14px; } .well {background-color:#eee; padding:10px;} pre {white-space:pre-wrap;} 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script> <h3>Text Json: eg from AJAX request</h3> <p>In practice this comes from a parent custom component as a parameter</p> <pre class="well" data-bind="text:text"></pre> <h3>Computed data model</h3> <p>Click on an item to edit that record</p> <div data-bind="foreach:items" class="well"> <div data-bind="click: $parent.setSelectedSS"> <span data-bind="text:value"></span> <span data-bind="text:text"></span><br/> </div> </div> <hr/> <h3>Editor</h3> <div data-bind="with:selectedItemSS" class="well"> <input data-bind="textInput:value"/> <span data-bind="text:value"></span><br/> </div> 

If you prefer, here is an alternative version that handles both changes to the JSON as well as edits through the interface through a single computed:

function MyViewModel(externalObservable) {
  var self = this;

  this.selectedItemSS = ko.observable();
  this.setSelectedSS  = function(item) {
    self.selectedItemSS(item);
  };

  // just for the demo
  this.messages       = ko.observableArray([]);

  this.items          = ko.observableArray([]);
  this.json           = externalObservable;
  this.previous_json  = '';

  ko.computed(function() {
    var items = this.items(),
        json  = this.json();

    // If the JSON hasn't changed compared to the previous run,
    // that means we were called because an item was edited
    if (json === this.previous_json) {
      var new_json = ko.toJSON(items);

      self.messages.unshift("items were edited, updating JSON: " + new_json);

      this.previous_json = new_json;
      this.json(new_json);

      return;
    }

    // If we end up here, that means that the JSON has changed compared
    // to the last run

    self.messages.unshift("JSON has changed, updating items: " + json);

    var arrayOfObjects = JSON.parse(json),
        arrayOfObservables;

    // clear out everything, or otherwise we'll end
    // up with duplicated objects when we update
    this.items.removeAll();

    arrayOfObservables = ko.utils.arrayMap(arrayOfObjects, function(object) {
      return {
        text: ko.observable(object.text),
        value: ko.observable(object.value)
      };
    });

    // iterate over all observables in order
    // for our computed to get a subscription to them
    ko.utils.arrayForEach(arrayOfObservables, function(item) {
      item.text();
      item.value();
    });

    this.items(arrayOfObservables);

    this.previous_json = json;

  }, this);
}

var externalObservableFromParam = ko.observable(),
    viewModel;


// Pretend here that this observable was handed to us
// from your components' params
externalObservableFromParam('[{"value": "1", "text": "Low"}, {"value": "2", "text": "Medium"}, {"value": "3", "text": "High"} ]');

viewModel = new MyViewModel(externalObservableFromParam);

ko.applyBindings(viewModel);

 function MyViewModel(externalObservable) { var self = this; this.selectedItemSS = ko.observable(); this.setSelectedSS = function(item) { self.selectedItemSS(item); }; // just for the demo this.messages = ko.observableArray([]); this.items = ko.observableArray([]); this.json = externalObservable; this.previous_json = ''; ko.computed(function() { var items = this.items(), json = this.json(); // If the JSON hasn't changed compared to the previous run, // that means we were called because an item was edited if (json === this.previous_json) { var new_json = ko.toJSON(items); self.messages.unshift("items were edited, updating JSON: " + new_json); this.previous_json = new_json; this.json(new_json); return; } // If we end up here, that means that the JSON has changed compared // to the last run self.messages.unshift("JSON has changed, updating items: " + json); var arrayOfObjects = JSON.parse(json), arrayOfObservables; // clear out everything, or otherwise we'll end // up with duplicated objects when we update this.items.removeAll(); arrayOfObservables = ko.utils.arrayMap(arrayOfObjects, function(object) { return { text: ko.observable(object.text), value: ko.observable(object.value) }; }); // iterate over all observables in order // for our computed to get a subscription to them ko.utils.arrayForEach(arrayOfObservables, function(item) { item.text(); item.value(); }); this.items(arrayOfObservables); this.previous_json = json; }, this); } var externalObservableFromParam = ko.observable(), viewModel; // Pretend here that this observable was handed to us // from your components' params externalObservableFromParam('[{"value": "1", "text": "Low"}, {"value": "2", "text": "Medium"}, {"value": "3", "text": "High"} ]'); viewModel = new MyViewModel(externalObservableFromParam); ko.applyBindings(viewModel); 
 body { font-family: arial; font-size: 14px; } .well { background-color: #eee; padding: 10px; } pre { white-space: pre-wrap; } ul { list-style-position: inside; } 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script> <h3>Text Json: eg from AJAX request</h3> <p>In practice this comes from a parent custom component as a parameter</p> <pre class="well" data-bind="text: json"></pre> <textarea data-bind="value: json" cols=50 rows=5></textarea> <h3>Computed data model</h3> <p>Click on an item to edit that record</p> <div data-bind="foreach: items" class="well"> <div data-bind="click: $parent.setSelectedSS"> <span data-bind="text:value"></span> <span data-bind="text:text"></span> <br/> </div> </div> <hr/> <h3>Editor</h3> <div data-bind="with:selectedItemSS" class="well"> <input data-bind="textInput:value" /> <span data-bind="text:value"></span> <br/> </div> <hr/> <h3>Console</h3> <ul data-bind="foreach: messages" class="well"> <li data-bind="text: $data"></li> </ul> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM