选择具有嵌套foreach选项的元素在KnockoutJS中不起作用

时间:2016-12-30 01:37:30

标签: javascript jquery html knockout.js frontend

我正在尝试使用Knockoutjs更新我的应用中的选择框元素。当您点击此代码段上的my recipe id: 31时,您可以看到它根据var RECIPE对象更新顶部附近的某些表单元素。但是,Fermentables项名称未更新(它们仍为“ - ”)。然而,这些Fermentables的Milling preference确实会更新。

Fermentables html是:

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

损坏的select元素是顶部元素,工作元素是底部元素。您可以使用嵌套的foreach看到损坏的更复杂,但我需要将类别显示为optgroups

我在已损坏的data-bind="value: catalog_id"上有select,但它不会更新为"-"以外的任何值。

这些框应显示Briess Bavarian Wheat DME 1 LbBriess 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>

1 个答案:

答案 0 :(得分:2)

Knockout中绑定的顺序有时很重要。在这种情况下,value的{​​{1}}绑定在<select>元素设置之前运行,因此当它尝试绑定值时,没有匹配的选项。

修复是强制后代元素在<option>之前被绑定,这可以通过在value上包含另一个绑定后代的绑定来实现。您可以创建一个执行此操作的自定义绑定(基于http://knockoutjs.com/documentation/custom-bindings-controlling-descendant-bindings.html上的示例),但内置的<select>绑定可以很好地完成工作。只需确保它在if绑定之前列出。

value

有一个与此相关的旧的Knockout问题:https://github.com/knockout/knockout/issues/1243

<select data-bind="if: true, value: theValue">...
// 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;*/
        }