Force a computed property function to run
Asked Answered
D

5

83

Given a computed property

vm.checkedValueCount = ko.computed(function(){
  var observables = getCurrentValues();  //an array of ko.observable[]
  return _.filter(observables, function(v) { return v() }).length;
});

suppose getCurrentValues() can return different sets of observables which are modified elsewhere in the code (and comes from a more complex structure than an observableArray).

I need checkedValueCount to update whenever

  • one of its dependencies change
  • getCurrentValues() returns a different set of observables.

The problem is that ko.computed seems to memoize the last returned value and only update when a dependency updates. This handles the first case but not the latter.

What I'm looking for is a way to force checkedValueCount to re-run. Something which I can use like:

changeCurrentValues();
vm.checkeValueCount.recalculate();

Put most simply, given that I have

a = ko.computed(function() { return Math.random() })

how can I force invoking a() twice to return different values.

Deanery answered 7/12, 2012 at 19:8 Comment(1)
See my updated "rewritten" answer.Acrylic
A
121

I realized my first answer missed your point, and won't solve your issue.

The problem is that a computed will only reevaluate if there is some observable that forces it to re-evaluate. There is no native way to force a computed to re-evaluate.

However, you can get around this with some hackery by creating a dummy observable value and then telling its subscribers that it has changed.

(function() {

    var vm = function() {
        var $this = this;

        $this.dummy = ko.observable();

        $this.curDate = ko.computed(function() {
            $this.dummy();
            return new Date();
        });

        $this.recalcCurDate = function() {
            $this.dummy.notifySubscribers();
        };        
    };

    ko.applyBindings(new vm());

}());​

Here is a Fiddle showing this approach

Acrylic answered 7/12, 2012 at 19:41 Comment(5)
Ah, I tried this but I must have hooked it up wrong. This looks hacky but good (and good enough that I can extend ko.computed to allow it to workDeanery
As always the simpler is the best.Pompano
This helpfull when you change large array of data that is not observable and after changes some portion of data must be displayed.Cheesecloth
Your use of notifySubscribers() is a good one. Better than what I was doing of building a random number and setting that to the value of dummy()Incomprehension
notifySubscribers() is what I was looking for. Thank you!Sexual
P
8

There is a method to force recalculation of all observables depending on yours:

getCurrentValues.valueHasMutated()
Pleistocene answered 10/11, 2014 at 18:42 Comment(4)
Computeds do not have this methodDeanery
getCurrentValues(); //an array of ko.observable[]Pleistocene
Oh I see. You're saying find an observable that is a dependency of the computed and call it's valueHasMutated method. This isn't really fundamentally any different from Josh's answer above, is it? In fact it forces you to know what is referenced and to know that those references are observable (and not computed).Deanery
it's good to mention this function nonetheless as it remains an option for readers, though perhaps someone could update the accepted answer to note cases where this function can be interchanged with notifySubscribers(), and if there is any penalty or advantage either way.Scare
P
4

This answer is conceptually the same as the one @josh gave, but presented as a more generic wrapper. Note: this version is for a 'writeable' computed.

I'm using Typescript so I've included the ts.d definition first. So ignore this first part if not relevant to you.

interface KnockoutStatic
{
    notifyingWritableComputed<T>(options: KnockoutComputedDefine<T>, context ?: any): KnockoutComputed<T>;
}

Notifying-writeable-computed

A wrapper for a writable observable that always causes subscribers to be notified - even if no observables were updated as a result of the write call

Just replace function<T> (options: KnockoutComputedDefine<T>, context) with function(options, context) if you don't use Typescript.

ko.notifyingWritableComputed = function<T> (options: KnockoutComputedDefine<T>, context)
{
    var _notifyTrigger = ko.observable(0);
    var originalRead = options.read;
    var originalWrite = options.write;

    // intercept 'read' function provided in options
    options.read = () =>
    {
        // read the dummy observable, which if updated will 
        // force subscribers to receive the new value
        _notifyTrigger();   
        return originalRead();
    };

    // intercept 'write' function
    options.write = (v) =>
    {
        // run logic provided by user
        originalWrite(v);

        // force reevaluation of the notifyingWritableComputed
        // after we have called the original write logic
        _notifyTrigger(_notifyTrigger() + 1);
    };

    // just create computed as normal with all the standard parameters
    return ko.computed(options, context);
}

The main use case for this is when you are updating something that would not otherwise trigger a change in an observable that is 'visited' by the read function.

For instance I am using LocalStorage to set some values, but there is no change to any observable to trigger re-evaluation.

hasUserClickedFooButton = ko.notifyingWritableComputed(
{
    read: () => 
    {
        return LocalStorageHelper.getBoolValue('hasUserClickedFooButton');
    },
    write: (v) => 
    {
        LocalStorageHelper.setBoolValue('hasUserClickedFooButton', v);        
    }
});

Note that all I needed to change was ko.computed to ko.notifyingWritableComputed and then everything takes care of itself.

When I call hasUserClickedFooButton(true) then the 'dummy' observable is incremented forcing any subscribers (and their subscribers) to get the new value when the value in LocalStorage is updated.

(Note: you may think the notify: 'always' extender is an option here - but that's something different).


There is an additional solution for a computed observable that is only readble:

ko.forcibleComputed = function(readFunc, context, options) {
    var trigger = ko.observable().extend({notify:'always'}),
        target = ko.computed(function() {
            trigger();
            return readFunc.call(context);
        }, null, options);
    target.evaluateImmediate = function() {
        trigger.valueHasMutated();
    };
    return target;
};


myValue.evaluateImmediate();

From @mbest comment https://github.com/knockout/knockout/issues/1019.

Plume answered 30/4, 2015 at 5:57 Comment(1)
What do you mean there's no value? Did it work before switching to this. Are you using deferred updates (the global setting). Just realized that there might be a conflict if you expect to be able to update the observable and then immediately use the value. If so try adding ko.tasks.runEarly() to my method.Plume
M
1

suppose getCurrentValues() can return different sets of observables which are modified elsewhere in the code

I assume getCurrentValues() is a function. If you could make it a computed, your checkedValueCount would just magically start working.

Can you make getCurrentValues be a computed instead of a function?

Mindexpanding answered 7/12, 2012 at 19:26 Comment(3)
I'm oversimplifying my actual scenario a lot but sure, let's say it can be (which would not be the case if it needed for example to take parameters) - I don't see how that would make any difference. Inside of getCurrentValues() I'm slicing and dicing other data to determine which nodes of a tree view need to be returned. the problem is that the function can return different observable values and I need the computed to pick up on it. Basically exactly what the docs talk about with dynamic dependencies but with the complication of them being added on the flyDeanery
It would make a difference if the "slicing and dicing" was based on observables. For example, let's say your slicing and dicing is the nodes that have .IsChecked() == true. You'd then have a computed called .currentValues() that would evaluate all nodes and return the ones with .IsChecked(). Can the slicing and dicing be done on observable properties?Mindexpanding
I'm really not sure what you're saying Judah,every single invocation can't be a ko observable in my case - but more importantly, I don't see how that could possibly matter, knockout doesn't do any reflection over what closures you've called (I haven't checked but unless javascript has gone completely into lisp-land, this is impossible). All the computed can do is at best track any observables that are called.Deanery
S
0

since there is no straight forward way to force update a computed, i have created an observable named toForceComputedUpdate, and i called it within the computed function so the computed will listen to its update, then to force update i call it like this toForceComputedUpdate(Math.random)

Slatternly answered 21/11, 2016 at 22:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.