我在网格中有一组嵌套的组产品选项。我想有一个弹出编辑器,列出每个分组产品选项的所有产品(productoptionrows),并允许用户检查它们之间的关系。我遇到了许多关系的例子,但没有看到自我引用的例子分组很多。
考虑以下数组数据结构:
[{
grouptitle: "User Band",
productoptionrows: [{
id: "1",
producttitle: "25-100",
relatedproductoptionrows: [{
id: "4",
title: '1 Year'
}, {
id: "5",
title: '2 Year'
}, {
id: "6",
title: '3 Year'
}]
}]
用户将能够定义具有标题的组并向该组添加产品列表。一旦用户添加了所有组和相关产品,用户就可以单击弹出按钮("查找")检查每个组的产品之间的关系。
当您点击"查找"我遇到的问题是在弹出窗口中。为每个产品选项设置其关系,并默认弹出窗口,以便已经检查它的关系。我认为我的问题的根源是我试图组合多个嵌套数组,但我不知道如何构建视图模型/数据来应对这种逻辑。
我在下面设置了一个小提琴手,显示我的问题如下:
/*Select Options*/
var initialData = [{
grouptitle: "User Band",
productoptionrows: [{
id: "1",
producttitle: "25-100",
relatedproductoptionrows: [{
id: "4",
producttitle: '1 Year'
}, {
id: "5",
producttitle: '2 Year'
}, {
id: "6",
producttitle: '3 Year'
}]
}, {
id: "2",
producttitle: "101-250",
relatedproductoptionrows: [{
id: "7",
producttitle: '1 Year'
}, {
id: "8",
producttitle: '2 Year'
}, {
id: "9",
producttitle: '3 Year'
}]
}, {
id: "3",
producttitle: "251-500",
relatedproductoptionrows: [{
id: "10",
producttitle: '1 Year'
}, {
id: "11",
producttitle: '2 Year'
}, {
id: "12",
producttitle: '3 Year'
}]
}]
}, {
grouptitle: "Please select the number of years license",
productoptionrows: [{
id: "4",
producttitle: "1 Year",
relatedproductoptionrows: []
}, {
id: "5",
producttitle: "2 Year",
relatedproductoptionrows: []
}, {
id: "6",
producttitle: "3 Year",
relatedproductoptionrows: []
}, {
id: "7",
producttitle: "1 Year",
relatedproductoptionrows: []
}, {
id: "8",
producttitle: "2 Year",
relatedproductoptionrows: []
}, {
id: "9",
producttitle: "3 Year",
relatedproductoptionrows: []
}, {
id: "10",
producttitle: "1 Year",
relatedproductoptionrows: []
}, {
id: "11",
producttitle: "2 Year",
relatedproductoptionrows: []
}, {
id: "12",
producttitle: "3 Year",
relatedproductoptionrows: []
}]
}];
$(document).ready(function () {
/*Models*/
var mappingOptions = {
'productoptionrows': {
create: function (options) {
return new productoptionrow(options.data);
}
}
};
var mappingOptionsPR = {
create: function (options) {
return new productoptionrow(options.data);
}
};
var productoptionrow = function (por) {
var self = ko.mapping.fromJS(por, {}, this);
self.relatedproductoptionrowscsv = ko.computed(function () {
return $(por.relatedproductoptionrows).map(function () {
return this.id;
}).get().join(',');
}, self);
self.selectedrelatedproductoptionrows = ko.observableArray($(por.relatedproductoptionrows).map(function () {
return this.id;
}).get());
};
var ProductOptionModel = function (data) {
var self = this;
self.productoptions = ko.mapping.fromJS(data, mappingOptions);
self.isOpen = ko.observable(false);
self.selectedrelatedproductoptionrows = ko.observableArray([]);
/*Control Events*/
self.addProductOption = function () {
var newoption = ko.mapping.fromJS({
grouptitle: "Please select the number of years license",
productoptionrows: ko.observableArray([{
id: "15",
producttitle: "25-100",
relatedproductoptionrows: []
}, {
id: "16",
producttitle: "101-250",
relatedproductoptionrows: []
}, {
id: "17",
producttitle: "251-500",
relatedproductoptionrows: []
}])
}, mappingOptions);
self.productoptions.push(newoption);
};
self.copyProductOption = function (productoption) {
var copy = ko.mapping.fromJS(ko.mapping.toJS(productoption), mappingOptions);
self.productoptions.push(copy);
};
self.removeProductOption = function (productoption) {
self.productoptions.remove(productoption);
};
self.addProductOptionRow = function (productoption) {
var newrow = ko.mapping.fromJS({
id: "15",
producttitle: "25-100",
relatedproductoptionrows: []
}, mappingOptionsPR);
productoption.productoptionrows.push(newrow);
};
self.removeProductOptionRow = function (productoption) {
$.each(self.productoptions(), function () {
this.productoptionrows.remove(productoption)
})
};
self.open = function (productoption, event) {
self.selectedrelatedproductoptionrows(productoption.relatedproductoptionrows);
self.isOpen(true);
};
self.close = function () {
self.isOpen(false);
}
};
ko.applyBindings(new ProductOptionModel(initialData), document.getElementById('page-wrapper'));
});

<link href="https://code.jquery.com/ui/1.12.1/themes/ui-lightness/jquery-ui.css" rel="stylesheet" />
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>
<script src="https://cdn.rawgit.com/gvas/knockout-jqueryui/075b303a/dist/knockout-jqueryui.min.js"></script>
<div id="page-wrapper">
<div>
<button title="Add Group Option" type="button" data-bind='click: $root.addProductOption'>Add Group Option</button>
</div>
<div id="options" data-bind="foreach: productoptions">
<div style="padding:10px;margin:20px;background-color:whitesmoke">
<table class="option-header" cellpadding="0" cellspacing="0">
<thead>
<tr>
<th>Group Title <span class="required">*</span></th>
<th>
<button title="Copy" type="button" class="" style="" data-bind='click: $root.copyProductOption'>Copy Group</button>
<button title="Delete Option" type="button" data-bind='click: $root.removeProductOption'>Delete Group Option</button>
</th>
</tr>
</thead>
<tbody>
<tr style="height:36px;">
<td>
<input type="text" data-bind='value: grouptitle'>
</td>
<td></td>
</tr>
</tbody>
</table>
<div>
<table class="option-header-rows" cellpadding="0" cellspacing="0">
<thead>
<tr class="headings">
<th>Id</th>
<th colspan="2" class="type-title">Product Title <span class="required">*</span></th>
<th>Related Ids</th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: productoptionrows">
<tr>
<td align="center">
<input required type="text" style="width:40px" data-bind='value: id'>
</td>
<td colspan="2">
<input type="text" value="25-100" data-bind='value: producttitle'>
</td>
<td>
<input type="text" data-bind='value: relatedproductoptionrowscsv' name="isdefault"><a href="#" data-bind="click: $root.open, disable: $root.isOpen">Lookup</a>
</td>
<td>
<button title="Delete Row" type="button" data-bind='click: $root.removeProductOptionRow'>Delete Row</button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td align="right">
<button title="Add New Row" type="button" data-bind='click: $root.addProductOptionRow'>Add New Row</button>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<!-- popup -->
<div data-bind="dialog: { isOpen: isOpen,title:'Select relations', modal:true }">
<div data-bind="foreach: $root.productoptions">
<div data-bind='text: grouptitle'></div>
<div data-bind="foreach: productoptionrows">
<div>
<input type="checkbox" data-bind="value:id, checkedValue: selectedrelatedproductoptionrows" style="width:auto" />
ID <span data-bind='text: id'></span> - <span data-bind='text: producttitle'></span>
</div>
</div>
</div>
</div>
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
</div>
&#13;
我真的希望有人能够理解我想要实现的目标并让我的工作正常,因为我已经对此持续了几天了。 提前致谢
答案 0 :(得分:1)
免责声明:我删除了&#34; UI&#34;部分代码,因为那是阻止我花时间回答您之前发布此问题的时间的原因......
您描述的问题可能非常复杂。关键是使用具有ko.computed
和read
选项的write
属性。
因此,您有两个列表:Products
和Options
。每种产品都可以有一个或多个选项。因此,每个选项可以包含0个或更多链接产品。 (这就是多对多关系的意思,对吗?)
我们首先渲染一个procuts列表。每个产品都会显示其选项以及复选框。它存储已检查选项的列表。
function Product(data) {
this.title = data.producttitle;
this.id = data.id;
this.options = data.relatedproductoptionrows;
this.selectedOptions = ko.observableArray([]);
};
使用HTML:
<div data-bind="foreach: options">
<label>
<input type="checkbox"
data-bind="checked: $parent.selectedOptions, checkedValue: $data">
<span data-bind="text: producttitle"></span>
</label>
</div>
每当您(取消)选中其中一个选项时,都会在selectedOptions
数组中添加或删除选项对象。
现在开始最困难的部分:当我们想要呈现Option
而不是Product
时,我们需要(A)计算哪些产品是相关的,我们需要(B)确保这些产品selectedOptions
阵列在我们选择改变关系时保持最新。
从(A)开始:我们可以定义与选项相关的产品,如下所示:
// Every product that has an option with my `id` is a related product
relatedProducts = products.filter(
p => p.options.some(o => o.id === this.id)
);
这些关系中的每一个都具有可以读取或写入的计算checked
状态。读/写ko.computed
所在的位置。对于每个关系(linkedObj
),定义checked
状态:(B)
checked: ko.computed({
// When the current `option` is in the linked product's
// selected options, it must be checked
read: () => p.selectedOptions().includes(linkedObj),
// When forcing the checked to true/false,
// we need to either add or remove the option to the
// linked product's selection
write: val => val
? p.selectedOptions.push(linkedObj)
: p.selectedOptions.remove(linkedObj)
})
我可以想象这个概念很难掌握......我的解释可能缺乏。以下示例显示了此概念的实际应用。请注意,它没有针对速度进行优化(大量循环遍历数组),只有已检查的属性才能被观察到。
const products = getProducts();
const options = getOptions();
function Product(data) {
this.title = data.producttitle;
this.id = data.id;
this.options = data.relatedproductoptionrows;
this.selectedOptions = ko.observableArray([]);
};
Product.fromData = data => new Product(data);
function Option(data, products) {
this.title = data.producttitle;
this.id = data.id;
this.products = products
// Only include products that allow this option
.filter(
p => p.options.some(o => o.id === this.id)
)
// Create a computed checked property for each product-
// option relation
.map(p => {
// The `option` objects in our product are different
// from this instance. So we find our representation
// via our id first.
const linkedObj = p.options.find(o => o.id === this.id);
return {
checked: ko.computed({
// Checked when this option is in the selectedOptions
read: () => p.selectedOptions().includes(linkedObj),
// When set to true, add our representation to the selection,
// when set to false, remove it.
write: val => val
? p.selectedOptions.push(linkedObj)
: p.selectedOptions.remove(linkedObj)
}),
title: p.title
};
});
}
var App = function(products, options) {
this.products = products.map(Product.fromData);
this.options = options.map(o => new Option(o, this.products));
};
ko.applyBindings(new App(products, options));
// Test data
function getProducts() {
return [{
id: "1",
producttitle: "25-100",
relatedproductoptionrows: [{
id: "4",
producttitle: '1 Year'
}, {
id: "5",
producttitle: '2 Year'
}, {
id: "6",
producttitle: '3 Year'
}]
}, {
id: "2",
producttitle: "101-250",
relatedproductoptionrows: [{
id: "7",
producttitle: '1 Year'
}, {
id: "8",
producttitle: '2 Year'
}, {
id: "9",
producttitle: '3 Year'
}]
}, {
id: "3",
producttitle: "251-500",
relatedproductoptionrows: [{
id: "10",
producttitle: '1 Year'
}, {
id: "11",
producttitle: '2 Year'
}, {
id: "12",
producttitle: '3 Year'
}]
}];
};
function getOptions() {
return [{
id: "4",
producttitle: "1 Year",
relatedproductoptionrows: []
}, {
id: "5",
producttitle: "2 Year",
relatedproductoptionrows: []
}, {
id: "6",
producttitle: "3 Year",
relatedproductoptionrows: []
}, {
id: "7",
producttitle: "1 Year",
relatedproductoptionrows: []
}, {
id: "8",
producttitle: "2 Year",
relatedproductoptionrows: []
}, {
id: "9",
producttitle: "3 Year",
relatedproductoptionrows: []
}, {
id: "10",
producttitle: "1 Year",
relatedproductoptionrows: []
}, {
id: "11",
producttitle: "2 Year",
relatedproductoptionrows: []
}, {
id: "12",
producttitle: "3 Year",
relatedproductoptionrows: []
}];
}
&#13;
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div style="display: flex">
<ul data-bind="foreach: products">
<li>
<p data-bind="text: title"></p>
<div data-bind="foreach: options">
<label>
<input type="checkbox" data-bind="checked: $parent.selectedOptions, checkedValue: $data">
<span data-bind="text: producttitle"></span>
</label>
</div>
</li>
</ul>
<ul data-bind="foreach: options">
<li>
<p data-bind="text: title"></p>
<div data-bind="foreach: products">
<label>
<input type="checkbox" data-bind="checked: checked">
<span data-bind="text: title"></span>
</label>
</div>
</li>
</ul>
</div>
&#13;