views:

512

answers:

1

I'm trying to move elements between two lists. Elements when clicked should move to the other list. Prototype JS code:

document.observe('dom:loaded', function() {

  $$('li.available-item').each(function(element){
    element.observe('click', function(){
      element.removeClassName('available-item');
      element.addClassName('selected-item');
      $('selected-list').insert(element);
    });
  });

  $$('li.selected-item').each(function(element){
    element.observe('click', function(){
      element.removeClassName('selected-item');
      element.addClassName('available-item');
      $('available-list').insert(element);
    });
  });

});

Sample elements:

<ul id="available-list">
  <li class="available-item">Apple</li>
  <li class="available-item">Orange</li>
  <li class="available-item">Grapes</li>
</ul>

<ul id="selected-list">
  <li class="selected-item">Pineapple</li>
  <li class="selected-item">Papaya</li>
</ul>

While it's working the first time I'm clicking on the elements, once an element moves to the other list they don't move to the other (original) list when clicked.

What am I doing wrong here?

+2  A: 

The reason is that when you move an item, it's still got the old event handler attached that will just move it again.

This is a classic place for using event delegation. Instead of watching for clicks on the elements, look for clicks on the lists (since clicks bubble up) and then move the relevant element. Something like this:

$('available-list').observe('click', function(event) {
    var li;

    li = event.findElement('li');
    if (li) {
        event.stop();
        li.addClassName('selected-item').removeClassName('available-item');
        $('selected-list').insert(li);
    }
});

...and the converse for selected-list.

You could even use just one handler for both lists:

$('available-list').observe('click', listClick);
$('selected-list').observe('click', listClick);

function listClick(event) {
    var fromType, toType, li;

    // Get the clicked list item
    li = event.findElement('li');
    if (li) {
        // We're handling it
        event.stop();

        // Determine whether moving to the selected or available list
        if (this.id.startsWith("selected")) {
            fromType = "selected";
            totype   = "available";
        }
        else {
            fromType = "available";
            totype   = "selected";
        }

        // Update class names
        li.addClassName(toType + '-item').removeClassName(fromType + '-item');
        $(toType + '-list').insert(li);
    }
});

It gets even simpler if you ditch the classes on the items (see below):

$('available-list').observe('click', listClick);
$('selected-list').observe('click', listClick);

function listClick(event) {
    var targetList, li;

    // Get the clicked list item
    li = event.findElement('li');
    if (li) {
        event.stop();
        targetList = this.id.startsWith("selected") ? "available-list" : "selected-list";
        $(targetList).insert(li);
    }
});

Somewhat OT, but you may not need those selected-item and available-item classes at all. With the above, you don't need them to find them anymore, and in your CSS, you can use descendant selectors for styling the elements:

#selected-list li {
    /* ...styles for the `li` elements in the `selected-list` ... */
}
#available-list li {
    /* ...styles for the `li` elements in the `available-list` ... */
}

If you only want to affect lis that are direct children of the lists, use child selectors instead of descendant selectors (note the >):

#selected-list > li {
    /* ...styles for the `li` elements in the `selected-list` ... */
}
#available-list > li {
    /* ...styles for the `li` elements in the `available-list` ... */
}
T.J. Crowder
Thanks a bunch!Yeah, I could understand that the old event handler wasn't being forgotten, but didn't know the way out. I believe in jQuery, the `.live()` method offers an easier solution for this.Didn't think along that line, of finding the element which was clicked and then manipulating it.Thanks again for your comprehensive reply.
asif
Just read the documentation for `findElement()`. Isn't using `Event.element()` better in this context, since it's accurate?
asif
@asif: Happy to help. I think jQuery's "live" is pretty much event delegation with some plumbing done for you. Re your second comment, `#findElement` without any CSS selector *is* `#element`. I'm told by Tobie Langel (one of the two chief project owners of Prototype) that he's planning to deprecate `element`, so that's why even when not using a selector I use `#findElement`.
T.J. Crowder
@asif (continuing): Also, note that in this case, if you have anything *inside* the `li` s -- like `span` s, for instance -- and the user clicks the text in the `span`, then `#element` will return the `span`, not the `li`. That's why the example I gave about *does* pass a CSS selector to `#findElement` specifying we're looking for the `li`. If the user clicks a `span` within the `li`, `#findElement` will look at the `span`, see it's not a match for the selector we gave, and look at its parent, etc. Very handy. :-)
T.J. Crowder
@TJ. I've just seen it as deprecated in the api.prototypejs.org site. "Returns the DOM element on which the event occurred. This method is deprecated, use findElement instead."
Thorpe Obazee
@TJ: thanks for the clarity. `#findElement` is very handy, I can see that now :)
asif
@asif: Ah, good, they've updated the docs. When Tobie first told me it was deprecated, I and several others on the mailing list said "Um, where?" ;-)
T.J. Crowder