The Montoya Herald, a weblog about Blueprint, jQuery, design, music and life, publishing on the web since September 2005. Written by Christian Montoya: developer, designer and entrepreneur.

The Montoya Herald — ChristianMontoya.com

Search

Things I Do

Supported By

Like What I Do?

My Amazon.com Wish List

On this domain

Elsewhere

How I learned to stop programming and love the DOM

Posted on December 10, 2007.

Yesterday I added a couple features to Construct, my visual layout editor based on Blueprint and powered by jQuery, which I first announced here. It was at version 0.2 and I was happy with what I had; you could add containers and columns, move around with keys or the mouse, expand and contract containers, etc. A decent start to a grid-based tool.

The next step was adding the ability to delete containers and columns. This had me a bit concerned, and it started to show that my current implementation was not very robust. As best as I can explain it, this is what I was doing: I treated the layout as an array of arrays, with a counter for the containers and an array of counters for the columns in the containers. I gave an ID to every new container:

var container = '#constructContainer-' + containerCtr;

and to every new column:

var column = '#constructColumn-' + containerPtr + '-' + columnCtrs[containerPtr];

So, add 6 containers and you had, "constructContainer-0" through "constructContainer-5." Add 2 columns in the second container, and you had "constructColumn-1-0" and "constructColumn-1-1." I then used containerPtr and columnPtrs[containerPtr] as the numbers for the currently selected container/column (which also had the class "selected" applied to them). If I needed to modify the currently selected column, for example, I would first build the ID with these numbers, and then apply some jQuery function to add a class or remove a class on that element.

If it is not already obvious why this was so lousy, let me explain further. I started to fret when I began thinking about how I would implement deletion. It was important that the numbers for the IDs of the elements would be consecutive, because I used +1 or -1 to move from container to container and from column to column, and I used an array to store the counter and pointer for the columns in each container. If I were going to delete a container, I would have to first remove that element from the DOM, and then update the IDs of every container after it, decrementing the numbers at then end of the ID for each and every one. At this point I still would not be done, because I would still have to remove an entry from the array of column counters and the array of column pointers, and would have to move the following items in those arrays up to fill the now empty slot, as well as remove the last item afterward. This was going to be gross, and I didn't want to bother trying to write it.

Obviously I had ideas on a better way to build the whole layout logic. I figured a list would be a much better data structure for keeping track of the containers, and lists for each container to keep track of the columns, but I wasn't sure how to write a list in Javascript, and I had some bad experiences still fresh on my mind from trying to make lists work in C last year. Besides, even if I did make some elegant, object-oriented logic model for keeping track of the layout, I still had to figure out a way to make this reference the actual elements being added and removed from the DOM, which would probably require the use of unique IDs, which meant that this still wasn't a solution to the original problem. When I realized this, my eyes glossed over and I decided not to touch this for a while.

Then, some time between when I was adding oil to my car and when I was showering, I had an epiphany. I started to remember the various pages of the jQuery documentation, especially the pages on Selectors and Traversing, and suddenly I realized: the DOM is my data structure, and jQuery provides all the methods I need to access it. I didn't need any separate arrays, counters, pointers, etc; all the information I would ever need was already stored in the DOM by nature, and all I needed to do was use jQuery to read that information and decide where to go and what to do. I knew what was coming: a complete code rewrite.

I rewrote all the code today. It took me an hour to go from scratch to completion, about 15 more minutes to add deletions and tweak a couple things, and the end result was version 0.3, far more simple and yet, far more robust than 0.2. Here are all the ways that jQuery made everything easy:

And here are the 15 minute pair, which would have taken much, much longer to write with the version 0.2 code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function containerDelete() {
  // remove the selected container from the DOM and select the next one
  $('.container.selected').next('.container').addClass('selected');
  $('.container.selected:first').remove();
  if( $('.container.selected').size() < 1 ) {
    // we deleted the last container, so now select the "new" last container 
    $('.container:last').addClass('selected');
  }
  return false;
}
 
function columnDelete() {
  // remove the selected column from the DOM and select the next one
  $('.container.selected .column.selected').next('.column').addClass('selected');
  $('.container.selected .column.selected:first').remove();
  if( $('.container.selected .column.selected').size() < 1 ) {
    // we deleted the last column, so now select the "new" last column
    $('.container.selected .column:last').addClass('selected');
  }
  return false;
}

Straight jQuery that works, and when it's done, there's nothing that has to be updated, no arrays/pointers/counters, and more importantly, I can continue to read the DOM as before, because the DOM is a persistent, robust data structure. At any time I can say, $('.container'); and I have the full array of containers in the DOM, regardless of whether or not there are other elements in between each one. And when I add other elements to the mix, none of this code will have to change, because I can linearize any set of elements in the DOM, regardless of where in the DOM they actually are.

In short, Construct 0.3 is fully DOM-friendly, and this is a model that will be totally flexible and robust from here on. And if you are wondering why anyone would build a visual layout editor with Javascript, in a web browser no less, I hope that explains it, but if not, there's another reason: the best way to do WYSIWYG is to make an interface that allows you to directly edit the final product.

Coming soon: more updates to Construct, specifically a way to generate a layout and stylesheet from the current state. Should be fun :)

Get a trackback link

1 Trackbacks/Pingbacks

  1. Pingback: willoller.com » Montoya and the DOM sittin’ in a tree on December 12, 2007

4 Comments

  1. tana on December 11, 2007

    I know people will flame me but …
    compared to ruby, javascript is useful - but really ugly ;)

    both ruby and python enjoy "def", why does javascript insist on "function" …

    Also, while i like .click i dont like
    .click(function())
    feels cumbersome…

  2. Christian Montoya on December 11, 2007

    tana, I used to hate Javascript's syntax. I still do, but to a lesser extent. But I know where most of it came from. And I don't know what your programming experience is, but C, PHP, etc. all use "function," so it is totally natural to me.

    But I've grown up about syntax, and I've realized that while every programmer wants a language with beautiful syntax, arguing/worrying/complaining about syntax is stupid. I don't care if it's def or function, class or object, import or include. I care about what I can do with the language, and that's what this is all about.

  3. Dave on December 11, 2007

    You're so right on this, too many people try to reproduce their data structures in Javascript rather than take advantage of the DOM. jQuery makes it so easy to traverse the tree that it takes away the excuses for doing that–as long as you're careful about your traversals.

    A bare $(".classname") can be very expensive on a large document because there is no easy way to find the matching nodes; it has to go through the entire document. If you can do something like $("#construct .classname") it can save a lot of time by avoiding the other junk in the page.

    If containerClick returns false you can just put it directly in the click() instead of wrapping it in an anonymous function:

    // deselect any currently selected container
    $('#construct .container.selected').removeClass('selected');
    // append a new container to the #construct space
    $('<div class="container selected"></div>')
       .appendTo('#construct').click(containerClick);
    

    Also, chaining more will reduce the number of redundant or partially redundant selectors, which will usually make things faster (and shorter).

  4. Christian Montoya on December 12, 2007

    Dave, thank you so much for those tips, I will definitely incorporate them into the Construct code.

Leave a comment

Use Markdown or basic HTML. For posting code, use Postable. Please keep comments respectful and on topic.