Jan Amoyo

on software development and possibly other things

Using LocalStorage to Publish Messages Across Browser Windows

No comments
Below is a simple JavaScript utility for publishing messages across browser windows of the same domain. This implementation uses the browser's localStorage and the storage event to simulate the behavior of an inter-window topic.
(function (global, window) {
  function Publisher() {
    var PUBLISH_PREFIX = 'publish_';
    this.publish = function (topic, message) {
      message.source    = window.name;
      message.timestamp = Date.now();
      window.localStorage.setItem(PUBLISH_PREFIX + topic,
        JSON.stringify(message));
    };
    this.subscribe = function (topic, callback, alias) {
      var subscriber = function (event) {
        if (event.key === PUBLISH_PREFIX + topic
            && event.newValue !== null) {
          callback(JSON.parse(event.newValue));
        }
      };
      window.addEventListener('storage', subscriber);
      return function () {
        window.removeEventListener('storage', subscriber);
      };
    };
  }
  global.jramoyo = { Publisher: new Publisher() };
})(this, window);
Lines 5 and 6 adds a source and timestamp property to the message to ensure uniqueness
Lines 7 and 8 converts the message to JSON and saves it to the localStorage
Lines 12 and 13 uses the event.key to filter which message should be processed by the callback
Line 14 converts the JSON value to a message objects and passes it as an argument to the callback
Lines 18-20 returns a function that when called, removes the subscriber from the topic

Below is a sample code from a publishing window:
jramoyo.Publisher.publish('greeting_topic', {
    name: 'Kyle Katarn'
});
And here is a sample code from a subscribing window:
jramoyo.Publisher.subscribe('greeting_topic',
    function (message) {
        alert('Greetings, ' + message.name);
    });

This works because every time an item is stored in the localStorage, all browser windows sharing the same localStorage will receive a storage event detailing what has changed (except for the window that wrote to the localStorage).

However, the problem with the above implementation is that it doesn't scale if the number of subscribers increases. Every time a storage event is fired, the JavaScript engine will have to iterate through each listener regardless whether the listener is interested in the event or not.

To address this problem, we can use a map to index the callbacks against the event.key. Below is the updated version of the above code:
(function (global, window) {
  function Publisher() {
    var PUBLISH_PREFIX = 'publish_';
    var listeners = []; 
    window.addEventListener('storage',
      function storageListener(event) {
        var array = listeners[event.key];
        if (array && array.length > 0) {
          var message = JSON.parse(event.newValue);
          array.forEach(function (listener) {
            listener(message);
          });
        }
      }, false);
    this.publish = function (topic, message) {
      message.source    = window.name;
      message.timestamp = Date.now();
      window.localStorage.setItem(PUBLISH_PREFIX + topic,
        JSON.stringify(message));
    }; 
    this.subscribe = function (topic, callback, alias) {
      var key = PUBLISH_PREFIX + topic,
        array = listeners[key];
      if (!array) {
        array = []; listeners[key] = array;
      }
      array.push(callback);
      return function () {
        array.splice(array.indexOf(callback), 1);
      };
    };
  }
  global.jramoyo = { Publisher: new Publisher() };
})(this, window);
Lines 5-14 registers a single storage event listener that uses a map to look-up callbacks identified by the event.key
Lines 22-27 saves the callback into the listeners map, identified by the derived key

No comments :

Post a Comment

Increasing ngRepeat Limit on Scroll

No comments
The example below shows how to increase the limitTo filter of ngRepeat everytime the div scrollbar reaches the bottom.

This technique is used to improve performance by only rendering ngRepeat instances that are visible from the view.

First, we create a directive that calls a function whenever the div scrollbar reaches the bottom:
module.exports = function (_module) {
  'use strict';
  _module.directive('bufferedScroll', function ($parse) {
    return function ($scope, element, attrs) {
      var handler = $parse(attrs.bufferedScroll);
      element.scroll(function (evt) {
        var scrollTop    = element[0].scrollTop,
            scrollHeight = element[0].scrollHeight,
            offsetHeight = element[0].offsetHeight;
        if (scrollTop === (scrollHeight - offsetHeight)) {
          $scope.$apply(function () {
            handler($scope);
          });
        }
      });
    };
  });
};
Line 5 compiles the expression passed to the directive.
Line 6 listens for scroll events on the directive element (requires jQuery).
Line 10 checks if the scrollbar has reached the bottom.
Line 12 applies the compiled expression to the scope.

Then, we apply the directive to our view:
<div buffered-scroll="increaseLimit();" ng-init="limit=15;">
  <table>
    <tr ng-repeat="item in items | limitTo:limit">
      ...
    </tr>
  </table>
</div>
Line 1 assigns a function expression to the directive and initializes the limit variable.
Line 3 declares the ngRepeat with a limitTo filter.

Finally, we create the scope function that increases the limit variable if the value is still less than the number of items iterated by ngRepeat.
$scope.increaseLimit = function () {
  if ($scope.limit < $scope.items.length) {
    $scope.limit += 15;
  }
};

A working example is available at Plunker.

The same technique can be used to implement "infinite scroll" by calling a function that appends data from the server instead of increasing the limitTo filter.

No comments :

Post a Comment