【问题标题】:Knockout computed observable not firing 'write'Knockout 计算的 observable 没有触发'write'
【发布时间】:2015-04-06 11:44:51
【问题描述】:

我有一个可以在 KO 中编辑的相当简单的对象数组

Here's a test case. 尝试点击项目并在下方编辑它们。它有效。

不过……

加载到数组中的数据来自一个 JSON 字符串:

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

这必须被解析并转换成一个JS对象。这是在计算函数中完成的,如下所示:

 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);
    },

现在我想要的是在模型更新时更新原始文本字符串。应该是模型上 ko.toJSON 的简单案例:

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

从小提琴中可以看出,self.text 没有更新。

这是为什么?

我尝试了以下方法:

  • 从读取函数返回 observableArray - 没有区别
  • 返回一个由可观察对象组成的 observableArray,每个对象都具有可观察的属性
  • 使用映射插件让所有可能的事情都可以观察到

我想这归结为 KO 是如何知道触发 write 函数的。当然,如果 ssArray 的内容发生变化,那么 write 会被解雇吗?但在我的情况下……

可能更复杂的是,这将是一个 KO 组件。文本输入实际上来自小部件传递的参数。所以我想它已经是一个可观察的了?所以它也需要更新父视图模型。

除此之外,我还尝试使用可排序插件来允许对这些项目进行重新排序 - 但我已将其从我的测试用例中删除。

【问题讨论】:

    标签: javascript knockout.js knockout-components


    【解决方案1】:

    您的计算的“写入”函数没有触发,因为您没有写入计算的 - 这意味着在某处调用 ssArray(some_value)

    这是一个可行的替代解决方案:

    1. 我们为各个文本/值对创建一个名为 items 的 observableArray
    2. 这个 observableArray 是通过手动调用 loadJSON 填充的。
    3. 我们创建了一个计算对象,它通过迭代建立对items observableArray 以及所有textvalue 可观察对象的订阅。每当添加、删除或更改任何一项时,我们都会将整个数组序列化回 JSON

    您当然可以订阅self.text 并自动触发loadJSON,但随后您将不得不处理“文本”触发“loadJSON”、触发我们的计算、写回text 的循环。

    (我隐藏了代码 sn-ps 以摆脱 HTML 和 CSS 代码块。单击“显示代码 sn-p”运行示例。)

        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>

    如果您愿意,这里有一个替代版本,它可以通过单个计算处理对 JSON 的更改以及通过接口进行的编辑:

    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>

    【讨论】:

    • 非常感谢您。我仍然对写作有点困惑——在我看来,我所做的与knockoutjs.com/documentation/computed-writable.html 的全选复选框示例没有太大区别。我想我想要的是 self.text(可能在 textarea 中)和项目列表之间的双向即时绑定。此外,文本不是来自 AJAX,它将通过 'params' 属性传递到此组件,并且将成为更大视图模型的一部分。也许我需要解耦所有内容并具有某种“发送回父视图模型”功能?
    • 另外,我从来没有见过你使用计算的技巧只是为了订阅事物而不暴露给 HTML。我不会想到的!所以看起来你的可计算创建了文本,而我的创建了对象数组。我很好奇为什么它不能按我的方式工作。
    • 与示例的不同之处在于 selectedAllProduce 计算出的 checked: 绑定写入的。当用户点击复选框时,checked 调用 selectedAllProduce(true)(或 false)并因此写入它,这会触发 write 函数。我认为您希望write 更像change 工作——但事实并非如此。 read 部分跟踪它在运行时访问的所有可观察对象,并在其中任何一个更改时再次执行。 write 部分仅在尝试写入时执行 - 例如,selectedAllProduce(true)ssArray('whatever')
    • @rwalter 我添加了您可能更喜欢的第二个实现。这个处理所有更新——包括对 JSON 的更改和手动编辑——通过单个计算。也许这个对你更有意义?
    • 是的 - 这完全有道理,非常感谢。并感谢您澄清readwrite
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-19
    • 2012-04-19
    • 1970-01-01
    • 2013-03-14
    相关资源
    最近更新 更多