Feature Detection

In the real world

HTML5: A Blessing and a Curse



Hmm. I want to use X. But how will I decide when I can use X??

In an ideal world...

I know! I'll just ask the browser if they support it!

Feature Detection

JS APIs:
var supportsAudio = !!(window.webkitAudioContext || window.AudioContext);

HTML elements:
var div = createElement('div');
div.innerHTML = '<svg/>';
var supportsInlineSVG = div.firstChild
    && div.firstChild.namespaceURI == 'http://www.w3.org/2000/svg';

CSS:
var supportsTextShadow = 
    (document.createElement("detect").style.textShadow === "");

Modernizr

A small JS library that detects the availability of native implementations

Load the Modernizr JS:
<script src="/i/js/modernizr.com-custom-2.6.1-01.js"></script>
Modernizr runs tests, adds class names and JS properties:
<html class="js no-touch postmessage history multiplebgs boxshadow opacity cssanimations csscolumns cssgradients csstransforms csstransitions fontface formdata">

Then you can use in CSS:
html.svg .logo {
  background-image: url('logo.svg');
}
Or in your JS:
if (Modernizr.touch) {
   $('button').on('touch', handleClick);
}

So sweet when it works

I totally just detected the shit out of that feature!

But when it doesn't...

Oh noesss....it didn't work!

It's time for Plan B.

Sigh. Guess I'll have to sniff the user agent instead...

User Agent Sniffing

iPad/iPhone:
var isIOS = (navigator.userAgent.match(/iPhone/i)) || (navigator.userAgent.match(/iPod/i);
Top mobile browsers:
var isMobile = (navigator.userAgent.match(/(Android (1.0|1.1|1.5|1.6|2.0|2.1))|(Windows Phone (OS 7|8.0))|(XBLWP)|(ZuneWP)|(w(eb)?OSBrowser)|(webOS)|(Kindle\/(1.0|2.0|2.5|3.0))/));
All mobile browsers:
(function(a,b){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))window.location=b})(navigator.userAgent||navigator.vendor||window.opera,'http://detectmobilebrowser.com/mobile');
        

Feature Detection
vs.
User Agent Sniffing


True stories!

The case of the
hard-to-use localStorage

localStorage

A client-side key-value storage API

Store data:
$('form').on('submit', function() {
  window.localStorage.set('username', $('input').val());
});
Retrieve data:
alert(window.localStorage.get('username'));

Detecting support


function supportsStorage() {
  try {
    return 'localStorage' in window && window['localStorage'] !== null;
  } catch (e) {
    return false;
  }
}

Detecting support, Take 2


function supportsStorage() {
  var key = '__lscachetest__';
  // It's not straightforward due to FF4 issues and quota detection.
  try {
    // Fix for iPad issue - sometimes throws QUOTA_EXCEEDED_ERR on setItem
    localStorage.removeItem(key);
    localStorage.setItem(key, key);
    localStorage.removeItem(key);
    return true;
  } catch (exc) {
    return false;
  }
}


☞ lscache.js


☞ Modernizr: localstorage.js

Detecting support, Take 3


var cachedSupportsStorage;

function supportsStorage() {
    var key = '__lscachetest__';
    var value = key;

    if (cachedSupportsStorage !== undefined) {
      return cachedSupportsStorage;
    }

    try {
      setItem(key, value);
      removeItem(key);
      cachedSupportsStorage = true;
    } catch (exc) {
      cachedSupportsStorage = false;
    }
    return cachedSupportsStorage;
}

Note to self


  • Don't just check that a browser implements an API...

  • Check that the browser lets you use the API the way you want to.

  • Don't let a feature detection script slow down your site.

Note to browsers


Browsers should provide functions that let you know quickly whether an API can be used how you want it.



For example:


localStorage.canSet();

localStorage.canGet();

The Score

Feature Detection   User Agent Sniffing
1 0

The case of the
missing FormData

FormData

A set of key/value pairs representing form fields and their values

var formData = new FormData (document.forms[0])


☞ MDN: FormData

Detecting support


if (window.FormData) {
  var formData = new FormData(form[0]);
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/action/' + target, true);
  xhr.onload = function(e) {
    var responseJSON = JSON.parse(this.responseText);
    callback(processJSON(responseJSON));
  };
  xhr.send(formData);
} else {
  $.ajax({
      url: url,
      type: 'POST',
      data: form.serialize(),
      dataType: 'json', success: function(responseJSON) {
        onSuccess(processJSON(responseJSON));
      }
  });
}

Sad Safari users


I can't signup on Safari!


Detecting Safari



function isSafari() {
  return ($.browser.webkit && !(/chrome/.test(navigator.userAgent.toLowerCase())));
} 

function sendForm(form, target, callback) {
  if (window.FormData && !isSafari()) {
    var formData = new FormData(form[0]);
    // ...
  } else {
    // ...
  }
}

Note to self


  • Be wary of using new APIs...

  • Especially for mission-critical parts of your app.

  • Its safer to use the old APIs/libraries, with their known issues.

Note to browsers


All APIs that store/transmit data should have accessors to check whether they work as expected.



For example:

var formData = new FormData(form);
if (!formData.hasItem('username')) {
  // use older technique
}

The Score

Feature Detection   User Agent Sniffing
1 1

The case of the
fancy CSS

CSS3 Styling

.round-glossy-button {
  border-radius: 3px;
  background: linear-gradient(to bottom, #f0f9ff 0%,#cbebff 47%,#a1dbff 100%); 
  box-shadow: 2px 2px 3px #888;
}

Sad, sad Android users


Making Android happy

.modal {
    @include box-shadow(none);
    @include background-clip(border-box);
    @include border-radius(0px);
    border: 1px solid black;
}

Detecting Android


function inUserAgent(str) {
  return (new RegExp(str)).test(navigator.userAgent.toLowerCase());
}

function isAndroid() {

  var isAndroidOS =inUserAgent('android') || inUserAgent('HTC_') || inUserAgent('Silk/');

  var isAndroidPG = (window.device && window.device.platform && window.device.platform == 'Android' || false;

  return isAndroidOS || isAndroidPG;
}

if (isAndroid() || getUrlParam('os') == 'android') {
  $('body').addClass('android');
}
.android .modal {
    @include box-shadow(none);
    @include background-clip(border-box);
    @include border-radius(0px);
    border: 1px solid black;
}

Note to self


  • Just because something "works", it doesn't mean it works well.

  • Be careful about using new features in bulk, as any performance problems will be exacerbated.

  • Sometimes you just have to block CSS features based on user agent.

Note to browsers


Browsers should know their limits, and not render CSS features they can't handle.

The Score

Feature Detection   User Agent Sniffing
1 2

The case of the
signup process

Signature Track

The Requirements


Physical keyboard

Our first attempt:

function isTouchSupported() {
    return (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch);
}

...but that won't always work, so...

function isMobileDevice() {
    var ua = navigator.userAgent || navigator.vendor || window.opera;
    return (/iPhone|iPod|iPad|Android|BlackBerry|Opera Mini|IEMobile/).test(ua);
}

Flash Plugin


Using SWFObject:

<script src="swfobject.js"></script>
function isFlashSupported() {
  swfobject.hasFlashPlayerVersion("8")
}

Webcam

No way to detect without asking user, so...

Note to self



  • Tell the user what they need before they get started.

  • ...multiple times.

  • Tell the user what you detected and why you're blocking them.

Note to browsers


Give developers a way to detect physical devices and plugins.



For example:

navigator.hasPhysicalKeyboard();
navigator.hasWebcam();
navigator.hasPlugin('Flash', '8.0');

The Score

Feature Detection   User Agent Sniffing
2 3

Which to choose?

Feature Detection!
User Agent Sniffing!