2017-05-03 5 views
0

Ich habe ein verschachteltes Array von Gruppenproduktoptionen in einem Raster. Ich würde gerne einen Popup-Editor haben, der alle Produkte (productoptionrows) jeder gruppierten Produktoption auflistet und es einem Benutzer erlaubt, die Beziehungen zwischen ihnen zu überprüfen. Ich bin auf Beispiele von vielen zu vielen Beziehungen gestoßen, habe aber kein Beispiel für eine Selbstreferenz gesehen, die viele zu viele gruppiert hat.Knockoutjs - Verschachteltes Gruppen-Array mit Selbstreferenzierung von vielen zu vielen Kontrollkästchen-Liste

ich die folgenden Array-Datenstruktur:

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

Ein Benutzer in der Lage sein wird, Gruppen mit einem Titel zu definieren, und eine Liste von Produkten zu dieser Gruppe hinzu. Sobald der Benutzer alle Gruppen und zugehörigen Produkte hinzugefügt hat, könnte der Benutzer auf eine Popup-Schaltfläche ("Suche") klicken, um die Beziehungen zwischen den Produkten jeder Gruppe zu überprüfen.

Das Problem, das ich habe, ist, in dem Popup mit, wenn Sie auf „Lookup“ für jedes Produkt Option klicken, ihre Beziehungen zu setzen und auch die Popup-Standard, so dass sie die Beziehungen bereits geprüft. Ich denke, die Wurzel zu meinem Problem ist im Versuch, mehrere verschachtelte Arrays zu kombinieren, aber ich bin nicht sicher, wie das Ansichtsmodell/die Daten zu strukturieren, um mit dieser Logik umzugehen.

Ich habe ein Fiedler gegründet unter dem unten mein Problem zeigen:

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

ich wirklich bin der Hoffnung, jemand verstehen kann, was im Versuch, diese Funktion zu erreichen und zu erhalten, wie ich Seit ein paar Tagen war ich dabei. Vielen Dank im Voraus

Antwort

1

Haftungsausschluss: Ich entfernte den „UI“ Teil des Codes, denn das ist, was mich angehalten, die Zeit von der Einnahme das vorherige Mal, wenn Sie diese Frage gestellt zu beantworten ...

Das Problem, das Sie‘ Die Beschreibung kann ziemlich kompliziert sein. Der Schlüssel besteht darin, ko.computed Eigenschaften zu verwenden, die eine read und eine write Option haben.

So haben Sie zwei Listen: Products und Options. Jedes Produkt kann eine oder mehrere Optionen haben. Jede Option kann daher 0 oder mehr verknüpfte Produkte enthalten. (Dies ist, was Sie mit der many-to-many-Beziehung, nicht wahr?)

Wir beginnen, indem sie eine Liste von procuts Rendering. Jedes Produkt zeigt seine Optionen mit einem Kontrollkästchen an. Es speichert eine Liste der aktivierten Optionen.

function Product(data) { 
    this.title = data.producttitle; 
    this.id = data.id; 

    this.options = data.relatedproductoptionrows; 
    this.selectedOptions = ko.observableArray([]); 
}; 

Mit der 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> 

Jedes Mal, wenn Sie (un) überprüfen eine der Optionen, wird die Option Objekt hinzugefügt oder entfernt von dem selectedOptions Array.

Jetzt beginnt der schwierigste Teil: wenn wir den Option anstelle des Product machen wollen, müssen wir zu (A) müssen Compute die Produkte bezogen sind, und wir müssen (B) sicherzustellen, dass diese Produkte selectedOptions Arrays bleiben aktuell, wenn wir uns dazu entscheiden, die Beziehung zu ändern.

Beginnend mit (A): Wir können die auf eine Option wie so verwandten Produkte definieren:

// 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) 
); 

Jede dieser Beziehungen hat einen berechneten checked Zustand, oder geschrieben werden, lesen kann. Das ist, wo die Lese-/Schreib ko.computed kommt in Für jede Beziehung (linkedObj), der checked Zustand definiert. (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) 
}) 

ich das Konzept der sich vorstellen kann sehr schwer zu begreifen ... und meine Erklärung könnte sein, fehlt. Das folgende Beispiel zeigt dieses Konzept in Aktion. Beachten Sie, dass es nicht für die Geschwindigkeit optimiert ist (viele Schleifen durch Arrays) und nur die überprüften Eigenschaften werden beobachtbar gemacht.

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>

Verwandte Themen