-
Notifications
You must be signed in to change notification settings - Fork 1
Home
NinjaScript is a jQuery library designed to allow unobstrusive scripting simply and easily. Essentially, we use jQuery selectors to apply behavior the same way we use CSS stylesheets to apply stylings. Additionally, NinjaScript makes it easy to package up useful behaviors and apply them quickly to disparate elements, or in different projects.
Unobtrusive scripting is an instance of the concept of Separation
of Concerns. Javascript's role in a webpage is properly the definition of the
way the page behaves, just the way that the markup describes its structure.
The way that HTML uses Javascript though is problematic. The use of attributes
to tie scripts to elements couples HTML and Javscript very tightly. We haven't
put "style" attributes in our HTML since 1998, so why do we still use
"onclick"?
One benefit of separating behavior from structure is that if you put all your behavior in one place, you'll know where to look when you're trying to figure out why it doesn't work. Having your behavior laid out in one file gives you one place to go to change it, which is a huge time savings.
Plus, UJS can make it easier to build your site such that it'll work for those benighted souls who don't have Javascript enabled or available. You simply write your Javascript such that it interacts HTML that doesn't require the Javascript to run, and if Javascript isn't avaiable in the client, it runs fine.
It used to be that NinjaScript was incompatible with jQuery 1.5. This is no longer the case, and the core of NinjaScript is now separate from outside Javascript frameworks. As of NinjaScript 0.9, the core event handling engine is a statically included version of Sizzle (the same CSS engine that jQuery uses.)
We take the awesomeness of CSS for granted. In CSS, when you apply a style to a selector, it always works and always applies to matching DOM nodes - no matter when those nodes were created.
JS does not work this way: if you attach event handlers to nodes matching
'.i_am_cool' now, future i_am_cool nodes won't necessarily get those handlers.
We can fix handlers with event delegation (as in jQuery).
That works fine for event handlers, but it can't help you when your Javascript needs to make changes to the document being displayed to the user. This is increasingly important when you want your Javascript to degrade gracefully, since you often want the pre-Javascript HTML to look one way, but have the Javascript modify it when it loads.
Suppose, for example, we want to unobtrusively add structure to some divs (i.e. rounded corners) or replace one element with another (convert forms to links or other behavior). These transformations will get run once when our script executes, and won't happen automatically to future matching elements.
NinjaScript allows you to specify behaviors - including transformations - and attach them to selectors once, and then count on them always applying to any future element that matches those selectors.
It also lets you package up named behaviors for reuse, and NinjaScript includes a bunch of predefined packaged behaviors for common utilities like AJAX submission and handling graceful degradation cases.
A simple example, using some prepackaged behaviors:
<script src="/js/jquery-1.4.js type="text/javascript" />
<script src="/js/jquery.ninja_script.js" type="text/javascript" />
<script type="javascript">
Ninja.behavior({
"form.login_form": Ninja.submitsAsAjax,
".alert_notice": Ninja.decays({ lifetime: 5000 })
})
</script>
This converts forms with class 'login_form' to AJAX equivalents, and makes all elements with class 'alert_notice' disappear after five seconds. All NinjaScript predefined behaviors have sensible defaults. If you don't pass the options object to decays(), it will decay after 10 seconds instead of 5.
A more complex example, showing how you can define your own behaviors:
<script src="/js/jquery-1.5.js type="text/javascript" />
<script src="/js/jquery.ninja_script.js" type="text/javascript" />
<script type="javascript">
Ninja.behavior({
"#mail form.new_mail": Ninja.ajax_submission,
"#message_list .item": {
transform: function(elem) {
$(elem).delay(5000).slideUp(600, function(){$(elem).remove()})
}
events: {
click: function(event) {
$(elem).remove()
}
}
}
".has_tooltip': {
mouseover: function(event){
myCreateMouseoverTip(elem)
}
}
})
</script>
That behavior block sets up three behaviors:
- It converts a normal form (could be a POST, GET, whatever) into an AJAX submission. By default, we'll put a "busy" overlay over the form until we get a response, and add any error messages to a list of error messages. This behavior is packaged as Ninja.submitsAsAjax
- It adds a decay behavior to messages, using jQuery effects.
- It applies a tooltip mouseover effect to elements with a "tooltip" class. We elide the details of what that effect is for the purposes of example.
Notice that behaviors are defined in three different, intermixable styles:
- Prepackaged, in the form of a method on the Ninja object (available everywhere as Ninja)
- With a "transform, events, helpers" syntax, which breaks out everything a behavior can do, completly explicitly.
- With an abbreviated events form, with the assumption that all we want to do is define a series of event handlers (and possibly a transformer)
NinjaScript applies "behaviors" to elements selected using jQuery's CSS-like selectors. A behavior consists of three things:
- A transformer: a function called "transform" that take the element as its argument, and changes it in ways that are appropriate to the behavior. One prepackaged behavior "make_ajax_link" takes a form consisting of a single submit button and converts it into an anchor tag with appropriate attributes.
- A list of event handlers - functions in two arguments that take action based on the event and the element. By default, NinjaScript event handlers swallow the event, preventing the default behavior and preventing the event from bubbling back up the DOM. You can use an array of [handler_function, *strings] to have NinjaScript allow "default" behavior, allow the event to "propagate", or allow the "immediate" propagation of the event (to other handlers on the same element).
- A list of helper methods, which are available to the event handlers.
For simplicity of syntax, NinjaScript allows you to omit the 'events' wrapper when defining events, though in complex behaviors including multiple event handlers and a transform we encourage you to include it for clarity. This code:
Ninja.behavior({
'.some_class': { events: {
click: function(event){ ... do something }
}}
})
May be rewritten more briefly as:
Ninja.behavior({
'.some_class': { click: function(event){ ... do something } }
})
NinjaScript is designed to be pretty easy to read, but a brief overview of how it works can aid in understanding.
Basically, NinjaScript is built around an event binding engine. As nodes are added to the DOM that match the selectors provided, the transform functions are run and event handlers are attached to the nodes as appropriate.
We use DOM mutation events to do trigger the binding, which covers most of the browsers out there, as well as a homebrewed event that is raised explicitly for certain dopey browsers (the obvious one) by the NinjaScript machinery.
The upshot is that event handling is much more efficient than delegation, plus we get element transformation along with, so it's a win all around.
To contribute to NinjaScript, fork it on GitHub and issue pull requests. Make sure you run the tests; this can be accomplished just by loading the file "SpecRunner.html" in a browser, locally. All the test libraries necessary are hosted on http://js-testing.lrdesign.com, so it should just work.