2/20/2013

Using CSS Media Queries as a Data Transport

Sorry for the long delay. I recently changed jobs and with family I had no time to get this out. I am going to try to return to every other week articles again.

My recent work involved creating web destinations based on both dynamic content, dynamic form, and responsive design with no preset dimensions. The pages would have to adjust to fit multiple sizes set at compile time. Each size could have its own specific properties and shared properties. The one thing I did know was that I could rely on any screen specific properties to have an identifier appended to the property name to make it unique. I needed a way to find the correct identifier for the current screen size. To make this work we decided to use CSS to store it. The solution worked for all modern browsers. Internet Explorer 8 received an alternate css and workflow since it doesn't support the MediaQueryList object.

We decided on this approach for 3 reasons:

  1. The CSS was output at compile time and written to disk. We could guarantee the output.
  2. We wanted an easy way to correlate screen size to identifier. The browser already has a mechanism for this in media queries.
  3. It was possible to avoid browser inconsistencies in JavaScript when it came to defining the viewport space. The browser was in control.

We decided to use the media query listener mechanism as a way to update a variable with the appropriate identifier. Modern browsers, excluding ie < 9, will honor media queries inside of their css. On page load and page size changes the browser will check the existing media queries to see if they match the new size. IE9 will honor media size queries but only on page load and will not respond to size changes.

A media query is defined by Mozilla as

A media query consists of a media type and at least one expression that limits the style sheets' scope by using media features, such as width, height, and color.

And, it looks similar to this:

@media screen and (min-width: 400px)

We made the identifier the first selector in the media query's block, and used the hash tag to make it an id. The risk of interference with any real css was minor since it was highly unlikely an element on any page would have that id. The css block was given a "display: block" value, but could as easily been any other property and value. We ignored the css value entirely. You may be able to leave the value empty, but for some reason I can't remember now I thought an empty value could cause trouble. The client side code was only interested in the selector name, which was the identifier it needed to build the page. An example block might be:

@media screen and (min-width: 400px) {
  #myValue { display: block }
  .regularCSS { ... }
  ......
}

At compile we also created a JavaScript array that stored the media queries we were using in the CSS. One intricacy of the MediaQueryList object is that the query it will respond to had to be defined previously. In other words, you needed to know the media query before it occured so that you could attach listeners to it. The array looked like this:

['@media all and (max-width: 500px)', '@media all and (min-width: 501px)'];

At this point, the media queries were defined in the CSS. The media queries had the selector block ready for the code to find. The code could identify the media queries to add a listener to. Time to put it all together.

The first function is defined above the loading code so that the function exists before it is used:

function findMatch(mql) {
  var rules;
  if (mql.matches) {
    arr = $.makeArray(document.styleSheets);
    arr = arr.forEach(function (val, index, arr) {
    rules = arr[index].rules || arr[index].cssRules;
    rulesLength = rules.length;
    for (innerCounter = 0; innerCounter < rulesLength; innerCounter++) {
      if (rules[innerCounter].cssText.indexOf(mql.media) !== -1) {
        value = rules[innerCounter].cssRules[0].selectorText.substring(1);
        window.cssTest.identifier = value;
        document.getElementById('sizeChangeResult').innerHTML = value;
        break;
        }
      }
    });
  }
}

The code will be passing in a MediaQueryList object. This function will be attached as a callback to the listener on every MediaQueryList object. The listener fires when the screen size loads or changes. This could also be accomplished using call or apply and passing the MediaQueryList object as "this". It is important to note that every MediaQueryList's listener will respond to the window size changes. The code needs to check if the MediaQueryList object being processed is the new active media query according to the browser. The if statement at the top performs this function using the MediaQueryList "matches" property. If this isn't the active one, the code finishes.

if (mql.matches)

The next block of code will collect all the styleSheets currently being used by the document. The collection object is turned into an array for enumeration. The next step is to loop through the MediaQueryList's rules. Unfortunately, the browser vendors differ on the exact syntax of the rules object. I believe the correct one according to the W3C is cssRules cssRules, however we have to work with what we have. Oddly, Internet Explorer returned objects for both rules and cssRules. Unfortunately, the returned objects were different and only the rules object worked for this purpose. Thus, we check for rules first.

rules = arr[index].rules || arr[index].cssRules;

Once we have all the rules we search through them to find the one matching the active query. The W3C spec outlines a robust model for reading style rules. I think the ideal would be to search only the CSSMediaRule objects which would limit the initial search to the '@media' clauses. Implementation seemed to differ enough between browsers that it felt best to work at the lowest common denominator, so I skipped this particular optimization. Once we have the matching media block, we can grab the first cssRule under the active media block. The first rule is the one storing our variable. We grab the selector from that rule, strip out the first character (the "#") and we have our value. Now that we have that value we can use it to guide the JavaScript code.

if (rules[innerCounter].cssText.indexOf(mql.media) !== -1) {
  value = rules[innerCounter].cssRules[0].selectorText.substring(1);
  window.cssTest.identifier = value;

Now that we know how to grab the value, it's time to attach the listener:

if (window.matchMedia || window.msMatchMedia) {
  block = window.cssTest.media_queries;
  for (var counter = 0; counter < block.length; counter++) {
    fragment = block[counter].substr(7);
    mql = window.matchMedia(fragment) || window.msMatchMedia(fragment);
    if (mql) {
      mql.addListener(findMatch);
      findMatch(mql);
      window.cssTest.mQListeners.push(mql);
    }
  }
}

The function starts with some old-fashioned feature detection. This ensures that we only add this functionality to browsers that can use the MediaQueryList functionality. We could also add an else clause to handle browsers that don't support this feature. I didn't include an else clause for this example in order to keep things focused. In order to proceed we grab the list of possible media queries from the JavaScript array discussed earlier. This piece could definitely use some optimization for future use. This is the conundrum from above where we need to know the media query clause ahead of time to attach a listener to it. In this sample, we grab the pre-populated block of queries and loop through them.

block = window.cssTest.media_queries;
for (var counter = 0; counter < block.length; counter++) {

Next we strip the @media substring out of the query clause and then pass it to the window for parsing. The window returns a MediaQueryList object that matches that media query. The window is not parsing or referencing our CSS to do this. The window is creating an object that is equipped to represent that media query. It is making an abstraction to represent a media query with the supplied properties. The abstraction is compared to CSS media queries when the window changes or loads. This is the reason for the MediaQueryList.matches function we used above. We need to ask the window if it's abstraction matches the screen. The CSS component is tangential and not directly related to the window's use of the MediaQueryList.

Finally, a listener is added to the new media query abstraction which will fire when the window loads or changes dimensions. Once attached, we manually call the findMatch function so that the code runs once on initial load. Finally, we keep the media queries in a global level namespace so they don't leave scope and get garbage collected.

mql.addListener(findMatch);
findMatch(mql);
window.cssTest.mQListeners.push(mql);

I think the approach here could be optimized. Before writing this article, I thought it an edge case. However, having explored it again, I think it might have more use than I initially gave it credit for. The real power here is using the CSS to define the JavaScript's reaction. No JavaScript tricks or caveats are required to measure the screen. The browser informs the code directly of what the browser considers the size to be. The code can also react to a wide range of sizes with only limited knowledge of potential configurations. I think I may be giving this approach a second look in my future projects.

Note: The code is running in the fiddle linked below. If you drag the browser width, the text in the result window will change based on size. Window width below 500 px will show "test1" and width above 501px will show "test2".