Workaround to dynamically load CSS resources

I have been struggling for a while to find a way to dynamically load CSS resources in a coherent and reliable manner. As I am working with dojo on my current project, I started with using its event connectors for a classical approach: creating a DOM link element with the desired CSS resource and adding it to the document’s header programmatically.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getCSS(filename){
  var fileref = document.createElement('script');
 
  fileref.setAttribute("type", "text/javascript");
  fileref.setAttribute("src", filename);
 
  var handleLoad = dojo.connect(fileref, 'onload', function(evt) {		
    console.log('the element ' + evt.target.src  + ' loaded');				
    dojo.disconnect(handleLoad);		
  });
 
  var handleError = dojo.connect(fileref, 'onload', function(evt) {		
    console.log('the element ' + evt.target.src  + ' failed loading');				
    dojo.disconnect(handleError);		
  });
}

While this approach should normally be the right one, there is a problem with the ‘onload’ event that should be fired after the link DOM element has been inserted into the page header and the CSS file has been loaded. However, Firefox is not firing the onload event for CSS link elements, and IE is doing it only the first time; that’s to say, the code above will hardly work and if it does, it will not take the browser’s cache into account. What a mess!

There is another approach that seems to work, though. After all, CSS is only text, and another way to add CSS to your pages is through ‘style’ elements. So the technique would consist on creating a DOM style element, loading its content with an AJAX call for the CSS file (or dynamic service!), then adding this content as the inner DOM element text. Well, there are small differences between IE and the other browsers, but the solution is working for me.

After reading in the dojo Trac that there is not such a thing as a ‘dojo.requireCSS’ component (which by the way works neatly for JS files), I found that the workaround explained above could be easily implemented with this framework. The reason that the example suggested is not become official is that, due to browser’s inconsistencies, it is not possible to control the onload events (it is based on link, not style elements). However, we can apply the workaround to the suggested dojo library to make it work. The next code block explains the result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
dojo.provide("dojo.requireCss");
 
(function(){
	var cssCounter = 0;
	var cssInFlight = {};
	var cssInFlightIntvl = null;
	var cssStartTime = 0;
	var cssCache = {};
	var callback = null;
 
	var _watchCssInFlight = function(){
		//summary: watches to make sure all css files have loaded.
 
		var stillWaiting = false;
		console.log(cssInFlight);
		for(var param in cssInFlight){
			if(typeof param == "string"){
				console.debug('waiting for ' + param);
				stillWaiting = true;
			}
		}
 
		if(stillWaiting){
			//Make sure we have not reached the timeout stage.
			var waitInterval = (dojo.config.cssWaitSeconds || 15) * 1000;
			if((cssStartTime + waitInterval) < (new Date()).getTime()){
				var err = "Timed out waiting for css files: ";
				for(param in cssInFlight){
					err += param + " ";
				}
				throw err;
			}
		}else{
			//All loaded. Clean up.
			console.debug("all done");
			clearInterval(cssInFlightIntvl);
			cssInFlightIntvl = null;
 
			// finally, launch callback if any
			if (callback != null){
				callback();
			}
		}
	}
 
	dojo.requireCss = function(/*String*/resourceName,/*function*/func){
		//summary: Converts the resource name ("some.thing") to a CSS URL
		//(http://some.domain.com/path/to/some/thing.css), then adds the CSS file
		//to the page. Full urls can be passed in (instead of a "some.thing" resourceName.
		//It also registers this CSS file with the dojo loader. This
		//means that if you do a dojo.addOnLoad() callback after calling this function,
		//the addOnLoad callback will not be fired until the CSS file has loaded.
		//Note that style sheets may be evaluated in a different order than the order
		//they appear in the DOM. If you want precise ordering of style rules, make
		//one call to this function, then in a dojo.addOnLoad() callback, load the other,
		//and repeat this call structure until you load all the stylesheets.
		//Example:
		//		dojo.requireCss("some.thing");
		//		dojo.requireCss("http://some.domain.com/path/to/some/thing.css");
 
		// Customization: avoid the process if the resource was loaded before
		if (cssCache[resourceName]){
			console.debug(resourceName + ' was already loaded');
			return true;
		}
 
		//Translate resource name to a file path
		var path = resourceName;
		if(path.indexOf("/") == -1){
			path = dojo._getModuleSymbols(resourceName).join("/") + '.css';
			path = ((path.charAt(0) == '/' || path.match(/^\w+:/)) ? "" : dojo.baseUrl) + path;
		}
		if(dojo.config.cacheBust){
			path += (path.indexOf("?") == -1 ? "?" : "&") + String(dojo.config.cacheBust).replace(/\W+/g,"");
		}
 
		//Create the style node
		var style = dojo.doc.createElement("style");
		style.type = "text/css";
		cssInFlight[path] = style;			
		console.debug(path + " created");
 
		// Customization: add it to an internal register so that it is not loaded twice
		cssCache[resourceName] = true;
 
		// now get the content with support for hooks
		dojo.xhrGet({url:path}).then(function(response){
			if (style.styleSheet){
				// IE	
				var imports = response.match(/@import\s+url\s*\(\s*['|"]([^'|"]+)['|"]\s*\);?/g);
				if (imports) {
					iMax = imports.length;
					for (i=0; i<iMax; i++) {
						imports[i].match(/['|"]([^'|^"]+)['|"]/);
						dojo.requireCss(RegExp.$1);
					}
				}
				style.styleSheet.cssText = response;
			} else {
				// the others
				style.appendChild(document.createTextNode(response));	
			}		    
 
			console.debug(path + " loaded");
			delete cssInFlight[path];
 
			if (typeof func == 'function'){
				func();
			}
 
		    return(response);
		});
 
		//Attach the node to document.
		if(!this.headElement){
			this._headElement = document.getElementsByTagName("head")[0];
 
			//Head element may not exist, particularly in html
			//html 4 or tag soup cases where the page does not
			//have a head tag in it. Use html element, since that will exist.
			//Seems to be an issue mostly with Opera 9 and to lesser extent Safari 2
			if(!this._headElement){
				this._headElement = document.getElementsByTagName("html")[0];
			}
		}
		console.debug(path + " adding to head");
		this._headElement.appendChild(style);
 
		//Start the inflight watch.
		cssStartTime = (new Date()).getTime();
		if(!cssInFlightIntvl){
			cssInFlightIntvl = setInterval(_watchCssInFlight, 100);
		}
	}
 
	// some public functions
	dojo.requireCss.setCallback = function(/*Function*/func){
		if (typeof func == 'function'){
			callback = func;
		}
	}
})();

Note: The original code by jburke on the dojo Trac project has been slightly modified to add functionality and to avoid some bugs. In particular:

  • The function _watchCssInFlight has been simplified to its limits due to an error on IE (an infinite waiting loop was started). However, I’d recommend to take the original version and test it on your project, since the functionality it adds is beyond this simplified version’s one.
  • A cssCache has been added so that resources are only loaded once.
  • IE does not originally support style.appendChild(textNode), so a workaround is needed to do so.
  • @import clauses in dynamic style tags are not supported by IE. To fix that, requireCss is now a recursive function: each CSS file loaded is scanned for @import declarations (take a look at the regexp!), and for every new CSS imported, an asynchronous call is done. Because files are only loaded once, this matches the normal browser behavior to solve this particular problem.
  • A callback can be set so that it is fired once all CSS resources in the current page have been loaded. It can be useful to control CSS loading, then launch JS asynchronous loading (with dojo.require, for instance), or any other action. The module could be extended to accept also callbacks after individual elements have been loaded.

Further tests have to be done with this component, and I take no responsibility if you use it for your own projects, but it has already solved a problem for me and, as far as I’ve checked, it is working on the latest versions of Firefox, IE, Safari, Opera and Chrome. Until there is an official solution to this integrated in the dojo toolkit, this fills the gap.

Leave a Reply

Your email address will not be published. Required fields are marked *