Thursday, October 20, 2011

Playing With DOM And ES5

A quick fun post about "how would you solve this real world problem".

The Problem

Given a generic array of strings, create an unordered list of items where each item contains the text of the relative array index without creating a singe leak or reference during the whole procedure.
As plus, make each item in the list clickable so that an alert with current text content occur once clicked.

The assumption is that we are in a standard W3C environment with ES5+ support.

The Reason

I think is one of the most common tasks in Ajax world. We receive an array with some info, we want to display this info to the user and we want to react once the user interact with the list.
If we manage to avoid references we are safer about leaks. If we manage to optimize the procedure, we are also safe about memory consumption over a simplified DOM logic ...
How would you solve this ? Give it a try, then come back for my solution.



The Solution

Here mine:

document.body.appendChild(
/* input */["a","b","c"].map(
function (s, i) {
this.appendChild(
document.createElement("li")
).textContent = s;
return this;
},
document.createElement("ul")
)[0]
).addEventListener(
"click",
function (e) {
if (e.target.nodeName === "LI") {
alert(e.target.textContent);
}
},
false
);

Did you solve it the same way ? :)

9 comments:

joseanpg said...

Nice post Andrea :)

I think that your code has a "leak": you are wasting an array when you use map. It's better with reduce:

document.body.appendChild(
   ['a','b','c'].reduce(
      function (container,str) {
         container.appendChild(document.createElement('LI'))
         .textContent = str;
         return container;
      },
      document.createElement('UL')
   )
).addEventListener('click',
   function (e) {
      if (e.target.nodeName === 'LI') {
         alert(e.target.textContent);
      }
   },
   false
);

Andrea Giammarchi said...

not a leak, ul is not referenced and the array will be an array of references with zero references count and discarded immediately as soon as accessed in index 0 ;)

joseanpg said...

First, did you see the quotation marks around "leak"? :)

Second, with map you create a new transient array, a new task for the garbage collector. Let's replace map by reduce and reduce the work to do by that poor engine ;)

Andrea Giammarchi said...

not sure reduce makes GC life any easier ... is the created array that goes in GC, not the DOM node referenced itself, since the array references counter is zero. It does not matter for this specific task though.

joseanpg said...

It's a map anti-pattern.

You could argue that saving is infinitesimal, but the saving exists.

bga said...

/* imho its cleanest way from point of view FP */
/* i know that is not super optimal :) */
document.body.appendChild(
  document.createElement("ul").appendChild(
    /* classic map-reduce */
    /* input */['a', 'b', 'c'].map(
      function(s, i) {
        const li = document.createElement("li")
        li.textContent = s
        return  li
      }
    )
    /* linear, not homogeneous */
    .reduce(
      function(df, li){ df.appendChild(li); return  df },
      document.createDocumentFragment()
    ) 
  )  
).addEventListener(
  "click",
  function(e) {
    if (e.target.nodeName === "LI") {
      alert(e.target.textContent);
    }
  },
  false
)

joseanpg said...

Very interesting bga ;) but I have a question: is it really necessary to create a DocumentFragment? Note that the UL element hasn't yet been hooked on his father.

On the other hand, if f and g has no free variables and they don't use the this keyword, we can apply "classical" deforestation to that classic map-reduce construction and then we obtain the version above:

        array.map(f).reduce(g,z)

is equivalent to

        array.reduce(g.compose(f),z)

Anonymous said...

Here's another solution, using innerHTML to build the list items.


(/*convert array into clickable set of LI*/
function(parent, element, arr, eventHdlr){

  element.addEventListener('click',eventHdlr,false);

  element.innerHTML = '<li>' + arr.join('</li><li>') + '</li>';

  parent.appendChild(element);
}
)(
  document.body,
  document.createElement('ul'),
  ['a','b','c'],
  function(e){ if( e.target.nodeName==='LI' ){ alert(e.target.textContent); } }
);

JavaScript Countdown Timer said...

Hi...Thanks for the solution. I think including a library and changing its properties. I don't think that's a good idea. If the library changes, your augmenting will probably fail or cause trouble; the defined interfaces of that library on the other hand probably won't change.

Thanks,
Jackie