KnockoutJS Bindings With Nested Templates - javascript

I'm having a problem with nested bindings with Knockout.JS
For example if I have the following in say an app.js file:
var UserModel = function() {
this.writeups = ko.observableArray([]);
}
var WriteupModel = function() {
this.type = 'some type';
}
var MyViewModel = function() {
this.newUser = new UserModel();
this.selectedUser = ko.observable(this.newUser);
this.selectedUser().writeups().push(new WriteupModel());
}
ko.applyBindings(new MyViewModel());
and the following for a view:
<div id="empReportView" data-bind="template: { name: 'empTmpl', data: selectedUser }"></div>
<script type="text/html" id="empTmpl">
<table>
<tbody data-bind="template: { name: 'empWuItem', foreach: $data.writeups } ">
</tbody>
</table>
</script>
<script type="text/html" id="empWuItem">
<tr>
<td data-bind="text: type"></td>
</tr>
</script>
Whenever another WriteupModel is pushed onto the writeups array belonging to the selectedUser the table doesn't update. This is a simplified version of what I'm trying to accomplish but it's to be assumed that when they create a writeup it should update the write-ups table based on the new information.
I'm new to Knockout so any help would be appreciated!
Thanks.
-=-= Edit 1 =-=-
One thing to note, if you reload the binding for the selectedUser it will spit out the empWuItem template for the added writeup. This just seems inefficient as the bindings should trigger when the WriteUp is added to the writeups observable array in the UserModel without have to "re-assign" the selectedUser property in the view model.

Push is a property of observable array:
this.selectedUser().writeups().push(new WriteupModel())
should be
this.selectedUser().writeups.push(new WriteupModel());

Related

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

Remove items from Knockout observable array

I have the below structure for knockout model. It contains an observable array which in turn contains an object.
function ViewModel() {
var self = this;
self.newItem = ko.observable({
manufacturer: ko.observable(),
itemnumber: ko.observable(),
itemDescription: ko.observable()
});
self.AllItems = ko.observableArray();
self.addItem = function() {
self.newItem().manufacturer("test");
self.newItem().itemDescription("data");
self.AllItems.push(self.newItem);
};
self.removeItem = function(data) {
self.AllItems.remove(data);
};
}
First issue:Through this script I am entering a new itemnumber in the textbox and then clicking on add item to have the new item with the itemnumber from the textbox added to the observable array but when I change the item number and hit add it changes all the itemnumber inside the array. How can i have unique data inside the array.
Second issue: I need to remove the specific items from the array but it's not deleting it. Can someone please tell me how I can delete items from the observable array based on say the itemnumber property.
<input type="text" data-bind="value: newItem().itemnumber"/>
<div>
Items: <button data-bind="click: addItem">Add Item</button>
</div>
<div>
<table>
<tbody data-bind="template: { name: 'itemTemplate', foreach: AllItems }"></tbody>
</table>
</div>
<script type="text/html" id="itemTemplate">
<tr>
<td>
<input data-bind="value: itemnumber" />
Remove Item
</td>
</tr>
</script>
I have created this fiddle for quick view of the issue. Just started learning knockout so any help is appreciated.
http://jsfiddle.net/N3JaW/138/
Try the following for adding new item, which will solve your first issue:-
HTML code
<input type="text" id="textBox" data-bind="value : textBoxVal"/>
<div>
Items: <button data-bind="click: addItem">Add Item</button>
</div>
<div>
<table>
<tbody data-bind="template: { name: 'itemTemplate', foreach: AllItems }"></tbody>
</table>
</div>
<script type="text/html" id="itemTemplate">
<tr>
<td>
<input data-bind="value: itemnumber" />
Remove Item
</td>
</tr>
</script>
JS code:-
function ViewModel() {
var self = this;
self.newItem = ko.observable({
manufacturer: "",
itemnumber: "",
itemDescription: ""
});
self.textBoxVal = ko.observable();
self.AllItems = ko.observableArray();
self.addItem = function() {
self.newItem().manufacturer= "test";
self.newItem().itemDescription= "data";
self.newItem().itemnumber = self.textBoxVal();
self.AllItems.push(self.newItem);
};
self.removeItem = function(data) {
self.AllItems.remove(data);
};
}
$(document).ready(function() {ko.applyBindings(new ViewModel()); });
Your first issue was because, each time you are trying to add a new item, you were changing the value of itemNumber, which is an observable.
Observable value will be changed every where it is binded, when it's value is changed.
Instead you need to create new object and do push into the observableArray.
Refer doc to know more about observableArray.
For your second problem change removeItem as given below:-
self.removeItem = function(data) {
var dtIndex = self.AllItems.indexOf(data); //Get the index of the object you want to remove.
self.AllItems.splice(dtIndex, 1); //Then do splice
};
You can refer the above doc, to know how to use splice.
EDIT based on the suggestion in the comment :-
For working code of edited answer click here.
Hope this will solve your problem.

How to bind a ko.obersavableArray that is nested in an object

I have my knockout page hub, and I need a ko.obeservableArray nested in a ko.observable object, this is where I define them:
function IncomeDeclarationHub() {
//data comes from a ajax call.
self.myIncomeDeclarationViewModel = ko.observable(new IncomeDeclarationViewModel(data));
}
function IncomeDeclarationViewModel(data) {
var self = this;
self.retentionAmount = ko.observable();
self.taxableMonth = ko.observable();
self.incDecDetGroViewModels = ko.observableArray();
if (data != null) {
var arrayLenght = data.IncDecDetGroViewModels.length;
for (var i = 0; i < arrayLenght; i++) {
var myObject = new IncomeDecDetGroViewModel(data.IncDecDetGroViewModels[i]);
self.incDecDetGroViewModels.push(myObject);
}
}
}
And this is my HTML code:
<span class="label">
Retention Amount
</span>
<input data-bind="value: myIncomeDeclarationViewModel.retentionAmount" />
<table>
<tbody data-bind="foreach: myIncomeDeclarationViewModel.incDecDetGroViewModels">
...
</tbody>
</table>
Ok so the thing is that incDecDetGroViewModels never gets populated, I used to have that ko.obersableArray outside the object, and it worked fine, now that I inserted it in my object myIncomeDeclarationViewModel is not populating the html table. Do I need to call it in a different way at the data-bind
myIncomeDeclarationViewModel is an observable, so you have to unwrap it to access it's properties. Add parenthesis to unwrap it (access the observable's underlying value) like this:
<span class="label">
Retention Amount
</span>
<input data-bind="value: myIncomeDeclarationViewModel().retentionAmount" />
<table>
<tbody data-bind="foreach: myIncomeDeclarationViewModel().incDecDetGroViewModels">
...
</tbody>
</table>
Here's a working jsFiddle based on your example
JsFiddle
well previously you can access just becoz it is in scope but right now you done some nesting so you just need to some looping in your view part to get that .
Something like this may be :
<table data-bind="foreach:myIncomeDeclarationViewModel">
<tbody data-bind="foreach:$data.incDecDetGroViewModels">
...
</tbody>
</table>
You can also ContainerLess foreach if you looking for something different like :
<!-- ko foreach:myIncomeDeclarationViewModel -->
//your table code
<!--/ko-->
I hope this solves the riddle .

How to update a knockout view model based on a separate callback, rather than user input

I have built a view model representing time slices with the following structure:
function TimeslotViewModel() {
this.timeslots = ko.observableArray();
this.updateTimeslots = function(timeslots) {
this.timeslots.destroyAll();
}
this.clearTimeslots = function() {
this.timeslots.destroyAll();
}
this.addTimeslot = function(timeslot) {
this.timeslots.push(timeslot);
}
}
function Timeslot(time, available) {
this.time = time;
this.available = available;
}
I'm trying to render this in a tabular format like so:
<div class="container">
<table class="table">
<thead>
<tr><th>Time</th><th>Status</th>
</thead>
<tbody data-bind="foreach: timeslots">
<td data-bind="text: time"></td>
<td data-bind="text: available"</td>
</tbody>
</table>
</div>
I've bound on page load:
$(function() {
ko.applyBindings(new TimeslotViewModel());
});
I'm trying to populate this table based on the callback result from an ajax call, but it doesn't seem to be working as expected. Here is what I tried:
$.getJSON(
"/myAjaxCall",
function (jsonData) {
var timeslotViewModel = new TimeslotViewModel();
timeslotViewModel.clearTimeslots();
$.each(jsonData, function (i, ts) {
var tsData = JSON.parse(ts);
var timeslot = new Timeslot(tsData.time, tsData.booked);
timeslotViewModel.addTimeslot(timeslot);
});
});
Unfortunately, I'm not seeing my view model's array get populated at all from this code. What is the right way to populate a view model based on a callback function's response?
You are creating a new viewmodel instead of updating the current one.
Replace this line
var timeslotViewModel = new TimeslotViewModel();
Either create a global viewmodel:
var myVm = new TimeslotViewModel();
ko.applyBindings(myVm);
//...
var timeslotViewModel = myVm;
Or get the current one from a node:
var timeslotViewModel = ko.contextFor($('.container').get(0)).$root

Knockout.js 2.2.1 can't find observable array

Not sure what's going wrong here, but KnockoutJS is having some issues finding my observable array that's inside my MasterViewModel. Using 2.2.1 with jQuery 1.8.x as well as not my first KJS app. Here it is:
Initialize
$(function() {
window.vm = new MasterViewModel();
ko.applyBindings(vm);
});
ViewModel
function MasterViewModel(data) {
var self = this;
self.currentAppView = ko.observable();
// Users
self.userList = ko.observableArray([]);
self.templateListGetter = ko.computed(function() {
$.getJSON("/user/list"), function(data) {
var mapped = $.map(data, function(item) { return new userModel(item) });
self.userList(mapped);
};
});
self.goToAppView = function(appView) {
location.hash = '!/' + appView;
};
Sammy(function() {
this.get('#!/:appView', function() {
self.currentAppView(this.params.appView);
$('.appview').hide();
ko.applyBindings(new window[this.params.appView+'VM']());
});
this.notFound = function(){
location.hash = "!/dashboard";
}
//this.raise_errors = true;
}).run();
}
The View
<table class="table table-bordered table-striped">
<tbody data-bind="foreach: userList">
<tr>
<td data-bind="text: guid"></td>
<td data-bind="text: firstName"></td>
<td data-bind="text: lastName"></td>
<td data-bind="text: email"></td>
<td data-bind="text: updated"></td>
<td data-bind="text: suspended"></td>
</tr>
</tbody>
</table>
I have a simple table that I am loading
Even after double-checking a couple things like adding defer="defer" to my JS tag and ensuring the userList exists, it simply cannot find the observableArray. It gives the error:
Message: ReferenceError: userList is not defined;
Bindings value: foreach: userList Error {}
Anyone have any idea what's going on?
Update
For those wondering what gets called every time the hash changes:
function usersVM() {
// Data
var self = this;
// Behaviours
$('#users').show();
}
It looks like you're initializing knockout with an undefined viewmodel?
ko.applyBindings(new window[this.params.appView+'VM']());, yet your actual viewmodel is window.vm. Case sensitivity ftw. Also, the viewmodel on window is already created / initialized. So you don't need the new operator.
So, change the applyBindings line to be
ko.applyBindings(window[this.params.appView+'vm']());
Updated Answer: By Poster
There was no necessity to keep running ko.applyBindings every time the route changed since it was already applying bindings on page load. So Sammy.js was changed to:
Sammy(function() {
this.get('#!/:appView', function() {
self.currentAppView(this.params.appView);
$('.appview').hide();
window[this.params.appView+'Route']();
});
this.notFound = function(){
location.hash = "!/dashboard";
}
//this.raise_errors = true;
}).run();
It does look like ko.computed or a regular function call to window.vm.getUserList() isn't running properly, but this will be saved for a different question.
function usersRoute() {
// Data
var self = this;
// Behaviours
$('#users').show();
window.vm.getUserList();
}

Categories

Resources