javascriptasp.net-mvcknockout.jsknockout-2.0knockout-mapping-plugin

Knockout add to array of an array


I am looking for a way to add an item to an array that belongs to an item within another array using knockout and knockout mapping.

I have the following, a Person which has an array of WorkItems that has an array of ActionPlans. Person > WorkItems > ActionPlans

Knockout code is as follows -

var PersonViewModel = function(data) {
var self = this;
ko.mapping.fromJS(data, trainingCourseItemMapping, self);

self.addWorkItem = function() {
    var WorkItem = new WorkItemVM({
        Id: null,
        JobSkillsAndExpDdl: "",
        JobSkillsAndExperience: "",
        ActionPlans: ko.observableArray(),
        PersonId: data.Id
        })
   self.WorkItems.push(WorkItem)
};

self.addActionPlan = function () {
    var actionPlan = new ActionPlanVM({
        Id: null,
        priorityAreaStage: "",
        goal: "",
        action: "",
        byWho: "",
        byWhen: ""
        WorkItemId: data.Id
    });
    self.ActionPlans.push(actionPlan);
};
}

Array mapping

var trainingCourseItemMapping = {
'WorkItem': {
    key: function(workitem) {
        return ko.utils.unwrapObservable(workitem.Id);
    },
    create: function(options) {
        return new WorkItemVM(options.data);
    },
    'ActionPlans': {
        key: function (actionPlanItem) {
            return ko.utils.unwrapObservable(actionPlanItem.id);
        },
        create: function (options) {
            return new ActionPlanVM(options.data);
        }

    }
}

Array item mapping

var WorkItemVM = function(data) {
    var self = this;
    ko.mapping.fromJS(data, trainingCourseItemMapping, self);
}

var ActionPlanVM = function(data) {
    var self = this;
    ko.mapping.fromJS(data, {}, self);
}

And within my view i want to have the following (edited) -

<tbody data-bind="foreach: WorkItems">
//body table html here
</tbody>

<!--ko foreach: WorkItems-->
<tbody data-bind="foreach: ActionPlans">
//body table html here
</tbody>
<!--/ko-->

Error

The error i am currently getting is -

Unable to process binding "click: function(){return addActionPlan }"

How can i push an item to the "nested" action plan array of WorkItems? Thanks

Edit -

Image as requested -

enter image description here

Preceding this is a "add work item" button within the main Form. When Save is pressed the WorkItem is shown within a table row (all working fine)


Solution

  • One thing to note is that you have ActionPlans WITHIN WorkItems, so your binds should reflect that too:

    <tbody data-bind="foreach: WorkItems">
    //body table html here
        <tbody data-bind="foreach: ActionPlans"> /*ActionPlans exists in this context*/
        //body table html here
        </tbody>
    </tbody>
    

    with your current HTML ActionPlans are not defined in their binding context This is where the specific error comes from, ActionPlans are not defined in a "sibling" context, they are properties of each WorkItem

    EDIT: You might also try virtual elements, knockout's containerless syntax, allthough this is not advised in general ( bad performance relative to the rest of the framework, some problems may occur with minifier's removing comments etc)

    <tbody data-bind="foreach: WorkItems">
        //body table html here
    
        </tbody>
    
    <!-- ko foreach: WorkItems -->
        <tbody data-bind="foreach: ActionPlans"> 
        </tbody>
    <!-- /ko -->
    

    You best option thought is to restruct your VM!

    EDIT 2: Try this in your personVM

    self.addActionPlanToWorkItem = function (workItem) {
        var actionPlan = new ActionPlanVM({
            Id: null,
            priorityAreaStage: "",
            goal: "",
            action: "",
            byWho: "",
            byWhen: ""
            WorkItemId: workItem.Id
       });
        workItem.ActionPlans.push(actionPlan);
    };
    

    Call it from anywhere and pass in your current active WorkItem, it will add a new empty ActionPlan to your model.

    Or you can maybe try out this way: Replace workItemVM with this

        var WorkItemVM = function(data) {
            var self = this;
            ko.mapping.fromJS(data, trainingCourseItemMapping, self);
            self.addActionPlan = function(actionPlanToAdd) 
            {
                self.ActionPlans.push(actionPlanToAdd);
             };
        }
    

    and call it on your workItems as

    someWorkItemInstance.addActionPlan(new ActionPPlanVM())