4

I am new to KnockoutJS and am curious to see if this is possible. I am trying to wrap a local storage object in a writeable computed so that I can take advantage of the auto-binding goodness of KnockoutJS. However, the "read" operation doesn't reference any observables - consequently the initial value never gets updated:

<select data-bind="foreach: logLevelsArray, value: currentLogLevel">
    <option data-bind="attr: { value: $index() }, text: $data"></option>
</select>

_.extend(DevUtilitiesViewModel.prototype, {
    ...
    logLevelsArray: ['error', 'warning', 'info', 'debug'],
    currentLogLevel: ko.computed({
        read: function() {
            return localStorage.getItem("logger-level");
        },
        write: function( newValue ) {
            localStorage.setItem("logger-level", newValue);
        }
    })
    ...
});

DevUtilitiesViewModel.currentLogLevel(); // 2 (default)
DevUtilitiesViewModel.currentLogLevel(4);
localStorage.getItem("logger-level"); // 4 - write was successful
DevUtilitiesViewModel.currentLogLevel(); // 2 - still the original value

I understand that this is the expected behavior and I understand why. I also understand that I can make currentLogLevel a simple observable and subscribe to it and update the local storage that way. But then I have to keep track of the subscription and dispose of it manually, write more code, and so forth. I'm just trying to see if there is a way to do what I am trying to do: provide an observable getter/setter for local storage.

Ryan Wheale
  • 18,754
  • 5
  • 58
  • 83
  • This is maybe not the best option, but since computed observable evaluates only when some of observables that it depends on changes, you could simply create another observable and make your `currentLogLevel` depend on it, like in this [demo](http://jsfiddle.net/5zz88q5d/). – Ilya Luzyanin Sep 20 '14 at 12:02

2 Answers2

6

You need to come up with a scheme to be notified of any changes to local storage so you can have a way to depend on them.

Decorate the localStorage.setItem() function (and optionally the localStorage.removeItem() function) to notify on any changes. Also listen to the storage event for changes coming from other open tabs.

With this, we'll need to register a dependency on your observable. Looks like the only way is to use an observable as a source of notifications and call it. You can wrap this logic up in a localStorageObservable.

(function () {
    var localStorageObserver = ko.observable();

    function notifier(fn) {
        return function () {
            fn.apply(this, arguments);
            localStorageObserver.notifySubscribers(arguments[0]);
        };
    }
    localStorage.setItem = notifier(localStorage.setItem);
    localStorage.removeItem = notifier(localStorage.removeItem);
    window.addEventListener('storage', function (event) {
        localStorageObserver.notifySubscribers(event.key);
    }, false);
    // not sure how to capture changes in the form:
    //     localStorage.property = value;

    ko.localStorageObservable = function (key) {
        var target = ko.dependentObservable({
            read: function () {
                localStorageObserver(); // register on any changes
                return localStorage.getItem(key);
            },
            write: function (value) {
                localStorage.setItem(key, value);
            }
        });
        target.key = key;
        return target;
    };
}());

With this, you can now sync up with local storage.

_.extend(DevUtilitiesViewModel.prototype, {
    ...
    logLevelsArray: ['error', 'warning', 'info', 'debug'],
    currentLogLevel: ko.localStorageObservable('logger-level'),
    ...
});
Jeff Mercado
  • 113,921
  • 25
  • 227
  • 248
  • Though, keep in mind that items are stored as strings so if you do set values to this "observable", what you get back will be strings. – Jeff Mercado Sep 20 '14 at 16:27
  • Brilliant man. Just curious - is the wrapper around `setItem` even necessary since we're listening to the "storage" event? Embarrassed to say I was not aware of the storage event until now. Opens up a bunch of doors. – Ryan Wheale Sep 20 '14 at 21:03
  • I think it is. From what I've found about it, the storage event will fire _only_ for changes on other tabs/windows. It will not fire for changes on the current tab. – Jeff Mercado Sep 20 '14 at 21:48
  • I just noticed that my initial implementation did not work. I used the `ko.dependencyDetection` object which is not exposed in the public API. This has been fixed. – Jeff Mercado Oct 04 '14 at 07:09
  • Yeah, I figured that out too. Your answer set me in the right direction though. Another problem is you can't set instance methods on localStorage as FF and IE8 automatically convert all property values to strings. You must update the Storage prototype. I will post my code on Monday, as it works in all browsers. – Ryan Wheale Oct 05 '14 at 18:12
  • If you do decorate the function on the Storage prototype, you might want to make sure that you differentiate between which storage is being accessed. Whether it is local, session or global. Depending on the level of granularity you want. – Jeff Mercado Oct 05 '14 at 18:24
  • yeah, I got that figured out too. I would post the code now but it's on a different computer. Another quirk in IE8 is that you can't overwrite the prototype for some reason, but it fires the "storage" event for all actions... even on your current window. – Ryan Wheale Oct 05 '14 at 20:01
1

If you only want to detect when you use your writable computable's write method, you can use a observable to handle the notification pretty easily:

currentLogLevel: ko.computed({
    owner: ko.observable(localStorage.getItem("logger-level")),
    read: function() { return this() },
    write: function( newValue ) {
        localStorage.setItem("logger-level", newValue);
        this( newValue );
    }
})

Bear in mind though that this won't detect any exterior modification of localStorage

rampion
  • 82,104
  • 41
  • 185
  • 301