简体   繁体   中英

Select element with nested foreach options not working in KnockoutJS

I am trying to update a select box element in my app using Knockoutjs. When you click my recipe id: 31 on this snippet, you can see it updates some form elements near the top, based on the var RECIPE object. However, the Fermentables item names are not updated (they remain as "-"). The Milling preference of these Fermentables does update, however.

The Fermentables html is:

           <div data-bind="foreach: fermentables">
                <select id="fermentable-variety-select" style="width:325px" data-bind="value: catalog_id">
                    <option value="-"> - </option>
                        <!-- ko foreach: fermentables_options -->
                        <optgroup data-bind="attr: {label: category}">
                            <!-- ko foreach: fermentables -->
                                <option data-bind="value: $data.catalog_id, text: $data.name + ' -- ' + $data.price.toFixed(2)"></option>
                            <!-- /ko -->
                        </optgroup>
                    <!-- /ko -->
                </select>
                <label>Milling preference: </label>
                <select data-bind="options: $root.milling_preferences, value: milling_preference"></select>
                <a href="#" data-bind="click: $root.removeFermentable, visible: $root.fermentables.countVisible() > 1">
                    Delete
                </a>
                <br><br>
            </div>

The broken select element is the top one, the working one is the bottom. You can see the broken one is more complex, using a nested foreach, but I needed that to display categories as optgroups .

I do have data-bind="value: catalog_id" on the broken select , but it will not update to any value except "-" .

These boxes should display Briess Bavarian Wheat DME 1 Lb and Briess Bavarian Wheat DME 3 LBS .

 // hard codes var HOPS = [ { "category": "Hop Pellets", "hops": [ { "name": "Ahtanum Hop Pellets 1 oz", "price": 1.99, "catalog_id": 1124 }, { "name": "Amarillo Hop Pellets 1 oz", "price": 3.99, "catalog_id": 110 }, { "name": "Apollo (US) Hop Pellets - 1 oz.", "price": 2.25, "catalog_id": 6577 }, ] } ] var FERMENTABLES = [ { "category": "Dry Malt Extract", "fermentables": [ { "name": "Briess Bavarian Wheat DME 1 Lb", "price": 4.99, "catalog_id": 496 }, { "name": "Briess Bavarian Wheat DME 3 LBS", "price": 12.99, "catalog_id": 1435 }, { "name": "Briess Golden Light DME 1 Lb", "price": 4.99, "catalog_id": 492 }, ] } ] var YEASTS = [ { "category": "Dry Beer Yeast", "yeasts": [ { "name": "500 g Fermentis Safale S-04", "price": 79.99, "catalog_id": 6012 }, { "name": "500 g Fermentis Safale US-05", "price": 84.99, "catalog_id": 4612 }, { "name": "500 g Fermentis SafCider Yeast", "price": 59.99, "catalog_id": 6003 }, ] } ] var RECIPE_DATA = [ { id: 31, name: "my recipe ", notes: "some notes", brew_method: "All Grain", boil_time: 60, batch_size: "4.00", fermentable_selections: [ { catalog_id: 496, milling_preference: "Unmilled" }, { catalog_id: 1435, milling_preference: "Milled" } ], hop_selections: [ { catalog_id: 110, weight: "4.00", minutes: 35, use: "Dry Hop" } ], yeast_selections: [ { catalog_id: 6012 } ] } ]; var API_BASE = "127.0.0.1:8000"; ko.observableArray.fn.countVisible = function(){ return ko.computed(function(){ var items = this(); if (items === undefined || items.length === undefined){ return 0; } var visibleCount = 0; for (var index = 0; index < items.length; index++){ if (items[index]._destroy != true){ visibleCount++; } } return visibleCount; }, this)(); }; function Fermentable(data) { var self = this; var options = data.options; self.fermentables_options = ko.computed(function(){ return options; }); self.catalog_id = ko.observable(data.catalog_id || ""); self.name = ko.observable(data.name || ""); self.milling_preference = ko.observable(data.milling_preference || "Milled"); self.is_valid = ko.computed(function(){ var valid = self.catalog_id() !== "" && self.catalog_id() !== "-"; return valid }); } function Hop(data) { var self = this; self.hops_options = ko.computed(function(){ return data.options; }); self.catalog_id = ko.observable(data.catalog_id || ""); self.name = ko.observable(data.name || ""); self.amount = ko.observable(data.amount || ""); self.time = ko.observable(data.time || ""); self.use = ko.observable(data.use || "Boil"); self.is_valid = ko.computed(function(){ var valid = self.amount() > 0 && self.catalog_id() !== "" && self.catalog_id() !== "-"; return valid }); } function Yeast(data){ var self = this; var permanent_yeasts_options = data.yeasts_options; self.catalog_id = ko.observable(data.catalog_id || ""); self.name = ko.observable(data.name || "-"); self.current_filter = ko.observable("-Any-"); self.yeast_groups_individual = ko.computed(function(){ if (self.current_filter() !== "-Any-"){ var options = _.filter(data.yeasts_options, function(option){ return option.category === self.current_filter(); }); return options; } else{ return permanent_yeasts_options; } } ); self.yeast_categories = ko.observableArray(); ko.computed(function(){ var starter_list = ['-Any-']; var categories = _.pluck(permanent_yeasts_options, 'category'); var final = starter_list.concat(categories); self.yeast_categories(final); }); self.is_valid = ko.computed(function(){ var valid = self.catalog_id() !== "" && self.catalog_id() !== "-"; return valid }); } function RecipeViewModel() { var self = this; // http://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call self.get_data = function(url_ending){ var URL = "http://%/api/&/".replace("&", url_ending).replace("%", API_BASE); var data = $.ajax({ dataType: "json", url: URL, async: false, }); return data.responseJSON; } self.styles = [ "--", "Standard American Beer", "International Lager", "Czech Lager", "Pale Malty European Lager", "Pale Bitter European Beer", "Amber Malty European Lager", "Amber Bitter European Lager", "Dark European Lager" ] self.styles_data = ko.observableArray(); ko.computed(function(){ var data = []; for (i = 0; i < self.styles.length; i++){ var text = self.styles[i]; if (text === "--"){ data.push({value: "--", display: "--"}); } else { var display_text = i.toString() + ". " + text; var this_entry = {value: text, display: display_text}; data.push(this_entry); } } self.styles_data(data); }); self.recipes = ko.observableArray(); // self.recipes( self.get_data("recipes/all-recipes") ); self.recipes(RECIPE_DATA); self.current_style = ko.observable("--"); // defaults self.total_price = ko.observable(0.0); // TODO: this should not default if the recipe has items already... self.hops_uses = ko.observableArray(['Boil', 'Dry Hop']); self.weight_units = ko.observableArray(['oz', 'lb']); self.milling_preferences = ko.observableArray(['Milled', 'Unmilled']); self.brew_methods = ko.observableArray(['Extract', 'Mini-Mash', 'All Grain', 'Brew-in-a-bag']); // start of input fields self.name = ko.observable(""); self.brew_method = ko.observable("Extract"); self.batch_size = ko.observable("5"); self.beer_style = ko.observable("Standard American Beer"); self.boil_time = ko.observable("60"); self.notes = ko.observable(""); self.hops_options = HOPS; self.hops = ko.observableArray([new Hop({options: self.hops_options}), new Hop({options: self.hops_options})]); self.fermentables_options = FERMENTABLES; self.fermentables = ko.observableArray( [ new Fermentable({options: self.fermentables_options}), new Fermentable({options: self.fermentables_options}) ] ); self.yeasts_options = YEASTS; self.yeasts = ko.observableArray([new Yeast({yeasts_options: self.yeasts_options})]); self.reset_form = function(){ var x = 'finish this'; } self.populate_recipe = function(data, event){ var context = ko.contextFor(event.target); var index = context.$index(); var recipe = self.recipes()[index]; var attrs = ['name', 'brew_method', 'boil_time', 'batch_size', 'notes'] for (i = 0; i < attrs.length; i++) { attr = attrs[i]; self[attr](recipe[attr]); } fermentables_data = recipe.fermentable_selections; new_fermentables_data = []; for (i = 0; i < fermentables_data.length; i++) { var data_set = fermentables_data[i] data_set['options'] = self.fermentables_options; // takes {options: ..; catalog_id: ..; milling_preference} // based on the results of http://127.0.0.1:8000/api/recipes/all-recipes/ var this_fermentable = new Fermentable(data_set); new_fermentables_data.push(this_fermentable); } self.fermentables(new_fermentables_data); } self.delete_recipe = function(data, event){ var recipe_id = data.id; var URL = "http://%/api/recipes/delete/&/".replace("%", API_BASE).replace("&", recipe_id); $.ajax({ url: URL, async: false, }); self.recipes( self.get_data("recipes/all-recipes") ); } self.valid_items = function(items){ var final_items = _.filter(items, function(item){ return item.is_valid(); }); return final_items; } self.valid_fermentables = ko.observableArray(); ko.computed(function(){ self.valid_fermentables(self.valid_items(self.fermentables())); }); self.valid_hops = ko.observableArray(); ko.computed(function(){ self.valid_hops(self.valid_items(self.hops())); }); self.valid_yeasts = ko.observableArray(); ko.computed(function(){ self.valid_yeasts(self.valid_items(self.yeasts())); }); self.prices_hash = ko.computed(function(){ var data = {}; var strings = ['fermentables', 'hops', 'yeasts']; for (i = 0; i < strings.length; i++) { var string = strings[i]; var attr = strings[i] + '_options'; for (j = 0; j < self[attr].length; j++) { var groups = self[attr][j][string]; for (k = 0; k < groups.length; k++) { var catalog_id = groups[k].catalog_id.toString(); var current_price = groups[k].price; data[catalog_id] = current_price; } } } return data; }); self.current_price = ko.computed(function(){ var total_price = 0; for (i = 0; i < self.valid_fermentables().length; i++){ var item = self.valid_fermentables()[i]; total_price = total_price + self.prices_hash()[item.catalog_id()]; } for (i = 0; i < self.valid_hops().length; i++){ var item = self.valid_hops()[i]; total_price = total_price + self.prices_hash()[item.catalog_id()]; } for (i = 0; i < self.valid_yeasts().length; i++){ var item = self.valid_yeasts()[i]; total_price = total_price + self.prices_hash()[item.catalog_id()]; } return total_price.toFixed(2); }); self.addFermentable = function(){ self.fermentables.push(new Fermentable({options: self.fermentables_options})) } self.addYeast = function(){ self.yeasts.push(new Yeast({yeasts_options: self.yeasts_options})); } self.addHop = function(){ self.hops.push(new Hop({options: self.hops_options})); } self.removeFermentable = function(fermentable){ self.fermentables.destroy(fermentable); } self.removeYeast = function(yeast){ self.yeasts.destroy(yeast); } self.removeHop = function(hop){ self.hops.destroy(hop); } // http://stackoverflow.com/questions/40501838/pass-string-parameters-into-click-binding-while-retaining-default-params-knockou self.removeItem = function(item, name){ // not finished name.remove(function(hop){ return hop.name === item.name; }); } self.purify_fermentables = function(fermentables){ var final_fermentables = []; for (i = 0; i < fermentables.length; i++){ var item = fermentables[i]; var object = {catalog_id: item.catalog_id, milling_preference: item.milling_preference}; final_fermentables.push(object); } return final_fermentables; } self.purify_hops = function(hops){ var final_hops = []; for (i = 0; i < hops.length; i++){ var item = hops[i]; var object = {catalog_id: item.catalog_id, amount: item.amount, time: item.time, use: item.use}; final_hops.push(object); } return final_hops; } self.purify_yeasts = function(yeasts){ var final_yeasts = []; for (i = 0; i < yeasts.length; i++){ var item = yeasts[i]; var object = {catalog_id: item.catalog_id}; final_yeasts.push(object); } return final_yeasts; } self.prepareJSON = function(){ // pure as in only the fields the server cares about var pure_fermentables = self.purify_fermentables(self.valid_fermentables()); var pure_hops = self.purify_hops(self.valid_hops()); var pure_yeasts = self.purify_yeasts(self.valid_yeasts()); object = { fermentables: pure_fermentables, hops: pure_hops, yeasts: pure_yeasts, name: self.name(), brew_method: self.brew_method(), batch_size: self.batch_size(), beer_style: self.beer_style(), boil_time: self.boil_time(), notes: self.notes(), } return object; } self.saveRecipeData = function(){ var recipe_data = ko.toJSON(self.prepareJSON()); // alert("This is the data you're sending (universal Javascript object notation):\\n\\n" + recipe_data) $.ajax({ url: "http://127.0.0.1:8000/api/recipes/receive-recipe/", headers: { "Content-Type": "application/json" }, method: "POST", dataType: "json", async: false, data: recipe_data, success: function(data){ console.log("Success! Saved the recipe"); } }); // http://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep // not working in browser... // await sleep(2000); self.recipes( self.get_data("recipes/all-recipes") ); } self.my_to_json = function(object){ return JSON.stringify(object, null, 4); } } ko.applyBindings(new RecipeViewModel()); 
  input[type="number"] { -moz-appearance: textfield; } input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } input, select { border-radius: 3px; } #notes-input { width: 650px; height: 220px; } .label-text { /*font-weight: bold;*/ } 
 <head> <style> </style> <script type='text/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.1/knockout-min.js'></script> <script type='text/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script> <script type='text/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js'></script> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> </head> <body> <div class="container"> <div class="row"> <h3>My Recipes</h3> <ul data-bind="foreach: recipes"> <li> <!-- http://stackoverflow.com/questions/13054878/knockout-js-how-to-access-index-in-handler-function --> <a data-bind="click: $root.populate_recipe"> <span data-bind="text: $data.name + ' id: ' + $data.id"></span> </a> <a data-bind="click: $root.delete_recipe">Delete Recipe</a> </li> </ul> </div> <div class="row"> <br><br> <div class="col-md-2 col-md-offset-2"> <span class="label-text">Recipe Name:</span> </div> <div class="col-md-2"> <input type="text" data-bind="value: name" maxlength="250" class="recipeSetupText" /> </div> <div class="col-md-4"> <span class="label-text">Brew Method:</span> <select data-bind="options: brew_methods, value: brew_method"></select> </div> </div> <div class="row"> <!-- http://stackoverflow.com/questions/8354975/how-can-i-limit-possible-inputs-in-a-html5-number-element --> <div class="col-md-2 col-md-offset-2"> <span class="label-text" id="batch-size-label">Batch Size:</span> </div> <div class="col-md-2"> <input type="number" data-bind="value: batch_size" style="width: 35px" /> <span class="unit">gallons</span> </div> <div class="col-md-4"> <span class="label-text">Style:</span> <select data-bind="options: styles_data, optionsValue: 'value', optionsText: 'display', value: current_style"></select> </div> </div> <div class="row"> <div class="col-md-4 col-md-offset-2"> <span class="label-text" id="boil-time-label">Boil Time:</span> <input type="number" data-bind="value: boil_time" style="width: 60px" /> <span class="unit">(minutes)</span> </div> </div> <h2>Current price: <span data-bind="text: current_price"></span></h2> <div> <h2>Fermentables</h2> <div data-bind="foreach: fermentables"> <select id="fermentable-variety-select" style="width:325px" data-bind="value: catalog_id"> <option value="-"> - </option> <!-- ko foreach: fermentables_options --> <optgroup data-bind="attr: {label: category}"> <!-- ko foreach: fermentables --> <option data-bind="value: $data.catalog_id, text: $data.name + ' -- ' + $data.price.toFixed(2)"></option> <!-- /ko --> </optgroup> <!-- /ko --> </select> <label>Milling preference: </label> <select data-bind="options: $root.milling_preferences, value: milling_preference"></select> <a href="#" data-bind="click: $root.removeFermentable, visible: $root.fermentables.countVisible() > 1"> Delete </a> <br><br> </div> <input data-bind="click: addFermentable" type="button" value="Add Fermentable"/> </div> <div class="row"> <h2 class="">Yeast</h2> <div data-bind="foreach: yeasts"> <span>Yeast Brand Filter:</span> <select data-bind="options: yeast_categories, value: current_filter" id="yeast-brand-select"> </select> <br/> <span>Yeast Variety:</span> <select id="yeast-variety-select" style="width:325px" data-bind="value: catalog_id"> <option value="-"> - </option> <!-- ko foreach: yeast_groups_individual --> <optgroup data-bind="attr: {label: category}"> <!-- ko foreach: yeasts --> <option data-bind="value: $data.catalog_id, text: $data.name + ' -- ' + $data.price.toFixed(2)"></option> <!-- /ko --> </optgroup> <!-- /ko --> </select> <a href="#" data-bind="click: $root.removeYeast, visible: $root.yeasts.countVisible() > 1">Delete</a> <br><br> </div> <br> <input data-bind="click: addYeast" type="button" value="Add Yeast"/> </div> <div class="row"> <h2 class="">Hops</h2> <div data-bind='foreach: hops'> <select id="hops-variety-select" style="width:325px" data-bind="value: catalog_id"> <option value="-"> - </option> <!-- ko foreach: hops_options --> <optgroup data-bind="attr: {label: category}"> <!-- ko foreach: hops --> <option data-bind="value: $data.catalog_id, text: $data.name + ' -- ' + $data.price.toFixed(2)"></option> <!-- /ko --> </optgroup> <!-- /ko --> </select> <label>Amount:</label> <input type="number" data-bind="value: amount" maxlength="6"> oz Time: <input type="text" data-bind="value: time" > Min. Use: <select data-bind="options: $root.hops_uses, value: use"></select> <a href="#" data-bind="click: function() { $root.removeItem($data, $root.hops) }, visible: $root.hops.countVisible() > 1">Delete</a> <br><br> </div> <br> <input data-bind="click: addHop" type="button" value="Add Hop" /> </div> <br> <textarea data-bind="value: notes" id="notes-input" placeholder="Write any extra notes here..." style="resize: both;"></textarea> <p> <button data-bind="click: saveRecipeData">Save recipe</button> </p> </div> <script src='index.js' type='text/javascript'></script> </body> 

The order of bindings in Knockout can sometimes be important. In this case, the value binding of the <select> is run before the <option> elements are set up, so when it tries to bind the value there isn't a matching option.

The fix is to force the descendant elements to be bound before value , which can be accomplished by including another binding on the <select> that simply binds the descendants. You could create a custom binding that does this (based on examples at http://knockoutjs.com/documentation/custom-bindings-controlling-descendant-bindings.html ), but the built-in if binding will do the job just fine. Simply make sure it's listed before the value binding.

<select data-bind="if: true, value: theValue">...

There's an old open Knockout issue related to this: https://github.com/knockout/knockout/issues/1243

 // hard codes var HOPS = [ { "category": "Hop Pellets", "hops": [ { "name": "Ahtanum Hop Pellets 1 oz", "price": 1.99, "catalog_id": 1124 }, { "name": "Amarillo Hop Pellets 1 oz", "price": 3.99, "catalog_id": 110 }, { "name": "Apollo (US) Hop Pellets - 1 oz.", "price": 2.25, "catalog_id": 6577 }, ] } ] var FERMENTABLES = [ { "category": "Dry Malt Extract", "fermentables": [ { "name": "Briess Bavarian Wheat DME 1 Lb", "price": 4.99, "catalog_id": 496 }, { "name": "Briess Bavarian Wheat DME 3 LBS", "price": 12.99, "catalog_id": 1435 }, { "name": "Briess Golden Light DME 1 Lb", "price": 4.99, "catalog_id": 492 }, ] } ] var YEASTS = [ { "category": "Dry Beer Yeast", "yeasts": [ { "name": "500 g Fermentis Safale S-04", "price": 79.99, "catalog_id": 6012 }, { "name": "500 g Fermentis Safale US-05", "price": 84.99, "catalog_id": 4612 }, { "name": "500 g Fermentis SafCider Yeast", "price": 59.99, "catalog_id": 6003 }, ] } ] var RECIPE_DATA = [ { id: 31, name: "my recipe ", notes: "some notes", brew_method: "All Grain", boil_time: 60, batch_size: "4.00", fermentable_selections: [ { catalog_id: 496, milling_preference: "Unmilled" }, { catalog_id: 1435, milling_preference: "Milled" } ], hop_selections: [ { catalog_id: 110, weight: "4.00", minutes: 35, use: "Dry Hop" } ], yeast_selections: [ { catalog_id: 6012 } ] } ]; var API_BASE = "127.0.0.1:8000"; ko.observableArray.fn.countVisible = function(){ return ko.computed(function(){ var items = this(); if (items === undefined || items.length === undefined){ return 0; } var visibleCount = 0; for (var index = 0; index < items.length; index++){ if (items[index]._destroy != true){ visibleCount++; } } return visibleCount; }, this)(); }; function Fermentable(data) { var self = this; var options = data.options; self.fermentables_options = ko.computed(function(){ return options; }); self.catalog_id = ko.observable(data.catalog_id || ""); self.name = ko.observable(data.name || ""); self.milling_preference = ko.observable(data.milling_preference || "Milled"); self.is_valid = ko.computed(function(){ var valid = self.catalog_id() !== "" && self.catalog_id() !== "-"; return valid }); } function Hop(data) { var self = this; self.hops_options = ko.computed(function(){ return data.options; }); self.catalog_id = ko.observable(data.catalog_id || ""); self.name = ko.observable(data.name || ""); self.amount = ko.observable(data.amount || ""); self.time = ko.observable(data.time || ""); self.use = ko.observable(data.use || "Boil"); self.is_valid = ko.computed(function(){ var valid = self.amount() > 0 && self.catalog_id() !== "" && self.catalog_id() !== "-"; return valid }); } function Yeast(data){ var self = this; var permanent_yeasts_options = data.yeasts_options; self.catalog_id = ko.observable(data.catalog_id || ""); self.name = ko.observable(data.name || "-"); self.current_filter = ko.observable("-Any-"); self.yeast_groups_individual = ko.computed(function(){ if (self.current_filter() !== "-Any-"){ var options = _.filter(data.yeasts_options, function(option){ return option.category === self.current_filter(); }); return options; } else{ return permanent_yeasts_options; } } ); self.yeast_categories = ko.observableArray(); ko.computed(function(){ var starter_list = ['-Any-']; var categories = _.pluck(permanent_yeasts_options, 'category'); var final = starter_list.concat(categories); self.yeast_categories(final); }); self.is_valid = ko.computed(function(){ var valid = self.catalog_id() !== "" && self.catalog_id() !== "-"; return valid }); } function RecipeViewModel() { var self = this; // http://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call self.get_data = function(url_ending){ var URL = "http://%/api/&/".replace("&", url_ending).replace("%", API_BASE); var data = $.ajax({ dataType: "json", url: URL, async: false, }); return data.responseJSON; } self.styles = [ "--", "Standard American Beer", "International Lager", "Czech Lager", "Pale Malty European Lager", "Pale Bitter European Beer", "Amber Malty European Lager", "Amber Bitter European Lager", "Dark European Lager" ] self.styles_data = ko.observableArray(); ko.computed(function(){ var data = []; for (i = 0; i < self.styles.length; i++){ var text = self.styles[i]; if (text === "--"){ data.push({value: "--", display: "--"}); } else { var display_text = i.toString() + ". " + text; var this_entry = {value: text, display: display_text}; data.push(this_entry); } } self.styles_data(data); }); self.recipes = ko.observableArray(); // self.recipes( self.get_data("recipes/all-recipes") ); self.recipes(RECIPE_DATA); self.current_style = ko.observable("--"); // defaults self.total_price = ko.observable(0.0); // TODO: this should not default if the recipe has items already... self.hops_uses = ko.observableArray(['Boil', 'Dry Hop']); self.weight_units = ko.observableArray(['oz', 'lb']); self.milling_preferences = ko.observableArray(['Milled', 'Unmilled']); self.brew_methods = ko.observableArray(['Extract', 'Mini-Mash', 'All Grain', 'Brew-in-a-bag']); // start of input fields self.name = ko.observable(""); self.brew_method = ko.observable("Extract"); self.batch_size = ko.observable("5"); self.beer_style = ko.observable("Standard American Beer"); self.boil_time = ko.observable("60"); self.notes = ko.observable(""); self.hops_options = HOPS; self.hops = ko.observableArray([new Hop({options: self.hops_options}), new Hop({options: self.hops_options})]); self.fermentables_options = FERMENTABLES; self.fermentables = ko.observableArray( [ new Fermentable({options: self.fermentables_options}), new Fermentable({options: self.fermentables_options}) ] ); self.yeasts_options = YEASTS; self.yeasts = ko.observableArray([new Yeast({yeasts_options: self.yeasts_options})]); self.reset_form = function(){ var x = 'finish this'; } self.populate_recipe = function(data, event){ var context = ko.contextFor(event.target); var index = context.$index(); var recipe = self.recipes()[index]; var attrs = ['name', 'brew_method', 'boil_time', 'batch_size', 'notes'] for (i = 0; i < attrs.length; i++) { attr = attrs[i]; self[attr](recipe[attr]); } fermentables_data = recipe.fermentable_selections; new_fermentables_data = []; for (i = 0; i < fermentables_data.length; i++) { var data_set = fermentables_data[i] data_set['options'] = self.fermentables_options; // takes {options: ..; catalog_id: ..; milling_preference} // based on the results of http://127.0.0.1:8000/api/recipes/all-recipes/ var this_fermentable = new Fermentable(data_set); new_fermentables_data.push(this_fermentable); } self.fermentables(new_fermentables_data); } self.delete_recipe = function(data, event){ var recipe_id = data.id; var URL = "http://%/api/recipes/delete/&/".replace("%", API_BASE).replace("&", recipe_id); $.ajax({ url: URL, async: false, }); self.recipes( self.get_data("recipes/all-recipes") ); } self.valid_items = function(items){ var final_items = _.filter(items, function(item){ return item.is_valid(); }); return final_items; } self.valid_fermentables = ko.observableArray(); ko.computed(function(){ self.valid_fermentables(self.valid_items(self.fermentables())); }); self.valid_hops = ko.observableArray(); ko.computed(function(){ self.valid_hops(self.valid_items(self.hops())); }); self.valid_yeasts = ko.observableArray(); ko.computed(function(){ self.valid_yeasts(self.valid_items(self.yeasts())); }); self.prices_hash = ko.computed(function(){ var data = {}; var strings = ['fermentables', 'hops', 'yeasts']; for (i = 0; i < strings.length; i++) { var string = strings[i]; var attr = strings[i] + '_options'; for (j = 0; j < self[attr].length; j++) { var groups = self[attr][j][string]; for (k = 0; k < groups.length; k++) { var catalog_id = groups[k].catalog_id.toString(); var current_price = groups[k].price; data[catalog_id] = current_price; } } } return data; }); self.current_price = ko.computed(function(){ var total_price = 0; for (i = 0; i < self.valid_fermentables().length; i++){ var item = self.valid_fermentables()[i]; total_price = total_price + self.prices_hash()[item.catalog_id()]; } for (i = 0; i < self.valid_hops().length; i++){ var item = self.valid_hops()[i]; total_price = total_price + self.prices_hash()[item.catalog_id()]; } for (i = 0; i < self.valid_yeasts().length; i++){ var item = self.valid_yeasts()[i]; total_price = total_price + self.prices_hash()[item.catalog_id()]; } return total_price.toFixed(2); }); self.addFermentable = function(){ self.fermentables.push(new Fermentable({options: self.fermentables_options})) } self.addYeast = function(){ self.yeasts.push(new Yeast({yeasts_options: self.yeasts_options})); } self.addHop = function(){ self.hops.push(new Hop({options: self.hops_options})); } self.removeFermentable = function(fermentable){ self.fermentables.destroy(fermentable); } self.removeYeast = function(yeast){ self.yeasts.destroy(yeast); } self.removeHop = function(hop){ self.hops.destroy(hop); } // http://stackoverflow.com/questions/40501838/pass-string-parameters-into-click-binding-while-retaining-default-params-knockou self.removeItem = function(item, name){ // not finished name.remove(function(hop){ return hop.name === item.name; }); } self.purify_fermentables = function(fermentables){ var final_fermentables = []; for (i = 0; i < fermentables.length; i++){ var item = fermentables[i]; var object = {catalog_id: item.catalog_id, milling_preference: item.milling_preference}; final_fermentables.push(object); } return final_fermentables; } self.purify_hops = function(hops){ var final_hops = []; for (i = 0; i < hops.length; i++){ var item = hops[i]; var object = {catalog_id: item.catalog_id, amount: item.amount, time: item.time, use: item.use}; final_hops.push(object); } return final_hops; } self.purify_yeasts = function(yeasts){ var final_yeasts = []; for (i = 0; i < yeasts.length; i++){ var item = yeasts[i]; var object = {catalog_id: item.catalog_id}; final_yeasts.push(object); } return final_yeasts; } self.prepareJSON = function(){ // pure as in only the fields the server cares about var pure_fermentables = self.purify_fermentables(self.valid_fermentables()); var pure_hops = self.purify_hops(self.valid_hops()); var pure_yeasts = self.purify_yeasts(self.valid_yeasts()); object = { fermentables: pure_fermentables, hops: pure_hops, yeasts: pure_yeasts, name: self.name(), brew_method: self.brew_method(), batch_size: self.batch_size(), beer_style: self.beer_style(), boil_time: self.boil_time(), notes: self.notes(), } return object; } self.saveRecipeData = function(){ var recipe_data = ko.toJSON(self.prepareJSON()); // alert("This is the data you're sending (universal Javascript object notation):\\n\\n" + recipe_data) $.ajax({ url: "http://127.0.0.1:8000/api/recipes/receive-recipe/", headers: { "Content-Type": "application/json" }, method: "POST", dataType: "json", async: false, data: recipe_data, success: function(data){ console.log("Success! Saved the recipe"); } }); // http://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep // not working in browser... // await sleep(2000); self.recipes( self.get_data("recipes/all-recipes") ); } self.my_to_json = function(object){ return JSON.stringify(object, null, 4); } } ko.applyBindings(new RecipeViewModel()); 
  input[type="number"] { -moz-appearance: textfield; } input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } input, select { border-radius: 3px; } #notes-input { width: 650px; height: 220px; } .label-text { /*font-weight: bold;*/ } 
 <head> <style> </style> <script type='text/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.1/knockout-min.js'></script> <script type='text/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script> <script type='text/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js'></script> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> </head> <body> <div class="container"> <div class="row"> <h3>My Recipes</h3> <ul data-bind="foreach: recipes"> <li> <!-- http://stackoverflow.com/questions/13054878/knockout-js-how-to-access-index-in-handler-function --> <a data-bind="click: $root.populate_recipe"> <span data-bind="text: $data.name + ' id: ' + $data.id"></span> </a> <a data-bind="click: $root.delete_recipe">Delete Recipe</a> </li> </ul> </div> <div class="row"> <br><br> <div class="col-md-2 col-md-offset-2"> <span class="label-text">Recipe Name:</span> </div> <div class="col-md-2"> <input type="text" data-bind="value: name" maxlength="250" class="recipeSetupText" /> </div> <div class="col-md-4"> <span class="label-text">Brew Method:</span> <select data-bind="options: brew_methods, value: brew_method"></select> </div> </div> <div class="row"> <!-- http://stackoverflow.com/questions/8354975/how-can-i-limit-possible-inputs-in-a-html5-number-element --> <div class="col-md-2 col-md-offset-2"> <span class="label-text" id="batch-size-label">Batch Size:</span> </div> <div class="col-md-2"> <input type="number" data-bind="value: batch_size" style="width: 35px" /> <span class="unit">gallons</span> </div> <div class="col-md-4"> <span class="label-text">Style:</span> <select data-bind="options: styles_data, optionsValue: 'value', optionsText: 'display', value: current_style"></select> </div> </div> <div class="row"> <div class="col-md-4 col-md-offset-2"> <span class="label-text" id="boil-time-label">Boil Time:</span> <input type="number" data-bind="value: boil_time" style="width: 60px" /> <span class="unit">(minutes)</span> </div> </div> <h2>Current price: <span data-bind="text: current_price"></span></h2> <div> <h2>Fermentables</h2> <div data-bind="foreach: fermentables"> <select id="fermentable-variety-select" style="width:325px" data-bind="if: true, value: catalog_id"> <option value="-"> - </option> <!-- ko foreach: fermentables_options --> <optgroup data-bind="attr: {label: category}"> <!-- ko foreach: fermentables --> <option data-bind="value: $data.catalog_id, text: $data.name + ' -- ' + $data.price.toFixed(2)"></option> <!-- /ko --> </optgroup> <!-- /ko --> </select> <label>Milling preference: </label> <select data-bind="options: $root.milling_preferences, value: milling_preference"></select> <a href="#" data-bind="click: $root.removeFermentable, visible: $root.fermentables.countVisible() > 1"> Delete </a> <br><br> </div> <input data-bind="click: addFermentable" type="button" value="Add Fermentable"/> </div> <div class="row"> <h2 class="">Yeast</h2> <div data-bind="foreach: yeasts"> <span>Yeast Brand Filter:</span> <select data-bind="options: yeast_categories, value: current_filter" id="yeast-brand-select"> </select> <br/> <span>Yeast Variety:</span> <select id="yeast-variety-select" style="width:325px" data-bind="if: true, value: catalog_id"> <option value="-"> - </option> <!-- ko foreach: yeast_groups_individual --> <optgroup data-bind="attr: {label: category}"> <!-- ko foreach: yeasts --> <option data-bind="value: $data.catalog_id, text: $data.name + ' -- ' + $data.price.toFixed(2)"></option> <!-- /ko --> </optgroup> <!-- /ko --> </select> <a href="#" data-bind="click: $root.removeYeast, visible: $root.yeasts.countVisible() > 1">Delete</a> <br><br> </div> <br> <input data-bind="click: addYeast" type="button" value="Add Yeast"/> </div> <div class="row"> <h2 class="">Hops</h2> <div data-bind='foreach: hops'> <select id="hops-variety-select" style="width:325px" data-bind="if: true, value: catalog_id"> <option value="-"> - </option> <!-- ko foreach: hops_options --> <optgroup data-bind="attr: {label: category}"> <!-- ko foreach: hops --> <option data-bind="value: $data.catalog_id, text: $data.name + ' -- ' + $data.price.toFixed(2)"></option> <!-- /ko --> </optgroup> <!-- /ko --> </select> <label>Amount:</label> <input type="number" data-bind="value: amount" maxlength="6"> oz Time: <input type="text" data-bind="value: time" > Min. Use: <select data-bind="options: $root.hops_uses, value: use"></select> <a href="#" data-bind="click: function() { $root.removeItem($data, $root.hops) }, visible: $root.hops.countVisible() > 1">Delete</a> <br><br> </div> <br> <input data-bind="click: addHop" type="button" value="Add Hop" /> </div> <br> <textarea data-bind="value: notes" id="notes-input" placeholder="Write any extra notes here..." style="resize: both;"></textarea> <p> <button data-bind="click: saveRecipeData">Save recipe</button> </p> </div> <script src='index.js' type='text/javascript'></script> </body> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM