1

I'm starting with knockout and my computed observable seems to fire always when the viewmodel is instantiated and i don't know why.

I've reduced the problem to the absurd just for testing: the computed property just prints a message in the console and it is not binded to any element at the DOM. Here it is:

(function() {
    function HomeViewModel() {
        var self = this;

(...)

        self.FullName = ko.computed(function () {
            console.log("INSIDE");
        });

(...)

    };
    ko.applyBindings(new HomeViewModel());
})();

How can it be avoided?

Update:

Here is the full code of the ViewModel just for your better understanding:

function HomeViewModel() {
    var self = this;

    self.teachers = ko.observableArray([]);
    self.students = ko.observableArray([]);

    self.FilterByName = ko.observable('');
    self.FilterByLastName = ko.observable('');

    self.FilteredTeachers = ko.observableArray([]);
    self.FilteredStudents = ko.observableArray([]);

    self.FilteredUsersComputed = ko.computed(function () {
        var filteredTeachers = self.teachers().filter(function (user) {                
            return (user.name.toUpperCase().includes(self.FilterByName().toUpperCase()) &&
                user.lastName.toUpperCase().includes(self.FilterByLastName().toUpperCase())
            );
        });
        self.FilteredTeachers(filteredTeachers); 
        var filteredStudents = self.students().filter(function (user) {
            return (user.name.toUpperCase().includes(self.FilterByName().toUpperCase()) &&
                user.lastName.toUpperCase().includes(self.FilterByLastName().toUpperCase())
            );
        });
        self.FilteredStudents(filteredStudents);
        $("#LLAdminBodyMain").fadeIn();
    }).extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 800 } });

    self.FilteredUsersComputed.subscribe(function () {
        setTimeout(function () { $("#LLAdminBodyMain").fadeOut(); }, 200);
    }, null, "beforeChange");

    $.getJSON("/api/User/Teacher", function (data) {            
        self.teachers(data);   
    });
    $.getJSON("/api/User/Student", function (data) {
        self.students(data);
    });
}

ko.applyBindings(new HomeViewModel());

})();

I need it to not be executed on load because on load the self.students and self.teachers arrays are not jet populated.

NOTE: Just want to highlight that in both codes (the absurd and full), the computed property is executed on loading (or when the ViewModel is first instantiated).

MorgoZ
  • 2,012
  • 5
  • 27
  • 54
  • 4
    Possible duplicate of [ko.computed do not fire function upon instantiating](https://stackoverflow.com/questions/43113909/ko-computed-do-not-fire-function-upon-instantiating) – Toffman Sep 22 '17 at 10:19
  • This is what you are looking for: https://stackoverflow.com/a/43114564/4205470 – Balázs Sep 22 '17 at 10:31
  • Can you explain some more why it's a problem that the computed is "firing" immediately? – Tomalak Sep 22 '17 at 10:36
  • I've updated the question with more code, just to let you better understand why am i asking this and the limits of possible solutions. Thank you. – MorgoZ Sep 22 '17 at 10:49
  • Yes, there are several mistakes in your approach. Let me write up a better one. – Tomalak Sep 22 '17 at 11:36

1 Answers1

1

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>
Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • Thank you, I've tried your solution and it is not functional to me. The fadeVisible binding checks if the valueAccesor (filteredUsers) is truthy, and it always is, so it is always in fade-in state. I need it to fade-out just before the filteredUsers are updated and the fade-out just after it. The only solution i could find is the way my code is: the fade-in in a subscribe function to be executed inmediatly before any changes are applied and the fade-out after the filteredUsers ends its rateLimit timeout. Thank you in any case for your recommendations. – MorgoZ Sep 22 '17 at 13:31
  • I've added a binding that does that, take a look. – Tomalak Sep 22 '17 at 13:53
  • 1
    Your last update works good. I just had to add a "$(element).hide()" at the start of the "init" to avoid briefly showing an empty list before firing the fadeOut, but it works fine. So, while i can't mark this answer as correct since it does not explains how to avoid a computed from firing at loading, you have helped me a lot in wrapping my head arround starting to use knockout. So i just want to thank you that. Salutes! – MorgoZ Sep 25 '17 at 11:01
  • *"How can I prevent the computed from firing immediately"* is the wrong question to ask, that's why I did not answer it and solved the actual problem instead. There are ways of treating the *symptom* (by using lazy evaluation or pure computeds, like some of the other comments suggest) but in fact there is no need to do this here. Simply approach the problem from the right angle and there is no strange behavior that needs fixing. That's what I wanted to point out, so I think that counts as an answer. – Tomalak Sep 25 '17 at 11:14