There are two main mistakes in your approach.
- You have a separate observable for filtered users. That's not necessary. The
ko.computed
will fill that role, there is no need to store the computed results anywhere. (Computeds are cached, they store their own values internally. Calling a computed repeatedly does not re-calculate its value.)
- You are interacting with the DOM from your view model. This should generally be avoided as it couples the viewmodel to the view. The viewmodel should be able operate without any knowledge of how it is rendered.
Minor points / improvement suggestions:
- Don't rate-limit your filter result. Rate-limit the observable that contains the filter string.
- Don't call your computed properties
...Computed
- that's of no concern to your view, there is no reason to point it out. For all practical purposes inside your view, computeds and observables are exactly the same thing.
- If teachers and students are the same thing, i.e. user objects to be displayed in the same list, why have them in two separate lists? Would it not make more sense to have a single list in your viewmodel, so you don't need to filter twice?
- Observables are functions. This means
$.getJSON("...", function (data) { someObservable(data) });
can be shortened to
$.getJSON("...", someObservable);
.
Here is a better viewmodel:
function HomeViewModel() {
var self = this;
self.teachers = ko.observableArray([]);
self.students = ko.observableArray([]);
self.filterByName = ko.observable().extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });
self.filterByLastName = ko.observable().extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });
function filterUsers(userList) {
var name = self.filterByName().toUpperCase(),
lastName = self.filterByLastName().toUpperCase(),
allUsers = userList();
if (!name && !lastName) return allUsers;
return allUsers.filter(function (user) {
return (!name || user.name.toUpperCase().includes(name)) &&
(!lastName || user.lastName.toUpperCase().includes(lastName));
});
}
self.filteredTeachers = ko.computed(function () {
return filterUsers(self.teachers);
});
self.filteredStudents = ko.computed(function () {
return filterUsers(self.students);
});
self.filteredUsers = ko.computed(function () {
return self.filteredTeachers().concat(self.filteredStudents());
// maybe sort the result?
});
$.getJSON("/api/User/Teacher", self.teachers);
$.getJSON("/api/User/Student", self.students);
}
With this it does not matter anymore that the computeds are calculated immediately. You can bind your view to filteredTeachers
, filteredStudents
or filteredUsers
and the view will always reflect the state of affairs.
When it comes to making user interface elements react to viewmodel state changes, whether the reaction is "change HTML" or "fade in/fade out" makes no difference. It's not the viewmodel's job. It is always the task of bindings.
If there is no "stock" binding that does what you want, make a new one. This one is straight from the examples in the documentation:
// Here's a custom Knockout binding that makes elements shown/hidden via jQuery's fadeIn()/fadeOut() methods
// Could be stored in a separate utility library
ko.bindingHandlers.fadeVisible = {
init: function(element, valueAccessor) {
// Initially set the element to be instantly visible/hidden depending on the value
var value = valueAccessor();
$(element).toggle(ko.unwrap(value)); // Use "unwrapObservable" so we can handle values that may or may not be observable
},
update: function(element, valueAccessor) {
// Whenever the value subsequently changes, slowly fade the element in or out
var value = valueAccessor();
ko.unwrap(value) ? $(element).fadeIn() : $(element).fadeOut();
}
};
It fades in/out the bound element depending the bound value. It's practical that the empty array []
evaluates to false, so you can do this in the view:
<div data-bind="fadeVisible: filteredUsers">
<!-- show filteredUsers... --->
</div>
A custom binding that fades an element before and after the bound value changes would look like follows.
- We subscribe to value during the binding's
init
phase.
- There is no
update
phase in the binding, everything it needs to do is accomplished by the subscriptions.
- When the DOM element goes away (for example, because a higher-up
if
or foreach
binding triggers) then our binding cleans up the subscriptions, too.
Let's call it fadeDuringChange
:
ko.bindingHandlers.fadeDuringChange = {
init: function(element, valueAccessor) {
var value = valueAccessor();
var beforeChangeSubscription = value.subscribe(function () {
$(element).delay(200).fadeOut();
}, null, "beforeChange");
var afterChangeSubscription = value.subscribe(function () {
$(element).fadeIn();
});
// dispose of subscriptions when the DOM node goes away
// see http://knockoutjs.com/documentation/custom-bindings-disposal.html
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
// see http://knockoutjs.com/documentation/observables.html#explicitly-subscribing-to-observables
beforeChangeSubscription.dispose();
afterChangeSubscription.dispose();
});
}
};
Usage is the same as above:
<div data-bind="fadeDuringChange: filteredUsers">
<!-- show filteredUsers... --->
</div>