使用create回调将嵌套对象映射为复杂JSON中的可观察对象 [英] Mapping a nested object as an observable from a complex JSON using the create callback
问题描述
我有一个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 inhandlerVM
. - 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屋!