javascripthtmljsonknockout-3.0

KnockoutJS - making NESTED JSON field observable / editable


I've started learning JS and KO recently, I've completed KO live tutorials and read through the documentation, but still, there are things that I can't figure out myself. Just wondering if you could give a hand on this simple application, so I could learn further.

I have a json object minified to a string, which is just an array containing of food items and every food item have portions array as well. So my learning goal would be to display each food item name, select dropdown list for food serving sizes and an input field for quantity field (I'd like to edit this value myself so needs to be observable). What I can't figure out is how do I fetch the quantity value into the input field when I keep changing the serving size dropdown list selection.

I've uploaded my current code into jsfiddle so it could be looked at easily, I believe I'm very close to making this little application to work.

https://jsfiddle.net/2zkeycjb/

HTML and JS code:

<!DOCTYPE html>
<html>

<head>
    <title>KO</title>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    <script type="text/javascript" src='https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.0/knockout-min.js'></script>
</head>

<body>

    <script type="text/javascript">
        var foodsJson = JSON.parse('[{"id":"ea17ca42-38d4-7e02-22ea-43b20fc7f3fc","name":"Macaroni","foodPortions":[{"id":"69869c2b-efbc-418f-98be-bb5651e595c5","quantity":1,"servingSize":"piece","calories":3},{"id":"0d7c01c9-4edd-4da5-91f6-fe18a14ff745","quantity":100,"servingSize":"g.","calories":157}]},{"id":"c6768773-4e0c-7255-4f0b-0205c7f78f42","name":"Salmon Maki","foodPortions":[{"id":"c9c6b7a9-83d0-4dc4-aebb-06948714da65","quantity":1,"servingSize":"cup","calories":305},{"id":"cf84c55f-8b91-4419-8868-275d8634f961","quantity":100,"servingSize":"g.","calories":184},{"id":"c8c4e813-6a50-4e8e-b5a2-4ae44e846b26","quantity":1,"servingSize":"oz (28.34 g.)","calories":52},{"id":"7fac51dd-2592-407e-a4da-6829a6536e6c","quantity":1,"servingSize":"piece","calories":28}]}]');

        function FoodPortion(foodPortion) {
            var self = this;

            self.portion = ko.observable(foodPortion);

            self.servingSize = ko.computed(function() {
                return self.portion().id;
            });

            self.servingSize = ko.computed(function() {
                return self.portion().servingSize;
            });

            self.quantity = ko.computed(function() {
                return self.portion().quantity;
            });

            self.calories = ko.computed(function() {
                return self.portion().calories;
            });
        }

        function Food(foodItem) {
            var self = this;

            self.food = ko.observable(foodItem);
            self.name = self.food().name;

            self.portions = ko.utils.arrayFilter(self.food().foodPortions, function(foodPortion) {
                return new FoodPortion(foodPortion);
            });

            self.selectedPortion = ko.observable();
        }

        function FoodViewModel() {
            var self = this;

            self.foods = $.map(foodsJson, function(foodItem) {
                return new Food(foodItem)
            });
        }
    </script>

    <div data-bind="foreach: foods">
        <span data-bind="text: name"></span>
        <select data-bind="
            options: portions,
            value: selectedPortion,
            optionsText: function(portion) { return portion.servingSize; }">
        </select>
        <input type="text" data-bind="value: selectedPortion.quantity">
        <br />
    </div>

    <script type="text/javascript">
        ko.applyBindings(new FoodViewModel());
    </script>

</body>

</html>

Also, here's an example JSON file which is used in the application

[{
        "id": "ea17ca42-38d4-7e02-22ea-43b20fc7f3fc",
        "name": "Macaroni",
        "foodPortions": [{
                "id": "69869c2b-efbc-418f-98be-bb5651e595c5",
                "quantity": 1.0,
                "servingSize": "piece",
                "calories": 3.0
            },
            {
                "id": "0d7c01c9-4edd-4da5-91f6-fe18a14ff745",
                "quantity": 100.0,
                "servingSize": "g.",
                "calories": 157.0
            }
        ]
    },
    {
        "id": "c6768773-4e0c-7255-4f0b-0205c7f78f42",
        "name": "Salmon Maki",
        "foodPortions": [{
                "id": "c9c6b7a9-83d0-4dc4-aebb-06948714da65",
                "quantity": 1.0,
                "servingSize": "cup",
                "calories": 305.0
            },
            {
                "id": "cf84c55f-8b91-4419-8868-275d8634f961",
                "quantity": 100.0,
                "servingSize": "g.",
                "calories": 184.0
            },
            {
                "id": "c8c4e813-6a50-4e8e-b5a2-4ae44e846b26",
                "quantity": 1.0,
                "servingSize": "oz (28.34 g.)",
                "calories": 52.0
            },
            {
                "id": "7fac51dd-2592-407e-a4da-6829a6536e6c",
                "quantity": 1.0,
                "servingSize": "piece",
                "calories": 28.0
            }
        ]
    }
]

Any help much much appreciated! Thanks


Solution

  • Use the with binding to change the context to the selected item.

    <!-- ko with: selectedPortion -->
    <input type="text" data-bind="value: quantity">
    <!-- /ko -->
    

    You should also map out all properties you wish to make modifiable to observable fields.

    function FoodPortion(foodPortion) {
        this.id = ko.observable(foodPortion.id);
        this.servingSize = ko.observable(foodPortion.servingSize);
        this.quantity = ko.observable(foodPortion.quantity);
        this.calories = ko.observable(foodPortion.calories);
    }
    

    fiddle