Short story: Rendering a view over ajax in Yii Framework might include JS and CSS files. There is no way for Yii to know that some of these files might already be present in your DOM. Having your browser re-download and add duplicates of these files can or will lead to problems. A short snipped of JS code will filter away all JS and CSS references from incoming content, while leaving inline script and style intact. No server-side changes required. Scroll down for code.

Longer story: If you attempt to render a view with processing of script in Yii Framework you’ll end up with script tags for whichever scripts the components in your view requires. For example, if you have a view which uses the CListView component, you’ll end up with jquery.listview and jquery.js. This is pretty much what you would expect when rendering a view, but it will lead to problems if you are rendering this in an ajax response and the scripts in question are present in the DOM. Yii has no way to know that you have already executed a request, which returned these scripts. There has been some talk on the Yii Framework forums on how to tackle this problem, but so far I’ve not seen any implementations or hacks that’s been satisfactory.

Server side tracking of client scripts
One way to go is to keep track of the script files that has been sent to the client on the server, and reset this state when a full page request is executed. This might seem like a good idea, but once a user starts browsing with several tabs at the same time it gets complicated. It will require hacks to prevent resetting in cases where requests fail midways etc. The state must be kept not only on a path basis, but also for submitted content (ie ajax post), which would further complicate things.

My proposal is to use jQuerys dataFilter hook (via $.ajaxSetup), which is invoked after ajax request and allows you to filter any data returned before it’s passed on to ordinary success-handlers etc.

Update: Thanks to NLAC for some code improvements.

Simply include this script after jQuery, or add this function to your existing code:

$.ajaxSetup({
	global: true,
	dataFilter: function(data,type){
		//  only 'text' and 'html' dataType should be filtered
		if (type && type != "html" && type != "text")
		{
            		return data;
		}
 
		var selector = 'script[src],link[rel="stylesheet"]';
 
      		// get loaded scripts from DOM the first time we execute.
        	if (!$._loadedScripts) {
			$._loadedScripts = {};
			$._dataHolder = $(document.createElement('div'));
 
            		var loadedScripts = $(document).find(selector);
 
			//fetching scripts from the DOM
		        for (var i = 0, len = loadedScripts.length; i < len; i++) 
			{
        		        $._loadedScripts[loadedScripts[i].src] = 1;
            		}
        	}
 
		//$._dataHolder.html(data) does not work
		$._dataHolder[0].innerHTML = data;
 
		// iterate over new scripts and remove if source is already in DOM:
		var incomingScripts = $($._dataHolder).find(selector);
		for (var i = 0, len = incomingScripts.length; i < len; i++)
		{
			if ($._loadedScripts[incomingScripts[i].src])
			{
	        	        $(incomingScripts[i]).remove();
            		}
            		else
            		{
                		$._loadedScripts[incomingScripts[i].src] = 1;
            		}
        	}
 
		return $._dataHolder[0].innerHTML;
	}
});

If you have improvements for this code, please email me at eirikhm@gmail.com

6 Responses to “Yii-Framework: Preventing duplicate JS/CSS includes for ajax requests”

  1. Clever solution. I was looking for something like this only.

    Many Thanks,
    Utkarsh

  2. But there are some issues like it is not loading css even if not loaded already. For example I used EAjaxUpload externsion and it was not loading fileuploader.css file.

    Also i tried to alert incomingScripts[i].src that is coming out to be undefined.

    Thanks,
    Utkarsh

  3. Ok, so what was originally gonna be a troubleshooting reply has turned into a Praise. This has got to be the easiest solution to this common problem.

    I would however state that it needs getting a bit into the code, in cases where you don’t really have a common implementation (or even in some common ones, like type JSON).

    I ended up slightly editing this to process type JSON which makes it possible to use this generic dataHook to parse JSON to object, retrieve the known HTML render attribute, search and clean duplicates on it, then converting it back to JSON so the App never has a problem with what it receives (always the expected type).

    Thank you very much.

  4. Hi, your code is really nice. I use this one as solution for the described problem. How ever you script has a little bug. CSS files don’t have an attribute src. So you have to use href instead.

    I modified your script as follows:
    $.ajaxSetup({
    global: true,
    dataFilter: function(data,type) {

    var getScriptUrl = function (entry)
    {
    if (entry.type == “text/css”)
    {
    return entry.href;
    }

    return entry.src;
    };

    // only ‘text’ and ‘html’ dataType should be filtered
    if (type && type != “html” && type != “text”)
    {
    return data;
    }

    var selector = ‘script[src],link[rel="stylesheet"]‘;

    // get loaded scripts from DOM the first time we execute.
    if (!$._loadedScripts)
    {
    $._loadedScripts = {};
    $._dataHolder = $(document.createElement(‘div’));

    var loadedScripts = $(document).find(selector);

    //fetching scripts from the DOM
    for (var i = 0, len = loadedScripts.length; i < len; i++)
    {
    $._loadedScripts[getScriptUrl (loadedScripts[i])] = 1;
    }
    }

    //$._dataHolder.html(data) does not work
    $._dataHolder[0].innerHTML = data;

    // iterate over new scripts and remove if source is already in DOM:
    var incomingScripts = $($._dataHolder).find(selector);
    for (var i = 0, len = incomingScripts.length; i < len; i++)
    {
    if ($._loadedScripts[getScriptUrl (incomingScripts[i])])
    {
    $(incomingScripts[i]).remove();
    }
    else
    {
    $._loadedScripts[getScriptUrl (incomingScripts[i])] = 1;
    }
    }

    return $._dataHolder[0].innerHTML;
    }
    });

    Now it works perfect!

    Regards Robert

  5. [...] schlecht handelbaren Problemen. Auf der Suche nach einer Lösung zu diesem Problem, bin ich auf das Blog von Eirik Hoem gestoßen (wer mehr über die Hintergründe des Problems erfahren möchte, kann sich dort gern [...]

  6. Nice one :) This was really helpful.

Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">