Christian Montoya

How I learned to stop programming and love the DOM

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:

  • When you hit the down arrow, you move from the currently selected container to the next container. With 0.2, I did this as so:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    if(containerCtr < 2) {
      // only zero or one containers, so quit
      return false;
    }
    var container = '#constructContainer-' + containerPtr;
    $(container).removeClass('selected');
    // point to next container
    containerPtr = containerPtr + 1; 
    // wrap around to first 
    if(containerPtr >= containerCtr) {
      containerPtr = 0;
    }
    container = '#constructContainer-' + containerPtr;
    $(container).addClass('selected');
    Notice how I was building the ID of the container I wanted to target twice so that I could actually target it. Now, with 0.3, I did:
    1
    2
    3
    4
    5
    6
    7
    8
    
    if( $('.container').size() < 2 ) {
      return false;
    }
    $('.container.selected').removeClass('selected').next('.container').addClass('selected');
    if( $('.container.selected').size() < 1 ) {
      // we were at the last child, so select the first one (wrap down)
      $('.container:first').addClass('selected');
    }
    Every line of code is based on jQuery, and there are no counters or pointers involved; I'm just traversing the DOM and making changes as I go.
  • To toggle the class "last" on the currently selected column in 0.2:
    1
    2
    
    var column = '#constructColumn-' + containerPtr + '-' + columnPtrs[containerPtr];
    $(column).toggleClass('last');
    In 0.3:
    1
    
    $('.container.selected .column.selected').toggleClass('last');
  • Adding a container in 0.2:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    var containerElPre = '<div class="container selected" id="constructContainer-';
    var containerElSuf = '"></div>';
     
    // create the container element with a unique ID
    var containerEl = '' + containerElPre + containerCtr + containerElSuf;
    // append this container to the #construct space
    $('#construct').append(containerEl);
     
    // need to remove .selected if there is a previous container
    if(containerCtr > 0) {
      // get the # for the previous container
      var previousContainer = '#constructContainer-' + containerPtr;
      $(previousContainer).removeClass('selected');
    }
     
    // set the current pointer to this container
    containerPtr = containerCtr;
    // add a counter for the columns in this container
    columnCtrs[containerPtr] = 0;
    // increment the number of containers
    containerCtr++;
     
    // add "onclick" event to select by click
    $('#constructContainer-' + containerPtr).click(function(){
      containerClick(this); // another long function
      return false;
    });
    The same exact thing in 0.3:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    var containerEl = '<div class="container selected"></div>';
     
    // deselect any currently selected container
    $('.container.selected').removeClass('selected');
    // append a new container to the #construct space
    $('#construct').append(containerEl);
     
    // add "onclick" event to select by click
    $('.container:last').click(function(){
      containerClick(this); // a very short function
      return false;
    });
    As far as improvements go, that is probably the best one.

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 :)

Thank you for reading • Published on December 10th, 2007 • Please take a moment to share this with your friends