Nifty DOM output stream class

| 7 Comments

I was porting some JavaScript code that used the TrimPath template library. The templating system was too much for my needs, so I came up with the simple little class below. It simulates a Java-style output stream. Pass the id of a DOM element to the constructor to get started. Call the clear() method first, if necessary. Then call the write() method as many times as needed. Finally, always remember to call flush() to force your output to appear.

// A little class for outputting HTML into a DOM element
function DOMStream(id) {
    this.elt = document.getElementById(id);
    this.buffer = [];
}
DOMStream.prototype.clear = function() { this.elt.innerHTML = ""; };
DOMStream.prototype.write = function() {
    for(var i = 0; i < arguments.length; i++) this.buffer.push(arguments[i]);
};
DOMStream.prototype.flush = function() {
    this.elt.innerHTML += this.buffer.join("");
    this.buffer.length = 0;
};

If it is not obvious, an important point of this class it to avoid inefficient repeated string concatenation. This is why it uses an array as a buffer, and why the write() method accepts multiple arguments--so you don't have to concatenate them yourself.

Here's a sample usage:

var out = new DOMStream("placeholder");
out.write('<h2>', section_title, '</h2>');
outputTable(out, data);  // Function defined elsewhere
out.flush();

With this class defined, I ported the trimpath template to equivalent JavaScript functions, and ended up with a modest reduction in the number of lines of code (not counting the substantial reduction in lines of code I got by no longer using the templating library.)

7 Comments

Wouldn’t this more correctly be called a DOMBuffer? After all, you *don’t* stream the output.

Also, wouldn’t it be slightly more efficient if `this.buffer` were a string that each argument gets appended to, rather than an array that each argument is pushed onto? It’d use slightly less memory throughout the lifetime of the time object and would make the eventual `join("")` (which builds an extra copy of the data) unnecessary.

Aristotle,

Thanks for your thoughts. I call it a stream because it has write() and flush() methods like a stream (I'm using the Java stream API as a model).

I push all the arguments onto an array to avoid repeated string concatenations. Each concatenation would create a new object that would exist only until the next concatenation. I haven't tested it, but my assumption is that this way is more efficient.

You are right however, that my implementation keeps a bunch of objects hanging around in the buffer array, until you call flush().

I agree with you David. From my experience, holding strings in an Array with an eventual join("") is far more effective then repeatedly concatenating strings using += (which misleads you to think you are changing an existing object and not creating a new one)

I do have two comments about this interesting DOMStream class:
First, I think your constructor should accept objects, and not only IDs, in case you want to write to an object you already have reference to, but not necessarily has an ID, so I'd put it like that:

function DOMStream(element) {
this.elt = element.innerHTML == undefined ? document.getElementById(element) : element;
this.buffer = [];
}

In addition, you can avoid looping through the arguments array in the write() function, which comes to overcome the limitation that the arguments object is not a real array, by using the apply method of the Function object:

DOMStream.prototype.write = function() {
this.buffer.push.apply(this.buffer, arguments);
};


This will shorten your code, probably make it run faster, and show everyone you are a sophisticated JavaScript writer (not that anyone doubts it...)

Splintor,

Thanks for your suggestions. I wrote the constructor to require an id because I was going for extreme simplicity.

The idea of using apply() is excellent. Thanks!

I converted the class to Prototype, added the log method and scriptdoc.

/**
* @classDescription A little class for outputting HTML into a DOM element
* @type {Object}
*/
var DOMStream = Class.create();
DOMStream.prototype = {
/**
* @param {String} id
* @constructor
*/
initialize: function(id) {
this.elt = document.getElementById(id);
this.buffer = [];
},
/**
* @method
*/
clear: function() {
this.elt.innerHTML = "";
},
/**
* @method
* @param {Array} objects to write
*/
write: function() {
this.buffer.push.apply(this.buffer, arguments);
},
/**
* @method
* @param {Array} objects to log
*/
log: function() {
this.buffer.push(new Date().toLocaleTimeString(), ': ');
this.buffer.push.apply(this.buffer, arguments);
this.buffer.push('');
},
/**
* @method
*/
flush: function() {
this.elt.innerHTML += this.buffer.join('');
this.buffer.length = 0;
}
}

David,

Apparently the Moz team created internal optimizations for string concatenation, so that it is actually faster than using the array approach. I didn't find this, apparently Alex Russell and David Schontzer did, and fixed my Builder constructor in Dojo (based on the .NET StringBuilder object) to use it if the environment was Moz.

Something to consider if you're looking for that little oomph in performance gain.

Tom

Tom,

Thanks for the optimization tip. I hadn't heard about this before.

Les,

Thanks for the docs. I haven't tried using Prototype yet, and I'm not convinced I like its technique for creating classes. By assigning the prototype object directly as you do, you loose the constructor property that is automatically created for you...

Books

Comprehensive coverage of Ruby 1.8 and 1.9

"The New Most Important Ruby Book"
Peter Cooper,
rubyinside.com

Completely updated for Ajax and Web 2.0

"A must-have reference"
Brendan Eich,
creator of JavaScript

The classic Java quick-reference

Advertising

Pages

Hosted By

Powered by Movable Type 4.21-en