I have a simple webpage with a large list of products (20,000+). When you can click on a product, it will load (via AJAX) a list of colors and display them inline. Html...
<div data-bind="foreach: products">
<span data-bind="click: $root.loadColors($data), text: $name"></span>
<ul data-bind="foreach: colors">
<li data-bind="text:$data" />
</ul
</div>
Shop view model:
function shopViewModel()
{
var self = this;
self.products = ko.observableArray([]);
self.loadColors = function(product)
{
var data = GetColorsByAjax();
product.colors(data);
}
}
Product view Model:
function productModel(data)
{
var self = this;
self.name = data.name;
self.colors = ko.observableArray([]);
}
When I have 20,000+ products, it uses a lot of memory. Each product has a colors array, which is always empty/null, until the user clicks on it, but it still uses a lot of memory.
Ideally, I'd like to remove the colors observableArray and somehow create it dynamically when user clicks on the product. Or separate it into a new viewModel.
I want to eliminate the empty observableArrays to minimise memory, but can't figure out how it do it.
I would use one of Knockout's control-flow bindings (if, with) to only bind the colors:foreach when there is actually a colors property on the productModel().
HTML:
<div data-bind="foreach: products">
<span data-bind="click: $root.loadColors($data), text: $name"></span>
<div data-bind="if: hasColors">
<ul data-bind="foreach: colors">
<li data-bind="text:$data" />
</ul>
</div>
</div>
Product View Model:
function productModel(data)
{
var self = this;
self.name = data.name;
self.hasColors = ko.observable(false);
self.colors = null;
}
Shop View Model
function shopViewModel()
{
var self = this;
self.products = ko.observableArray([]);
self.loadColors = function(product)
{
var data = GetColorsByAjax();
if(product.colors == null) {
product.colors = ko.observableArray(data);
product.hasColors(true);
} else {
product.colors(data);
}
}
}
You don't have to store an empty observable array: you can default to undefined and Knockout will treat it as an empty array in a foreach binding.
Here's a demonstration: http://jsfiddle.net/zm62T/
Related
I have a ViewModel which I am binding to view list item.
var MyViewModel = function() {
var self = this;
self.addItems = function(vm) {
vm.inventoryItems.push('New Item');
}
};
var myVM= new MyViewModel();
ko.applyBindings(myVM);
The view model has a property called inventoryItems (which is from a service).
I am bidning that to view using ,
<ul data-bind="foreach:inventoryItems">
<li>
<input class="form-control" type="text" data-bind="value: $data" />
</li>
</ul>
<div class="text-right">
<a data-bind="click: $parent.addItems">+ Add more</a>
</div>
Now, the items that are already in the collection , inventoryItems are getting rendered fine.
When I am adding a new item using, I can see the items being added via console, but the view is not getting updated!
self.addItems = function(vm) {
vm.inventoryItems.push('New Item');
}
The below code snippet will make your inventoryItems observable
var MyViewModel = function () {
var self = this;
self.inventoryItems = ko.observableArray();
self.addItems = function (vm) {
vm.inventories.push('New Item');
self.inventoryItems(vm.inventories);
}
};
var myVM = new MyViewModel();
ko.applyBindings(myVM);
I have a model being used by multiple view models, and i need some other javascript components to update the model, observed by my vm's. I have no idea how to do this since in the tutorial, they "mix" the model in the viewmodel.
Here is my code :
var ConversationModel = {
conversations: ko.observableArray(),
open: function(userId){
for(var i = 0; i < this.conversations.length; i++){
if(this.conversations[i].userId == userId){
return;
}
}
var self = this;
var obj = ko.observable({
userId: userId
});
self.conversations.push(obj);
UserManager.getUserData(userId, function(user){
$.getJSON(Routes.messenger.getConversation, "receiver=" + userId, function(data){
obj.receiver = user;
obj.data = data;
});
});
}
};
function ConversationDialogViewModel() {
var self = this;
this.conversations = ko.computed(function(){
return ConversationModel.conversations;
});
console.log(this.conversations());
this.conversations.subscribe(function(context){
console.log(context);
});
}
You can find a (reasonably) good example here how to combine:
Components
Per page ViewModel
Central ServiceProviders (for example, to call APIs or to provide state information between different components)
Please note the code is ES2015 (new Javascript) but you can also write in plain Javascript if you want. The gulp script includes stringifying any html templates in the components, so they get combined and loaded as one file but are edited as separate elements.
An example component:
const ko = require('knockout')
, CentralData = require('../../service-providers/central-data')
, CentralState = require('../../service-providers/central-state')
, template = require('./template.html');
const viewModel = function (data) {
//Make service providers accessible to data-bind and templates
this.CentralData = CentralData;
this.CentralState = CentralState;
this.componentName = 'Component One';
this.foo = ko.observable(`${this.componentName} Foo`);
this.bar = ko.observableArray(this.componentName.split(' '));
this.barValue = ko.observable("");
this.bar.push('bar');
this.addToBar = (stuffForBar) => {
if(this.barValue().length >= 1) {
this.bar.push(this.barValue());
CentralData.pushMoreData({firstName: this.componentName,secondName:this.barValue()});
}
};
this.CentralState.signIn(this.componentName);
if (CentralData.dataWeRetrieved().length < 10) {
var dataToPush = {firstName : this.componentName, secondName : 'Foo-Bar'};
CentralData.pushMoreData(dataToPush);
}
};
console.info('Component One Running');
module.exports = {
name: 'component-one',
viewModel: viewModel,
template: template
};
and component template:
<div>
<h1 data-bind="text: componentName"></h1>
<p>Foo is currently: <span data-bind="text: foo"></span></p>
<p>Bar is an array. It's values currently are:</p>
<ul data-bind="foreach: bar">
<li data-bind="text: $data"></li>
</ul>
<form data-bind="submit: addToBar">
<input type="text"
name="bar"
placeholder="Be witty!"
data-bind="attr: {id : componentName}, value : barValue" />
<button type="submit">Add A Bar</button>
</form>
<h2>Central State</h2>
<p>The following components are currently signed in to Central State Service Provider</p>
<ul data-bind="foreach: CentralState.signedInComponents()">
<li data-bind="text: $data"></li>
</ul>
<h2>Central Data</h2>
<p>The following information is available from Central Data Service Provider</p>
<table class="table table-bordered table-responsive table-hover">
<tr>
<th>Component Name</th><th>Second Value</th>
</tr>
<!-- ko foreach: CentralData.dataWeRetrieved -->
<tr>
<td data-bind="text: firstName"></td><td data-bind="text: secondName"></td>
</tr>
<!-- /ko -->
</table>
<h3>End of Component One!</h3>
</div>
For your purposes, you can ignore the Central state provider and psuedo APIs, but you might find the model useful as your app gets more complicated.
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
So I'm trying to add content to an observable array, but it doesn't update. The problem is not the first level content, but the sub array. It's a small comments section.
Basically I've this function to declare the comments
function comment(id, name, date, comment) {
var self = this;
self.id = id;
self.name = ko.observable(name);
self.date = ko.observable(date);
self.comment = ko.observable(comment);
self.subcomments = ko.observable([]);
}
I've a function to retrieve the object by the id field
function getCommentByID(id) {
var comment = ko.utils.arrayFirst(self.comments(), function (comment) {
return comment.id === id;
});
return comment;
}
This is where I display my comments
<ul style="padding-left: 0px;" data-bind="foreach: comments">
<li style="display: block;">
<span data-bind="text: name"></span>
<br>
<span data-bind="text: date"></span>
<br>
<span data-bind="text: comment"></span>
<div style="margin-left:40px;">
<ul data-bind="foreach: subcomments">
<li style="display: block;">
<span data-bind="text: name"></span>
<br>
<span data-bind="text: date"></span>
<br>
<span data-bind="text: comment"></span>
</li>
</ul>
<textarea class="comment" placeholder="comment..." data-bind="event: {keypress: $parent.onEnterSubComment}, attr: {'data-id': id }"></textarea>
</div>
</li>
</ul>
And onEnterSubComment is the problematic event form
self.onEnterSubComment = function (data, event) {
if (event.keyCode === 13) {
var id = event.target.getAttribute("data-id");
var obj = getCommentByID(parseInt(id));
var newSubComment = new comment(0, self.currentUser, new Date(), event.target.value);
obj.subcomments().push(newSubComment);
event.target.value = "";
}
return true;
};
It's interesting, because when I try the same operation during initialization(outside of any function) it works fine
var subcomment = new comment(self.commentID, "name1", new Date(), "subcomment goes in here");
self.comments.push(new comment(self.commentID, "name2", new Date(), "some comment goes here"));
obj = getCommentByID(self.commentID);
obj.subcomments().push(subcomment);
If anyone can help me with this, cause I'm kind of stuck :(
You need to make two changes:
1st, you have to declare an observable array:
self.subcomments = ko.observableArray([]);
2nd, you have to use the observable array methods, instead of the array methods. I.e. if you do so:
obj.subcomments().push(subcomment);
If subcomments were declared as array, you'd be using the .push method of Array. But, what you must do so that the observable array detect changes is to use the observableArray methods. I.e, do it like this:
obj.subcomments.push(subcomment);
Please, see this part of observableArray documentation: Manipulating an observableArray:
observableArray exposes a familiar set of functions for modifying the contents of the array and notifying listeners.
All of these functions are equivalent to running the native JavaScript array functions on the underlying array, and then notifying listeners about the change
I am a newbee to knockout, I'm trying to move from the MVC ViewModel binding.
I have a complex model:
SearchStudentsModel which has 2 properties
Collection of Students (Subset of students)
Number of Students overall
Note that the length of the collection isn't equal to the number overall.
I need to implement a search functionality
Student will have all the regular properties plus IsActive indicator.
I use ul and li tags to data-bind the details.
The search screen should facilitate the user in marking the active flag with an indicator (on and off) and immediately data should be saved in the database.
All the examples I referred to talk about only one level of model. I have a SearchStudent model and within that I have a collection of students.
How should the binding be for this hierarchy of models?
I have refactored your jsFiddle. Hoping you can now understand knockoutJS better. It is not your whole page/Knockout, but I think with this snippet your problem can be solved.
the markup:
<button id="searchEmployees" type="button" data-bind="click: search">Search</button>
<li data-bind="foreach: Employees">
ID: <span data-bind="text: Id"></span><br/>
Name: <span data-bind="text: Name"></span><br/>
Active: <span data-bind="click: ToggleActivation, text: IsActive"></span> <-- click<br/>
</li>
<span data-bind="text: Employees().length"></span> of
<span data-bind="text: AllEmployees().length"></span>
the js/viewmodel
function Employee(id, name, isactive){
var self = this;
self.IsActive = ko.observable(isactive);
self.Id = ko.observable(id);
self.Name = ko.observable(name);
self.ToggleActivation = function () {
if(self.IsActive() === true)
self.IsActive(false);
else
self.IsActive(true);
};
}
var searchEmployeeViewModel = function () {
var self = this;
self.Employees = ko.observableArray([]);
self.AllEmployees = ko.observableArray([]);
self.search = function () {
//Ajax call to populate Employees - foreach on onsuccess
var employee1 = new Employee(2, "Jane Doe", true);
var employee2 = new Employee(3, "Kyle Doe", false);
var employee3 = new Employee(4, "Tyra Doe", false);
var employee = new Employee(1, "John Doe", true);
self.AllEmployees.push(employee);
self.AllEmployees.push(employee1);
self.AllEmployees.push(employee2);
self.AllEmployees.push(employee3);
self.Employees.push(employee);
}
}
$(document).ready(function () {
ko.applyBindings(new searchEmployeeViewModel());
});
or you can simply use my jsFiddle if you do not like reading my code here ;)