I work on a search page that allows filtering, searching and paginating through the list of results. Each time list of currently displayed items is changed, knockout takes a lot of time to render new values. My knockout knowledge is limited but I can see in the DevTools that things are being handled very inefficiently:
item template is parsed for each element (no template caching?)
each item is inserted into the DOM separately (no bulk operations?)
Do you have any suggestions for fixing these issues?
I tried to extract the relevant code:
$.get("/api/Search/GetSearchResults?page=" + bla)
.then(function (result) {
self.contentListViewModel.load(result.SearchHits);
//...
});
//----------------
var ContentListViewModel = function (options) {
self.searchHits = ko.observableArray([]);
//...
this.load = function (elements) {
for (var i = 0; i < elements.length; i++) {
elements[i] = new ContentElementViewModel(elements[i]);
//...
}
self.searchHits(elements);
}
}
//----------------
var ContentElementViewModel = function (dto, options) {
//just setting couple of observable variables and couple of methods
}
Relevant HTML:
<ul data-bind="foreach: { data: searchHits, afterRender: afterRenderSearchHits }, as: 'hit', masonry: { enable: true, hits: searchHits }, css: { 'listify': !pinterestEnabled() }">
<li data-bind="template: { name: $data.template() }"></li>
</ul>
The answer is to avoid using 'template' binding. It triggers multiple jQuery.parseHTML calls that are expensive.
Slow code:
<ul id='list1' data-bind="foreach: { data: people }">
<li data-bind="template: 'item'"></li>
</ul>
<script id='item'>
<h3 data-bind="text: name"></h3>
</script>
Fast code:
<ul id='list2' data-bind="foreach: { data: people }">
<li>
<h3 data-bind="text: name"></h3>
</li>
</ul>
JS Bin example.
JS Perf
This response does not answer how to keep DOM manipulation to minimum, I'll ask another, more specific question for that.
Related
I am trying to implement multiple filters on the same model. The attributes I want to apply the filter are arrays.
//Exam Model
App.Exam = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
courses : DS.hasMany('course',{ async: true }),
});
//Course Model
App.Course = DS.Model.extend({
name: DS.attr('string'),
description:DS.attr('string'),
professors: DS.attr(),
subjects: DS.attr(),
languages: DS.attr(),
exam: DS.belongsTo('exam', { async: true })
});
In the ExamsExam route after the model is resloved I extract the data I want to apply the filter on.
App.ExamsExamRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('exam', params.exam_id).then(function (exam) {
console.log("found single exam", exam);
return exam;
});
},
afterModel: function(model, transition){
var self = this;
var professorList = [];
var subjectList = [];
var languageList = [];
var promise = new Ember.RSVP.Promise(function(resolve, reject){
var courses = model.get('courses');
courses.forEach(function(course){
self.store.find('course', course.get('id')).then(function(course){
var profs = course.get('professors');
var subjects = course.get('subjects');
var languages = course.get('languages');
profs.forEach(function(prof) {
if (professorList.indexOf(prof) === -1) {
professorList.pushObject(prof);
}
});
subjects.forEach(function(subject) {
if (subjectList.indexOf(subject) === -1) {
subjectList.pushObject(subject);
}
});
languages.forEach(function(language) {
if (languageList.indexOf(language) === -1) {
languageList.pushObject(language);
}
});
});
});
var data = {
professorList: professorList,
subjectList: subjectList,
languageList: languageList
};
resolve(data);
});
promise.then(function(data) {
console.log(data);
model.set('professorNameList', data.professorList);
model.set('subjectList', data.subjectList);
model.set('languageList', data.languageList);
});
}
});
And this is my template
<script type="text/x-handlebars" data-template-name="exams/exam">
<h2>Exam page</h2>
<div class="row">
<div class="col-md-3 well">
<ul class="list-group well">
{{#each course in model.languageList}}
<li class="">
<label>
{{input type='checkbox'}}
{{course}}
</label>
</li>
{{/each}}
</ul>
<ul class="list-group well">
{{#each prof in model.professorNameList}}
<li class="">
<label>
{{input type='checkbox'}}
{{prof}}
</label>
</li>
{{/each}}
</ul>
<ul class="list-group well">
{{#each subject in model.subjectList}}
<li class="">
<label>
{{input type='checkbox'}}
{{subject}}
</label>
</li>
{{/each}}
</ul>
</div>
<div class="col-md-9">
{{#each course in model.courses}}
<div class="well">
Course name - {{course.name}}<br>
Professors - {{course.professors}}<br>
Subjects - {{course.subjects}}
</div>
{{/each}}
</div>
</div>
</script>
Now how do I change the content of the model so that if a user selects the language filter, only the courses belong to that selected language must be displayed.
Plus if the user selects language and subjects filter, only the filters matching that criteria should be displayed.
There is very little documentation on filtering via checkbox in ember.
Someone please suggest/guide me on how to approach this problem and get a clean solution.
Here is the JS BIN DEMO for better illustration of what I want to achieve.
Building on #joostdevries's answer...
Using every() with a callback is a fine solution, but it "feels" a little complicated. What you are looking for is basically an intersect between the arrays. For example, common professors to both an array of selected professors and array of professors in the model. Ember provides just such function, called ... wait for it ... intersection (see here) :). It returns an array containing the elements common to both arrays or an empty (0 length) array if there are no common elements.
Here is the same filteredCourses property, using the intersection method.
filteredCourses: function() {
var selectedProfessors = this.get('selectedProfessors'),
selectedLanguages = this.get('selectedLanguages'),
selectedSubjects = this.get('selectedSubjects'),
courses = this.get('model.courses');
var intersectFn = Ember.EnumerableUtils.intersection;
return courses.filter(function(course) {
return intersectFn(course.get('professors') || [], selectedProfessors).length ||
intersectFn(course.get('languages') || [], selectedLanguages).length ||
intersectFn(course.get('subjects') || [], selectedSubjects).length;
});
}.property('selectedProfessors.length', 'selectedLanguages.length', 'selectedSubjects.length')
First, we alias the intersection function as follows:
var intersectFn = Ember.EnumerableUtils.intersection;
This step is purely cosmetic - I just don't feel like typing Ember.EnumerableUtils.intersection every time; instead I just want to type intersectFn. Then, I just use the function to see if the arrays intersect. If they do - the length of resulting array would be greater than 0, which evaluates to true; otherwise - the length is 0, which evaluates to false. The one last quirk in all of this is that sometimes the property will be undefined which messes up the intersection method. For such cases, I set the array to empty.
So, course.get('professors') || [] means, if professors property (array) is defined - use it; otherwise - use an empty array.
Working solution here
With store.filter, you have a callback function which returns a boolean that decides whether or not something matches the filter:
filteredCourses: function() {
return courses.filter(function(course) {
return selectedProfessors.every(function(prof) {
return course.get('professors').contains(prof);
}) && selectedLanguages.every(function(lang) {
return course.get('languages').contains(lang);
}) && selectedSubjects.every(function(subj) {
return course.get('subjects').contains(subj);
});
});
}.property()
Here's an updated JSBin: http://emberjs.jsbin.com/comosepuno/1/. The checkbox component is borrowed from https://github.com/RSSchermer/ember-multiselect-checkboxes
I am creating an edit screen where I want people to delete items from a list. The list is displayed normally, until the "controller" object goes into edit mode. Then the user can delete items. Items should be flagged for deletion and displayed as such, then when the user saves the edit, they are deleted and the server notified.
I actually have this all working, but the only way I could do it was using literal conditions in the bindings, which looks ugly and I don't really like. Is there a better way of doing it?
Working Fiddle: http://jsfiddle.net/L1e7zwyv/
Markup:
<div id="test">
<a data-bind="visible: IsViewMode, click: edit">Edit</a>
<a data-bind="visible: IsEditMode, click: cancel">Cancel</a>
<hr/>
<ul data-bind="foreach: Items">
<li data-bind="css: CssClass">
<span data-bind="visible: $parent.IsViewMode() || $data._Deleting(), text: Value"></span>
<!-- ko if: $parent.IsEditMode() && !$data._Deleting() -->
<input type="text" data-bind="value: Value"/>
<a data-bind="click: $parent.deleteItem">Del</a>
<!-- /ko -->
</li>
</ul>
</div>
Code:
function ItemModel(val)
{
var _this = this;
this.Value = ko.observable(val);
this._Deleting = ko.observable();
this.CssClass = ko.computed(
function()
{
return _this._Deleting() ? 'deleting' : '';
}
);
}
function ManagerModel()
{
var _this = this;
this.Items = ko.observableArray([
new ItemModel('Hell'),
new ItemModel('Broke'),
new ItemModel('Luce')
]);
this.IsEditMode = ko.observable();
this.IsViewMode = ko.computed(function() { return !_this.IsEditMode(); });
this.edit = function(model, e)
{
this.IsEditMode(true);
};
this.cancel = function(model, e)
{
for(var i = 0; i < _this.Items().length; i++)
_this.Items()[i]._Deleting(false);
this.IsEditMode(false);
};
this.deleteItem = function(model, e)
{
model._Deleting(true);
};
}
ko.applyBindings(new ManagerModel(), document.getElementById('test'));
you could:
wrap another span around to separate the bindings but this would be less efficient.
use both a visible: and if: binding on the same element to achieve the same functionality,
write a function on the itemModel isVisible() accepting the parent as an argument making your binding visible: $data.isVisible($parent).
Afterthought: If this comes up in multiple places you could write a helper function to combine visibility bindings
// reprisent variables from models
var v1 = false;
var v2 = false;
var v3 = false;
// Helper functions defined in main script body - globally accessible
function VisibilityFromAny() {
var result = false;
for(var i = 0; i < arguments.length; i++ ) result |= arguments[i];
return Boolean(result);
}
function VisibilityFromAll() {
var result = true;
for(var i = 0; i < arguments.length; i++ ) result &= arguments[i];
return Boolean(result);
}
// represent bindings
alert(VisibilityFromAny(v1, v2, v3));
alert(VisibilityFromAll(v1, v2, v3));
The third option is the most popular technique with MVVM aficionados like yourself for combining variables in a single binding from what I've seen, it makes sense and keeps all the logic away from the view markup in the view models.
Personally I like the syntax you have at present, (even though I count myself amongst the MVVM aficionado gang as well) this clearly shows in the view markup that the visibility of that element is bound to 2 items rather then hiding these details in a function.
I try to think of view models as a model for my view, not just a place where logic resides. When possible I also try to move complex logic back the view model and use descriptive names for my variables so the code is more readable.
I would suggest adding this to your view model -
var isViewable = ko.computed(function () { return IsViewMode() || _Deleting(); });
var isEditable = ko.computed(function() { return IsEditMode() && !_Deleting(); });
And in your view -
<li data-bind="css: CssClass">
<span data-bind="visible: isViewable, text: Value"></span>
<!-- ko if: isEditable -->
<input type="text" data-bind="value: Value"/>
<a data-bind="click: $parent.deleteItem">Del</a>
<!-- /ko -->
</li>
This cleans the bindings up and allows you to more easily adjust the logic without having to do many sanity checks in your view and view model both. Also I personally name variables that return a boolean such as this as isWhatever to help be more descriptive.
The benefit is that as your view and view model grow larger you can keep the DOM clean of clutter and also your view model becomes testable.
Here is a 'code complete' version of your fiddle with this added -
http://jsfiddle.net/L1e7zwyv/3/
For a navigation menu, I have two groups of links, each group and link showing up or not dependent on a user's role. The roles are looked up when the link structure is being built and the list of links is built accordingly. The returned JSON gets parsed, put into observable arrays with no problem, but when I actually try and apply the bindings, the binding fails because the observables are blank. Here is the HTML...
<ul id="user-menu" class="menu" data-bind="foreach: areas">
<li>
<a data-bind="attr: { href: areaLink }">
<img data-bind="attr: { src: iconUri }" />
<span data-bind="text: areaName"></span>
</a>
</li>
</ul>
<ul id="admin-menu" class="menu" data-bind="foreach: adminAreas">
<li>
<a data-bind="attr: { href: areaLink }">
<img data-bind="attr: { src: iconUri }" />
<span data-bind="text: areaName"></span>
</a>
</li>
</ul>
Knockout view model in the background...
var navigation = (function() {
function Area() {
var self = this;
self.areaName = ko.observable();
self.areaLink = ko.observable();
self.iconUri = ko.observable();
self.sequenceNo = ko.observable();
self.isAdmin = ko.observable();
self.loadFromVM = function (vm) {
self.areaName(vm.name || '');
self.areaLink(vm.link || '');
self.iconUri(vm.iconUri || '');
self.sequenceNo(vm.sequenceNo || '');
self.isAdmin(vm.isAdmin);
}
}
function viewModel() {
var self = this;
self.areas = ko.observableArray([]);
self.adminAreas = ko.observableArray([]);
self.setup = function () {
var data = {}; // population with basic session data
$.getJSON('....php', { JSON.stringify(data) }, function (results) {
for (var i = 0; i < results.length; i++) {
var area = new Area();
area.loadFromVM(results[i]);
if (area.isAdmin()) {
self.adminAreas().push(area);
} else {
self.areas().push(area);
}
}
});
};
}
var vmInstance;
return {
setup: function () {
vmInstance = new viewModel();
vmInstance.setup();
ko.applyBindings(vmInstance, $('#user-menu')[0]);
ko.applyBindings(vmInstance, $('#admin-menu')[0]);
}
};
})();
And then I bring it together with this in the navigation view file...
navigation.setup();
So after I get my JSON back, parse it, and organize it when I loop through the success function of the $.getJSON method, putting a watch on self.areas() and self.adminAreas() does show that the arrays have the exact information I want them to. But by the time they have to be applied, calling vmInstance.areas().length or vmInstance.adminAreas().length returns zero. Even more oddly, putting in an alert with the length of the arrays right after the $.getJSON call but within the setup() function will cause the alert to fire first, show zeroes, then goes through the get, populates the array, then fires zeroes again.
Not exactly sure what's going on here, but I can't remember seeing this kind of behavior in another project so I'm not quite sure what I'm doing wrong here. Any ideas or advice would be greatly appreciated.
EDIT: Nevermind on the Fiddle. It doesn't really capture my actual error.
adminarea object is not initialized.you made the adminArea variable but instead of this you have used same area variable to set values.
var adminArea = new Area();
adminArea.areaName('test admin area');
adminArea.areaLink('#');
adminArea.iconUri('http://evernote.com/media/img/getting_started/skitch/windows8/win8-checkmark_icon.png');
adminArea.sequenceNo(1);
adminArea.isAdmin(true);
Fiddle Demo
I'm following John Papa's jumpstart course about SPA's and trying to display a list of customers loaded via ASP.NET Web API the knockout foreach binding is not working. The Web API is working fine, I've tested it on it's own and it is returning the correct JSON, because of that I won't post the code for it. The get method simply returns one array of objects, each with properties Name and Email. Although not a good practice, knockout is exposed globaly as ko by loading it before durandal.
I've coded the customers.js view model as follows
define(['services/dataservice'], function(ds) {
var initialized = false;
var customers = ko.observableArray();
var refresh = function() {
return dataservice.getCustomers(customers);
};
var activate = function() {
if (initialized) return;
initialized = true;
return refresh();
};
var customersVM = {
customers: customers,
activate: activate,
refresh: refresh
};
return customersVM;
});
The dataservice module I've coded as follows (I've not wrote bellow the function queryFailed because I know it's not being used)
define(['../model'], function (model) {
var getCustomers = function (customersObservable) {
customersObservable([]);
var options = {url: '/api/customers', type: 'GET', dataType: 'json'};
return $.ajax(options).then(querySucceeded).fail(queryFailed);
function querySucceeded(data) {
var customers = [];
data.forEach(function (item) {
var c = new model.Customer(item);
customers.push(c);
});
customersObservable(customers);
}
};
return {
getCustomers: getCustomers
};
});
Finaly the model module was built as follows:
define(function () {
var Customer = function (dto) {
return mapToObservable(dto);
};
var model = {
Customer: Customer
};
return model;
function mapToObservable(dto) {
var mapped = {};
for (prop in dto)
{
if (dto.hasOwnProperty(prop))
{
mapped[prop] = ko.observable(dto[prop]);
}
}
return mapped;
}
});
The view is then simply a list, it is simply:
<ul data-bind="foreach: customers">
<li data-bind="text: Name"></li>
</ul>
But this doesn't work. Any other binding works, and I've looked on the console window, and it seems the observable array is being filled correctly. The only problem is that this piece of code doesn't show anything on screen. I've reviewed many times the files but I can't seem to find the problem. What's wrong with this?
You can use the knockout.js context debugger chrome extension to help you debug your issue
https://chrome.google.com/webstore/detail/knockoutjs-context-debugg/oddcpmchholgcjgjdnfjmildmlielhof
Well, I just spent a lot of time on an local issue to realize that the ko HTML comment format, if used, should be like this:
<!-- ko foreach: arrecadacoes -->
and NOT like this:
<!-- ko: foreach: arrecadacoes -->
: is NOT used after ko...
I know this question is a little old but I thought I'd add my response in case someone else runs into the same issue I did.
I was using Knockout JS version 2.1.0 and it seems the only way I can get the data to display in a foreach loop was to use:
$data.property
so in the case of your example it would be
$data.Name
Hope this helps
I don't see anywhere in your code that you've called ko.applyBindings on your ViewModel.
KO has a known issue while using foreach in a non-container element like the one above <ul> so you have to use containerless control flow syntax.
e.g.
<ul>
<!-- ko foreach: customers-->
<li data-bind="text: Name"></li>
<!-- /ko -->
</ul>
Ref: http://knockoutjs.com/documentation/foreach-binding.html
EDIT: Answered myself, error was because of old version of knockout, always use newest version, and check existing!
i've been following knockouts tutorial, and tried to do something myself, but get the error, even when I basically have the same code.
<ul data-bind="foreach: tasks">
<li>
<input data-bind="value: title" />
</li>
</ul>
<script type="text/javascript">
(function () {
function Task(data) {
this.title = ko.observable(data.contentName);
}
function TaskListViewModel() {
// Data
var self = this;
self.tasks = ko.observableArray([]);
// Load initial state from server, convert it to Task instances, then populate self.tasks
$.getJSON('<%= Url.Action("GetContentList", "TranslateContentMenu") %>',
{
languageId: $('#LanguageIdNameValuePairs').val()
}, function (allData) {
var mappedTasks = $.map(allData, function (item) { return new Task(item) });
self.tasks(mappedTasks);
});
}
var test = new TaskListViewModel();
console.log(test);
ko.applyBindings(new TaskListViewModel());
}())
</script>
The service i am calling, returns this result:
[{"contentId":"1b07790c","contentName":"test"},{"contentId":"1b07790c","contentName":"test"},{"contentId":"1b07790c","contentName":"test"}]
and this is the error, straight out of firebug console:
Error: Unable to parse binding attribute.
Message: ReferenceError: title is not defined;
Attribute value: value: title
You shouldn't get such error because at this time tasks array should be empty and knockout shouldn't generate <li> tag. Make sure that you are initializing tasks array with [] or with nothing not with something like this [""].
Also you can try to initialize tasks with empty Task object:
self.tasks = ko.observableArray(new Task());
The error i got was because i was using knockout 1.2.1, newest version as of today was 2.1.0, upgrading knockout made it work.
As #Artem has pointed out in the comments this does work in the jsFiddle he constructed. The only thing I can put this down to is your badly formed HTML. You wrote:
<ul data-bind="foreach: tasks">
<li>
<input data-bind="value: title" />
</li>
Knockout does not like badly formed HTML. You should try:
<ul data-bind="foreach: tasks">
<li>
<input data-bind="value: title" />
</li>
</ul>