Knockoutjs - 嵌套组数组,具有自引用多对多复选框列表

时间:2017-05-03 11:58:35

标签: javascript jquery knockout.js

我在网格中有一组嵌套的组产品选项。我想有一个弹出编辑器,列出每个分组产品选项的所有产品(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> &nbsp;&nbsp;
                                <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;
&#13;
&#13;

我真的希望有人能够理解我想要实现的目标并让我的工作正常,因为我已经对此持续了几天了。 提前致谢

1 个答案:

答案 0 :(得分:1)

免责声明:我删除了&#34; UI&#34;部分代码,因为那是阻止我花时间回答您之前发布此问题的时间的原因......

您描述的问题可能非常复杂。关键是使用具有ko.computedread选项的write属性。

因此,您有两个列表:ProductsOptions。每种产品都可以有一个或多个选项。因此,每个选项可以包含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)
})

我可以想象这个概念很难掌握......我的解释可能缺乏。以下示例显示了此概念的实际应用。请注意,它没有针对速度进行优化(大量循环遍历数组),只有已检查的属性才能被观察到。

&#13;
&#13;
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;
&#13;
&#13;