debuggable

 
Contact Us
 

BaseUrl finding, Simple event broadcasting and element sorting in JS

Posted on 15/3/07 by Felix Geisendörfer

Hey folks,

it's been a while since my last post on JS (and in general) and I apologize for that. There is just way too much stuff for me to do right now (client work, my own web app, school finals, etc.) but most of that will be over at some point and then I'll have a thousand interesting things to blog about as well as the time for it. Meanwhile here comes some fun JS stuff I'm using in the web app I'm working on right now and I hope you'll enjoy it. When you see the '$' sign in the code then it's the jQuery library I'm absolutely in love with and not the evil prototype one ; ). All code I'll post now can be written without it, but I don't have the time to present alternative solutions right now.

Alright let's get started. First of all I advocate the usage of a namespace for the functions I'll present since that makes it less likely to run into conflicts with 3rd party scripts as well as allowing for the code base to stay more maintainable. I'm assuming you are using CakePHP so please create a file called 'common.js' in /app/webroot/js/. The other file you put in there is the latest 'jquery.js' that you downloaded from jquery.com. In your html layout you include jquery.js first and common.js second.

The first thing I got for you is a function capable of determining the base url where your application is installed. This is very useful when doing lot's of ajax requests and you want to be able to always use '/controller/action' style url references like you do in CakePHP internally. It's also nice to know image files will always be located in '/img/...'. The way I do it is inspired by the script.aculo.us lib and looks like this:

var Common =
{
    baseUrl: null
   
    , setBaseUrl: function(url)
    {
        Common.baseUrl = url || $('script[@src$=js/common.js]')
            .attr('src')
            .replace(/js\/common.js$/,'');
    }
   
    , url: function(url)
    {
        return this.baseUrl+url.replace(/^\/+/, '');
    }
}

Common.setBaseUrl();

The code should be pretty straight forward. After defining the Common object (used as a namespace) we call it's setBaseUrl function without parameters. This causes a jQuery selector to find the script element of our common.js file and use it's src attribute to determine the baseUrl. From that point on we can do calls like this in our other JS files:

$.getJSON(Common.url('/tasks/view/1.json'), function(Task)
{
    alert('It is time to "'+Task.name+'"!');
})

Alright this is useful (at least I hope so), but there is more stuff to come. One thing I found myself working a lot with was collections of objects of the same type that need to exchange messages through events with one another. For example you have a class called 'Task' and all of them are managed by another object called 'TaskList'. Now let's say the User is able to click on each Task in your TaskList which causes this particular Task to get the focus (a couple of DOM elements getting an 'active' class). When one Task get's focused, naturally all other tasks need to loose their focus. Here is some code similar to what I'm using in my application right now which will make this become very easy and fail-safe:

A new generic broadcastEvent function for our Common namespace. It basically loops through all listeners (array of JS objects) and sees if they have listeners for the particular task defined and call those up. It also set's the context of the listener function to the object that is listening and uses whatever additional parameters are passed to broadcastEvent for calling it:

var Common =
{
    broadcastEvent: function(listeners, event)
    {
        var params = arguments;
       
        $.each(listeners, function()
        {
            if (typeof this.events[event] == 'function')
            {
                this.events[event].apply(this, [].slice.call(params, 2));
            }
        });
    }
}

And here comes the TaskList that object that manages all Task's on the screen. It basically uses a table (element) as it's construct parameter, loops through all rows with a 'td' in them (those that are not part of the table header) and passes the table row elements as an initialization parameter to the Task object that is created for each one of it and added to a list of tasks. TaskList.broadcastEvent uses Common.broadcastEvent to allow the tasks inside the TaskList to communicate with one another easily which you'll see in the Task class.

var TaskList = function(element)
{
    this.construct(element);
};

TaskList.prototype =
{
    element: null
    , tasks: []

    , construct: function(element)
    {    
        this.element = $(element)[0];
       
        var self = this;
       
        $('tr[td]', this.element).each(function()
        {
            self.add(new Task(self, this));
        });
    }
   
    , add: function(TaskObject)
    {
        this.tasks.push(TaskObject);
    }
   
    , broadcastEvent: function(event)
    {
        var params = [this.tasks, event];
        params.push.apply(params, [].slice.call(arguments, 1));
       
        Common.broadcastEvent.apply(null, params);
    }
};

And here comes the Task class that triggers the event broadcasting and listens to events. It's constructor takes two arguments. The first one is a reference to the parent TaskList object and the second on is the table row (tr) element that hold the DOM representation of this Task. When the name of the task ('a.name') is clicked then it causes that Task to get the focus and all others to be blured (as they receive the focus event and react on it):

var Task = function(parent, element)
{
    this.construct(parent, element);
};

Task.prototype =
{
    id: null
    , parent: null
   
    , construct: function(parent, element)
    {
        this.parent = parent;
        this.element = element;
       
        this.bindEvents();
    }
   
    , bindEvents: function()
    {
        var self = this;
       
        $('td.task a.name', this.element).bind('click', function()
        {
            self.focus();
            return false;
        });
    }
   
    , focus: function()
    {
        $('a.name, a.time', this.element)
            .removeClass('active')
            .addClass('active');
           
        this.parent.broadcastEvent('focus', this);
    }
   
    , blur: function()
    {
        $('a.name, a.time', this.element)
            .removeClass('active');
    }
       
    , events:
    {      
        focus: function(Task)
        {
            if (Task !== this)
            {
                this.blur();
            }
        }
    }
};

Alright that's it. The code above is of course just meant for educational purposes and not the complete code I'm using in my web app which is a little more complex and would only distract from demonstrating the Common.broadcastEvent function. So if it doesn't run it's probably because I didn't test the simplified version presented here.

So let's come to my last little trick, sorting elements in JS. Javascript has a very cool function build into the Array class named sort that is very useful for doing all kinds of sorting as it allows you to define a callback for comparing the array elements yourself. It basically works like this:

The following code uses jQuery to get all list items inside of a unorder list and then sorts the array using the anchor text (for simplicity, innerHTML).

var elements = $('ul#my-list li').get();

elements.sort(sortByName);

function sortByName(a, b)
{
    if (a.innerHTML < b.innerHTML)
    {
        return -1
    }
   
    if(a.innerHTML > b.innerHTML)
    {
        return 1
    }
   
    return 0
}

But since we don't only want to sort them inside the array but also in the DOM, we need to get a little more tricky. In the simplest case this means to remove all li elements from our unordered list before sorting and them put them back into our list after we sorted them:

var elements = $($('ul#my-list li')
    .remove()
    .get()
    .sort(sortByName))
    .appendTo('#my-list');

If you don't like the train-wreck style you'd write the above like this:

var elements = $('ul#my-list li').remove().get();
elements.sort(sortByName)
$(elements).appendTo('#my-list');

So of course if you work with large lists of items you might want to actually swap elements using the Array.sort callback instead of re-populating the entire list, but for this example I wanted to keep it simple. Also here is a live demo where you can check out how the method presented here works on a small list. You basically can not tell that all elements where removed and then added again.

Alright, I hope you find some of the above stuff useful and if you have any questions feel free to ask them in the comments.

-- Felixd

 
&nsbp;

You can skip to the end and add a comment.

Walker Hamilton said on Mar 30, 2007:

Hey Felix....I know you've disappeared into the great unknown, but when you get a chance, you should fix your cakeNews site so that it doesn't display those webModel errors all the time....

Also, you might consider adding some different blogs in there....(getting rid of WithCake, adding @theKeyboard, getting rid of H3rald).

I have your cakenews in my bookmarks bar ( I use it all the time! )

Felix Geisendörfer said on Mar 30, 2007:

Walker Hamilton: Hm what errors is it throwing? Can't see any right now. But I can update the blogs in the next couple days I guess. I wasn't aware people are still using it ; ). (I'm pretty much on google reader these days)

Walker Hamilton said on Mar 30, 2007:

Quite often (seemingly when it updates the rss cache) it throws web model errors, I'll try and show ya one.

Oh, I just got it to do it:

Warning: curl_setopt() [function.curl-setopt]: CURLOPT_FOLLOWLOCATION cannot be activated when in safe_mode or an open_basedir is set in /www/htdocs/w00645aa/cakenews/app/models/web_model.php on line 127

Warning: curl_setopt() [function.curl-setopt]: CURLOPT_FOLLOWLOCATION cannot be activated when in safe_mode or an open_basedir is set in /www/htdocs/w00645aa/cakenews/app/models/web_model.php on line 127

Yeah, I use netnewswire as my reader, but I still like to hit up your cakenews site.....i dunno, something about it....

Matt Hamann said on May 27, 2009:

Having trouble with your first example...I'm getting the error "Uncaught Syntax error, unrecognized expression: [@src$=js/common.js]" when running the function in common.js under jQuery 1.3.2. Any idea what might be wrong?

This post is too old. We do not allow comments here anymore in order to fight spam. If you have real feedback or questions for the post, please contact us.