简体   繁体   English

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

[英]Mapping a nested object as an observable from a complex JSON using the create callback

I've got a complex object in a JSON format. 我有一个JSON格式的复杂对象。 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. 我正在使用Knockout映射,自定义create回调,并试图确保应该观察到的每个对象都将被这样映射。

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. 以下代码是我所拥有的示例:它使用户可以添加cartItems ,将其保存(作为JSON),清空购物车,然后加载保存的项目。

The loading part fails: It doesn't display the loaded option (ie, the loaded cartItemName ). 加载部分失败:它不显示已加载的选项(即,已加载的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. 我猜想这与选项列表中的对象和以cartItemName为边界的对象之间存在一些不匹配有关(请参阅此文章 ),但我无法弄清楚。

Code ( 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> 

What needs to be changed to fix it? 需要修改什么来解决它?

PS: 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. PS:我还有另一段代码( 在此处查看 ),即使更改了选项,该示例也显示了所选值的持久性-尽管optionsValue是一个简单的字符串,而这里是一个对象。

EDIT: 编辑:

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. 我发现了问题:调用ko.mapping.fromJS(data.cartItemName)创建了一个新的productVM对象,该对象不是availableProducts数组内的对象之一。 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 . 结果,没有一个选项与包含在已加载的cartItemNameproductVM相对应,因此Knockout会完全清除该值并传递undefined

But the question remains: how can this be fixed? 但是问题仍然存在:如何解决?

In the transition from ViewModel -> plain object -> ViewModel you loose the relation between the products in your cart and the ones in your handlerVM . 在从ViewModel -> plain object -> ViewModel的过渡中,您松开了购物车中的产品与handlerVM的产品之间的关系。

A common solution is to, when loading a plain object, manually search for the existing viewmodels and reference those instead. 常见的解决方案是,在加载普通对象时,手动搜索现有的视图模型并引用它们。 Ie: 即:

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

In code, inside loadCart , before setting the new viewmodels: 在代码中,在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)
    }
)

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

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. 我们寻找english产品名称,并用它来确定匹配项。 You can also see there's a difference in what is observable and what isn't. 您还可以看到,可观察与不可观察之间存在差异。

My advice would be to use unique id properties for your product, and start using those. 我的建议是为您的产品使用唯一的id属性,然后开始使用这些属性。 You can create a simpler optionsValue binding and matching new and old values happens automatically. 您可以创建一个更简单的optionsValue绑定并自动匹配新旧值。 If you like, I can show you an example of this refactor as well. 如果您愿意,我也可以向您展示此重构的示例。 Let me know if that'd help. 让我知道是否有帮助。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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