使用create回调将嵌套对象映射为复杂JSON中的可观察对象 [英] Mapping a nested object as an observable from a complex JSON using the create callback

查看:70
本文介绍了使用create回调将嵌套对象映射为复杂JSON中的可观察对象的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个JSON格式的复杂对象.我正在使用Knockout映射,自定义create回调,并尝试确保应该观察到的每个对象-都将被这样映射.

I've got a complex object in a JSON format. I'm using Knockout Mapping, customizing the create callback, and trying to make sure that every object that should be an observable - would actually be mapped as such.

以下代码是我所拥有的示例: 它使用户可以添加cartItems,将其保存为JSON,清空购物车,然后加载保存的项目.

The following code is an example of what I've got: It enables the user to add cartItems, save them (as a JSON), empty the cart, and then load the saved items.

加载部分失败:它不显示已加载的选项(即已加载的cartItemName).我猜想这与选项列表中的对象和以cartItemName为边界的对象之间的某些不匹配有关(请参阅此

The loading part fails: It doesn't display the loaded option (i.e., the loaded cartItemName). I guess it's related to some mismatch between the objects in the options list and the object bounded as the cartItemName (see this post), but I can't figure it out.

代码(小提琴):

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>

需要修改哪些内容以解决该问题?

What needs to be changed to fix it?

PS:我还有另一段代码(请在此处)演示了即使更改选项后,选定值的持久性-尽管optionsValue是一个简单的字符串,而这里是一个对象.

P.S.: I've got another piece of code (see it here) that demonstrates a persistance of the selected value even after changing the options - though there optionsValue is a simple string, while here it's an object.

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

I figured out the problem: the call ko.mapping.fromJS(data.cartItemName) creates a new productVM object, which is not one of the objects inside availableProducts array. As a result, none of the options corresponds to the productVM contained in the loaded cartItemName, so Knockout thereby clears the value altogether and passes undefined.

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

But the question remains: how can this be fixed?

推荐答案

在从ViewModel -> plain object -> ViewModel过渡时,您松开了购物车中的商品与handlerVM中的商品之间的关系.

In the transition from ViewModel -> plain object -> ViewModel you loose the relation between the products in your cart and the ones in your handlerVM.

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

A common solution is to, when loading a plain object, manually search for the existing viewmodels and reference those instead. I.e.:

  • 我们从普通对象中创建一个新的cartItemVM
  • cartItemName中,有一个对象在handlerVM中不存在.
  • 我们在handlerVM中寻找一种类似于该对象的产品,然后用找到的对象替换该对象.
  • We create a new cartItemVM from the plain object
  • Inside its cartItemName, there's an object that does not exist in handlerVM.
  • We look in handlerVM for a product that resembles this object, and replace the object by the one we find.

在代码中,在loadCart内部,然后设置新的视图模型:

In code, inside loadCart, before setting the new viewmodels:

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产品名称,并使用它来确定匹配项.您还可以看到,在可观察与不可观察之间存在差异.

As you can see, the equality comparison is kind of ugly. We look for the english product name and use it to determine the match. You can also see there's a difference in what is observable and what isn't.

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

My advice would be to use unique id properties for your product, and start using those. You can create a simpler optionsValue binding and matching new and old values happens automatically. If you like, I can show you an example of this refactor as well. Let me know if that'd help.

这篇关于使用create回调将嵌套对象映射为复杂JSON中的可观察对象的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆