I'm trying to generate a table using properties of two viewmodels, which are sub viewmodels of the main viewmodel, which ko.applyBindings() is called using.
The idea is to generate a row for each element in SubVM1 where the first cell is the element's name. Then for every element in SubVM2, an additional cell is added to the row.
The rows generate correctly and the first cell shows the SubVM1 name, but it is only followed by one cell, instead of however many elements are in SubVM2.
Also, the function in the data-bind doesn't work either. I've tried declaring the Value function as a prototype of SubV2 but it errors out as undefined.
Regardless, something I'm not sure about is clearly happening with the binding context, and help would be appreciated.
<tbody data-bind="foreach: {data: SubViewModel1, as: 'SubVM1'}">
<tr>
<td data-bind="text: SubVM1.Name" />
<!-- ko foreach: {data: $root.SubViewModel2, as: 'SubVM2'} -->
<td data-bind="text: Value(SubVM1.Type)"></td>
<!-- /ko -->
</tr>
</tbody>
Edit: Partially done jsfiddle: http://jsfiddle.net/jgr71o8t/1
There are a couple of things that I could see.
The first one is that the <td data-bind='' />.
Knockout does not generally like self closed tags. Always use the closing tag, in this case <td data-bind=''></td>
The second is that anything you want updated on the screen should be an ko.observable or ko.observableArray. any changes to properties after ko.applyBindings will not be reflected on the screen
HTML
<table border="1">
<tbody data-bind="foreach: {data: subViewModel1, as: 'SubVM1'}">
<tr>
<td data-bind="text: name"></td>
<!-- ko foreach: {data: $root.subViewModel2, as: 'SubVM2'} -->
<td data-bind="text: SubVM2.Value(SubVM1.Type)"></td>
<!-- /ko -->
</tr>
</tbody>
</table>
JS Fiddle Demo with knockout bindings on all properties
function MasterViewModel() {
var self = this;
self.subViewModel1 = ko.observableArray([]);
self.subViewModel2 = ko.observableArray([]);
}
function SubVM1ViewModel() {
var self = this;
self.name = ko.observable("Sub View Model 1");
self.otherProperty = ko.observable(43);
}
function SubVM2ViewModel() {
var self = this;
self.title = ko.observable('Sub View Model 2');
self.subVM1List = ko.observableArray([]);
}
SubVM2ViewModel.prototype.Value = function (type) {
for (var i = 0; i < this.subVM1List().length; i++) {
if (this.subVM1List()[i].Type === type) {
return this.subVM1List()[i].name();
}
}
};
var masterVM = new MasterViewModel();
var subVM2 = new SubVM2ViewModel();
subVM2.subVM1List.push(new SubVM1ViewModel());
masterVM.subViewModel1.push(new SubVM1ViewModel());
masterVM.subViewModel2.push(subVM2);
ko.applyBindings(masterVM);
JS Fiddle Demo with straight javascript properties
function MasterViewModel() {
var self = this;
self.subViewModel1 = [];
self.subViewModel2 = [];
}
function SubVM1ViewModel() {
var self = this;
self.name = "Sub View Model 1";
self.otherProperty =43;
}
function SubVM2ViewModel() {
var self = this;
self.title = 'Sub View Model 2';
self.subVM1List = [];
}
SubVM2ViewModel.prototype.Value = function (type) {
for (var i = 0; i < this.subVM1List.length; i++) {
if (this.subVM1List[i].Type === type) {
return this.subVM1List[i].name;
}
}
};
var masterVM = new MasterViewModel();
var subVM2 = new SubVM2ViewModel();
subVM2.subVM1List.push(new SubVM1ViewModel());
masterVM.subViewModel1.push(new SubVM1ViewModel());
masterVM.subViewModel2.push(subVM2);
ko.applyBindings(masterVM);
Related
Really strange knockout error I am having. It's a pretty complex scenario so please see this fiddle:
http://jsfiddle.net/yx8dkLnc/
Essentially I have a double nested collection, the first collection FishMeasurements contains a collection of objects which have the species information associated with it, and a collection of Measurements which hold all measurements associated with that species.
Now when I try and remove items from the nested collection in this HTML:
<!-- ko foreach: FishMeasurements() -->
<h3><span data-bind="text: SpeciesName"></span><span data-bind="text: SpeciesId" style="display: none;"></span></h3>
<table class="table table-striped">
<thead>
<tr>
<th>Length</th>
<th>Count</th>
<th>Weight</th>
<th>Fish Id</th>
<th> </th>
</tr>
</thead>
<tbody data-bind="foreach: Measurements()">
<tr>
<td><span data-bind="text: LengthInMillimeters"></span></td>
<td><span data-bind="text: Count"></span></td>
<td><span data-bind="text: WeightInPounds"></span></td>
<td><span data-bind="text: FishCode"></span></td>
<td>Remove</td>
</tr>
</tbody>
</table>
<!-- /ko -->
The remove measurement function doesn't work when the Measurements collection has more than one object. I click the remove link, and it throws an error that says:
VM617:163 Uncaught TypeError: Cannot read property 'Measurements' of null(…)
The strange thing about this is, if I only add one item to the Measurements collection, the delete button work fine, but as soon as I add multiple measurements, if I click remove on any item in the table but the first row, this error is generated. However, if I click the first item in the table per species, there is no error and all records are removed!
Something tells me its treating Measurements like one object instead of a collection, because it only works on index 0. But I'm not sure because in console I am able to type:
mappedModel.FishMeasurements()[0].Measurements()[1]
And get a full ko_mapping object returned, so it's not null. But for some reason when I click the remove, it is null. As long as there is only one measurement per species, clicking remove works fine, as soon as there are more it breaks.
What am I doing wrong?
When you addMeasurement for the first time speciesId, speciesName are getting defined because fishMeasurementBySpecies === undefined and therefore when you remove the first item you have a valid measurement.SpeciesId() as a parameter inside removeMeasurement function but for the second time and more since fishMeasurementBySpecies is not undefined anymore then speciesId, speciesName never get set and then whenremoveMeasurementis called,measurement.SpeciesId() is null.
In order to make your model works, you need to apply below changes.
define var speciesId = mappedModel.SelectedSpecies(); before your if statment
var speciesId = mappedModel.SelectedSpecies();
if (fishMeasurementBySpecies === undefined || fishMeasurementBySpecies === null) {
Put () for Measurements inside removeMeasurement function where you want to get the length
if(fishMeasurementBySpecies.Measurements().length === 0)
Below I provide you an example of what you want to do by using manual view model instead of using mapping plugin.
Example: https://jsfiddle.net/kyr6w2x3/118/
Your example :http://jsfiddle.net/yx8dkLnc/1/
VM:
var data = {
"AvailableSpecies":
[
{"Id":"f57830b8-0766-4374-b481-82c04087415e","Name":"Alabama Shad"},
{"Id":"3787ce10-e61c-4f03-88a5-ff648bb55480","Name":"Alewife"},{"Id":"e923214f-4974-4663-9158-d6979ce637f1","Name":"All Sunfish Spp Ex Bass And Crappie"} ],
"SelectedSpecies": null, "CountToAdd":0,"LengthToAdd":0,"WeightToAdd":0,"GenerateFishCode":false,"FishMeasurements":[]
};
function AppViewModel(){
var self = this;
self.AvailableSpecies = ko.observableArray(data.AvailableSpecies);
self.SelectedSpecies = ko.observable();
self.CountToAdd = ko.observable();
self.LengthToAdd = ko.observable();
self.WeightToAdd = ko.observable();
self.FishCode = ko.observable();
self.FishMeasurements = ko.observableArray([]);
self.addMeasurement = function(item) {
var SpeciesExists = false;
ko.utils.arrayForEach(self.FishMeasurements(), function (item) {
if(item.SpeciesId() == self.SelectedSpecies().Id) {
var len = item.Measurements().length;
// you may have a better way to generate a unique Id if an item is removed
while(item.Measurements().findIndex(x => x.Id() === len) > 0){
len++;
}
item.Measurements.push(new MeasurementsViewModel({LengthInMillimeters:self.LengthToAdd(),
Count:self.CountToAdd(),
WeightInPounds:self.WeightToAdd(),
FishCode:self.FishCode(),
Id:len ++,
ParentId:self.SelectedSpecies().Id
})
);
SpeciesExists = true;
}
});
if(!SpeciesExists){
self.FishMeasurements.push(new FishMeasurementsViewModel({SpeciesName:self.SelectedSpecies().Name,
SpeciesId:self.SelectedSpecies().Id,
Measurements:[{LengthInMillimeters:self.LengthToAdd(),
Count:self.CountToAdd(),
WeightInPounds:self.WeightToAdd(),
FishCode:self.FishCode(),
Id:1}]
})
);
}
}
self.removeMeasurement = function(data){
ko.utils.arrayForEach(self.FishMeasurements(), function (item) {
if(item && item.SpeciesId() == data.ParentId()) {
ko.utils.arrayForEach(item.Measurements(), function (subItem) {
if(subItem && subItem.Id() == data.Id()) {
item.Measurements.remove(subItem);
}
});
}
if(item && item.Measurements().length == 0){
self.FishMeasurements.remove(item);
}
});
}
}
var FishMeasurementsViewModel = function(data){
var self = this;
self.SpeciesName = ko.observable(data.SpeciesName);
self.SpeciesId = ko.observable(data.SpeciesId);
self.Measurements = ko.observableArray($.map(data.Measurements, function (item) {
return new MeasurementsViewModel(item,self.SpeciesId());
}));
}
var MeasurementsViewModel = function(data,parentId){
var self = this;
self.LengthInMillimeters = ko.observable(data.LengthInMillimeters);
self.Count = ko.observable(data.Count);
self.WeightInPounds = ko.observable(data.WeightInPounds);
self.FishCode = ko.observable(data.FishCode);
self.Id = ko.observable(data.Id);
self.ParentId = ko.observable(parentId ? parentId : data.ParentId);
}
var viewModel = new AppViewModel();
ko.applyBindings(viewModel);
I'am trying to create a table with a 100 rows but I dont want to use push because then the page gets rendered for each push. The table should be empty at first but when I click a button the table will create all the rows.
The problem here is that I dont se the rows when I click the button. But I can only set self.row = ko.observableArray() once?
JS
RowModel = function(numbers) {
var self = this;
self.numbers = numbers;
}
TableViewModel = function() {
var self = this;
self.rows = null;
self.createRows = function() {
var arr = [];
var numbers = [];
for(var i=0; i < 100; i++){
for(var p=0; p < 4; p++){
numbers[p] = p;
}
arr[i] = new RowModel(numbers);
}
self.rows = ko.observableArray(arr);
}
};
ko.applyBindings(new TableViewModel());
HTML
<button data-bind="click: createRows">Create</button>
<table>
<tbody data-bind="foreach: rows">
<tr>
<td data-bind="text: numbers[0]"></td
<td data-bind="text: numbers[1]"></td>
<td data-bind="text: numbers[2]"></td>
<td data-bind="text: numbers[3]"></td>
</tr>
</tbody>
</table>
Always create the observable up front, and set the data using a function call with the data e.g.
self.rows = ko.observable([]);
Then set the data (not as a push for each, as you correctly said) using:
self.rows(arr);
The full code is here in this fiddle:https://jsfiddle.net/brianlmerritt/y0x0wwy5/
I have 2 observableArray connected to each other. When a "feature" is clicked, I try to show "tasks" of it. However KO, does not update UI when I clicked a feature. On the console, I can track my viewModel, and I can see tasks successfully loaded on selectedFeature. However, UI does not update, even all arrays are defined as observable.
Here is a live demo on fiddle.
Please tell me where I am missing?
function GetFeatures() {
var url = "/Project/GetFeatures";
$.get(url, "", function (data) {
$.each(JSON.parse(data), function (i, item) {
projectVM.features.push(new featureViewModelCreator(item, projectVM.selectedFeature));
});
});
};
function GetTasks(selectedFeature) {
var url = "/Task/GetTaskList";
$.get(url, { "FeatureId": selectedFeature.FeatureId }, function (data) {
$.each(JSON.parse(data), function (i, item) {
selectedFeature.tasks.push(new taskViewModelCreator(item, selectedFeature.selectedTask));
});
});
};
function taskViewModelCreator(data, selected) {
var self = this;
self.TaskId = data.TaskId;
self.Title = data.Name;
self.Status = data.Status.Name;
self.CreatedDate = data.CreatedDate;
self.UserCreatedFullName = data.UserCreated.FullName;
this.IsSelected = ko.computed(function () {
return selected() === self;
});
}
function featureViewModelCreator(data, selected) {
var self = this;
self.FeatureId = data.FeatureId;
self.Name = data.Name;
self.Status = data.Status.Name;
self.CreatedDate = data.CreatedDate;
self.UserCreatedFullName = data.UserCreated.FullName;
self.tasks = ko.observableArray();
this.IsSelected = ko.computed(function () {
return selected() === self;
});
self.selectedTask = ko.observable();
self.taskClicked = function (clickedTask) {
var selection = ko.utils.arrayFilter(self.model.tasks(), function (item) {
return clickedTask === item;
})[0];
self.selectedTask(selection);
}
}
function projectViewModelCreator() {
var self = this;
self.ProjectId = 1;
self.features = ko.observableArray();
self.selectedFeature = ko.observable();
self.featureClicked = function (clickedFeature) {
self.selectedFeature(clickedFeature);
GetTasks(clickedFeature);
}
}
var projectVM = new projectViewModelCreator();
ko.applyBindings(projectVM, $('.taskmanTable')[0]);
GetFeatures();
On the UI
<div class="taskmanTable">
<table class="table table-hover featureList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: features">
<tr data-bind="click: $root.featureClicked, css: { active : IsSelected } ">
<td><span data-bind="text: Name"> </span></td>
</tr>
</tbody>
</table>
<table class="table table-hover taskList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: selectedFeature.tasks">
<tr>
<td><span data-bind="text:Title"></span></td>
</tr>
</tbody>
</table>
</div>
Here is the correct version with key notes: here. KO documentation is quite a detailed one.
You have mentioned an interesting note about UI code style: "As I know, we don't use () on UI". I did not put attention to this fact before.
We can really omit brackets for an observable: ko observable;
View contains an observable with no brackets:
<label>
<input type="checkbox" data-bind="checked: displayMessage" /> Display message
</label>
Source code:
ko.applyBindings({
displayMessage: ko.observable(false)
});
We can omit brackets for an observable array on UI: ko observable array
View contains: <ul data-bind="foreach: people">, while
View model has:
self.people = ko.observableArray([
{ name: 'Bert' },
{ name: 'Charles' },
{ name: 'Denise' }
]);
We can omit brackets on UI for 'leaf' observables or observables arrays. Here is your modified code sample. data-bind="if: selectedFeature" and data-bind="foreach: selectedFeature().tasks"> only leaf observable braces are omitted.
Finally, can we omit brackets for 'parent' observables? We can do it by adding another ko UI-statement (with instead of if, example 2).
The with binding will dynamically add or remove descendant elements
depending on whether the associated value is null/undefined or not
But, I believe, we can not omit brackets for parent nodes outside UI statement, because it is equal to a javascript statement: projectVM.selectedfeature().tasks. Othervise projectVM.selectedfeature.tasks will not work, because observables does not have such property tasks. Instead an observable contains an object with that property, which is retrieved by calling it via brackets (). There is, actually, an example on knockoutjs introduction page. <button data-bind="enable: myItems().length < 5">Add</button>
The code below uses the following fact (which can be found here, example 2):
It’s important to understand that the if binding really is vital to
make this code work properly. Without it, there would be an error when
trying to evaluate capital.cityName in the context of “Mercury” where
capital is null. In JavaScript, you’re not allowed to evaluate
subproperties of null or undefined values.
function GetFeatures() {
var data = {
Name: "Test Feature",
FeatureId: 1
}
projectVM.features.push(new featureViewModelCreator(data, projectVM.selectedFeature));
};
function GetTasks(selectedFeature) {
var data = {
Title: "Test Feature",
TaskId: 1
}
selectedFeature().tasks.push(new taskViewModelCreator(data, selectedFeature().selectedTask));
};
function taskViewModelCreator(data, selected) {
var self = this;
self.TaskId = data.TaskId;
self.Title = data.Title;
// Step 3: you can omit $root declaration, I have removed it
// just to show that the example will work without $root as well.
// But you can define the root prefix explicitly (declaring explicit
// scope may help you when you models become more complicated).
// Step 4: data-bind="if: selectedFeature() statement was added
// to hide the table when it is not defined, this statement also
// helps us to avoid 'undefined' error.
// Step 5: if the object is defined, we should referense
// the observable array via -> () as well. This is the KnockoutJS
// style we have to make several bugs of that kind in order
// to use such syntax automatically.
this.IsSelected = ko.computed(function() {
return selected() === self;
});
}
function featureViewModelCreator(data, selected) {
var self = this;
self.FeatureId = data.FeatureId;
self.Name = data.Name;
self.tasks = ko.observableArray();
this.IsSelected = ko.computed(function() {
return selected() === self;
});
self.selectedTask = ko.observable();
}
function projectViewModelCreator() {
var self = this;
self.ProjectId = 1;
self.features = ko.observableArray();
self.selectedFeature = ko.observable();
self.featureClicked = function(clickedFeature) {
self.selectedFeature(clickedFeature);
GetTasks(self.selectedFeature);
}
}
var projectVM = new projectViewModelCreator();
ko.applyBindings(projectVM, $('.taskmanTable')[0]);
GetFeatures();
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="taskmanTable">
<table class="table table-hover featureList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: features">
<tr data-bind="click: $root.featureClicked, css: { active : IsSelected } ">
<td><span data-bind="text: Name"> </span></td>
</tr>
</tbody>
</table>
<hr/>
<table data-bind="if: selectedFeature()" class="table table-hover taskList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: selectedFeature().tasks()"><!-- $root -->
<tr>
<td><span data-bind="text: Title"></span></td>
</tr>
</tbody>
</table>
</div>
I can't understand why these Knockout table bindings aren't working:
Javascript:
$(function () {
var FileObject = function(id, name) {
this.id = id;
this.name = name;
};
var FilesModel = function() {
this.filesSelected = ko.observable(false);
this.myFiles = ko.observableArray([new FileObject(1, 'test_1')]);
this.myFiles.push(new FileObject(2, 'test_2'));
};
var filesModel = new FilesModel();
window.filesModel = filesModel;
ko.applyBindings(filesModel);
filesModel.myFiles().push(new FileObject(3, 'test_3')); // This never shows
alert(filesModel.myFiles().length); // Shows 3 items
});
HTML:
<h3>TABLE 1</h3>
<table>
<tbody data-bind="foreach: myFiles">
<tr>
<td>FILE:</td>
<td data-bind="text: name"></td>
</tr>
</tbody>
</table>
<h3>TABLE 2</h3>
<table>
<tbody data-bind="foreach: myFiles()">
<tr>
<td>FILE:</td>
<td data-bind="text: name"></td>
</tr>
</tbody>
</table>
In both of these tables, the first 2 files will show, but the 3rd file doesn't. What am I missing?
You're really close. Two main things to point out:
Status isn't an observable, and you're attempting to unwrap it with text: status().
You're pushing the new FileObject into an unwrapped array, meaning you're bypassing the observable altogether. Push new items directly into the observable array, you'll have better luck.
I've put together a jsbin example based on your original source.
Specifically, this:
filesModel.myFiles().push(new FileObject(3, 'test_3')); // This never shows
Should be:
filesModel.myFiles.push(new FileObject(3, 'test_3')); // Now it does
In your HTML, you were trying to data-bind status(), but status is not an observable. One approach is to make your FileObject members observables.
Also, your third FileObject was never showing because your syntax was wrong. Instead of filesModel.myFiles().push, it should be just filesModel.myFiles.push
See updated fiddle
$(function () {
var FileObject = function(id, name, size, status, progress) {
this.id = ko.observable(id);
this.name = ko.observable(name);
this.status = ko.observable(status);
};
var FilesModel = function() {
this.filesSelected = ko.observable(false);
this.myFiles = ko.observableArray([new FileObject(1, 'test_1')]);
this.myFiles.push(new FileObject(2, 'test_2', 3, 'status'));
};
var filesModel = new FilesModel();
window.filesModel = filesModel;
ko.applyBindings(filesModel);
filesModel.myFiles.push(new FileObject(3, 'test_3')); // This never shows
alert(filesModel.myFiles().length); // Shows 3 items
});
var baseUri = '#ViewBag.ApiUrl';
var viewmodel = function () {
var self = this;
self.VoucherDetails = ko.observableArray([]);
$.getJSON(baseUri, this.VoucherDetails);
alert(self.VoucherDetails);
};
<table>
<tbody data-bind='foreach: VoucherDetails'>
<tr>
<td data-bind="text : $data.empcode">
<span data-bind=" text : $data.empcode"></span> test
</td>
<td data-bind=" text : empcode">
<span data-bind=" text : empcode"></span>
</td>
</tr>
</tbody>
</table>
This is my partial view and there is no value coming in empcode or $data.empcode, I am new to knockoutjs. What is wrong in my code?
Your code looks incomplete:
$.getJson never sets the value of VoucherDetails (no callback is passed).
You never call ko.applyBindings
It should be:
var baseUri = '#ViewBag.ApiUrl';
var viewmodel = function () {
var self = this;
self.VoucherDetails = ko.observableArray([]);
$.getJSON(baseUri, function(data){
//create your VoucherDetails here based on the data (push each item to VoucherDetails using $.each
});
};
ko.applyBindings(viewModel);
http://knockoutjs.com/documentation/observableArrays.html