Low Pro: Unobtrusive Scripting For Prototype

Low Pro is a set of extensions to the Prototype library that make it easier to implement unobtrusive DOM scripting and forms the JavaScript component of the Unobtrusive JavaScript Plugin For Rails but can be used separately. It’s essentially a compilation of various tried and tested, previously published techniques adapted for use with Prototype and mainly centres around enhancing event handling and DOM manipulation. Low Pro uses portions of code and/or inspiration from Dean Edwards, Matthias Miller, Sylvian Zimmer, Justin Palmer and John Resig.

To start out with Low Pro, download the 0.2 distribution from my Subversion repository and included it along with the Prototype JavaScript file. Low Pro depends on Prototype so don’t forget to include it first:

<script src="/js/prototype.js" type="text/javascipt"></script>
<script src="/js/lowpro.js" type="text/javascipt"></script>

In the distribution is a standard commented version and a compressed version. Now for a quick tour of the features.

DOM navigation and manipulation

As Prototype’s DOM manipulation is very innerHTML centred Low Pro adds few useful methods to elements to help you navigate and manipulate the DOM in the nice clean W3C way.

$('doomed').remove(); // removes the element from the DOM and returns it
$('item').nextElement(); // returns the next element in the document (excluding text nodes)
$('item').previousElement(); // guess...
$('thing').insertAfter(element); // the DOM gives you insertBefore but not this
$('bong').replaceElement(element); // replaces an element and returns the substitute

You can get to all of these methods via the DOM object as well.

DOM.remove(element);
DOM.insertAfter(oneElement, anotherElement);

Also, Low Pro aids you in creating node structures via the DOM with a Prototype version of DOM Builder. Each HTML tag has a builder function called $[tag]. You can nest these to create complex DOM structures easily.

var listItem = $li({ id : 'item-1' }, 
                 $strong("Some text")
               ); 
// returns a node equivilent to: <li id="item-1"><strong>Some text</strong></li>

$('a_list').appendChild(listItem);

Read more about DOM builder over at The Web’s Bollocks.

Events

The event handling code in Prototype is one of it’s weakest points so Low Pro replaces out Event.observe() and Event.stopObserving() totally with a version of Dean Edwards’ addEvent() and removeEvent() routines. This offers much more consistency across browsers and a few neat fixes:

Event.observe('thing', 'click', function(e) {
  this.hide()  // this refers to the triggering element.
  return false; // stops the default behaviour (even in Safari!)
});

All other methods remain backwards compatible. For convenience, observe() and stopObserving() are mixed in to elements. A trigger method is also provided to allow you to trigger event handlers programmatically.

Event.trigger(element, 'click');

Event.onReady() allows you to stack up callbacks that will trigger as soon as the DOM is ready rather than onload which triggers after the whole page is loaded.

Event.onReady(function() {
  $('thing').visualEffect('highlight');
});

Repeated calls will simply stack up callbacks.

Behaviours

Low Pro adds declaritive behaviours to Prototype. Event.addBehavior() allows you to specify element behaviours via CSS selectors.

Event.addBehavior({
  'a.todo:click' : function(e) {
    new Ajax.Request('todo/add', ... );
  },
  'div.feature:mouseover' : function(e) {
    this.hide();
  }
});

These behaviours are applied as soon as the DOM is loaded and are reapplied to new elements after Ajax calls. An alternative more OO approach is to create a Behaviour ‘class’ which can be attached to elements. Each element gets it’s own instance of the behaviour class that retains it’s state throughout the life of the page. Within behaviour classes, this.element always points to the attached element.

var Resize = Behavior.create({
  initialize : function() {
    // gets called when the element is loaded.
  },
  onclick : function(e) {
    // use on methods to set event handlers on a object
  }
});

Finally, I’ve included Sylvian Zimmer’s optimisation of the $$ selector which speeds up node selections quite dramatically. It is, in my experience, a little bit buggy though so if you encounter problems you can revert to the normall $$ code.

LowPro.optimize$$ = false;

So, that’s it. Any feature suggestions are much appreciated as are bug reports. Please log them on my Trac and I’ll fix them ASAP.

UPDATE: Low Pro is compatible with any of the Prototype 1.5 releases but will not work with 1.4 or below.

32 Comments (Closed)

Great work!

MarioMario at 06.09.06 / 13PM

Hi Great, I really like it. I have a question. How did you generate the packed version?

Seb

SebSeb at 06.09.06 / 13PM

Seb: I have a Perl version of Dean Edwards’ JavaScript packer that I run the source through via a RakeFile. I’m in the process of making a Ruby version of this at the moment so soon it will integerate into Rake more easily.

DanDan at 06.09.06 / 14PM

Great! Btw, you should convert your syntax highlighting colors to a TextMate theme :) Very nice.

MartinMartin at 06.09.06 / 14PM

Martin, Vibrant Ink is quite close.

Luke RedpathLuke Redpath at 06.09.06 / 20PM

Great stuff, I’d love to see some of these ideas integrated into Prototype.

I tend to stick with the bigger javascript libraries, because they are more likely to be supported down the road.

I use Behaviour.js but that hasn’t been updated in a while, which is worrisome.

So integrating stuff like this into Prototype would greatly benefit the community.

RobRob at 07.09.06 / 17PM

This is fantastic. I’ve injected this into a production site and nothing broke, even in IE! After the I updated my code to leverage the new features, considerably shorting my code and making the whole thing more maintanable.

I’m loving the new DOM methods

MislavMislav at 08.09.06 / 13PM

It looks like this does a all of the things Justin Palmers event:Selectors does (plus a bunch more!). I understand that Justin worked on this, aswell. Can any one shed some light on how much of event:Selectors functuinality in in here?

Thanks.

AlexAlex at 08.09.06 / 18PM

Basically, I was trying to replace eventSelectors with Event.addBehavior. Trying to do something like this:

Event.addBehavior({ ‘div#navItems ul.sortable’ : function(element) { Sortable.create(element,{dropOnEmpty:false,containment:[element],constraint:false,onUpdate:updateOrder}); } });

...fails. element==undefined.

Setting “LowPro.optimize$$ = false” produces a different error: “expression has no properties” from prototype 1.5.0_pre1.

petepete at 09.09.06 / 00AM

This is the right way to do what you posted:

Event.onReady(function() { var sortable = $$(‘div#navItems ul.sortable’); Sortable.create(aqtree3clickable,{dropOnEmpty:false,containment:[element],constraint:false,onUpdate:updateNtreeItemOrder}); });

That will give you the same effect as EventSelectors.start(...);

Yours would ot work because it is an event object that would be returned into element, not an html Element.

finalarenafinalarena at 09.09.06 / 01AM

...in posting that I just realised that Event.onReady is really quite different to use than eventSelectors. For example, in eventSelectors, a rule gets applied to every matching html element. Whearas, using onReady you have to manually iterate the results set from $$.

e.g. to activate a set of nested sortables:

eventSelectors:

var Rules = { ‘div#navItems ul.sortable’ : function(element) { Sortable.create(element,{dropOnEmpty:false,containment:[element],constraint:false,onUpdate:updateNtreeItemOrder}); } EventSelectors.start(Rules);


onReady:

Event.onReady(function() { var ul_sortables = $$(‘div#navItems ul.sortable’);

ul_sortables.each(function(node){ 
                              Sortable.create(node,{dropOnEmpty:false,containment:[node],constraint:false,onUpdate:updateNtreeItemOrder});
                    });
});

finalarenafinalarena at 09.09.06 / 01AM

pete: The reason your code isn’t working is that the element isn’t passed to the handler function this points to the element as in a normal event handler. This is different than event selectors but allows use exactly the same code as you would in inline event attributes (which I did so that it would ease peoples transition to unobtrusive scripting).

finalarena: You don’t need to use Event.onReady to start behaviours. Any behaviours added with Event.addBehaviors automatically gets triggered when the DOM is ready. In the words of some Apple marketing bloke: It Just Works. Taking the triggering of the behaviors out of the hands of the developer is one of the main improvements on event selectors.

Alex: Event.addBehaviors() doesn’t contain any of Justin’s code but was of course heavily inspired by it. It does everything event selectors does but is a little easier to use (see above) and contains a few extra features that’ll I’d be writting more about.

DanDan at 09.09.06 / 12PM

Thanks Dan. I get the use of Event.addBehaviors, except that in that case posted there is no useful event that can be expressed as a css selector.:

In an example where i want to make a nested UL sortable:

Event.addBehavior({
'ul.sortabe' : function(e) {
  alert(e);
}
});

e will be undefined as there is no ‘loaded’ event.

Event.addBehavior({
'ul.sortabe:click' : function(e) {
  alert(e);
}
});

will work as ‘e’ is the click mouseEvent. But in this latter case, how do I get hold of the element that was clicked?

finalarenafinalarena at 09.09.06 / 12PM

finalarena: this refers to the element.

DanDan at 09.09.06 / 12PM

Trackback: Low Pro macht Prototype weniger aufdringlich Low Pro bildet eigentlich die JavaScript-Hälfte des Unobtrusive JavaScript Plugin For Rails. Aber auch ohne Rails empfiehlt es sich das Set von JavaScript-Klassen zusammen mit der Prototype-Bibiothek zu nutzen.

Low Pro erweitert die Fähigkeiten von Prototype um…

Dirk GinaderDirk Ginader at 11.09.06 / 08AM

Anybody tried use lowpro with json.js?

TonyTony at 12.09.06 / 12PM

Can someone explain to me why this.nodeType returns undefined, as when I use the none OO approach this works fine?

var bwFieldHelp = Behavior.create({ initialize : function() { alert(this.nodeType); } }); Event.addBehavior({ 'span.infotext' : bwFieldHelp });

AndriAndri at 12.09.06 / 13PM

“Within behaviour classes, this.element always points to the attached element.”

Note to self: RTFM :)

AndriAndri at 12.09.06 / 13PM

excellent! I have a few methods i’ve been using to implement prototype unobtrusively as well. thank you for putting it into such a neat package. it’s much appreciated, and will be well used. now someone just needs to fix the retardedness of .foreach

naterkanenaterkane at 15.09.06 / 23PM

I’ve found a bug in the optimized $$-function (both the original and Justin Palmer’s rewritten version). Take a look at these examples:

<!-- html --> <p><a href="#"><span id="span-a">A</span></a></p> <p><span id="span-b">B</span></p> // js, using the selectorLite addon console.log($$('p a span')); // => [<span id="span-a">,<span id="span-b">]

The $$-call should not return span-b since it’s not wrapped in an A-element.

It works when the “deeper”/more nested html is after the less “deep” html

<!-- html --> <p><span id="span-b">B</span></p> <p><a href="#"><span id="span-a">A</span></a></p> // js, using the selectorLite addon console.log($$('p a span')); // => [<span id="span-a">,<span id="span-b">]

The deal is that this.index will get increased (++this.index) for the first P and it’s children. When the SPAN is found it’ll continue to the next P and now this.index pointer isn’t reset to start looking for an A in the P. It just checks the P for containing SPANs.

Maybe not the best description but I think you get the point.

Martin StrömMartin Ström at 16.09.06 / 11AM

Maybe a good enhacement for future versions would be a multi css selector on behaviours.

For example with this code:

.miTag1) { goToMyFunction(e); },

.miTag2) { goToMyFunction(e); }

It would be nice to make this

.miTag1, .miTag2) { goToMyFunction(e); }

In event-selectors this is correct but when I converted my site to addopt lowpro, I had to rewrite my multi selectors.

Very good library btw!!

Javier MartinezJavier Martinez at 18.09.06 / 10AM

Could you help me with the following?

var deleteButtonSelector = ’#deleteButton’; Event.addBehavior({ deleteButtonSelector : deleteButtonBehaviour });

...this fails silently. Whereas:

Event.addBehavior({ ’#deleteButton’: deleteButtonBehaviour });

...works as expected. Any ideas?

justAsimple...justAsimple... at 19.09.06 / 13PM

justAsimple: You can’t use variables as the key in an object literal like that. You could do this instead:

@var behaviours = {}; behaviours[deleteButtonSelector] = deleteButtonBehaviour; // ... and so on….

Event.addBehaviour(behaviours);@

DanDan at 19.09.06 / 13PM

Fantastic. Thanks for great help and a great set of extensions.

justAsimple...justAsimple... at 19.09.06 / 14PM

Is there a way of passing extra parameters to Behaviour?

addButton : Behavior.create({
onclick : function(e) {
    alert('called');
        }
}),

justAsimplejustAsimple at 20.09.06 / 11AM

...sorry, hitting spacebar cause the form to submit too early….

addButton : Behavior.create({
    onclick : function(e) {
        alert('called');
      }
})

Event.addBehavior(
    '#addButton' : addButton
);

What I’d really like to do is to be able to pass another param to the function addButton when it is called. I actually want to pass an instance of another object (call it ‘ParentObject’), such that the addButton behaviour can become:

addButton : Behavior.create({
    onclick : function(e) {
        alert(ParentObject.property);
      }
})

The reason for all this is that i have a larger object that is used to create a sortable & editable menu. This menu class therefore contains things like the ajax URLs and settings. I’d like to expose these settings to the bahaviour callback functions.

justAsimple...justAsimple... at 20.09.06 / 11AM

json.js - yeah, the it's the addEvent stuff which is causing problems with the json.js library. Some circular reference is created which makes it go wild. I haven't yet had a chance to delve into why and what is causing it - but if anyone has solved this already shout out!

tomtom at 20.10.06 / 11AM

Think I found a typo at lowpro.js line 65, this line should read:

if (isIE) this.ieAttrSet(attrs, attr, el);

This fixed a bug for IE 6.0.2800 for DOM.Builder functions.

vicvic at 07.11.06 / 16PM

What would cause the following code not to work in IE 6 & 7 :

Event.observe(window, ‘keydown’, function(e) { keypressHandler(e); } );

function keypressHandler(e) { if (document.all) var e = window.event.keyCode; else e = e.which; }

alert('a key was pressed!');

it works perfectly in FF…

Ran Bar-OnRan Bar-On at 09.11.06 / 20PM

Hi, I can’t seem to get lowpro working. In the firefox javascript console I get the following error

Error: destination has no properties Source File: http://localhost/scripts/prototype.js Line: 29

It seems to have a problem with the Element.addMethods function (it can’t retrieve the Element.Methods )

Could this be because of my FF version (1.0.8) ?? or even my prototype version (1.4.0)

TejasqTejasq at 27.11.06 / 17PM

Tejsaq: As noted in the article Low Pro works with Prototype 1.5. It’s not compatible with 1.4.

DanDan at 27.11.06 / 18PM

oh yeah, missed that line – thanks!

TejasTejas at 27.11.06 / 18PM

About This Article