Saturday, April 6, 2013

Callbacks: A Necessary Evil When Refactoring JavaScript Classes (sometimes)

‹prev | My Chain | next›

I continue my efforts to refactor my monolithic ICE Code Editor out into separate JavaScript classes. I need this because I have at least two use cases for ICE and too many conditionals strewn about to handle them.

There was so much code that it was hard to know where to begin, but I started with the newer requirements—the ability to embed ICE directly into web pages:



In the original version of the code, changes to the code wound up doing two things: updating the preview layer and saving the code to a local data store. When the ACE code editor content is first established, the handleChange() callback is established:
function setContent(data) {
  editor.getSession().removeListener('change', handleChange);
  editor.setValue(data, -1);
  editor.getSession().setUndoManager(new UndoManager());
  editor.getSession().on('change', handleChange);
  update(); // visualization layer
}
The handleChange() function is then called on subsequent updates. It immediately saves the contents to localStorage and calls the resetTimer() which will update the preview layer in 1.5 seconds (the delay prevents updates for every little change). I do not need this local data store when embedding ICE, so only part of this code needs to move into the new ICE.Embedded and ICE.Editor classes.

I already have the resetTimer() method in my new ICE.Editor class:
Editor.prototype.resetUpdateTimer = function() {
  var that = this;
  clearTimeout(this.update_timer);
  this.update_timer = setTimeout(
    function() { that.updatePreview(); that.update_timer = undefined; },
    1.5 * 1000
  );
};
Now I need to figure out how to get those handleChange() functions to work in an object setting. That turns out to be fairly straight-forward. I define the handleChange() function directly in the method:
Editor.prototype.setContent = function(data) {
  var that = this;
  function handleChange() {
    that.resetUpdateTimer();
  }

  this.editor.getSession().removeListener('change', handleChange);
  this.editor.setValue(data, -1);
  this.editor.getSession().setUndoManager(new UndoManager());
  this.editor.getSession().on('change', handleChange);
  this.updatePreview();
};
This ensures that removing the listener will work since the function will remain the same between calls. It is also a cheap way to maintain the current object context.

Now comes the first real split. I need the ability for the embedded ICE editor to timeout after a certain amount of time. This is a requirement specific to the embedded version of the editor, so it needs to go in last night's ICE.Embedded class:
Embedded.prototype.timeoutPreview = function() {
  var that = this;
  clearTimeout(this.embed_timeout);
  this.embed_timeout = setTimeout(
    function() {
      that.editor.hidePreview();
    },
    3*1000
  );
};
The editor class will have to hide the preview (remove the iframe that holds the visualization layer) in the hidePreview() method. The trouble is, I need to clear the embedded timeout if a reader happens to make a change. Unfortunately the Editor class knows when updates to the content occur. This is how callbacks start.

For now, I keep it simple. I add an onUpdate option to the Editor constructor:
function Editor(el, options) {
  this.el = el;

  if (typeof(options) != "object") options = {};
  this.edit_only = !!options.edit_only;
  this.onUpdate = options.onUpdate || function(){};
  // ...
}
I then call this method whenever the preview layer is updated:
Editor.prototype.updatePreview = function() {
  if (this.edit_only) return;
  // Code to update the preview layer here...
  this.onUpdate();
};
Finally, I need the Embedded object to request that, when ever the editor is updated, it should result in a call to reset the disable-preview timer:
function Embedded(script) {
  this.script = script;
  this.sourcecode = this.processSource();
  this.el = this.createEmbeddedElement();

  var that = this;
  this.editor = new ICE.Editor(this.el, {
    onUpdate: function() {that.timeoutPreview();}
  });
  this.editor.setContent(this.sourcecode);
  this.editor.onUpdate();
}
This is less glamorous than the first efforts to separate my codebase, but this seems a reasonable separateion of concerns overall. Best of all, it works. I can now embed the new, class-based ICE Code Editor in web pages with a time to prevent the visualization from running on and on, sucking up CPU.

With this, I also realize my first improvement. The embedded version of the ICE Code Editor is very close to fully functional and it no longer saves data to localStorage. There is no reason to be saving these embedded code samples to localStorage—they are example on a web page, not something that people need to have saved automatically. I may add that ability later as an option, but, for now, this is a nice win.


Day #714

No comments:

Post a Comment