I have a 2 view models
function group(){
name = "";
description = "";
members = ko.observableArray([]);
}
and
function groupTypes(){
gorupA = ko.observableArray([]);
groupB = ko.observableArray([]);
}
I have a regular JS array groupArray with group type objects as elements.
I've separate templates for groupA-template and groupB-template
I did:
var groupTypeInstance = new groupTypes;
groupTypeInstance.groupA.push(groupArray.slice(0,4));
ko.applyBindingsToNode(
document.getElementById('group-a'),
{
template: {
name: "groupA-template",
foreach: groupTypeInstance.groupA
}
}
);
This displayed the UI correctly.
But afterwards when I do groupTypeInstance.groupA.push(groupArray[5]), nothing happens. UI doesnt change. Console doesn't show any error. Array has the extra element when I print it in console. Why is the UI not getting updated?
Please ask for more details if needed.
Your code as it is do not work. You misspelled groupA and forgot the this. on the variables. Also, you cannot add individual array itens to another array by calling push() (it will work but not as you probably intended). Here is the code to do what you want, based on what you posted:
$(function(){
function group(){
this.name = "";
this.description = "";
this.members = ko.observableArray([]);
}
function groupTypes(){
this.groupA = ko.observableArray([]);
this.groupB = ko.observableArray([]);
}
var groupArray = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6"] ;
var groupTypeInstance = new groupTypes();
groupTypeInstance.groupA.push.apply(groupTypeInstance.groupA, groupArray.slice(0,4));
ko.applyBindingsToNode(
document.getElementById('group-a'),
{
template: {
name: "groupA-template",
foreach: groupTypeInstance.groupA
}
}
);
setInterval(function(){
groupTypeInstance.groupA.push(groupArray[5]);
}, 1000);
});
<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>
<script type="text/html" id="groupA-template">
<h3 data-bind="text: $data"></h3>
</script>
<div id="group-a">
</div>
Related
I currently have a list of students and a list of classes in a school object. I would like each class object to be able to display a filtered list of students based on the class id property.
I have tried to access the parent object via custom binding but have not had any success.
Perhaps I am looking at the problem the wrong way? I have spent a couple days on this and whichever way I tackle it I always need to access a value on a parent object.
Are there any methods of accessing what I need? I am beginning to think that it is not possible to access a global style variable.
function School()
{
var self = this;
self.ClassVMs = ko.observableArray([]).indexed('Number');
self.ChildVMs = ko.observableArray([]).indexed('Number');
}
function ClassVM(classId, text)
{
var self = this;
self.Number = ko.observable();
self.Text = ko.observable(text);
self.ClassId = ko.observable(classId);
}
function ChildVM(classId, text)
{
var self = this;
self.Number = ko.observable();
self.ClassId = ko.observable(classId);
self.Text = ko.observable(text);
}
I have a Fiddle with my setup. Any and all guidance is appreciated. Thanks
You do not need global variables to solve this. Knockout has $root and $parent to step slightly outside of the scope you're in inside a foreach. In addition, if really needed, you can always make sure the view models get another type of view model as its dependency. In fact, if one view model has a list of sub view models it already has such a dependency.
What you need to think about is what your UI/UX is going to be like. Are you designing your view models to support a view where the user is "viewing" a student and enrolls him/her in classes? Or is the app user viewing a class and adding students one by one?
Here's a variant that shows a little bit of both:
function School(classes) {
var self = this;
self.classes = ko.observableArray(classes);
self.students = ko.observableArray([]);
self.enroll = function(child, someClass) {
if (self.students().indexOf(child) < 0) {
self.students.push(child);
}
if (someClass.students().indexOf(child) < 0) {
someClass.students.push(child);
}
};
self.enrollNewChild = function(someClass) {
if (!!someClass.childToBeEnrolled()) {
self.enroll(someClass.childToBeEnrolled(), someClass);
someClass.childToBeEnrolled(null);
}
};
self.enrollInClass = function(child) {
if (!!child.classToBeEnrolledIn()) {
self.enroll(child, child.classToBeEnrolledIn());
child.classToBeEnrolledIn(null);
}
};
}
function Class(id, txt) {
var self = this;
self.id = ko.observable(id);
self.txt = ko.observable(txt);
self.students = ko.observableArray([]);
self.studentsCsv = ko.computed(function() {
return self.students().map(function(s) { return s.txt(); }).join(", ");
});
self.childToBeEnrolled = ko.observable(null);
}
function Child(id, txt) {
var self = this;
self.id = ko.observable(id);
self.txt = ko.observable(txt);
self.classToBeEnrolledIn = ko.observable(null);
}
var english = new Class(1, "English 1");
var math1 = new Class(2, "Mathematics 1");
var math2 = new Class(2, "Mathematics 2");
var john = new Child(1, "John Doe");
var mary = new Child(1, "Mary Roe");
var rick = new Child(1, "Rick Roll");
var marc = new Child(1, "Marcus Aurelius");
var school = new School([english, math1, math2]);
ko.applyBindings(school);
// Method 1:
school.enroll(john, english);
school.enroll(john, math2);
school.enroll(marc, english);
school.enroll(mary, math2);
school.enroll(mary, english);
school.enroll(rick, english);
school.enroll(rick, math1);
td { background-color: #eee; padding: 2px 10px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<h3>Classes:</h3>
<table>
<tbody data-bind="foreach: classes">
<tr>
<td><span data-bind="text: txt"></span></td>
<td><span data-bind="text: studentsCsv"></span></td>
<td>
Add student
<select data-bind="options: $root.students, value: childToBeEnrolled, optionsText: 'txt', optionsCaption: 'Choose...'"></select>
<button data-bind="click: $root.enrollNewChild">enroll now</button>
</td>
</tr>
</tbody>
</table>
<h3>School Students</h3>
<table>
<tbody data-bind="foreach: students">
<tr>
<td><span data-bind="text: txt"></span></td>
<td>
Enroll in:
<select data-bind="options: $root.classes, optionsText: 'txt', value: classToBeEnrolledIn, optionsCaption: 'Choose...'"></select>
<button data-bind="click: $root.enrollInClass">enroll now</button>
</td>
</tr>
</tbody>
</table>
It does not answer your question directly ("display a filtered list of students based on the class id property") because I think that's an XY-problem, and you're better off trying to find a solution like the above where you have proper references instead of having to use id and some kind of lookup mechanism.
Yes I can see how it could be considered a bit of an XY problem. You both made look at it differently which was a great help.
The way I originally would have liked wasn't looking feasible. My compromise was to add a computed to the root view model and display the list this way.
function School()
{
var self = this;
self.ClassVMs = ko.observableArray([]).indexed('Number');
self.ChildVMs = ko.observableArray([]).indexed('Number');
self.DisplayClassId = ko.observable(1);
self.Display = function(x)
{
console.log(x);
self.DisplayClassId(x);
}
}
var viewModel = new School();
viewModel.filteredItems = ko.computed(function () {
var filter = viewModel.DisplayClassId();
if (!filter) {
return viewModel.ChildVMs();
} else {
var filtered = ko.utils.arrayFilter(viewModel.ChildVMs(), function (item) {
return (item.ClassId() === filter);
});
return filtered;
}
})
Fiddle for reference
I'm fairly stuck on this, I'm playing catchup on course work.
How do I go about adding values to an array using string from text boxes on the page?
var book = [
{
"name":"Book 1",
"publisher":"Publisher 1",
"isbn":"ISBN 1"
},
{
"name":"Book 2",
"publisher":"Publisher 2",
"isbn":"ISBN 2"
},
{
"name":"Book 3",
"publisher":"Publisher 3",
"isbn":"ISBN 3"
},
];
book.push({"name":"Moby Dick","publisher":"Ryan Spain","isbn":"00147097"});
I know I can push it into the array but I want to add values to name, publisher and isbn from values attained from text boxes in HTML.
EDIT: Adding information
Imagine you have three inputs with the following ids "name", "publisher", "isbn".
var name = document.getElementById('name');
var pub = document.getElementById('publisher');
var isbn = document.getElementById('isbn');
You could add a button when the user is ready to add a book to your list:
<button id="add">Add</button>
var button = document.getElementById('add');
Then you could add an onclick:
button.onclick = function(){
book.push({'name': name.value, 'publisher': pub.value, 'isbn':isbn.value});
}
Getting information:
Is how you would do it.
book[0].name = "Book test";
You could loop through all your books and update them one by one:
for(var i = 0; i < book.length; i++){
book[i].name = "Book " + i;
//etc
}
I have a fairly simple array of objects that can be edited in KO
Here's a test case. Try clicking on the items and editing them down below. It works.
However...
The data loaded into the array comes from a JSON string:
self.text = ko.observable('[{ "value": "1", "text": "Low" }, ..... ]');
This must be parsed and converted into a JS object. This is done in the computed function like this:
self.ssArray = ko.computed({
read: function() {
// Convert text into JS object
// Not using ko.utils because I want to use try/catch to detect bad JS later
var arrayJS = JSON.parse(ko.utils.unwrapObservable(self.text));
// Make an array of observables
// Not using ko.mapping in order to get back to basics
// Also mapping function throws an error re: iterations or something
var obsArrayJS = ko.utils.arrayMap(arrayJS, function(i) {
return {
"value": ko.observable(i.value),
"text": ko.observable(i.text)
};
});
// return array of objects with observable properties.
return obsArrayJS;
// Tried this but made no difference:
//return ko.observableArray(obsArrayJS);
},
Now what I want is for the original text string to be updated whenever the model is updated. It should be a simple case of ko.toJSON on the model:
write: function(value) {
self.text(ko.toJSON(this.ssArray));
},
As you can see from the fiddle, self.text is not updated.
Why is this?
I have tried the following:
returning an observableArray from the read function - makes no difference
return an observableArray of observable objects each with observable properties
using the mapping plugin to make everything possible observable
I guess it boils down to how KO knows to fire the write function. Surely if the contents of ssArray change then write is fired? But not in my case...
Possible further complication is that this will be a KO component. The text input will actually come from a parameter passed from the widget. So I guess it will already be an observable? So it will need to update the parent viewmodel too.
In addition to this I'm trying to use the sortable plugin to allow reordering of these items - but I've removed that from my test case.
The 'write' function of your computed is not firing, because you are not writing to the computed — that would mean calling ssArray(some_value) somewhere.
This is an alternative solution that works:
We create an observableArray named items for our individual text/value pairs
This observableArray is populated by calling loadJSON manually.
We create a computed that establishes subscriptions to the items observableArray, as well as to all the items text and value observables by iterating over them. Whenever either items are added or removed or change, we serialize the whole array back to JSON
You could certainly subscribe to self.text and trigger loadJSON automatically, but then you will have to take care of the circle of 'text' triggering 'loadJSON', triggering our computed, writing back to text.
(I have hidden the code snippets in order to get rid of the HTML and CSS code blocks. Click "Show code snippet" to run the examples.)
function MyViewModel() {
var self = this;
this.selectedItemSS = ko.observable();
this.setSelectedSS = function(item) {
self.selectedItemSS(item);
};
// Data in text form. Passed in here as a parameter from parent component
this.text = ko.observable('[{"value": "1", "text": "Low"}, {"value": "2", "text": "Medium"}, {"value": "3", "text": "High"} ]');
this.items = ko.observableArray([]);
this.loadJSON = function loadJSON(json) {
var arrayOfObjects = JSON.parse(json),
arrayOfObservables;
// clear out everything, or otherwise we'll end
// up with duplicated objects when we update
self.items.removeAll();
arrayOfObservables = ko.utils.arrayMap(arrayOfObjects, function(object) {
return {
text: ko.observable(object.text),
value: ko.observable(object.value)
};
});
self.items(arrayOfObservables);
};
this.loadJSON( this.text() );
ko.computed(function() {
var items = this.items();
// iterate over all observables in order
// for our computed to get a subscription to them
ko.utils.arrayForEach(items, function(item) {
item.text();
item.value();
});
this.text(ko.toJSON(items));
}, this);
}
ko.applyBindings(new MyViewModel());
function MyViewModel() {
var self = this;
this.selectedItemSS = ko.observable();
this.setSelectedSS = function(item) {
self.selectedItemSS(item);
};
// Data in text form. Passed in here as a parameter from parent component
this.text = ko.observable('[ \
{\
"value": "1",\
"text": "Low"\
},\
{ \
"value": "2",\
"text": "Medium"\
},\
{\
"value": "3",\
"text": "High"\
} ]');
this.items = ko.observableArray([]);
this.loadJSON = function loadJSON(json) {
var arrayOfObjects = JSON.parse(json),
arrayOfObservables;
// clear out everything, or otherwise we'll end
// up with duplicated objects when we update
self.items.removeAll();
arrayOfObservables = ko.utils.arrayMap(arrayOfObjects, function(object) {
return {
text: ko.observable(object.text),
value: ko.observable(object.value)
};
});
self.items(arrayOfObservables);
};
this.loadJSON( this.text() );
ko.computed(function() {
var items = this.items();
// iterate over all observables in order
// for our computed to get a subscription to them
ko.utils.arrayForEach(items, function(item) {
item.text();
item.value();
});
this.text(ko.toJSON(items));
}, this);
}
ko.applyBindings(new MyViewModel());
body { font-family: arial; font-size: 14px; }
.well {background-color:#eee; padding:10px;}
pre {white-space:pre-wrap;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<h3>Text Json: eg from AJAX request</h3>
<p>In practice this comes from a parent custom component as a parameter</p>
<pre class="well" data-bind="text:text"></pre>
<h3>Computed data model</h3>
<p>Click on an item to edit that record</p>
<div data-bind="foreach:items" class="well">
<div data-bind="click: $parent.setSelectedSS">
<span data-bind="text:value"></span>
<span data-bind="text:text"></span><br/>
</div>
</div>
<hr/>
<h3>Editor</h3>
<div data-bind="with:selectedItemSS" class="well">
<input data-bind="textInput:value"/>
<span data-bind="text:value"></span><br/>
</div>
If you prefer, here is an alternative version that handles both changes to the JSON as well as edits through the interface through a single computed:
function MyViewModel(externalObservable) {
var self = this;
this.selectedItemSS = ko.observable();
this.setSelectedSS = function(item) {
self.selectedItemSS(item);
};
// just for the demo
this.messages = ko.observableArray([]);
this.items = ko.observableArray([]);
this.json = externalObservable;
this.previous_json = '';
ko.computed(function() {
var items = this.items(),
json = this.json();
// If the JSON hasn't changed compared to the previous run,
// that means we were called because an item was edited
if (json === this.previous_json) {
var new_json = ko.toJSON(items);
self.messages.unshift("items were edited, updating JSON: " + new_json);
this.previous_json = new_json;
this.json(new_json);
return;
}
// If we end up here, that means that the JSON has changed compared
// to the last run
self.messages.unshift("JSON has changed, updating items: " + json);
var arrayOfObjects = JSON.parse(json),
arrayOfObservables;
// clear out everything, or otherwise we'll end
// up with duplicated objects when we update
this.items.removeAll();
arrayOfObservables = ko.utils.arrayMap(arrayOfObjects, function(object) {
return {
text: ko.observable(object.text),
value: ko.observable(object.value)
};
});
// iterate over all observables in order
// for our computed to get a subscription to them
ko.utils.arrayForEach(arrayOfObservables, function(item) {
item.text();
item.value();
});
this.items(arrayOfObservables);
this.previous_json = json;
}, this);
}
var externalObservableFromParam = ko.observable(),
viewModel;
// Pretend here that this observable was handed to us
// from your components' params
externalObservableFromParam('[{"value": "1", "text": "Low"}, {"value": "2", "text": "Medium"}, {"value": "3", "text": "High"} ]');
viewModel = new MyViewModel(externalObservableFromParam);
ko.applyBindings(viewModel);
function MyViewModel(externalObservable) {
var self = this;
this.selectedItemSS = ko.observable();
this.setSelectedSS = function(item) {
self.selectedItemSS(item);
};
// just for the demo
this.messages = ko.observableArray([]);
this.items = ko.observableArray([]);
this.json = externalObservable;
this.previous_json = '';
ko.computed(function() {
var items = this.items(),
json = this.json();
// If the JSON hasn't changed compared to the previous run,
// that means we were called because an item was edited
if (json === this.previous_json) {
var new_json = ko.toJSON(items);
self.messages.unshift("items were edited, updating JSON: " + new_json);
this.previous_json = new_json;
this.json(new_json);
return;
}
// If we end up here, that means that the JSON has changed compared
// to the last run
self.messages.unshift("JSON has changed, updating items: " + json);
var arrayOfObjects = JSON.parse(json),
arrayOfObservables;
// clear out everything, or otherwise we'll end
// up with duplicated objects when we update
this.items.removeAll();
arrayOfObservables = ko.utils.arrayMap(arrayOfObjects, function(object) {
return {
text: ko.observable(object.text),
value: ko.observable(object.value)
};
});
// iterate over all observables in order
// for our computed to get a subscription to them
ko.utils.arrayForEach(arrayOfObservables, function(item) {
item.text();
item.value();
});
this.items(arrayOfObservables);
this.previous_json = json;
}, this);
}
var externalObservableFromParam = ko.observable(),
viewModel;
// Pretend here that this observable was handed to us
// from your components' params
externalObservableFromParam('[{"value": "1", "text": "Low"}, {"value": "2", "text": "Medium"}, {"value": "3", "text": "High"} ]');
viewModel = new MyViewModel(externalObservableFromParam);
ko.applyBindings(viewModel);
body {
font-family: arial;
font-size: 14px;
}
.well {
background-color: #eee;
padding: 10px;
}
pre {
white-space: pre-wrap;
}
ul {
list-style-position: inside;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<h3>Text Json: eg from AJAX request</h3>
<p>In practice this comes from a parent custom component as a parameter</p>
<pre class="well" data-bind="text: json"></pre>
<textarea data-bind="value: json" cols=50 rows=5></textarea>
<h3>Computed data model</h3>
<p>Click on an item to edit that record</p>
<div data-bind="foreach: items" class="well">
<div data-bind="click: $parent.setSelectedSS">
<span data-bind="text:value"></span>
<span data-bind="text:text"></span>
<br/>
</div>
</div>
<hr/>
<h3>Editor</h3>
<div data-bind="with:selectedItemSS" class="well">
<input data-bind="textInput:value" />
<span data-bind="text:value"></span>
<br/>
</div>
<hr/>
<h3>Console</h3>
<ul data-bind="foreach: messages" class="well">
<li data-bind="text: $data"></li>
</ul>
I have a view that displays data from a foreach loop in different categories depending on the type. Each category will contain a number of users - I created an object that will check to see if the number of users in a category are more than 10 then the text for the visible bind will show. And for the category that doesn't have more than 10 it will not show the text.
My question: if the first category doesn't have 10 it won't show text does that mean that it won't also show text for the remaining categories?
Help with: the visible binding is not working even though a category would contain more than 10 and not sure why.
Here is my JSFiddle: http://jsfiddle.net/xNdJk/1/
JavaScript:
var userViewModel = function (data) {
var _self = this;
_self.Name = ko.observable(data.Name);
_self.Letter = ko.observable(data.Letter);
_self.ShowLetter = ko.computed(function () {
return (roleViewModel.UserCount > 13);
});
};
var typeViewModel = function (data) {
var _self = this;
_self.ContentType = ko.observable(data.ContentType);
_self.Name = ko.observable(data.Name);
_self.Rank = ko.observable(data.Rank);
_self.UserCount = ko.observable(data.UserCount);
_self.Users = ko.observableArray([]);
};
View:
<div class="collapse in" data-bind="template: { name: 'list', foreach: $data.Users }">
</div>
<div id="letter" data-bind="visible:ShowLetter, text: Letter"></div>
You are mixing classes and instances, you have created a secondModel class but you never instance it, here is a working example
http://jsfiddle.net/xNdJk/2/
var viewModel = function(){
this.Letter = ko.observable('Hello, World!');
this.secondModel = new secondModel();
this.shouldShowMessage = ko.computed(function() {
return (this.secondModel.UserCount() > 13);
}, this);
}
var secondModel = function(){
var self = this;
self.UserCount = ko.observable(153);
}
I'm having a nested array which represents a navigation (see the jsFiddle below). I want to render this navigation with knockoutJS but have no clue how. I already went through the official documentation, but they only cover a simple list/collection.
http://jsfiddle.net/a4swJ/
You need to use the template binding.
Update: I removed the containerless foreach and used to foreach option of the template binding instead. Working example below also updated
HTML:
<script type="text/html" id="node-template">
<li>
<a data-bind="text: Title, attr:{href: Target}"></a>
<ul data-bind="template: { name: 'node-template', foreach: Children }"></ul>
</li>
</script>
<ul data-bind="template: { name: 'node-template', foreach: Nodes }"></ul>
JS:
function NavigationNode(target, title)
{
var self = this;
self.Target = target || "[No Target]";
self.Title = title || "[No Title]";
self.Children = ko.observableArray([]);
};
function NavigationViewModel()
{
var self = this;
self.Nodes = ko.observableArray([]);
var node1 = new NavigationNode("/parent", "Parent");
var node2 = new NavigationNode("/parent/sub1", "Sub 1");
var node3 = new NavigationNode("/parent/sub1/sub2", "Sub 2");
node2.Children().push(node3);
node1.Children().push(node2);
self.Nodes.push(node1);
ko.applyBindings(this);
};
new NavigationViewModel();
Here's a jsFiddle.