StealJS - Script Manager
There's a lot more to making JavaScript apps than writing JavaScript. StealJS is a collection of command and browser based JavaScript utilities that make building, packaging, sharing, and consuming JavaScript applications easy.
Download FeaturesStealJS is made of several components:
Dependency ManagementThe steal script is a script loader and dependency management tool. Features:
- Load JavaScript, CSS, Less, CoffeeScript, and a variety of client-side templates.
- Load files relative to the current file, steal's root folder, your server, or from other domains.
- Load a single file only once (the whole dependency management thing).
steal('dojo','jquery','prototype');
JS/CSS Concatenation and Compression
Steal's build plugin compresses an application into a single minimized JavaScript and CSS file. Features:
- Works with any application, even ones not using the steal script.
- Configurable compressors (defaults to Google Closure).
- Compresses Less and CoffeeScript.
- Pre-processes and compresses client-side templates (templates don't have to be parsed or included with the page).
- Expandable architecture makes it easy add other file types to the build script.
- Can intelligently combine shared dependencies into separate files for caching.
./js steal/buildjs mypage.htmlCode Generators
Steal's generate plugin makes it easy to get started developing. Features:
- Creates folders, files and scripts your app needs.
- It's very easy to make custom generators.
./js steal/generate/app myappPackage Management
Steal's get plugin is a very basic JavaScript version of ruby gems. Features:
- Download and install plugins from remote SVN or GIT repositories.
- Installs dependencies.
./js steal/getjs http://github.com/jupiterjs/funcunitCode Cleaner
Steal clean beautifies your code and checks it against JSLint.
./js steal/clean path/to/page.htmlLogging
Steal dev logs messages cross browser. Messages are removed in production builds.
steal.dev.log('something is happening');
Why
StealJS is an extremely strong attempt at solving a few of the the most core problems in JavaScript development:
Development vs ProductionThere's a tension between development and production JavaScript needs. When developing JavaScript, you want:
- Lots of logically separated files.
- Changes in a file to only require a refresh of the browser. (It's JavaScript not JavaCompile!)
This is in contrast to production where you want a few compressed and cacheable files.
Steal makes this easy with not only JavaScript, but with other resources like CSS, client side templates, Less, and CoffeeScript!
Yes, this means what you think it does. You can edit CoffeeScript/Less/template/CSS files and just refresh the browser to see changes. When you finally make a production build, steal will convert, package, and compress these files with your other JavaScript and CSS files.
Optimized WebsitesThere's also tension between script loading performance and caching. You want to:
- Have as few http requests as possible.
- Exploit the cache.
This is especially problematic when multiple pages have shared dependencies. Lots of shared scripts better exploit the cache, but also increase the number of requests.
StealJS makes it easy to find the perfect balance. It can build apps for multiple pages at the same time, understand the shared dependencies, and create cacheable shared downloads.
Framework AgnosticMost server frameworks come with varying levels of similar functionality. Ruby on Rails is particularly great and bundling Less CSS and JavaScript.
But, what if you wanted to bundle the same Less files with ASP.NET MVC?
StealJS works with any server framework. As we (or mabye you) add new features, those will be available to users of any web framework.
Resources- Documentation
- Github Repository
- Website (a work in progress).
- Get Help (on the JavaScriptMVC forums).
Download steal and unzip it into a public folder where you will have JavaScripts. You should see something like:
/your_public_folder /steal /js.bat /js
If you don't already, make sure you have Java 1.6 installed.
Using GeneratorsThe easiest way to create a new app that uses steal is to use steal.generate to create an application skeleton. Open a command line to your public folder. In windows write:
js steal\generate\app myapp
In Mac/Linux write:
./js steal/generate/app myapp
Warning: The remainder of this guide only shows the windows command. Mac/Linux users should change js to ./js and \ to /. (We make the Mac/Linux people figure it out because they are smarter :-).
This creates a myapp folder in your public directory that looks like:
/myapp
/docs
/scripts
/build.js
/clean.js
/test
/resources
/example.js
/example.coffee
/example.less
/myapp.html
/myapp.js
/myapp.css
Dependency Management and Script Loading
If you open myapp.html in a web browser, you should see something like:

*** This won't work on the filesystem from Chrome because it has draconian XHR limitations. If you run it from a server, it will work perfectly.
Now open myapp.html in a text editor, you will find a script tag like:
<script type='text/javascript'
src='../steal/steal.js?myapp/myapp.js'></script>
This loads the steal script. The steal script is what does dependency management. The myapp/myapp.js tells the steal script to load your app at myapp/myapp.js.
Open myapp/myapp.js. In it you'll see something like:
steal( 'resources/example' ) // 1
.css( 'myapp' ) // 2
.plugins(
'steal/less',
'steal/coffee' ) // 3
.then(function(){ // 4
steal.coffee('resources/example') // 5
.less('resources/example'); // 6
});
This:
- Loads 'myapp/resources/example.js'.
- Loads 'myapp/myapp.css'
- Loads 'steal/less/less.js' and 'steal/coffee/coffee.js'
- Adds a function to be called back once all prior files have been loaded and run.
- Loads 'myapp/resources/example.coffee'.
- Loads 'myapp/resources/example.less'.
*** This callback is needed because script loading is not synchronous. The 'steal/coffee' and 'steal/less' plugins add steal.coffee and steal.less.***
The steal script provides a number of helper functions to make loading scripts very easy. Once you have loaded all the scripts for your app, it's time to build it.
JS/CSS Concatenation and CompressionIf you used the generators to create your application, compressing your app is very straightforward:
js myapp/scripts/build.js
This packages your app into myapp/production.js and myapp/production.css.
To uses these files instead of all your uncompressed files, change your page to load steal.production.js instead of steal.js:
<script type='text/javascript'
src='../steal/steal.production.js?myapp/myapp.js'>
</script>
Reload myapp.html and you should see it load only 2 JavaScript files and one CSS file.
Package ManagementSteal.get downloads and installs plugins from a url. Typically it's run from the steal/getjs script.
The following copies the FuncUnit repo to a local funcunit folder.
js steal\getjs http://github.com/jupiterjs/funcunit -name funcunit
Official Plugins
Steal maintains a list of official plugins compatible with steal development. You can install these just by providing their name:
js steal\getjs funcunit
The following are the list of official StealJS plugins. As StealJS itself is in Beta, the following plugins should not be considered production ready:
- steal - Update steal itself.
- funcunit - Functional testing platform.
- jquery - jQuery 1.4.3 and the JavaScriptMVC components.
- phui - Very early alpha UI widgets.
- documentjs - A documentation engine.
- mustache - mustache templates.
- ss/statemachine - Implements jQuery.Controller as a fininte-state-machine
- ss/router - The Sherpa routing system for jQuery.Controller
Steal clean uses JS Beautifier to clean your code and JSLint to check it for trouble spots like global or unused variables.
Cleaning a Single Script
To clean a single script, you can write:
js steal/cleanjs path/to/my/script.js
It will replace the script with a beautified script. If you want to also add JSLint (feelings beware), run it with:
js steal/cleanjs path/to/my/script.js -jslint true
Cleaning a StealJS app
If you used the generator to build your app, you can clean myapp's scripts with:
js myapp/scripts/clean.js
Add JSLint with:
js myapp/scripts/clean.js -jslint trueLogging
Finally, a small but nice feature of StealJS is that you can leave log messages without worrying that they show up in your production build. For example, steal.build will remove the following from the production build.
steal.dev.log("Something has happened"); //***
*** If you want to see this work, change to development mode and open Firebug.
ConclusionWe built StealJS to provide a single solution to many of the script management problems we faced with our client work. I hope it helps you in the same way it has helped us - turning around higher quality apps faster.
In the next two weeks, we plan to release a production StealJS. This will be followed by months of adding new tools, resources, and documentation.
In the future, we hope to work with other projects like LabJS, CommonJS, and RequireJS, to offer a 'standard' solution to the problems addressed by StealJS.
JavaScriptMVC Features
I spent a lot of time the last few weeks arguing why jQuery needs the Enterprise, and why it needs JavaScriptMVC (part 2). But, I should have been explaining why YOU need JavaScriptMVC. So I put together a list of what JavaScriptMVC provides.
The JavaScriptMVC Framework is feature rich. It does everything you need in between jQuery and your project (with the exception of packaged UI widgets). The following is a rough list of the features of the framework's major components followed by the features of the project as a whole:
Steal- an independent code manager
Dependency Management- Load JavaScript, CSS, Less, CoffeeScript, and a variety of client-side templates.
- Load individual files only once.
- Can load relative to the current file.
- Can load from other domains.
- Works with any application, even those not using the dependency management solution.
- Configurable compressors (defaults to google closure).
- Compresses Less and CoffeeScript into a production build.
- Pre-processes, compresses and packages a variety of client-side templates.
- Expandable architecture makes it easy to add other resources.
- Can conditionally remove chunks of code from production build.
- Build standalone jQuery plugins.
- Log messages, but they will be removed in production build.
- Generate an application skeleton..
- Very easy to create your own generators.
- Download and install plugins from SVN or GIT repositories.
- Installs dependencies.
- Runs install scripts.
- We maintain an official list of good plugins.
- Runs http://jsbeautifier.org/ on your project’s code
- Runs JSLint on your project’s code
- an independent web testing framework
Comprehensive Functional Testing- Test clicking, typing, moving the mouse, and drag drop.
- Follow the user between pages
- High fidelity event playback
- Multi-browser and operating system support.
- Automated and interpretable with Continuous Integration solutions
- Email results to gmail users plugin.
- Write and debug tests with only the browser.
- Simple, chain-able api that parallels jQuery.
- Upcoming ‘recorder’ app.
- a low to mid-level jQuery development toolset
Best Practices- Encourages logically separated, deterministic code
- Service / Domain layer (Model)
- Uniform client-side template interface supports jq-tmpl, EJS, JAML, Micro, Mustache (View)
- Efficient compareDocumentPosition
- Cookies*
- Delegate with the open child selector.
- Set and animate innerWidth, innerHeight, outerWidth, outerHeight.
- Quickly get multiple current css properties on an element
- Convert form data into nested JavaScript objects.
- String helpers (including a working regexp split)
- Vector math
- JSON utilities+
- A simple, powerful, lightweight class system.
- Default events enable powerful Event Oriented Architectures.
- Bind to destroyed event for proper widget cleanup.
- Delegatable, highly customizable drag-drop events
- Hashchange event for browser history*
- Delegatable, configurable, hover events
- Normalized, cross-browser resize events
- Auto-magic cleanup (unbind, undelegate, unsubscribe)
- Organized methods and events
- Shared methods and properties
- Namespaces
- Automatically extendable
- Remove plugins without removing element
- Object Oriented
- an independent JavaScript documentation application
Feature Rich- Inline demos with source and HTML panels
- Taggable documentation
- Rapid, auto-suggest search
- Savable favorites
- Test results pages
- Disqus comments (coming soon)
- Can document any page’s JavaScript with no extra work
- Extends JSDoc syntax
- Corrects misspellings of directives (ie @codend instead of @codeend)
- Adds undocumented JavaScript functionality because it understands JavaScript.
- Customizable, flexible relationships
- JavaScript objects can add documentation across files
- an integrated collection of best practices for developing jQuery applications
Ease of Development- Everything you should be doing requires no extra work. When you create an application, it's ready to test, document, compress, clean, and share.
- Provides a standard, modular, file structure to build everything from the smallest plugin to the largest application.
- Almost everything is extremely well documented, with code examples, demos, and write-ups at JupiterJS.com. With the 3.0 release, everything will be documented.
- We are here to help. I don't think there is an un-answered question on our forum. I don't think a question has been left un-answered for more than a day.
- Every component is tested and developed independently but also combined into an automated nightly test.
- When we make fixes, it's extremely easy to upgrade JavaScriptMVC.
- JavaScriptMVC apps are lightweight. You only use only what you need, but combined and compressed, the MVC parts and their dependencies are only 15kb (compared to jQuery's 70kb).
- We've taken considerable care to fine tune the performance of nearly every part of JavaScriptMVC. It's techniques are 'fast-by-default'.
- Everything works standalone. You can use controller, steal, FuncUnit, the compare plugin, everything, by itself. If your app grows to need other parts, we've got you covered.
- JavaScriptMVC has been solving the challenges of enterprise jQuery development for years. (It was the first library to support comprehensive event delegation!) We have continually refined JavaScriptMVC's API and features to make it as easy as possible learn and use.
Hopefully at least something in this list interests you! If not, well ... I'm not interested in what you are doing.
To be sure, JavaScriptMVC has a TON of features. Often people interpret this as 'too enterprisey'. But, a lot of care has gone into making it as easy to use each part as possible. We want you to fall into fall into the pit of success. When you need those features, they are there.
Finally, while possible to assemble your own framework and build great things, it will undoubtedly be:
- More difficult to assemble.
- Less likely to interoperate (there's no extra work to document, clean, test, and build your app with JavaScriptMVC).
- More error prone.
- Harder to teach / get support.
- Larger (repeat work between stand-alone components)
So PLEASE take a look at JavaScriptMVC and even a better look at its beta 3.0 release. Your spouse / girlfriend / pet will appreciate all the free time it gives you.
jQuery's Object Literal Coding Conventions
jQuery has internal coding conventions. It uses tabs over spaces. It uses lots of white paces and you can always find a {} around if/else statements. I'm adopting these conventions for JavaScriptMVC. JavaScriptMVC had notoriously flexible (ie bad) coding conventions. We were more concerned with adding features, tests, etc than making our code look nice. But, for the 3.0 release, the features have stabilized. We're paying more attention to things like documentation and even coding conventions.
So, I spent a few hours today making JavaScriptMVC code look like jQuery code with a few simple regular expressions. My goal was to make our Object literals look like:
httpSuccess: function( xhr ) {
One nice feature you may not know is that you can search the jQuery source for a method easily by searching for METHOD:. I like that. So, I used a few regular expressions to convert our varied object literal methods to jQuery style object literal methods. Here's what they do in order you should run them:
//Converts method : function -> method: function
RegExp =/(\w+)\s*:\s*function/
Replace ="$1: function"
//Converts : function(f -> : function( f
RegExp =/: function\(([^ \)])/
Replace =": function( $1"
//Converts : function( f){ -> : function( f ) {
RegExp =/: function\(([^\)]*)([^ ])\)\s*\{/
Replace =": function($1$2 ) {"
//Converts : function(){ -> : function() {
RegExp =/: function\(\)\s*\{/
Replace =": function() {"
I'm positive there's an easier way to do this, but this might get you started cleaning up your own code. Maybe someday we'll roll this into JavaScriptMVC.
Why jQuery Needs JavaScriptMVC Part 2
After writing my previous article on Why jQuery Needs JavaScript, I feel like I didn't communicate why JavaScriptMVC is important for jQuery developers. Here's a much more succinct way of saying it.
JavaScriptMVC makes doing everything you should be doing as easy as possible
Here's some of the things you should be doing:
- Testing (especially automatic and functional testing)
- Documenting
- Breaking up code into logically organized files
- Compressing and concatenating your JavaScript files
- Using and organizing client side templates
- Making plugins that clean themselves up are internally organized, and extendable.
- Error reporting
All of these things are hard or impossible to do right with jQuery alone.
You can add your own automated testing library - Selenium or qUnit, qUnit isn't automated. Selenium is hard to write tests for.
You can add your own documentation engine - JSDoc, but make sure you keep track of every file!
You can add your own way of loading and compressing scripts - RequireJS.
You can use other client side template libraries - jquery-tmpl, but you won't be able to compress them into your build or put them in external files as easily.
You can be careful to structure your jQuery plugins so they can be easily removed from an element, remove all event handlers, and provide some mechanism for extending or overwriting your plugin.
You can devise your own way of doing error reporting.
Or
You can download JavaScriptMVC and run:
js steal/generate/app APPNAME
and get all of these things for free.
This is why jQuery needs JavaScriptMVC - to make it easy to do the things you should have been doing in the first place.
We've spent almost 3 years refining the same feature set to make crazy easy do these things. If this doesn't make sense to you, please read why JavaScriptMVC is not too Enterprisey.
How FuncUnit works
FuncUnit is a mashup of a bunch of awesome JavaScript projects. It can be useful knowing how they all work together to make FuncUnit tick. This article details the components and how they fit together. This is a primer for anyone wanting to contribute to FuncUnit.
FuncUnit is is comprised of the following parts:
- Selenium - Provides browser automation
- QUnit - A browser-based testing an assertion API.
- jQuery - Used for CSS selector and DOM utilities.
- EnvJS - A simulated browser environment.
- Syn - A synthetic event library.
FuncUnit supports 2 modes: Browser and Selenium mode.
Browser ModeBrowser mode is when you open up your browser to a funcunit html page. This page includes the funcunit script which has inside it QUnit, jQuery, Syn and the FuncUnit API.
When you use FuncUnit (or S), it basically sends those commands to a popup window. This is pretty straightforward.
Selenium ModeSelenium mode is used when you use envjs to open the funcunit html page. Here, EnvJS reads the html page and then loads and runs any script tags it finds (just like a normal browser). But instead of sending commands to a popup window, funcunit.js starts selenium, serializes those commands, and send them to a custom Selenium server build. The selenium server has jQuery and Syn and will run the commands the same way browser mode ran the commands. Selenium server will return the results to FuncUnit which reports them to the console.
The SecretNow that you know the secret to FuncUnit's awesomeness - how you are able to write a test with just a browser and funcunit.js, but automatically run it - please help us make this project even better. Here are some things we want to work on:
- An in-browser Selenium like IDE.
- Cucumber-like API.
- Plugins for testing Flash / Silverlight integration.
FuncUnit - Fun Web Application Testing
Last week we released Syn and mentioned how all testing solutions suffer from some fatal flaw. Today, we are releasing a beta of FuncUnit, which has "solved" JavaScript web application testing.
Features- Functional Testing - Test complex behavior like drag-drop.
- High Fidelity - Create identical events that a mouse and keyboard create. Run tests in a real browser.
- Automated - Test multiple browsers from the command line.
- Easy to Write - Write, run, and debug tests in the browser, without configuring a server or other tool. Write tests in a familiar jQuery-style API.
- Free - It's open source! Save yourself $5,000 for similar solutions.
- Multi-system - Runs in IE 6+, FF 2+, Safari 4+, Opera 9+, Chrome, on PC, Mac, and Linux.
FuncUnit is a web application testing framework that combines Selenium, jQuery, QUnit, and Envjs. It focuses on making testing as easy and fun as possible.
Writing tests is a chore and won't be done unless it's stupidly easy. Plus, tests won't get run unless they can be run automatically.
The problem with other automated testing solutions is that everyone has to install and configure cumbersome external software to even begin writing tests. After setting up the software, you write the tests with some obtuse API in a language other than JavaScript. But the worst part is debugging these tests across the software-browser bridge. The tests are almost impossible to debug!
We fixed this.
Write, Run, and Debug with the BrowserFuncUnit lets you write tests by just creating an html file that includes funcunit.js.
Simply open the html page in your browser and it runs your tests. If you made a mistake, or your app breaks, use your favorite debugger (Firebug) to catch the problem. It's easy.
Automate Your TestsWhen you've setup your testing server with the browsers you want to support, running the same tests is as simple as opening the test page with envjs:
envjs http://test.myco.com/tests/filemanager.html
We typically set this up to run as a nightly build.
APIFuncUnit's API is super sweet. If you know QUnit and jQuery, it will be second nature. The following tests the handy autosuggest from phpguru. See it in action here. (Turn off your popup blocker!)
module("autosuggest",{
setup : function(){
S.open('autosuggest.html')
}
});
test("JavaScript results",function(){
S('input').click().type("JavaScript")
// wait until we have some results
S('.autocomplete_item').visible(function(){
equal( S('.autocomplete_item').size(),
5,
"there are 5 results")
})
});
Demos
Make sure you have your popup blocker off!
- A basic AutoComplete
- Jupiter's Phui tests (Runs Menu, Grid, Sortable, Filler) [Some might break in IE, this is a problem with the widgets, not FuncUnit].
JavaScriptMVC's FuncUnit docs.
UseSetting up FuncUnit is almost exactly like setting up QUnit. After downloading FuncUnit into a public folder on your server (or a local file on your hard drive):
- Create an empty test script (ex: mytest.js).
- Create a qunit like page (ex: mytest.html) that will load your test and display your results. Use this as a template (It's also in the download).
- Have that page include funcunit.js, qunit.css, and your test script (mytest.js)
Open mytest.html, you should see something like:

*** you probably want to change the name of these files ***
Writing TestsAll interaction with the tested page is done through the FuncUnit object. This is aliased as S. S is to FuncUnit as $ is to jQuery.
Typically a test is broken down in 5 parts:
- Opening a page
- Performing user actions (like clicking and typing).
- Waiting for the page to complete its response (animations and Ajax)
- Getting properties of the page (like the text of an element)
- Asserting the properties are correct (like the text == "Hello World")
Testing a dynamic webpage is largely asynchronous. To avoid LOTS of nested callback functions, most FuncUnit commands are queued. But these commands take a callback function that runs after the command completes, but before the next command runs.
For example, the following performs 3 drags that each take a second. But, between the second and third drag, it checks the #start element's text.
S("#start")
.drag({ to:"#first", duration: 1000 })
.drag({ to:"#second", duration: 1000 },
function(){
equals(S("#start").text(),"2 drops")
})
.drag({ to:"#third", duration: 1000 })
Opening a page
To open a page is simple. Write:
S.open("path/to/mypage.html")
The path to the page should be relative to the FuncUnit test page (mytest.html). Typically an open should be done in a module's setup function.
ActionsActions are used to simulate user behavior such as clicking, typing, moving the mouse. FuncUnit provides the following actions:
- click - clicks an element (mousedown, mouseup, click).
- dblclick - two clicks followed by a dblclick.
- rightClick - a right mousedown, mouseup, and contextmenu.
- type - Types characters into an element.
- move - mousemove, mouseover, and mouseouts from one element to another.
- drag - a drag motion from one element to another.
- scroll - scrolls an element.
The following might simulate typing and resizing a "resizable" textarea plugin:
S('textarea').click().type( "Hello World" );
S('.resizer').drag( "+20 +20" );
Waits
Waits are used to wait for a specific condition to be met before continuing to command. Waits look like most jQuery setter methods. For example, the following waits until the width of an element is 200 pixels and tests its offset.
var sm = S("#sliderMenu");
sm.width( 200, function(){
var offset = sm.offset();
equals( offset.left, 200)
equals( offset.top, 200)
})
You can also provide a test function that when true, continues to the next action or wait command. The following is equivalent to the previous example:
var sm = S("#sliderMenu");
sm.width(
function( width ) {
return width == 200;
},
function(){
var offset = sm.offset();
equals( offset.left, 200)
equals( offset.top, 200)
}
)
Getters
Getters are used to test the conditions of the page. Most getter commands also correspond to a jQuery method of the same name.
The following checks that a textarea, which can only be resized vertically, is 20 pixels taller after a drag, but the same width:
var txtarea = S('textarea'),
// save references to width and height
startingWidth = txtarea.width(),
startingHeight = txtarea.height();
S('.resizer').drag("+20 +20", function(){
equals(txtarea.width(),
startingWidth,
"width stays the same");
equals(txtarea.height(),
startingHeight+20,
"height got bigger");
});
Assertions
FuncUnit uses qUnit for assertions so you can use:
- ok - ok(state, message)
- equals - equals(actual, expected, message)
- same - same(actual, expected, message)
Running tests in the browser is easy - just open the FuncUnit page. But running tests via the command line is equally as easy. Simply open your test page with envjs:
envjs http://localhost/tests/grid.html
Or if you are using Mac/Linux:
./envjs http://localhost/tests/grid.htmlSetting Up Your Test Server
We'll be putting this up in JavaScriptMVC's FuncUnit docs.
ConclusionWe believe FuncUnit finally makes testing JavaScript approachable. We hope you can use to to write better, more robust applications that move web development along faster than ever. If you have questions or comments please post on FuncUnit's Google Group.
Thanks!FuncUnit is a mashup of some of the best open source tools available:
- Selenium - Browser automation
- QUnit - Testing API
- jQuery - CSS selector and DOM utilities (your app does not need jQuery)
- EnvJS - Simulated browser environment
- Syn - A synthetic event library
- FuncUnit Homepage
- Google Group
- GitHub
- @funcunit on twitter
Syn - a Standalone Synthetic Event Library.
Syn is used to simulate user actions such as typing, clicking, dragging the mouse. It creates synthetic events and performs default event behavior.
Features- Enables complex JavaScript functional testing.
- Standalone (does not need jQuery).
- Cross Browser (IE6+, FF1.5+, Opera 9.6+, Safari 4+, Chrome 4+) and multi-platform (Win, Safari, Linux).
- 99.9% browser sniffing free (only 1 sniff to prevent crashing Safari).
syn.js (59KB. Size doesn't matter. Users don't download this)
DemoSynthetic Recorder Demo. If you find a bug, it's the recorder and not syn.js's fault. But please let us know! Eventually, we will turn the recorder into something like the Selenium IDE.
DocumentationJavaScriptMVC's Syn documentation.
WhyTesting rich, dynamic web applications sucks. At Jupiter, we've tried almost every testing solution available (qUnit, Quick Test Pro, Selenium, JsUnit, Env.js, TestCase) and all of them suffer from some fatal flaw.
Problems:
- Manual - A tester has to run the tests manually on every supported browser. People are lazy.
- Unit Tests Only - We need to test the app as a whole and complex UI behavior like drag-drop.
- Low fidelity - We need to make sure the tests are reporting accurate results.
- Difficult to write - We sling JS like a ninja monkey throws poo. We want to write tests in a nice JS API.
- Expensive - A QTP license is 5k a person! I'd rather buy a vacation.
- Support - We want to test Mac and Linux browsers.
We've solved all of these problems in our upcoming FuncUnit testing framework. It's a mashup of qUnit, Selenium, Rhino, and Env.js. But its core library, Syn, which does the work of simulating user actions with very high fidelity, is what we are releasing today.
UseUsing Syn feels good and should be done with a clean conscience, as it may lead to feelings of guilty pleasure. There's only several things a user does and these correspond nicely to the Syn API:
- click - a mousedown and mouseup on the same element.
- dblclick - two clicks in rapid succession.
- type - typing a sequence of characters.
- key - typing a single character
- move - moving the mouse.
- drag - a mousedown followed by a move and a mouseup.
These user interactions are called actions. The following is an example of clicking an element with id='hello', typing "Hello World", and then dragging your mouse from that element to an element with id='trash'.
Syn.click( {},'hello' )
.type( 'Hello World' )
.drag( $('#trash') );
* jQuery is used to get the trash element for brevity. It is not required.
You'll notice that Syn is chainable. You want to use chaining because actions don't complete immediately (thank IE's .focus() for this). Provide a callback to know when a command has completed and use a delay to for timeouts between commands. The following asserts that 'hello' has been removed from the page:
Syn.click( {},'hello' )
.type( 'Hello World' )
.delay() //waits 600ms seconds by default
.drag( $('#trash') , function() {
ok( $('#hello').length == 0, "removed hello" )
});
Notice that an element id is absent from the type and drag methods. If an element id is not provided, the previous command's element is used.
Lets explore a how to use each action in detail.
Click and DblclickA click is the following sequence of events followed by the event's default actions:
- mousedown
- focus (Called only if the element was focusable, and mousedown's default actions were not prevented)
- mouseup
- click
A dblclick is just two clicks followed by a dblclick event. The options for a click action become properties on the event. The following sets clientX and clientY for the mousedown and mouseup of a click action:
Syn.click({clientX: 533, clientY: 100}, 'foo')
A few important points on click:
- ClientX and ClientY are relative to the viewport (window).
- You can provide pageX and pageY for positions relative to the page (such as jQuery's offset).
- If jQuery is available, the clientX and clientY will be set to the center of the element.
- If you want to set the control key values of the click action, you have to type or key that button.
The key action is the following sequence of events followed by the event's default actions:
- keydown (Except for the print key)
- keypress (For keys that generate a keypress)
- focus (for keys that change focus, like \t)
- keyup
The type action calls the key action for each character. Check the keycodes documentation for a list of key names. The following types "JavaScriptMVC" then presses backspaces to delete the "MVC", leaving "JavaScript".
Syn.type( 'JavaScriptMVC\b\b\b', 'foo' )
Special Characters
Some characters don't have a type-able key such as right, ctrl, and delete. To type these keys you just have to surround them with [ and ]. The following types JavaScriptMVC, moves the cursor to the left 3 times, and then types delete 3 times:
Syn.type( 'JavaScriptMVC[left][left][left][delete][delete][delete]', 'foo')
Command Keys
The command keys ctrl, alt, and shift often want to be held while another key is pressed. For these keys, you have to call the key like [command], and release the key with [command-up]. The following types JavaScriptMVC, selects the MVC text, and then deletes it.
Syn.type( 'JavaScriptMVC[shift][left][left][left][shift-up]\b', 'foo')Drag and Move
A move action is the following events
- mousemove
- mouseout (the cursor leaves an element)
- mouseover (the cursor enters an element)
A move creates a linear sequence of mouse move, outs and overs between two points. A drag action is a mousedown followed by a move and a final mouseup. To make a drag motion from one point to another over 2 seconds:
Syn.drag(
{
from: {clientX: 100, clientY: 100},
to: {clientX: 200, clientY: 200},
duration: 2000
},
'foo')
Or if you want to provide pageX and pageY:
Syn.drag(
{
from: {pageX: 100, pageY: 100},
to: {pageX: 200, pageY: 200}
},
'foo')
You can shorten this to:
Syn.drag(
{from: '100x100', to: '200x200' },
'foo')
But most commonly, you want to drag one element to another element. To drag the foo to the bar element:
Syn.drag(
document.getElementById('bar'),
'foo')
This drags from the center of one element to the other.
ConclusionYou should care about syn if you:
- Work on the Selenium or some other functional testing library.
- Want to test complex UI behavior.
Our naive hope is that Syn might be the Sizzle of functional testing - a standalone, extendable, drop in, synthetic event library.
Organize jQuery Widgets with jQuery.Controller
Do you like organized code and hate nested functions that are impossible to reuse? Want to be able to extend plugins and widgets? Do you want these widgets to clean themselves up after they are removed? Great! JavaScriptMVC-extracted jQuery.Controller is the easiest and most robust way to build jQuery widgets.
Features- Extendable widgets.
- Deterministic and debug-able code.
- Auto cleanup.
- Namespaced widgets.
jquery.controller.js (minimized 7.8kb) [includes jquery.class.js, jquery.event.destroyed.js]
Demos DocumentationJavaScriptMVC's controller docs.
UseUsing controller is easy. Extend $.Controller with the name and methods of your widget. Methods that look like "event" or "selector event" are bound (or delegated) when a new controller is created. The init method is called after the events are bound.
The following creates a tab widget.
// create a new Tabs class
$.Controller.extend("Tabs",{
// initialize code
init : function(el){
// activate the first tab
this.activate( $(el).children("li:first") )
// hide other tabs
var tab = this.tab;
this.element.children("li:gt(0)").each(function(){
tab($(this)).hide()
})
},
// helper function finds the tab for a given li
tab : function(li){
return $(li.find("a").attr("href"))
},
// on an li click, activates new tab
"li click" : function(el, ev){
ev.preventDefault();
this.activate(el)
},
//hides old activate tab, shows new one
activate : function(el){
this.tab(this.find('.active').removeClass('active')).hide()
this.tab(el.addClass('active')).show();
}
})
// creates a Tabs on the #tabs element
$("#tabs").tabs();
There are a few important things to notice:
- Creating a Controller class creates a jQuery plugin with the underscored class name. In this case, it created the tabs plugin: $("#tabs").tabs();
- The tabs() plugin creates a new Tabs instance on each element matched by the selector. The tabs instance is created with the element and any parameters passed to tabs(). In this case it is called like new Tabs(el).
- When an instance of Controller is created, events are bound, this.element is set to the Controller's element, and init is called to setup the widget.
Many jQuery widgets suffer from an "everything-but-the-kitchen-sink" mentality. They pack every conceivable type of related functionality. This causes software bloat and slower load times for users. The root problem is the lack of a standard way to organize and extend widgets.
Controller fixes this problem by using Class. The following extends the Tabs widget above to make history enabled tabs.*
//inherit from tabs
Tabs.extend("HistoryTabs",{
// ignore clicks
"li click" : function(){},
// listen for hashchanges
"hashchange" : function(){
var hash = window.location.hash;
this.activate(hash === '' || hash === '#' ?
this.element.find("li:first") :
this.element.find("a[href="+hash+"]").parent()
)
}
})
// create a history tabs
$("#historytab").history_tabs()
*This assumes you are using something like Ben Alman's jQuery Hashchange Event Plugin.
By using Controller, we are able to keep functionality bloat to a minimum while providing a base class for future complex functionality.
Deterministic CodeHave you ever tried understanding someone else's jQuery code? You'll probably see a nested mess of anonymous callback functions wrapped in a self calling anonymous function. Despite there being as many jQuery widgets as there are atoms in the universe, there isn't a great way to organize jQuery functionality.
If you're working on a big project, or with other developers, unorganized code can create a big problem real fast. You need code that is deterministic - if you see functionality expressed in the page, you know where that functionality is expressed in code.
Controller provides this by organizing all entry points to a widget (methods and events) and adding the controller's name to the element's className. We'll break these down in the next two sub-sections; but the result is if something happens on the page, say an "li" in a ".tabs" element is clicked, you know to look for "li click" in a Tabs controller.
Organized Methods and Events
Controllers' prototype properties organize public methods and event handlers. This makes finding 'where stuff gets happens' in a widget really easy. For example, if you wanted to activate the last tab in a Tabs, you could call:
$('#tabs').tabs("activate", $("#tabs li:last"))
But the REALLY clever thing about controller is how it organizes event handlers. By naming a function like "event" or "selector event", Controller knows to listen for those events. Event handlers are self labeling! We typically refer to these methods as actions.
Controller even lets you parameterize actions, making it possible to configure the event or selector when the controller is created. And, with the jquery.controller.subscribe.js plugin, you can listen to OpenAjax messages. The following are example actions:
// listens for all clicks in this controller
click : function(el, ev){...}
// changes in .foo .bar element in this controller
".foo .bar change" : function(el, ev) {...}
// listen to this.options.activateEvent events in an li
"li {activateEvent}" : function(){...}
//listen for OpenAjax hub "recipe.created" messages
"recipe.created subscribe" : function(called, recipe){...}
//listens for drag events with jquery.event.drag.js
".drag dragover" : function(el, ev, drag, drop){ ... }
Controller Element Label
Controllers label their element's className with their name. For example, after a HistoryTabs controller is added to the #historytab element, it would look like:
<div id='historytab' class='history_tabs'></div>
You can also see all controllers on an element with $("#foo").data('controllers').
In summary, Controller organizes functionality so it is easy to find. It provides a flexible but orderly way to organize widgets.
Auto-magic CleanupIf you're building a JavaScript application, removing a widget from the page is almost as important as adding one. Unfortunately, the vast majority of jQuery plugins are designed to enhance a static page and this important feature is ignored. Typically, the only way to remove a widget is to remove its element. Even when the element is removed, the widget is often still bound to other objects such as the document which causes errors.
Controller makes cleaning up widgets easy, and in most cases automatic. It also lets you remove a controller without removing the element.
In the previous section, we showed how controller auto-binds actions. Controller also auto-unbinds those actions when the controller is destroyed. A controller can be destroyed in two ways:
- The controller's element is removed from the page with jQuery manipulators like .remove(), .html().
- The controller's destroy method is called like:
$('.hider:first').controller().destroy()
Controller also provides bind and delegate methods that allow you to listen to events on elements outside the controller's element. These event handlers will automatically be removed when the controller is destroyed.
The following controller displays "Click to Hide" and hides itself when the document is clicked.
$.Controller.extend("CloseOnClick",{
init : function(){
// call close on document clicks
this.bind(document, 'click', 'close');
// remove and save old content
this.oldChildren = this.element.children().remove();
// show text to hide
this.element.html("<p>Click to hide</p>")
},
close : function(){
this.element.hide()
},
destroy : function(){
.... //we will fill this in later
}
});
$('.hider').close_on_click()
When a 'hider' element is removed, or destroy is called on a CloseOnClick controller, the controller's event handlers are unbound, removing the document click handler.
If your widget has made any modifications to the element, overwrite the destroy function to set things back to their original state.
The following sets back the original contents of the element:
destroy : function(){
this.element.children.remove();
this.element.append(this.oldChildren)
this.oldChildren = null;
// always call super to call base destroy
this._super()
}
Namespaces
It's important to keep plugins from stomping on each other. If you create a Controller with a namespace, it includes the namespace in the jQuery plugin. For example:
$.Controller.extend("Jupiter.Tabs");
// creates -->
$("#tabs").jupiter_tabs()
This isn't exactly namespacing and not 100% protection against collision. But in practice, we've not had any problems.
ConclusionsController is JavaScriptMVC's, and by extension, Jupiter's secret-sauce. It helps us build functionality in a robust and repeatable way. It's hard to express the amazing feeling walking onto a project with 50 controllers I have never seen and instantly start contributing and fixing bugs.
Controller also helps us develop faster because we don't have to invent a new pattern for every widget.
Controller is not for all jQuery plugins. If you are building something very small, unlikely to be extended, or DOM related, controller is probably not the best fit. But if you find yourself needing to organize your team's jQuery code around a proven pattern, take Controller for a spin.
A Simple, Powerful, Lightweight Class for jQuery
jQuery's functional programming style is downright elegant for manipulating the DOM. But it lacks the structure and code reuse patterns that professional scripters need. John Resig's Simple Class inspired, jQuery.Class is a lightweight, but powerful class-like system that bridges the gap between jQuery's functional programming style and Object Oriented Programming.
Features- Static and prototypal inheritance
- Introspection
- Namespaces
- Setup and initialization methods
- Easy callback creation
jquery.class.js (minimized 2kb)
DemoExtend a tabs widget to a history tabs widget demo.
DocumentationJavaScriptMVC's Class docs.
UseTo create a monster class with static and prototype properties:
$.Class.extend("Monster",
// static properties
{
count: 0
},
// prototype properties
{
// constructor function
init : function(name){
//save the name
this.name = name;
this.energy = 10;
//increment the static count
this.Class.count++;
}
})
//create a monster
var dragon = new Monster('dragon');
Inheritance
To extend (or inherit) from a class just call .extend on the class and provide the new name of your class and the static and prototype properties:
Monster.extend("SeaMonster",{},{
swim : function(distance){
this.energy -= distance
}
})
If you want to call an base class's static or prototype method from the extended class:
SeaMonster.extend("LochNessMonster",{},{
swim : function(distance){
this._super(distance/2);
}
})
Notice how _super calls the base class's method.
IntrospectionYou can access the full and short name of the class for groovy introspection techniques. The following uses the class's name to change where the ajax request is sent:
$.Class.extend("Model",{
findAll : function(success){
// use the class's name to find items
$.get('/'+this.shortName.toLowerCase()+'s',
success,
'json')
}
},{});
// create a task model
Model.extend('Task')
//use it to make a request to /tasks
Task.findAll(function(tasks){
alert(tasks.length);
})
Namespaces
Namespaces are a GREAT idea. It's nice when code doesn't clobber each other.
$.Class.extend("MyCo.Project.MyClass");
MyCo.Project.MyClass.shortName //-> MyClass
MyCo.Project.MyClass.fullName //-> MyCo.Project.MyClass
Setup and Initialization Methods
When a class or instance of a class is created, setup and init methods are called on the class or instance. Setup is called before init and can be used to normalize arguments to init. Init is the class or instance's constructor function. But unlike normal constructor functions, you don't have to worry initialization code that will prevent further inheritance*.
The following makes sure all classes extending Controller will have their init function called with a jQuery element instead of an element:
$.Class.extend("Controller",{
setup : function(el){
arguments[0] = jQuery(el);
return arguments;
}
})
// A dummy tabs class
Controller.extend('Tabs',{
init : function(el){
this.element = el;
}
})
var tab1 = new Tabs($("#foo")),
tab2 = new Tabs($("#bar")[0]);
//call jQuery methods on element
tab1.element.show();
tab2.element.hide();
*You probably won't need setup methods, but it's cool if you need a lot of pre-processing for extending classes. The problem with normal constructor functions is that they typically need to be callable without arguments to be able to setup a correct prototype chain.
Easy Callback CreationSimilar to jQuery's proxy, Class provides a static and prototype callback function that returns a callback that has 'this' or the context set to the Class or instance.
The following uses callback to return a function that will call the new task's show method with 'this' set to the new task.
$.Class.extend("Task",{
init : function(name){
this.name = name
},
get : function(){
$.get('task',
{name:this.name},
this.callback('show'),
'text')
},
show : function(instructions){
alert(this.name+instructions)
}
})
new Task("dishes").get()
The callback function lets you curry arguments and even gets a little Aspect Oriented Program-ish and lets you chain multiple callbacks*:
$.Class.extend("Task",{
init : function(name){
this.name = name
},
get : function(date, callback){
$.get('task',
{name:this.name},
// call show, then callback, curry date
this.callback(['show', callback], date),
'text')
},
show : function(date, instructions){
// return arguments for next function
return [this.name, date, instructions]
}
})
new Task("dishes").get(
new Date(),
function(name, date, instructions){
console.log(name, date, instructions);
}
)
*Chaining callbacks is helpful when there are multiple processing steps before some final callback is called. It makes it so you don't have to curry the final callback over and over again. ;
ConclusionsUse classes where appropriate. There has been some fun poked at classy-ing up jQuery. But there is an important difference between an API for modifying the DOM and a sortable-paginated-groupable grid.
jQuery lacks a clear and repeatable pattern for organizing code. It also lacks tools to extend and add functionality to existing widgets. This is to jQuery's strength. It makes jQuery useful to everyone.
But, as code gets more complex and teams get bigger, the need for patterns and tools to bridge the jQuery to Object Oriented Architecture gap becomes unavoidable. We hope jQuery.Class can help.
Set inner/outer width/height with jQuery Dimensions.Etc Plugin
You often want to animate or set an object to be a specific width or height on the page. But if that element has padding, border, or margin, it will probably take up more space than you expect. The jquery.dimensions.etc.js plugin lets you set and animate the outerWidth, outerHeight, innerWidth and innerHeight of DOM elements.
Downloadjquery.dimensions.etc.js (minified 2kb)
Features- Set and animate innerWidth, innerHeight, outerWidth, and outerHeight.
- Fast. Uses the curStyles plugin.
Change the dimensions of an element in the dimensions demo.
DocumentationRead JavaScriptMVC's dimensions docs.
UseSetting and animating the dimensions with the innerWidth, innerHeight, outerWidth, and outerHeight helpers is pretty straightforward:
// include padding
$('#foo').innerHeight(20)
$('#foo').animate({innerWidth: 80})
// include padding + border
$('#bar').outerHeight(40)
$('#bar').animate({outerWidth: 40})
// padding + border + margin
$('#zed').outerHeight(50, true)
$('#zed').animate({outerWidthMargin: 600})
The numeric value is the length of the dimension in pixels.
These helpers adjust the height and width of the element but include the padding, border, and margin in their calculation. For example, setting outerHeight(40) on an element with "padding:5; border: solid 1px black" will have it's height set to 40 - 2*5 - 2*1 = 28.
ConclusionsWe use outerWidth and outerHeight quite often to make an element the size we want it to appear on the page. It enables us to use and adjust border and padding on most widgets we create. Before outerWidth and outerHeight, we had to manually adjust for border and padding. Hopefully you can avoid our problem by making use of this plugin. Good luck.
Get Multiple Computed Styles FAST with the curStyles jQuery Plugin
Do you use jQuery's css or curCSS functions to get multiple styles on the same element? Don't you know this is slow? The JavaScriptMVC extracted curStyles plugin boosts performance when reading multiple computed styles on a single element.
Downloadjquery.curstyles.js (minified 0.9kb) [works with any version of jQuery]
Features- Gets multiple computed style values on a single element.
- Takes hyphenated (padding-left) or camelCase (paddingLeft) names.
- Performs faster than curCSS.
See how to make a 2-3X faster jQuery.fn.height plugin in the curStyles demo.
DocumentationJavaScriptMVC's curStyles docs.
UseCall curStyles on your jQuery element to get a JavaScript object of style-value pairs. For example:
$('#foo').curStyles('paddingTop','marginTop')
might return something like:
{
paddingTop: '2px',
marginTop: '12px'
}
The values returned are the element's computed style values.
Computed Style ValuesAn element's computed style value is the measurable value (typically in px) of all the styles applied on the element. In IE, these values are calculated from properties on the currentStyle property of an element. In all other (standard) browsers, these values are found using getComputedStyle.
A Faster jQuery.fn.heightThe following makes a jQuery.fn.height plugin and is shown in the demo.
$.fn.fastHeight = function(){
//start with the offsetHeight
var sum = this[0] && this[0].offsetHeight;
// subtract out the current values
$.each(
this.curStyles(
"borderTopWidth",
"borderBottomWidth",
"paddingTop",
"paddingBottom"),
function(name, val){
sum -= parseInt(val) || 0;
});
return sum;
}
Conclusions
This plugin is very useful for optimizing performance critical paths. If you need to get only a handful of computed properties from an element over and over again, you can typically double its speed by using curStyles.
Convert Form Elements to JavaScript Object Literals with jQuery formParams Plugin
Want to quickly extract form data to a more usable format? Of course you do! We use JavaScriptMVC's formParams plugin constantly to turn form data into much easier to manipulate JavaScript Objects. We're releasing formParams standalone so everyone can save a little time on this extremely common task.
Downloadjquery.formparams.js (minified 1kb) [works with all versions of jQuery]
Features- Converts form data into object literals like: {foo: 'bar'}
- Creates nested objects with input names like: 'foo[bar]'
- Can convert values that look like numbers and booleans to Numbers and Booleans.
- Works without a form element.
JavaScriptMVC's formParams documentation.
UseJust call formParams on any jQuery wrapped element (form or not):
$('#myform').formParams() //-> Object
#myform html might look like:
<form id='myform'> <input type='text' name='donate' /> <input type='text' name='credit[name][first]' /> <input type='text' name='credit[name][last]' /> <input type='text' name='credit[number]' /> <input type='text' name='credit[expire]' /> <input type='text' name='credit[security]' /> </form>
The result of formParams might look like:
//$('#myform').formParams() ->
{
donate: 50.55,
credit : {
name : {
first: "Justin",
last: "Meyer"
},
number: "0000-2342-4654-5555"
expire: "5/4/2011",
security: 555
}
}
Sweet huh? You can turn off the Number/Boolean conversion (leaving all values as strings) by calling form params with false:
$('#myform').formParams(false) //-> Object
Oh, and one other cool thing ... if you have a bunch of checkboxes with the same name like:
<input type='checkbox' checked='checked'
name='items' value='model' />
<input type='checkbox' checked='checked'
name='items' value='view' />
<input type='checkbox'
name='items' value='controller'/>
formParams will put the values of the checked checkboxes in an array like:
{items: ['model','view']}
How handy is that!
ConclusionsformParams is quite handy. Because it already depends on an existing form, it's very useful for adding unobtrusive AJAX behavior to forms. We'll often use it to validate a JavaScript object literal before converting the literal to JSON with jquery-json before sending it to a JSON-REST service. That typically looks like:
$('form#recipe').submit( function( ev ) {
ev.preventDefault();
var recipe = $(this).formParams().recipe;
if( !recipe.title && recipe.instructions ) {
alert( 'provide a title and instructions' );
}else{
$.post("/recipes.json",
$.toJSON(recipe),
function(){ .... },
'json'
}
})
Enjoy!
compareDocumentPosition plugin for jQuery
Being able to quickly compare two elements' positions in the browser is extremely useful for a variety of tasks. The DOM Level 3 specification describes the compareDocumentPosition method. It returns a bitmask with a whole bunch of useful information. But not all browsers support it :(
For a really solid walkthrough of compareDocumentPosition, check out John Resig's Comparing Document Position article. In this article, he gets close to providing a fast compareDocumentPosition for all browsers. JavaScriptMVC's standalone jquery.compare plugin steals his ideas and DOES provide a fast compareDocumentPosition in all the browsers jQuery supports.
Downloadjquery.compare.js (minified 0.8kb) [works with all versions of jQuery]
Demo DocumentationJavaScriptMVC's jquery/dom/compare docs.
UseTo use the compare plugin, just call compare on your jQuery wrapped elements with another element or jQuery element:
$('#foo').compare( $("#bar") ) //-> Number
The number is actually a bitmask that represents multiple several pieces of relational information about the two elements. Here's what each bit means:
Bits Number Meaning 000000 0 Elements are identical. 000001 1 The nodes are in different documents (or one is outside of a document). 000010 2 Node B precedes Node A. 000100 4 Node A precedes Node B. 001000 8 Node B contains Node A. 010000 16 Node A contains Node B.This means if a compare results in a 10, node B is before and contains node A. Typically, you just want to know one bit's information. To do this, use the bitwise AND operator (&) like:
if( $('#foo').compare( $("#bar") ) & 2 ){
//do something because #bar is before #foo
}
How it works
The plugin uses all the techniques in John's article, but fixes the 'Safari' problem. Older versions of Safari lack compareDocumentPosition and sourceIndex. This makes comparing node order (the 2 and 4 values of the bitmask) challenging. A naïve approach might be to walk up the parent nodes until an intersection happens. But, as I've learned from event delegation, calling parentNode is surprisingly slow. But using document Ranges and comparing them is fast. The following code adds the 2 and 4 values in Safari:
var range = document.createRange(),
sourceRange = document.createRange(),
compare;
range.selectNode(this[0]);
sourceRange.selectNode(b);
compare = range.compareBoundaryPoints(Range.START_TO_START,
sourceRange);
number += (compare === -1 && 4)
number += (compare === 1 && 2)
This code uses Safari's implemented compareBoundryPoints method in the DOM Level 2 Traversal-Range recommendation. CompareBoundaryPoints compares 2 ranges. You have to provide which parts of the ranges to compare and it returns -1,0, or 1 to indicate the the boundary positions.
Ajax Fixtures Plugin for jQuery
Are you developing jQuery hotness so fast that the slow-poke server team can't supply you AJAX services fast enough? If so, you might be interested in the JavaScriptMVC-extracted jQuery.Fixture plugin. Fixtures simulate AJAX responses so you can keep developing JavaScript functionality even if the server's services aren't working. The fixture plugin is easy to setup and remove once the services are online.
Downloadjquery.fixture.js - works with jQuery 1.2 and greater
Features- Easy to toggle fixtures on and off (just remove the plugin)
- Dynamic fixtures
- Pre-made REST fixtures
- Fixture helpers
JavaScriptMVC's jQuery.fixture documentation
UseIt's easy to use fixtures. Just ...
- Add the script to your page.
- Set the fixture parameter to your simulated response file.
For example ....
$.ajax({url: "/task.json",
dataType: "json",
type: "get",
success: myCallback,
fixture: "fixtures/task.json"
});
... uses "fixtures/task.json" as the response instead of "task.json". The "fixtures/task.json" file might look like:
{
"name" : "take out trash",
"description" : "to the curb",
"id" : 5
}
When the tasks service comes online, just remove the fixtures script tag and jQuery will make requests as normal.
MethodsThe fixture plugin overwrites $.ajax, $.get and $.post to accept a fixture parameter or argument.
//... a property with $.ajax
$.ajax({fixture: FIXTURE_VALUE})
//... a parameter in $.get and $.post
$.get ( url, data, callback, type, FIXTURE_VALUE )
$.post( url, data, callback, type, FIXTURE_VALUE )
Dynamic Fixtures
Services almost always return different results depending on the data sent to the service. Sometimes a single static file is unable to approximate the result of these services. In these cases, by providing a function as the fixture object, fixtures can dynamically generate the result of the request from the data passed into the service (just like your server would do).
$.ajax({
type: "get",
url: "tasks",
data: {limit: 1000},
dataType: "json",
fixture: function( settings ){
var tasks = [];
for( var i=0; i < settings.data.limit; i++ ) {
tasks.push({id: i, name: "task "+i})
}
return [tasks]
}
})
Dynamic Fixtures are really helpful for performance testing your app. Wan't 10k tasks? Make the fixture return 10k tasks. Want requests to take 5 seconds to return, set jQuery.fixture.delay to 5000.
Rest HelpersThe fixture plugin includes several pre-made fixtures (-restUpdate, -restCreate, -restDestroy) that simulate a REST interface. The following example uses -restCreate to simulate creating tasks.:
$.post(
'/messages.php',
messageData,
function(){ ... },
'json',
'-restCreate')
Fixture Helpers
$.fixture.make is used to create fixtures that retrieve JSON data from the server. The fixtures it generates supports limit, offset, filtering, and sorting. The following creates a fixture for 1000 threaded messages and then gets 100-150th child messages of message 5.
//makes a threaded list of messages
$.fixture.make(
["messages","message"],
1000,
function(i, messages){
return {
subject: "This is message "+i,
body: "Here is some text for this message",
date: Math.floor( new Date().getTime() ),
parentId: i < 100 ? null : Math.floor(Math.random()*i)
}
})
//uses the messages fixture to return messages
// limited by offset, limit, order, etc.
$.ajax({
url: "messages",
data:{
offset: 100,
limit: 50,
order: "date ASC",
parentId: 5},
},
success: function(messages){ ... },
fixture: "-messages"
});
Conclusions
Fixtures are a big part of Jupiter and JavaScriptMVC's "secret sauce". In our years doing JavaScript consulting, rarely is the back-end complete. Instead of waiting around, we use fixtures to keep moving forward. The back-end teams can use these fixtures as documentation of the service they need to build.
Even after services are built, fixtures are useful for error, performance, and usability testing. And when new features are added, you can easily start developing from the fixtures again.
Fixtures kick ass.
jQuery Default Events
Event Oriented Architectures (EOAs) are an emerging best practice when designing reusable JavaScript widgets. jQuery's trigger, triggerHandler, and jQuery.event.special[EVENT]._default methods have enabled widget authors to easily expose custom events to developers. However, these methods lack the scalability or expressiveness to enable powerful EOAs.
JavaScriptMVC's jquery.event.default plugin provides an easier and more scalable way of providing default events.
Downloadjquery.event.default.js (minified 1.3kb) [works with latest jQuery on github]
DemoDefault tabs - a tabs widget that uses default events.
Event Oriented ArchitecturesEOAs are simply widgets that produce synthetic events that you can listen to similar to a native event. For example, jQuery UI's tabs widget produces events like tabselect that you can register event handlers on like:
$('#mytabs').bind('tabselect', function(){
//called before a tab is selected
})
Why EOAs?
There are many other methods of exposing a widget's API. The most common is taking a callback function when the widget is created. jQuery.UI's tab widget also allows you to do this:
$("#mytabs").tabs({
select : function(){
//called when a tab is selected
}
})
But, this only allows one responder! It's possible that many objects want to know when something happens in your widget.
If you use Dojo, you might be lucky enough to use dojo.connect. Connect allows you to bind to any other function. By just exposing a function, any other object can be notified when that function is called.
//in the tabs widget
myTabsWidget = {
select : function(){
//tabs select functionality
}
};
// somewhere in your code
dojo.connect(myTabs,"select",null, function(){
//called when select is called on myTabs
})
Another multiple-callback solution is using a library like OpenAjax.hub. Hub allows you to publish and subscribe messages like:
//in your tabs widget
OpenAjax.hub.publish("tab.select",tab)
//in your code
OpenAjax.hub.subscribe("tab.select", function(called, tab){
//do stuff!
})
But these options suck compared to synthetic events for UI widgets because:
- Developers already understand DOM events and how to listen to them.
- Bubbling events naturally group related widgets. Ex: respond to all 'activate' events by listening on a parent element.
jQuery makes creating EOAs simple, if underpowered. Bind, delegate, and live can listen to arbitrary events on elements. Trigger creates synthetic events that bubble like a DOM events. The following shows the first hidden element in the page:
//listen for show events on the document
$(document).bind('show', function(ev){
//show the target
$(ev.target).show()
});
//trigger show on the first hidden element
$(':hidden:first').trigger("show")
Trigger makes it easy to create synthetic events and expose a callback API. But to make these callback functions more powerful and DOM-like, we need to provide default events.
Default EventsWe're all familiar with default events in the DOM. Here's a few examples:
- clicking a link -> Follows the link
- clicking submit in a form -> posts the form
- mousedown on text -> starts selecting text
And with jQuery, we are used to preventing default events by returning false or calling prevent default:
$('a').click(function(ev){
ev.preventDefault(); //these 2 lines
return false; //do the same thing
})
What if we could set default behaviors for certain events on our widgets and let others prevent them in the same way? How awesome!
_defaultIn jQuery 1.4.3, a _default function can be set on special events. For more info check out jQuery's forum. The _default function is called after all event handlers for an event type have been called. For example:
$.event.special.show ={
_default : function(ev){
$(ev.target).show()
}
};
$(':hidden:first').trigger("show")
Underpowered?
jQuery doesn't go far enough in making default events practical for complex applications. Consider the following problems:
- What if multiple plugins define a show default event?
- What if there are multiple tabs plugins, each with its own default tabselect event?
JavaScriptMVC's jquery.event.default.js plugin makes using default events scalable and organized.
jQuery.Event.DefaultThe default plugin lets you add default functionality by binding a default event handler. Just prefix the event with "default.". The following provides default show behavior for '.tab' elements inside a '#tab' element:
$("#tabs").delegate('.tab','default.show',function(){
$(this).show()
})
Of course, global default events are still supported, but without stomping on other code:
$(document).bind('default.show',function(ev){
$(ev.target).show()
})
$(':hidden:first').trigger("show")
Use bind, delegate, or live to listen for default events. The default event handler only gets called if preventDefault is not called.
triggerDefault and triggerDefaultsThe jquery.event.default.js file provides two useful helpers to trigger events and return if default events were triggered.
// synthetic event bubbles
$('#foo').triggerDefaults('show'); //-> true/false
// synthetic event does not bubble
$('#foo').triggerDefault('show'); //-> true/false
The Demo
The demo uses default events to provide a tabs widget. The default 'show' event is prevented on the second tab until the first tab's checkbox is checked.
Here's the code:
// create a tabs plugin
$.fn.tabs = function(){
// finds the tab from the tab button
var sub = function(el){
return $(el.find("a").attr("href"))
}
this.each(function(){
var tab = $(this);
// set the first tab button as active
tab.find("li:first").addClass('active')
// hide all the other tabs
tab.find(".tab:gt(0)").hide();
// listen for a click on a tab button
tab.delegate("li","click", function(ev){
ev.preventDefault();
var el = $(this);
if( // not active button
!el.hasClass('active') &&
// default wasn't prevented
sub(el).triggerDefaults("show")){
// remove active and hide old active
sub(tab.find(".active").removeClass("active"))
.hide();
//mark as active
el.addClass("active");
}
})
// show a tab if default isn't prevented
.delegate(".tab","default.show", function(ev){
$(this).show();
})
})
};
// create tabs widget
$("#tabs").tabs();
// listen on the second tab for show
$("#second").bind("show",function(ev){
//if complete isn't checked
if(! $("#complete")[0].checked ){
//prevent the default action!
ev.preventDefault();
}
});
Here's how the code works:
- When an li in the tabs is clicked, it checks if it isn't active and uses sub to find the tab content for that li.
- It triggers a show action on the tab content.
- It listens to show events on the #second tab, if the checkbox isn't checked, it prevent default events.
- Assuming that default events are allowed, the ".tab" default "show" event will show the tab.
- After the event has finished, control returns back to the original li click handler. If default events were allowed, it hides the old tab.
Default events and event oriented architecture is, to be sure, an advanced JavaScript technique. However, the JavaScriptMVC default event plugin makes building sophisticated and extendable widgets slightly easier. It's a powerful technique, use it with care.
Delegate-able Hover Events for jQuery
Want to use live and delegate with hover events? Now you can! We are releasing JavaScriptMVC's delegate-able hover plugin. This plugin only works for the current 1.4.3 nightly release.
Downloadjquery.event.hover.js (minified 3kb)
Features- Listen to hover events with live, delegate, and bind
- Customize hover behavior locally or globally
Bind, delegate, or use live on any of the following events:
- hoverinit - called on mouseenter, use this event to customize delay and.distance
- hoverenter - an element is being hovered
- hovermove - the mouse moves on an element that has been hovered
- hoverleave - the mouse leaves the element that has been hovered
Example:
// listen for hover on 'option's in a menu
$('#menu').delegate(".option","hoverenter",function(){
$(this).addClass("hovering");
})
// when the mouse leaves restore
.delegate(".option","hoverleave",function(){
$(this).removeClass("hovering");
})
Configuring Distance and Delay
An element is hovered when the mouse moves less than a certain distance in specific time over the element. You can configure that distance and time by adjusting the distance and delay values.
You can set delay and distance globally by adjusting the static properties:
$.Hover.delay = 10 $.Hover.distance = 1
Or you can adjust delay and distance for an individual element in hoverenter:
$(".option").live("hoverinit", function(ev, hover){
//set the distance to 10px
hover.distance(10)
//set the delay to 200ms
hover.delay(10)
})