Rails page.replace and Ext JS

Posted by mick on June 21st, 2008 filed in Ext, Ruby on Rails

I tore my hair out tracking down a problem last week and figured I should share my solution.

I’m currently experimenting with using the Ext Javascript library to create much of the front end of a Ruby on Rails application. I hit a problem where using Rails’ page.replace was failing on Firefox. Specifically any script tags embedded in the html were not being run.

Turns out the problem is caused by both Ext and the Prototype library defining a defer() method, each with different parameters and semantics from the other. Due to the order I include things, the Ext version of defer wins, and is used everywhere. Rails’ page.replace eventually causes defer() to be called, but it needs to call Prototype’s defer rather than Ext’s defer.

The problem has been discovered by various people. For example, there have been some posts on the Ext forum about it (here and here) and the rails community feel it should be fixed in Ext.

A solution

There are no doubt many ways to consider addressing this problem, but the simplest I can think of is to patch /ext/adapter/prototype/ext-prototype-adapter.js so that the Ext defer function changes from:


    defer: function(C, E, B, A) {
        var D = this.createDelegate(E, B, A);
        if (C) {
            return setTimeout(D, C)
        }
        D();
        return 0
    },
 

to:


    defer: function(C, E, B, A) {
        if(arguments.length == 0) {
            // this is for compatability with the Prototype
            // library which has a defer() function
            // that takes no parameters, and whose
            // meaning is the same as Ext's defer(10)
            C = 10;
        }
       
        var D = this.createDelegate(E, B, A);
        if (C) {
            return setTimeout(D, C)
        }
        D();
        return 0
    },
 

That way the Ext defer provides the same semantics as the Prototype defer in the case where the caller provides no parameters. As such, it no longer matters that page.replace ends up calling Ext’s defer since Ext’s defer now does what page.replace expects.

Alternatively …

Another way around this problem is to stop using page.replace and to use Ext’s Ext.Updater facility instead. Converting an app from using page.replace to Ext.Updater is not a trivial exercise, however, and so I’d still suggest applying the above patch even if you do plan to convert across to Ext.updater, especially since the patch addresses the defer problem directly, and defer may be used by prototype (and Rails) in many places.


4 Responses to “Rails page.replace and Ext JS”

  1. Brian Says:

    I ran into this problem, as well. In our case, we were invoking asynchronous AJAX requests through Prototype’s Ajax.Request. About 5% of the time, we would get errors since the onSuccess handler was being invoked prior to the AJAX response being sent. This is due to the differences in the way that Ext implements defer compared to Prototype.

    Your solution wouldn’t work in our case since Prototype actually does call defer with a single argument (the number 1) in the case of AJAX requests. Thus, I came up with an enhancement to your solution to handle the problem. I took it one step further and defined my own Function.prototype.defer method from which I can choose between the Prototype defer method and the Ext defer method.

    /*
    There is a pretty nasty conflict issue between Ext and Prototype with the Function.prototype.defer method.
    Prototype implements the method and relies on its behavior for certain things (such as asynchronous AJAX requests).
    Ext implements the method with different behavior and relies on it pretty heavily, as well.
    Due to the order of our includes (Prototype, then Ext), Ext’s defer method “wins” and is used even when Prototype
    is trying to use its own defer method. This can cause some strange AJAX issues with onSuccess handlers
    being invoked before the AJAX request has successfully been completed.

    In order to solve, we are going to override defer with our own custom implementation. This implementation
    is going to have to know about the “special” ways that Ext and Prototype invoke this method and invoke
    the proper method depending on which one was intended. Luckily, Prototype only ever calls this method with
    either no arguments or with a single argument value of the number 1. Thus, in those two cases, we will
    invoke Prototype’s defer method. In all other cases, we will invoke Ext’s implementation.
    Note that Prototype’s defer method is actually just a chained call to delay.curry(0.01) (per prototype.js – 1.6.0.2).
    Note that we need to keep a reference to the Ext defer method prior to overwriting the method.
    */
    Function.prototype.oldExtDefer = Function.prototype.defer;
    Function.prototype.defer = function() {
    if(arguments.length==0 || (arguments.length==1 && arguments0==1)) {
    return this.delay.curry(0.01).apply(this, arguments);
    }
    return this.oldExtDefer.apply(this, arguments);
    };

  2. mick Says:

    Thanks for the enhancement Brian. I’ve repeated it below in a hopefully more nicely formatted form:

    Function.prototype.oldExtDefer = Function.prototype.defer;

    Function.prototype.defer = function() {
        if(arguments.length==0 ||
             (arguments.length==1 && arguments[0]==1))
        {
            return this.delay.curry(0.01).apply(this, arguments);
        }
        return this.oldExtDefer.apply(this, arguments);
    };
     

  3. AndrewBoldman Says:

    Hi, cool post. I have been wondering about this topic,so thanks for writing.

  4. Kelly Brown Says:

    Hi, very nice post. I have been wonder’n bout this issue,so thanks for posting

Leave a Comment

//