Knockout add to array of an array - javascript

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

Related

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 pass multiple JSON file to Onservable Arrays when value in one file is used as argument for another file?

I am attempting to use $.getJSON to call two json file and assigned their values to observableArrays: Currently the JSON data is hard coded to the observableArray. Example fiddle here.
I attempted to do this:
self.CountryData = ko.observableArray([]);
self.CountryDetailData = ko.observableArray([]);
$.when( $.getJSON( 'country.json' ), $.getJSON( 'country-detail.json' ))
.done(function( country, countryDetail ) {
self.CountryData(country[0]);
self.CountryDetailData(countryDetail[0]);
});
Next, I attempted to merge both files:
var data = country[0].concat(countryDetail[0]);
Finally, for experimental purpose, I simply manually combined both files by simple taking the values from one file and placing it into the other but it is not given me the expected result.
Here is a working JS Fiddle. using the hard-coded JSON data.
Instead of hardcode JSON data, how do I call JSON files and assigned to observables?
I cannot get this section of code to work:
self.CountryDetails = function (country) {
var data = ko.computed(function () {
return ko.utils.arrayFilter(self.CountryDetailData(), function (item) {
return item.CountryId === country.CountryId;
});
});
self.CountryId(data()[0].CountryId);
self.Location(data()[0].Location);
self.Coordinates(data()[0].Coordinates);
self.Coastline(data()[0].Coastline);
self.Climate(data()[0].Climate);
self.Terrain(data()[0].Terrain);
}
Error: Message: CountryId is not defined;
UPDATE:
I decided to apply a native approach so that I can get the CountryID from the table and pass as argument: I also place the $.getJSON in the function in which it is being used so that the result is available when executed:
Code:
self.CountryDetails = function (country) {
$.getJSON( 'country-detail.json')
.done(function( result ) {
self.CountryDetailData(result);
var data = ko.computed(function () {
return ko.utils.arrayFilter(self.CountryDetailData(), function (item) {
// get CountryId from view
var cId = $('#country-list tbody tr:first-child td:first-child').html();
/* Because I am unable to get ID on page load, I am doing a work-around.
If the country parameter is undefine then use the alternative method to
get the countryId from the first row or and first cell. */
currentId = (country === undefined ? cId : country.CountryId);
return item.CountryId === currentId;
});
});
self.CountryId(data()[0].CountryId);
self.Location(data()[0].Location);
self.Coordinates(data()[0].Coordinates);
self.Coastline(data()[0].Coastline);
self.Climate(data()[0].Climate);
self.Terrain(data()[0].Terrain);
});
}
Now I am able to use a JSON file and load the additional data same as when the JSON values are hardcoded.
You basically want a CountrySelector, something that has a list of continents and countries and offers the functionality of selecting a continent first and a a country based on that.
For simplicity, let's remove all the Ajax and reduce it to a pure, data-centric model:
function CountrySelector(data) {
var self = this;
// base data
self.countries = ko.observableArray(data.countries);
self.continents = ko.observableArray(data.continents);
// state data
self.selectedContinent = ko.observable();
self.selectedCountry = ko.observable();
self.selectedCountryDetails = ko.observable();
self.filteredCountries = ko.computed(function () {
var selectedContinent = self.selectedContinent();
return ko.utils.arrayFilter(self.countries(), function (country) {
return selectedContinent && country.Continent === selectedContinent;
});
});
// subscriptions
self.filteredCountries.subscribe(function (countries) {
self.selectedCountry(countries[0]);
});
self.selectedCountry.subscribe(function (country) {
var selectedDetails = ko.utils.arrayFirst(data.details, function (details) {
return details.CountryId === country.CountryId;
});
self.selectedCountryDetails(selectedDetails);
});
// state init
self.selectedContinent(self.continents[0]);
}
This expects to be initialized with a data object:
{
continents: [/* ... list of continent names ... */],
countries: [/* ... list of country objects ... */],
details: [/* ... list of country detail objects ... */],
}
Now you have something that works independently of the kind of data source (you could initialize it with hard-coded data, from localStorage, from another viewmodel, via Ajax, whatever).
To initialize it via Ajax, we send two requests:
var countryReq = $.getJSON('country.json');
var countryDetailReq = $.getJSON('country-detail.json');
wait for them and massage the data into shape (jQuery's .then() comes in handy here):
var combinedData = $.when(countryReq, countryDetailReq).then(function (countries, details) {
// figure out disintct continent names
var continents = ko.utils.arrayMap(countries, function (country) {
return country.Continent;
});
continents = ko.utils.arrayGetDistinctValues(continents);
continents.sort();
// assemble the data the viewmodel expects
return {
countries: countries,
continents: continents,
details: details
};
});
As soon as this is done we can apply bindings:
combinedData.done(function (data) {
ko.applyBindings( new CountrySelector(data) );
});
into a view like this:
<ul data-bind="with: selectedCountryDetails">
<li>CountryId: <span data-bind="text: CountryId"></span></li>
<li>Location: <span data-bind="text: Location"></span></li>
<li>Coordinates: <span data-bind="text: Coordinates"></span></li>
<li>Coastline: <span data-bind="text: Coastline"></span></li>
<li>Climate: <span data-bind="text: Climate"></span></li>
<li>Terrain: <span data-bind="text: Terrain"></span></li>
</ul>
<div id="country-select">
<select data-bind="options: continents, value: selectedContinent"></select>
</div>
<table id="country-list">
<thead>
<tr>
<th>CountryID</th>
<th>Country Name</th>
<th>City</th>
<th>Continent</th>
<th>CountryAbbr</th>
</tr>
</thead>
<tbody data-bind="foreach: filteredCountries">
<tr data-bind="
click: $root.selectedCountry,
clickBubble: false,
css: {
active: CountryId === $root.selectedCountry().CountryId
}
">
<td data-bind="text: CountryId"></td>
<td data-bind="text: Country"></td>
<td data-bind="text: City"></td>
<td data-bind="text: Continent"></td>
<td data-bind="text: CountryAbbr"></td>
</tr>
</tbody>
</table>

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

KnockoutJS Bindings With Nested Templates

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

Categories

Resources