Mike Devenney
Mike Devenney

Reputation: 1845

Calling functions in nested observablearrays (knockout.js)

I have a view model with a few nested observableArrays and I need call a function to remove items from one of the arrays. Instead of a diagram, I'll try to describe the model in English (it's included below in code as well). The view model is a training plan which has an observableArray of week objects. Each week object, among other properties, has an observableArray of Days objects. Each day object, among other properties, has an observable array of Workout objects. Each workout object, among other properties, has an observableArray of Interval objects. I'm new to knockout and not all that steady on my feet with javascript (and so may be doing some stuff wrong here, please feel free to critique). The problem is with the removeWorkout function on the Day object. I'm following Example 2 on the knockout website here: example. I think I'm doing what the page says, but when I inspect "this" in the function, it is a Day object and not the Workout object that called the function. Where am I going wrong?

My javascript file:

  $(document).ready(function() {  
        var trainingPlan;
        var weekNumber = 0;
        var workoutNumber = 0;    
        var planID = { id: $('#ID').val() };

        $.getJSON('/PlanBuilder/GetPlanJson', planID, function (model) {
        trainingPlan = ko.mapping.fromJSON(model.Message);

        trainingPlan.addWeek = function() {
            var wk = new week();

            weekNumber++;
            wk.Name = 'Week ' + weekNumber;

            for (var i = 1; i < 8; i++) {
                var dy = new day();
                dy.DayNumber = i;
                wk.Days.push(dy);
            }

            var summ = new day();
            summ.DayNumber = "Summary";
            wk.Days.push(summ);

            this.Schedule.push(wk);
        };

        ko.applyBindings(trainingPlan);
    });

    function week() {
        this.PlanID = ko.observable();
        this.StartDate = ko.observable();
        this.EndDate = ko.observable();
        this.Name = ko.observable();
        this.Days = ko.observableArray(); // a list of day objects
    };

    function day() {
        var self = this;

        self.DayNumber = ko.observable();
        self.TodaysDate = ko.observable();
        self.Name = ko.observable();
        self.Workouts = ko.observableArray(); // a list of workout objects
        self.addWorkout = function () {
            var wrk = new workout();
            wrk.Type = "Workout " + workoutNumber++;
            self.Workouts.push(wrk);
        };
        self.removeWorkout = function () {
            // 'this' should represent the item that originated the call (a workout)  Not the case...
            self.Workouts.remove(this);
        };
    };

    function workout() {
        this.Type = ko.observable(); //need to figure out what/how to handle enums
        this.WarmUp = ko.observableArray(); // a list of intervals
        this.Main = ko.observableArray(); // a list of intervals
        this.CoolDown = ko.observableArray(); // a list of intervals
        this.Status = ko.observable();
        this.Completed = ko.observable();
    };

    function interval() {
        this.timeValue = ko.observable();
        this.timeUnit = ko.observable();
        this.rpeUnits = ko.observable();
        this.heartRateZone = ko.observable();
        this.description = ko.observable();
        this.distanceValue = ko.observable();
        this.distanceUnit = ko.observable();
    };
});

My view:

<div class="container">
    <div class="page-header">
        <h2>@ViewBag.HeaderText</h2>
    </div>

    <div class="panel panel-default">
        <div class="panel-heading">
            <h3>Plan Summary</h3>
        </div>
        <div class="panel-body">
            <div id="NewPlanForm" class="form-horizontal">
                @Html.AntiForgeryToken()
                @Html.HiddenFor(model => model.ID )
                <div class="form-group">
                    @Html.LabelFor(model => model.Name, new { @class = "control-label col-md-3" })
                    <div class="col-md-9">
                        @Html.KnockoutTextBoxFor(model => model.Name, new { @class = "form-control" })
                        @Html.ValidationMessageFor(model => model.Name)
                    </div>
                </div>

                <div class="form-group">
                    @Html.LabelFor(model => model.Author, new { @class = "control-label col-md-3" })
                    <div class="col-md-9">
                        @Html.KnockoutTextBoxFor(model => model.Author, new { @class = "form-control" })
                        @Html.ValidationMessageFor(model => model.Author)
                    </div>
                </div>

                <div class="form-group">
                    @Html.LabelFor(model => model.ShortDescription, new { @class = "control-label col-md-3" })
                    <div class="col-md-9">
                        @Html.KnockoutTextBoxFor(model => model.ShortDescription, new { @class = "form-control" })
                        @Html.ValidationMessageFor(model => model.ShortDescription)
                    </div>
                </div>

                <div class="form-group">
                    @Html.LabelFor(model => model.LongDescription, new { @class = "control-label col-md-3" })
                    <div class="col-md-9">
                        @Html.KnockoutTextAreaFor(model => model.LongDescription, new { @class = "form-control" })
                        @Html.ValidationMessageFor(model => model.LongDescription)
                    </div>
                </div>

                <div class="form-group">
                    @Html.LabelFor(model => model.Discipline, new { @class = "control-label col-md-3" })
                    <div class="col-md-9">
                        @Html.KnockoutTextBoxFor(model => model.Discipline, new { @class = "form-control" })
                        @Html.ValidationMessageFor(model => model.Discipline)
                    </div>
                </div>

                <div class="form-group">
                    @Html.LabelFor(model => model.Level, new { @class = "control-label col-md-3" })
                    <div class="col-md-9">
                        @Html.KnockoutTextBoxFor(model => model.Level, new { @class = "form-control" })
                        @Html.ValidationMessageFor(model => model.Level)
                    </div>
                </div>
            </div>
            <div class="pull-right">
                <div id="btnSavePlan" class="btn btn-primary">Save Plan</div>
            </div>
        </div>
    </div>
</div>

<div id="SchedulePanel" class="panel panel-default">
    @Html.HiddenFor(model => model.ID)
    <div class="panel-heading">
        <h3>Plan Schedule</h3>
    </div>
    <div class="panel-body">
        <div class="pull-right">
            <input id="numWeeks" type="text" class="form-control input-sm" placeholder="number"/>
            <a id="btnAddWeek" class="btn btn-primary" data-bind="click: addWeek">Add Week(s)</a>
        </div>
        <div id="ScheduledInstructions">
            Click the Add Week button to get started adding workouts to your schedule
        </div>
        <hr />
        <div id="weeks" data-bind="template: { name: 'weekTemplate', foreach: Schedule}">
        </div>
    </div>
</div>

<script id="weekTemplate" type="text/html">
    <div style=" margin-left: auto; margin-right: auto; ">
        <h4 data-bind="text: Name"></h4>
        <div data-bind="id: weekName, template: { name: 'dayTemplate', foreach: Days }" >
        </div>
    </div>
</script>

<script id="dayTemplate" type="text/html">
    <div class="dayBuilder">
        <span data-bind="text: DayNumber"></span>
        <div data-bind="id: dayName, template: { name: 'workoutTemplate', foreach: Workouts }" >
        </div>
        <div class="addWorkout">
            <span class="glyphicon glyphicon-plus" data-bind="click: addWorkout" title="Add Workout"></span>
        </div>
    </div>   
</script>

<script id="workoutTemplate" type="text/html">
    <div class="workout">
        <span data-bind="text: Type"></span>
        <span class="glyphicon glyphicon-remove pull-right" data-bind="click: $parent.removeWorkout"></span>
    </div>
</script>

Upvotes: 1

Views: 128

Answers (1)

Anthony Chu
Anthony Chu

Reputation: 37530

removeWorkout() "belongs" to $parent, which is a day, not a workout; therefore this is a day object. Knockout supplies the model that originated the call as the first parameter, so this should work...

self.removeWorkout = function (workout) {
    // 'this' should represent the item that originated the call (a workout)  Not the case...
    self.Workouts.remove(workout);
};

Edit: Here's the docs (see note 1)... Knockout: The "click" binding

Upvotes: 1

Related Questions