2017-05-03 24 views
0

我有一个网格中的组产品选项的嵌套数组。我想要一个弹出式编辑器,列出每个分组产品选项的所有产品(产品选项),并允许用户检查它们之间的关系。我曾经遇到过多对多关系的例子,但没有看到多对多的自我参照的例子。Knockoutjs - 自引用多对多复选框列表的嵌套组阵列

考虑以下数组:

[{ 
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' 
    }] 
}] 

用户将能够定义组带有标题和添加的产品的列表到该组。一旦用户添加了所有组和相关产品,用户就可以点击一个弹出按钮(“查找”)来检查每个组的产品之间的关系。

我遇到的问题是在弹出窗口中,当您单击每个产品选项的“Lookup”来设置其关系时,还会默认弹出窗口,以便它已关闭。我认为我的问题的根本是试图结合多个嵌套数组,但我不知道如何构建视图模型/数据来应付这种逻辑。

我已成立了一个小提琴手下方,显示我下面的问题:

/*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>

我真的希望有人能明白我尝试实现我怎样得到这个工作现在已经有好几天了。 在此先感谢

回答

1

免责声明:我删除了你的代码的“UI”的一部分,因为这就是从抽出时间来回答前面的时候你张贴了这个问题把我拦住......

的问题,你”重新描述可能相当复杂。关键是使用ko.computed属性,该属性具有readwrite选项。

所以你有两个列表:ProductsOptions。每个产品可以有一个或多个选项。因此每个选项可以有0个或更多的链接产品。 (这就是你说的多对多关系,对吧?)

我们从渲染一个列表中开始。每个产品都显示其复选框的选项。它存储选中选项的列表。

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: [] 
 
    }]; 
 
}
<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>