Monday, August 5, 2013

building custom tags in html5 and javascript, part 2

Several months ago, I toyed around with the idea of building custom, JavaScript powered tags. My motivation for this was simple: It makes building the client-side portion of web application easy. But, it only handles one side of the equation elegantly: binding existing markup to JavaScript objects.

Ideally, I'd like to work the other way around too. I'd like to be able to create a new object and have the corresponding markup generated for me. And I'd like to drop the object right into the DOM without performing any object-to-node conversion.

We can start with a markup template. And we can plan on consuming the data-id attribute to identify nodes within the object [to avoid conflict with any existing id's in the DOM]. So, let's consider a simple "Blurb" ... like so:

<div class='blurb'>
  <h3 data-id="blurbTitle" style='text-decoration: italic;'>Title will go here</h3>
  <p data-id="blurbBody" style='color: silver;'></p>
</div>


And we want a JavaScript "class" definition that shows and hides the body when on click:

var Blurb = function(title, body) {

  this.init = function(title, body) {
    this.setTitle(title);
    this.setBody(body);
  }

  this.setTitle = function(t) {
    this.blurbTitle.innerHTML = t;
  }

  this.setBody = function(b) {
    this.blurbBody.innerHTML = b;
  }

  this.onclick = function() {
    if (this.blurbBody.style.display == 'none') {
      this.blurbBody.style.display = 'block';
    } else {
      this.blurbBody.style.display = 'none';
    }
  }


  this.init(title, body);

}


Using that template and class, we want to be able to throw a Blurb on the page by doing this:

var b = New(Blurb);
b.blurbTitle.innerHTML = "Some Title";
b.body.innerHTML = "Some body text";
document.body.appendChild(b);


Or this:

var b = New(Blurb);
b.setTitle("Some Title");
b.setBody("Some body text");
document.body.appendChild(b);


Or this:

var b = New(Blurb, "Some Title", "Some body text.");
document.body.appendChild(b);


All we're missing is a simple, easy-to-understand link between the template and the class definition. We need a call to something to make the Blurb constructor operate on a node based on the markup from the template, with the title and body nodes imported into the object as properties. (The outer div will be this.)

Sounds easy enough. But, it's a two-step process. Firstly, you'll notice I used a New() function instead of the new keyword, and that's for good reason. The new keyword will create a new this, which won't be our DOM node. And it's pretty tricky, if not impossible, to "consecrate" an existing object as a DOM node. So, by making a simple concession to use a New() function instead we make our job a lot easier.

So, let's write the fundamentals. Let's write a method that applies our Blurb constructor to a DOM node based on our template. (For now, let's assume we're attaching our template markup as text to class definitions themselves. I.e., Blurb.templateMarkup = "<div ... ")

function New(DomClass) {
  // create a container for the template markup
  var rv = document.createElement('div');

  // drop the markup in and grab the first child, which is what we're really after
  rv.innerHTML = DomClass.templateMarkup;
  rv = rv.firstChild;

  // and "bless" the return value with the class constructor, supplying any ~additional~
  // arguments that were supplied to this New() call
  DomClass.apply(rv, Array.prototype.slice.call(arguments, 1));

  return rv;
}


Simple enough. That gets us a DOM node that's "blessed" as an instance of the supplied object. The missing link: import data-id's from the template as object properties. And for now, we'll assume this must be done "pre-blessing" in simple, recursive manner. So, let's create a little helper function to do that work, which we can define and call in the context of New().

function New(DomClass) {
  // create and bless the object as before
  var rv = document.createElement('div');
  rv.innerHTML = DomClass.templateMarkup;
  rv = rv.firstChild;

  // before blessing it, import the properties
  importProperties(rv, rv.childNodes);

  DomClass.apply(rv, Array.prototype.slice.call(arguments, 1));
  return rv;


  // hoisted functions:

  // given a list of nodes, recursively attaches any with data-id's as properties of obj
  function importProperties(obj, nodes) {
    for (var i = 0; i < nodes.length; i++) {
      if (!nodes[i].getAttribute) continue;
      var id = nodes[i].getAttribute('data-id');
      if (id) {
        obj[id] = nodes[i];
        importProperties(obj, nodes[i].childNodes);
      }
    }
  }

}


And we're done. Our desired calls ...

var b = New(Blurb, "Some Title", "Some body text.");
document.body.appendChild(b);


... (etc.) will work as expected now. Give it a try.

Next time, we'll use what we've done here to improve our work from part 1 and bridge any gaps between the two. Hopefully, we'll end up with a robust system for more seamless, elegant, and straightforward interactions between our script and the DOM.

2 comments:

  1. Very Nice Blog I like the way you explained these things. I’ve been looking for ways to improve my website and overall rankings.I hope your future article will help me further.Take SEO Training in Chennai to mould yourself.

    ReplyDelete