使用 create 回调将嵌套对象映射为复杂 JSON 中的 observable

Posted

技术标签:

【中文标题】使用 create 回调将嵌套对象映射为复杂 JSON 中的 observable【英文标题】:Mapping a nested object as an observable from a complex JSON using the create callback 【发布时间】:2018-10-31 07:38:51 【问题描述】:

我有一个 JSON 格式的复杂对象。我正在使用 Knockout Mapping,自定义 create 回调,并尝试确保每个应该是 observable 的对象 - 实际上都会被映射。

以下代码是我所拥有的示例: 它使用户能够添加cartItems,保存它们(作为 JSON),清空购物车,然后加载保存的项目。

加载部分失败:它不显示加载的选项(即加载的cartItemName)。我猜这与选项列表中的对象与以cartItemName 为边界的对象之间的某些不匹配有关(请参阅此post),但我无法弄清楚。

代码(fiddle):

var cartItemsAsJson = "";
var handlerVM = function () 
  var self = this;
  self.cartItems = ko.observableArray([]);
  self.availableProducts = ko.observableArray([]);
  self.language = ko.observable();
  self.init = function () 
    self.initProducts();
    self.language("english");
  
  self.initProducts = function () 
    self.availableProducts.push(
      new productVM("Shelf", ['White', 'Brown']),
      new productVM("Door", ['Green', 'Blue', 'Pink']),
      new productVM("Window", ['Red', 'Orange'])
    );
  
  self.getProducts = function () 
    return self.availableProducts;
  
  self.getProductName = function (product) 
    if (product) 
      return self.language() == "english" ? 
        product.productName().english : product.productName().french;
    
  
  self.getProductValue = function (selectedProduct) 
    // if not caption
    if (selectedProduct) 
      var matched = ko.utils.arrayFirst(self.availableProducts(), function (product) 
        return product.productName().english == selectedProduct.productName().english;
      );
      return matched;
    
  
  self.getProductColours = function (selectedProduct) 
    selectedProduct = selectedProduct();
    if (selectedProduct) 
      return selectedProduct.availableColours();
    
  
  self.addCartItem = function () 
    self.cartItems.push(new cartItemVM());
  
  self.emptyCart = function () 
    self.cartItems([]);
  
  self.saveCart = function () 
    cartItemsAsJson = ko.toJSON(self.cartItems);
    console.log(cartItemsAsJson);
  
  self.loadCart = function () 
    var loadedCartItems = ko.mapping.fromJSON(cartItemsAsJson, 
      create: function(options) 
        return new cartItemVM(options.data);
      
    );
    self.cartItems(loadedCartItems());
  


var productVM = function (name, availableColours, data) 
  var self = this;
  self.productName = ko.observable( english: name, french: name + "eux" );
  self.availableColours = ko.observableArray(availableColours);

var cartItemVM = function (data) 
  var self = this;
  self.cartItemName = data ?
     ko.observable(ko.mapping.fromJS(data.cartItemName)) :
     ko.observable();
  self.cartItemColour = data ?
     ko.observable(data.cartItemColour) :
     ko.observable();

var handler = new handlerVM();
handler.init();
ko.applyBindings(handler);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://rawgit.com/SteveSanderson/knockout.mapping/master/build/output/knockout.mapping-latest.js
"></script>
<div>
  <div data-bind="foreach: cartItems">
    <div>
      <select data-bind="options: $parent.getProducts(),
                optionsText: function (item)  return $parent.getProductName(item); ,
                optionsValue: function (item)  return $parent.getProductValue(item); , 
                optionsCaption: 'Choose a product',
                value: cartItemName"
      >
      </select>
    </div>
    <div>
      <select data-bind="options: $parent.getProductColours(cartItemName),
                optionsText: $data,
                optionsCaption: 'Choose a colour',
                value: cartItemColour,
                visible: cartItemName() != undefined"
      >
      </select>
    </div>
  </div>
  <div>
    <button data-bind="text: 'add cart item', click: addCartItem" />
    <button data-bind="text: 'empty cart', click: emptyCart" />
    <button data-bind="text: 'save cart', click: saveCart" />
    <button data-bind="text: 'load cart', click: loadCart" />
  </div>
</div>

需要改变什么来解决它?

PS:我还有另一段代码(见here),它展示了即使在更改选项后所选值的持久性——尽管optionsValue 是一个简单的字符串,而这里它是一个对象。

编辑:

我发现了问题:调用ko.mapping.fromJS(data.cartItemName) 创建了一个新的productVM 对象,它不是availableProducts 数组中的对象之一。结果,没有一个选项对应于已加载的cartItemName 中包含的productVM,因此Knockout 将完全清除该值并传递undefined

但问题仍然存在:如何解决这个问题?

【问题讨论】:

【参考方案1】:

在从 ViewModel -&gt; plain object -&gt; ViewModel 转换时,您会失去购物车中的产品与 handlerVM 中的产品之间的关系。

一种常见的解决方案是,在加载普通对象时,手动搜索现有的视图模型并引用它们。即:

我们从普通对象创建一个新的cartItemVM 在其cartItemName 中,有一个在handlerVM 中不存在的对象。 我们在handlerVM 中查找与该对象相似的产品,并将该对象替换为我们找到的对象。

在代码中,在loadCart 中,在设置新视图模型之前:

loadedCartItems().forEach(
    ci => 
      // Find out which product we have:
      const newProduct = ci.cartItemName().productName;
      const linkedProduct = self.availableProducts()
          .find(p => p.productName().english === newProduct.english());

      // Replace the newProduct by the one that is in `handlerVM`
      ci.cartItemName(linkedProduct)
    
)

小提琴:https://jsfiddle.net/7z6010jz/

如您所见,相等比较有点难看。我们查找english 产品名称并使用它来确定匹配。您还可以看到可观察和不可观察的差异。

我的建议是为您的产品使用独特的id 属性,然后开始使用这些属性。您可以创建一个更简单的optionsValue 绑定并自动匹配新旧值。如果您愿意,我也可以向您展示此重构的示例。让我知道这是否有帮助。

【讨论】:

谢谢。我当然有兴趣看到你的重构。我更新了the fiddle 我认为你的想法。是吗?

以上是关于使用 create 回调将嵌套对象映射为复杂 JSON 中的 observable的主要内容,如果未能解决你的问题,请参考以下文章

如何将回调从父组件传递到嵌套路由组件?

jOOQ & PostgreSQL:将从复杂 jsonb 中提取的嵌套 json 对象映射到自定义类型

使用 RKObjectMapping 的嵌套映射

RestKit 将未嵌套关系映射到拥有对象

JS中promise对象的作用与使用

Restkit 0.20 嵌套对象数组映射问题