jQuery be nice

Getting jQuery to behave with very large queries and DOM manipulation, for when you get “Unresponsive script” errors…

We’ve all been there – a giant page with lots of repeated DOM elements that we need to do … something … with. In my case it was a very large form with 3000+ fields that all needed to be validated; we were using jQuery unobtrusive validation, for the sake of maintainability and a “sane” way to integrate with MVC’s view models, we were sticking with this.
Now before you think the page should be re-factored, there simply wasn’t time in the schedule for this, plus design constraints dictated that the page worked in a certain way.

So anyway, the style in which the unobtrusive validation script is designed I dub “big hammer”, it basically initialises each field one-by-one at startup – this process takes 10+ seconds on the page in question, on a really nice machine in Chrome. On an older XP box with IE7, it could sometimes time out. Clearly something is being done wrong.

Right, so by now you’re thinking “cool story, bro”, but I am getting to the point…
Read more

Augment objects, for when extend is not enough

Don’t you hate it when you have two semi-complex configuration objects, and you need to “mash” them together?
Here is what I mean, is take these objects:

var cfg = {
        plugins: [‘a’, ‘b’],
        config: {
                show: {
                        immediately: true,
                        order: ‘natural’
                }
        }
},
options = {
        plugins: [‘c’],
        config: {
                show: {
                        order: ‘reverse’
                }
        }
};
 

And mash those into:

{
        plugins: [‘a’, ‘b’, ‘c’],
        config: {
                show: {
                        immediately: true,
                        order: ‘reverse’
                }
        }
}
 

If you use jQuery extend, by default the values in cfg are overwritten by those in options.
Read more

Long poll jsonp

UPDATE: Implemented changes to the code as suggested by Akeru, thanks!
UPDATE2: Updated to the newer version of jQuery-jsonp, thanks Julian, the Ugly spinner loading is now gone!

Go straight to the code
or
Download it in a zip file

So, whatever happened to comet? Wasn’t it supposed to save the masses from wasteful ajax polling? I certainly appreciate Alex Russel’s work on the matter, but it really seems as though this very useful technology is falling to the wayside. I think this is mainly due to the difficulty and requirements of a successful implementation on the most popular web server, the main problem is Apache’s use of threading, this post explains the problem eloquently. Of course Apache have a whole project that deals with this issue to a degree, but I don’t think that it is easy to implement, once you dig a little into the documentation, you quickly see that this is not really a thing that just works out of the box… You need to read a book on the subject first. That is a bit of a deal-breaker for me; after all, the technique is not that difficult to understand, so why should you need to read a whole book to implement it? Read more

CSS Formatter

So I created a CSS parser and formatter on the weekend; I think it’s kinda neat as it formats CSS “just the way I like it”.
Why would I do this, when there are so many CSS formatters available? Well, the aim of most formatters is to “optimise” the CSS
(ie: make it as small as possible), whereas the aim of my formatter is to make it as readable as possible. Read more

WSG meeting was fun!

Had a ton of fun presenting, and met some interesting people. Even some that want to help out with PAX which would be great!

Thanks very much to Russ for organising the night!!!

The future of Mozilla

I attended the free Mozilla booze-up Labs event the other month; it was quite interesting; Ben Galbraith spoke about what Mozilla is up to, and what they are planning in the near future… It was interesting because I got the feeling that whilst they have a few interesting projects in the pipeline, they don’t really have anywhere specific to take their browser engine; it has gotten to a point where it performs on par with all the other browsers, and all the other browsers perform on par with it.
So where should Mozilla go from here? I think it is blindingly obvious what they could do, though I don’t think they’re gonna like my suggestion… Before I mention what it is, lets take a closer look at Mozilla. Read more

WSG Nov 11th

I’m speaking at the WSG about my PAXJS framework next month! You can register at the WSG website! I always enjoy the WSG meetings, there is a great mix of people from different areas and backgrounds, and much discussion anout web development in general. Note that the cost is $10 to attend.

Manage your data with the jQuery Collections plugin





Download the script

It’s a classic problem: you have a small set of data records that you want your users to interact with at the same time, usually with a need for ordering of the records. I’ve seen this exact problem pop up in just about every project that I’ve every worked on. The most obvious solution to the problem is an inline-editable datagrid-style widget. Often this provides a “good enough” solution, and many UI designers convince their users to integrate this type of widget, as it is relatively easy to do.
This sometimes leads to dis-joined user experiences, inconsistent look & feel, depending on what solution is employed and how much time you spend on integrating it.

First, here are some examples of data grids:

Datagrid from extjs.com

Datagrid based on mootools

Datagrid from paxjs.com

These widgets have the following in common:

  • Data is displayed in a single row by default
  • When editing a row, the data from other rows is not always accessible
  • The look and feel is “like a spreadsheet”, and usually doesn’t offer too much in the way of customisation.

This is of course not too bad, as long as it is consistent with the rest of your application, and performs the necessary
tasks for your user. Some datagrids allow you to customise colours and fonts, but that is usually about as far as you can take
it without dwelving deeply into customisation and spending lots of time setting up work arounds for how the
grid is laid out and interacts with your data.

So how do you create a good solution, which easily fits in with your design, and back-end api without spending a
bunch of time customising the datagrid script, and laboriously hacking at the CSS?

The collection manager

The premise is quite simple, it works a little like a datagrid in that there is a concept of data-rows, but
it doesn’t matter what HTML you use for each row; this gives us flexibility to customise exactly how the layout
looks, and at the same time has enough smarts to help you manage the collection of items very easily.

To create a collection, you simply setup a template, a target element, a add button and initialise
the collection using the collections manager script.

Let us start with the most simple of examples.

Example: Hello world

In this example, you can add names – enter a few, and click the “Submit form” button to see how the values map to back-end objects.



The html
<div id=‘collectionTarget0′ class=‘clearfix’></div>
<div class=‘clear’></div>

<button type=‘button’ id=‘addButton0′>Add item</button>

We have a div as the target to render the collection manager into, plus an “add” button. The clearing div is optional,
depending on your layout requirements. This HTML is inside a standard form tag that submits to collections.php, which simply
shows you the contents of the submitted form.

The template
<div id=‘collectionTemplate0′ style=‘display: none’>
    Name: <input name=‘name’ type=‘text’>

</div>

The template is where things get interesting; it is plain html, BUT, (and it’s a big BUT), there are no element IDs.
This is because the collections manager script takes the template, and adds IDs to the form elements as needed; it uses
the collection manager’s namePattern to name the field and give it a unique ID. This is a requirement, as we can
have multiple data rows.

The initialisation script
jQuery( document ).ready( function() {

    jQuery(‘#collectionTarget0′).collections( {
        template: jQuery(‘#collectionTemplate0′),
        addButton: jQuery(‘#addButton0′)

    } );
} );

The initialisation script simply brings everything together, it uses the template as the query target, specifies the
template element, and then the add button.

Let us look at a slightly more useful example, a customer profile – lets assume we have a data structure that maps to this JSON:

var customerProfile = {
    name: ,

    comments: ,
    customerType: [‘n’, ‘r’, ‘v’],
    newsletter: true

};

The customerType maps to one of the following values: n = new, r = repeat, v = vip; news letter is true or false.

Example 1: Collection manager for customer profiles

In this example, we start with two rows of example data, and use the default settings for everything else. We also include a
delete button, so that you can remove collection items. Change some of the values, or perhaps add a new row, then click the
“Submit form” button to see how the back-end sees your values.


The html
<div id=‘collectionTarget1′ class=‘clearfix’></div>

<div class=‘clear’></div>
<button type=‘button’ id=‘addButton’>Add</button>

Again, we have a div and an “add” button. Also, the HTML is inside a standard form tag that submits to collections.php.

The template
<div id=‘collectionTemplate1′ class=‘clearfix cp_collection’ style=‘display: none’>

    <input name=‘id’ type=‘hidden’>
    <div class="span-3">

        <div class="span-3">
            <label class=‘above’ for=‘name’>Name</label>

            <input class=‘coll medium’ name=‘name’ type=‘text’>

        </div>
        <div class="span-3">
            <label class=‘above’ for=‘customerType’>Customer Type</label>

            <select class=‘coll’ name=‘customerType’>
                <option value=‘n’>New</option>

                <option value=‘r’>Repeat</option>
                <option value=‘v’>VIP</option>

            </select>
        </div>
    </div>

    <div class="span-9">
        <label class=‘above’ for=‘comments’>Comments</label>

        <textarea class=‘mediumText coll’ name=‘comments’></textarea>
    </div>

    <div class="span-3">
        <label class=‘above’>Send newsletter</label>

        <div>
            <input class=‘coll’ name=‘spam’ type=‘radio’ value=‘true’>Yes

            <input class=‘coll’ name=‘spam’ type=‘radio’ value=‘false’>No

        </div>
    </div>
    <div class="span-2 last">

        <label class=‘above’>Action</label><br/>
        <button type=‘button’ name=‘deleteButton’>Delete</button><br/>

    </div>
</div>

The template is more complex than the hello world example, but it is just plain HTML with no IDs, just names for the form
fields. You will note that there is also a “Delete” button in this template, which allows us to remove items.

The initialisation script
jQuery(document).ready(function(){

    jQuery(‘#collectionTarget1′).collections( {
        template: jQuery(‘#collectionTemplate1′),
        data: [

            { id: 1, name: ‘David Lann’, comments: ‘David gets a discount at your discression’, customerType: ‘v’ },

            { id: 2, name: ‘Jill Jones’, comments: ‘call on 0295551231, or 0405551599′, spam: ‘true’ }

        ],
        addButton: jQuery(‘#addButton’),
        deleteButtonName: ‘deleteButton’

    } );
} );

The initialisation script now contains some initial data and a reference to the “Delete” button. The use of the
deleteButtonName parameter specifes that the script will attach a delete function to that button for each
record.

Example 2: collection manager with order

Here we expand on the previous example by adding ordering and move up / down buttons; this is a unique feature that the collection
manager has, compared to standard data grids: a concept of order.
As you move a field up or down, we persist the value of the order fields, so assuming the fields were in the correct order to start
with, we should have the correct order once we submit the form. Note that usually the order field would be hidden, but we’re showing
it here to make it easier to see what is happening.
Change some of the values, and move the fields around, then click the “Submit form” button to see the values, taking
note of the order field values.


The html
<div id=‘collectionTarget2′ class=‘clearfix’></div>

<div class=‘clear’></div>
<button type=‘button’ id=‘addButton2′>Add</button>

Again, we have a div and an “add” button. Also, the HTML is inside a standard form tag that submits to collections.php.

The template
<div id=‘collectionTemplate2′ class=‘clearfix cp_collection’ style=‘display: none’>

    <input name=‘id’ type=‘hidden’>
    <div class="span-3">

        <div class="span-3">
            <label class=‘above’ for=‘name’>Name</label>

            <input class=‘coll medium’ name=‘name’ type=‘text’>

        </div>
        <div class="span-3">
            <label class=‘above’ for=‘customerType’>Customer Type</label>

            <select class=‘coll’ name=‘customerType’>
                <option value=‘n’>New</option>

                <option value=‘r’>Repeat</option>
                <option value=‘v’>VIP</option>

            </select>
        </div>
    </div>

    <div class="span-9">
        <label class=‘above’ for=‘comments’>Comments</label>

        <textarea class=‘mediumText coll’ name=‘comments’></textarea>
    </div>

    <div class="span-3">
        <label class=‘above’>Send newsletter</label>

        <div>
            <input class=‘coll’ name=‘spam’ type=‘radio’ value=‘true’>Yes

            <input class=‘coll’ name=‘spam’ type=‘radio’ value=‘false’>No

        </div>
        <div class="span-3">
            <label class=‘above’ for=‘order’>Order</label>

            <input class=‘coll short orderField’ name=‘order’ type=‘text’>

        </div>
    </div>
    <div class="span-2 last">

        <label class=‘above’>Action</label><br/>
        <button type=‘button’ name=‘deleteButton’>Delete</button><br/>

        <button type=‘button’ name=‘moveUpButton’>Up</button><br/>

        <button type=‘button’ name=‘moveDownButton’>Down</button>
    </div>

</div>

The template is almost the same as the previous example, except that we now have an order field.

The initialisation script
jQuery( document ).ready( function() {

    jQuery(‘#collectionTarget2′).collections( {
        template: jQuery(‘#collectionTemplate2′),
        //  Initial data

        data: [
            { id: 1, name: ‘David Lann’, customerType: ‘v’, order: ’1′ },

            { id: 2, name: ‘Jill Jones’, customerType: ‘n’, spam: ‘true’, order: ’2′ }

        ],
        addButton: jQuery(‘#addButton2′),
        deleteButtonName: ‘deleteButton’,

        beforeAdd: function( data, args ) {
            //  Set the order, using the collection count

            var collectionCount = parseInt( jQuery( args.addButton ).attr( ‘collectionCount’ ), 10 );

            data[‘order’] = data[‘order’] || collectionCount;
            return data;

        },
        bindings: {
            //  Move up collection, freezing the order field values; you could

            //  optionally freeze other fields here
            ‘moveUpButton’: {

                event: ‘click’,
                func: function( e, collection ) {

                    jQuery.fn.moveUpCollection( collection, [‘order’] );

                }
            },

            //  Move down collection, freezing the order field values
            ‘moveDownButton’: {

                event: ‘click’,
                func: function( e, collection ) {

                    jQuery.fn.moveDownCollection( collection, [‘order’] );

                }
            }

        }
    } );
} );

The initialisation script now contains some initial data and a reference to the “Delete” button. The use of the
deleteButtonName parameter specifes that the script will attach a delete function to that button for each
record.

Example 3: collection manager for customer profiles with ajax delete

Yet again we expand on the previous example, this time we add ajax delete…
Change some of the values, and move the fields around, then click the “Submit form” button to see the values, taking
note of the order field values. We have also included the “namespace” parameter, which affects how the data is presented
to the back-end, but more about this later.


The html
<div id=‘collectionTarget3′ class=‘clearfix’></div>

<div class=‘clear’></div>
<button type=‘button’ id=‘addButton3′>Add</button>

Again, we have a div and an “add” button. Also, the HTML is inside a standard form tag that submits to collections.php.

The template
<div id=‘collectionTemplate3′ class=‘clearfix cp_collection’ style=‘display: none’>

    <input name=‘id’ type=‘hidden’>
    <div class="span-3">

        <div class="span-3">
            <label class=‘above’ for=‘name’>Name</label>

            <input class=‘coll medium’ name=‘name’ type=‘text’>

        </div>
        <div class="span-3">
            <label class=‘above’ for=‘customerType’>Customer Type</label>

            <select class=‘coll’ name=‘customerType’>
                <option value=‘n’>New</option>

                <option value=‘r’>Repeat</option>
                <option value=‘v’>VIP</option>

            </select>
        </div>
    </div>

    <div class="span-9">
        <label class=‘above’ for=‘comments’>Comments</label>

        <textarea class=‘mediumText coll’ name=‘comments’></textarea>
    </div>

    <div class="span-3">
        <label class=‘above’>Send newsletter</label>

        <div>
            <input class=‘coll’ name=‘spam’ type=‘radio’ value=‘y’>Yes

            <input class=‘coll’ name=‘spam’ type=‘radio’ value=‘n’>No

        </div>
        <div class="span-3">
            <label class=‘above’ for=‘order’>Order</label>

            <input class=‘coll short orderField’ name=‘order’ type=‘text’>

        </div>
    </div>
    <div class="span-2 last">

        <label class=‘above’>Action</label><br/>
        <button type=‘button’ name=‘deleteButton’>Delete</button><br/>

        <button type=‘button’ name=‘moveUpButton’>Up</button><br/>

        <button type=‘button’ name=‘moveDownButton’>Down</button>
    </div>

</div>

The template is structurally the same as the previous example; of course the names of the fields are different.

The initialisation script
jQuery( document ).ready( function() {

    var config = {
        template: jQuery(‘#collectionTemplate3′),
        //  Initial data

        data: [
            { id: 1, name: ‘David Lann’, customerType: ‘v’, order: ’1′ },

            { id: 2, name: ‘Jill Jones’, customerType: ‘n’, spam: ‘true’, order: ’2′ }

        ],
        addButton: jQuery(‘#addButton3′),
        deleteButtonName: ‘deleteButton’,

        beforeAdd: function( data, args ) {
            //  Set the order, using the collection count

            var collectionCount = parseInt( jQuery( args.addButton ).attr( ‘collectionCount’ ), 10 );

            data[‘order’] = data[‘order’] || collectionCount;
            return data;

        },
        bindings: {
            //  Move up collection, freezing the order field values

            ‘moveUpButton’: {
                event: ‘click’,

                func: function( e, collection ) {
                    jQuery.fn.moveUpCollection( collection, [‘order’] );

                }
            },

            //  Move down collection, freezing the order field values
            ‘moveDownButton’: {

                event: ‘click’,
                func: function( e, collection ) {

                    jQuery.fn.moveDownCollection( collection, [‘order’] );

                }
            }

        },
        //      Override the delete function, to send ajax request if necessary
        deleteFunction: function( template, args, data, deleteButton ) {

            jQuery( deleteButton ).text( ‘Deleting…’ ).attr( ‘disabled’, ‘disabled’ );

            
            //      If we have data, send it to the server to delete, otherwise simply remove

            var dataLength = 0;
            for( var x in data )dataLength++;

            
            if( dataLength > 0 ) {

                //      Add delete parameter to the data
                data[‘action’] = ‘delete’;

               
                var request = jQuery.ajax( {

                    type: ‘POST’,
                    data: data,

                    url: ‘collections.php’,
                    dataType: "text",

                    error: function() {
                        alert( "Ajax error occured" );

                    },
                    success: function( html ) { //      Remove collection

                        jQuery.fn.removeCollection(template, args, data);

                    }
                } );

            } else {
                jQuery.fn.removeCollection( template, args, data );

            }
        },
        namePattern: "contact{number}[{name}]"

    };
   
    jQuery(‘#collectionTarget3′).collections(config);
} );

The initialisation script now also has a deleteFunction parameter, with a function that ensures that we only
access the back-end if we need to; this allows the back-end to only

Back end integration

Of course the collections manager is useless without data from the back-end; it is very
easy to integrate the collection manager, thanks laregly to the namePattern parameter, which allows tight
control of how the fields are named; out of the box, you can quickly integrate with just about any php, python, java
(struts, springBeans), .NET, etc…

The namePattern parameter allows you to specify where the record number and name appears; in the last example,
we use a namePattern like so:

namePattern: "contact{number}[{name}]"

This creates individual objects for each data row, rather than the default of one array of data rows. This can be particularly
convenient if you have a set number of a type of object, for example if you have exactly four contact objects named “contact1″,
…, “contact4″, this will allow you to easliy map directly to those objects.
The default namePattern is:

namePattern: "collection[{number}][{name}]"

Which will map to one array named “collection” of data rows.

API

Here is a list of options, and what you can do with the collections manager

  • target – The target of the collection manager (defaults to jQuery result)
  • template – Element to use for cloning. Note: do NOT give any items IDs, they will be removed
  • data – Optionally include some data, default is 1 record
  • startCount – What number to start the count from.
  • minItems – Minimum number of items to show at any time (default 1)
  • maxItems – Maximum number of items to show (default 10)
  • namePattern – Pattern for naming the form fields (for mapping to back-end objects)
  • deleteButtonName – Name for delete button to use in the template
  • deleteFunction – Delete function override, params: ( template, args, data, deleteButton )
  • addButton – Button to assign the add function to.
  • bindings – name: {event:function}. WIP: we may be able to get rid of the specific addButton and DeleteButton events here.
  • beforeAdd – Function that runs before an item is added, params: ( data, args ), if we return anything, it will be used as data
  • afterAdd – Function that runs after an item is added, params: ( data, args )
  • beforeRemove – Function that runs before an item is removed – return false to cancel removal of item. Params: ( data, args )
  • afterRemove – Function that runs after an item is removed, params: ( data, args, wasRemoved )