Deferred, AMD compatible JavaScript feature detection
The reason developing another feature detection
If you happen to come across this article you will most likely at least have heard of Modernizr1 or have used it in one of your projects. For a very long time I refused to use JavaScript based feature detections and tried hard to avoid the need for things alike. But finally, while developing my own Qoopido.js2 library, I came to the point where I finally needed it. In contrast to most other people I know (and sometimes tend to do as well) I did not simply include Modernizr being "the holy grail" regarding JavaScript based feature detection but had a look at the code before. What I saw was, well, a bit disillusioning:
- code style & structure look incoherent (even messy at times)
- some of the tests where simply not sufficient (so could return false results)
- writing reusable code is not encouraged (similar tests repeat each other)
- no built-in solution for tests requiring async behaviour (and there are cases where this is necessary)
Most if not all of the points mentioned above seem to be caused by the numerous authors and the limited functionality provided by the core itself. So in the end my personal concerns convinced me to think about how I would approach the matter.
An existing foundation
If you have already visited this site and read some of my writings you might have come across my article about Javascript object inheritance. The article more or less built the foundation for the development of my growing Qoopido.js library which was also mentioned in the former paragraph. So I already had part of what I needed for a modular and extendable feature detection that stays DRY wherever possible.
How to deal with async behaviour
Being a huge fan of jQuerys' deferreds but trying to move away from jQuery towards Vanilla JavaScript I have been prefering Q.js recently. It might be a tad on the slow side but builds a very robust and frequently improved base for many of the things I do or have done lately that need async behaviour.
Developing a basic support class
Now that I had found solutions for the most basic requirements extendability and asynchronity I started defining the scope of work for the base class of my JavaScript feature detection. Having chosen Qoopido.js as the structural foundation another major requirement was that the concept had to be AMD compatible. As this requirement makes sense for a modular feature detection in numerous ways anyway it was a very welcome addition.
I ended up defining a basic "support" module with the following methods:
- getPrefix(): Determines the browsers vendor prefix (if any) for functions and CSS property names
- getMethod(methodName, element = window): Retrieve the vendor prefixed name of a function for a certain element, defaulting to "window" (if any)
- getProperty(propertyName, element = window): Retrieve the vendor prefixed name of a DOM property (if any)
- getCssProperty(propertyName): Retrieve the vendor prefixed name of a CSS property (if any)
- supportsPrefix(): boolean result of getPrefix
- supportsMethod(methodName, element = window): boolean result of getMethod
- supportsProperty(propertyName, element = window): boolean result of getProperty
- supportsCssProperty(propertyName): boolean result of getCssProperty
- testPrefix(): Deferred test of getPrefix (caches result)
- testMethod(methodName, element = window): Deferred test of testMethod (caches result)
- testProperty(propertyName, element = window): Deferred test of testProperty (caches result)
- testCssProperty(propertyName): Deferred test of testCssProperty (caches result)
- testMultiple(testId[, ...]): Deferred method to issue multiple tests
- addTest(id, test): General method to add any kind of test
Beside that the class itself has a property "test" in which all tests added via addTest are stored by there "id". The "id" matches the path to the test as required by AMD/require.js.
From the names of the methods you will have already guessed that it will not be necessary to write tests for simple CSS properties nor browser functions because these are already included in the basic "support" class.
Getting ready to write the first test
As stated in the introduction certain types of feature detections require an async behaviour. The first test I came about to develop was a test for dataURI support of images. To reliably determine support the test needs to create an image element in javascript, assign a dataURI to it's "src" attribute, register to it's load event and check if the dimensions match what was expected. Even though a dataURI has a rather short loading time it cannot be handled syncronically.
Writing a test for dataURI support of images
Every single test follows the coding scheme I developed for Qoopido.js which mainly consists of two parts:
- module initialization/registration
- module definition
At this point I will just show you the full code used for the dataURI test and explain it afterwards:
;(function(pDefinition, window) {
'use strict';
function definition() {
return window.qoopido.initialize('support/capability/datauri', pDefinition, arguments);
}
if(typeof define === 'function' && define.amd) {
define([ '../../support', '../../dom/element', '../../pool/dom' ], definition);
} else {
definition();
}
}(function(modules, dependencies, namespace, window) {
'use strict';
return modules['support'].addTest('/capability/datauri', function(deferred) {
var sample = modules['dom/element'].create(window.qoopido.shared.pool.dom.obtain('img'));
sample
.one('error load', function(event) {
if(event.type === 'load' && sample.element.width === 1 && sample.element.height === 1) {
deferred.resolve();
} else {
deferred.reject();
}
sample.element.dispose();
}, false)
.setAttribute('src', 'data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==');
});
}, window));
As you see the module mainly consists of two anonymous function representing the above mentioned parts. What you see in the first function is that the test requires the "support" base class (as any test) as well as two additional Qoopido.js modules:
- dom/element: DOM element abstraction providing event handling normalization
- pool/dom: provides pooling methods for DOM elements
The module registers itself to the "support" class by calling its addTest method with an "id" of "/capability/datauri" and the actual test as second parameter. Any test which will get called by the "support" base class will receive its deferred as first parameter. In contrast to other Qoopido.js modules tests simply return a function and not and object with its own methods which takes away a whole level of complexity and makes writing your own tests quite straight forward.
The test obtains an image element from the global pool of DOM objects first. It than registers a once-only listener for "error" and "load" event and implicitly handles if to resolve the deferred or reject it. Finally the image element gets returned to the global DOM element pool by calling its dispose method.
If you looked at the code carefully you might have come across the second parameter with a value of "false" used when registering the once-only listener. Passing "false" as the second parameter means that it is a once-only listener for ANY single of the listed events. So whenever one of the passed events occurs the listener gets removed for the other events as well.
After registering the listener the images "src" attribute is set to a dataURI representing a transparent 1 x 1px GIF image.
Writing your own tests
There are not many things to keep in mind when developing your own tests but these might help you get started quickly:
- use a strict & consistent naming convention for the module name …
- … which you should also use for directory structure and its "id"
- a test cannot extend another test but …
- … a test can require one or more other tests and issue them via require.js and testMultiple
In addition: Feel free to look at the code of any of the existing tests to find out how easy it is to write your own tests!
Feel free to contribute your own tests as well. I am very happy about any kind of contribution to the library :)
a full fledged example
To illustrate the capabilities and all tests already included I set up a demo on Codepen which you will find included below.
Feel free to download Qoopido.js from my GitHub account3 and use it in your projects but remember to give feedback and report bugs so I can keep improving it!