Frizz-Free JavaScript With ConditionerJS

Advertisement

Setting up JavaScript-based functionality to work across multiple devices can be tricky. When is the right time to load which script? Do your media queries matches tests, your geolocation popups tests and your viewport orientation tests provide the best possible results for your website? ConditionerJS1 will help you combine all of this contextual information to pinpoint the right moment to load the functionality you need.

Before we jump into the ConditionerJS demo, let’s quickly take a look at the Web and how it’s changing, because it’s this change that drove the development of ConditionerJS in the first place. In the meantime, think of it as a shampoo but also as an orchestra conductor; instead of giving cues to musicians, ConditionerJS tells your JavaScript when to act up and when to tune down a bit.

Applying conditioner to guinea pigs results in very smooth fur.
As you can clearly see, applying conditioner to guinea pigs results in very smooth fur. Take a moment and imagine what this could mean for your codebase.

The Origin Of ConditionerJS

You know, obviously, that the way we access the Web has changed a lot in the last couple of years. We no longer rely solely on our desktop computers to navigate the Web. Rather, we use a wide and quickly growing array of devices to get our daily dose of information. With the device landscape going all fuzzy, the time of building fixed width desktop sites has definitely come to an end. The fixed canvas is breaking apart around us and needs to be replaced with something flexible — maybe even something organic.

What’s a Web Developer to Do?

Interestingly enough, most of the time, our content already is flexible. The styles, visuals and interaction patterns classically are rigid and are what create challenging to downright impossible situations. Turns out HTML (the contents container) has always been perfectly suited for a broad device landscape; the way we present it is what’s causing us headaches.

We should be striving to present our content, cross-device, in the best possible way. But let’s be honest, this “best possible way” is not three-width based static views, one for each familiar device group. That’s just a knee-jerk reaction, where we try to hang on to our old habits.

The device landscape is too broad and is changing too fast to be captured in groups. Right this moment people are making phone calls holding tablets to their heads while others are playing Grand Theft Auto on their phones until their fingers bleed. There’s phones that are tablets and tablets that are phones, there’s no way to determine where a phone ends a tablet starts and what device might fall in between, so let’s not even try.

Grouping devices is like grouping guinea pigs.2
Grouping devices is like grouping guinea pigs; while it’s certainly possible, eventually you will run into trouble.

To determine the perfect presentation and interaction patterns for each device, we need more granularity than device groups can give us. We can achieve a sufficient level of detail by looking at contextual information and measuring how it changes over time.

Context On The Web

The Free Dictionary defines “context”3 as follows:

“The circumstances in which an event occurs; a setting.”

The user’s context contains information about the environment in which that user is interacting with your functionality. Unlike feature detection, context is not static. You could be rotating your device right now, which would change the context in which you’re reading this article.

Measuring context is not only about testing hardware features and changes (such as viewport size and connection speed). Context can (and is) also influenced by the user’s actions. For instance, by now you’ve scrolled down this article a bit and might have moved your mouse a few pixels. This tells us something about the way you are interacting with the page. Collecting and combining all of this information will create a detailed picture of the context in which you’re currently reading this content.

Correctly measuring and responding to changes in context will enable us to present the right content in the right way at the right moment.

Note: If you’re interested in a more detailed analysis of context, I advise you to read Designing With Context4 by Cennydd Bowles.

Where And How To Measure Changes In Context

Measuring changes in context can easily be done by adding various tests to your JavaScript modules. You could, for example, listen to the resize and scroll events on the window or, a bit more advanced, watch for media query changes.

Let’s set up a small Google Maps module together. Because the map will feature urban areas and contain a lot of information, it should render only on viewports wider than 700 pixels. On smaller screens, we’ll show a link to Google Maps. We’ll write a bit of code to measure the window’s width to determine whether the window is wide enough to activate the map; if not, then no map. Perfect! What’s for dinner?

Don’t order that pizza just yet!

Your client has just called and would like to duplicate the map on another page. On this page, the map will show a less crowded area of the planet and so could still be rendered on viewports narrower than 700 pixels.

You could add another test to the map module, perhaps basing your measurement of width on some className? But what happens if a third condition is introduced, and a fourth. No pizza for you any time soon.

Clearly, measuring the available screen space is not this module’s main concern; the module should instead mostly be blowing the user’s mind with fantastic interaction patterns and dazzling maps.

This is where Conditioner comes into play. ConditionerJS will keep an eye on context-related parameters (such as window width) so that you can keep all of those measurements out of your modules. Specify the environment-related conditions for your module, and Conditioner will load your module once these conditions have been met. This separation of concerns5 will make your modules more flexible, reusable and maintainable — all favorable characteristics of code.

Setting Up A Conditioner Module

We’ll start with an HTML snippet to illustrate how a typical module would be loaded using Conditioner. Next, we’ll look at what’s happening under the hood.

<a href="http://maps.google.com/?ll=51.741,3.822"
   data-module="ui/Map"
   data-conditions="media:{(min-width:30em)} and element:{seen}"> … </a>

Codepen Example #1

We’re binding our map module using data attributes instead of classes6, which makes it easier to spot where each module will be loaded. Also, binding functionality becomes a breeze. In the previous example, the map would load only if the media query (min-width:30em) is matched and the anchor tag has been seen by the user. Fantastic! How does this black magic work? Time to pop open the hood.

See the Pen ConditionerJS – Binding and Loading a Map Module7 by Rik Schennink (@rikschennink158) on CodePen169.

A Rundown of Conditioner’s Inner Workings

The following is a rundown of what happens when the DOM has finished loading. Don’t worry — it ain’t rocket surgery.

  1. Conditioner first queries the DOM for nodes that have the data-module attribute. A simple querySelectorAll10 does the trick.
  2. For each match, it tests whether the conditions set in the data-conditions attribute have been met. In the case of our map, it will test whether the media query has been matched and whether the element has scrolled into view (i.e. is seen by the user). Actually, this part could be considered rocket surgery.
  3. If the conditions are met, then Conditioner will fetch the referenced module using RequireJS1711; that would be the ui/Map module. We use RequireJS because writing our own module loader would be madness — I’ve tried.
  4. Once the module has loaded, Conditioner initializes the module at the given location in the DOM. Depending on the type of module, Conditioner will call the constructor or a predefined load method.
  5. Presto! Your module takes it from there and starts up its map routine thingies.

After the initial page setup has been done, Conditioner does not stop measuring conditions. If they don’t match at first but are matched later on, perhaps the user decides to resize the window, Conditioner will still load the module. Also, if conditions suddenly become unsuitable the module will automatically be unloaded. This dynamic loading and unloading of modules will turn your static Web page into a living, growing, adaptable organism.

Available Tests And How To Use These In Expressions

Conditioner comes with basic set of tests that are all modules in themselves.

  • “media” query and supported
  • “element” min-width, max-width and seen
  • “window” min-width and max-width
  • “pointer” available

You could also write your own tests, doing all sorts of interesting stuff. For example, you could use this cookie consent test12 to load certain functionality only if the user has allowed you to write cookies. Also, what about unloading hefty modules if the battery13 falls below a certain level. Both possible. You could combine all of these tests in Conditioner’s expression language. You’ve seen this in the map tests, where we combined the seen test with the media test.

media:{(min-width:30em)} and element:{seen}

Combine parenthesis with the logical operators and, or and not to quickly create complex but still human-readable conditions.

Passing Configuration Options To Your Modules

To make your modules more flexible and suitable for different projects, allow for the configuration of specific parts of your modules — think of an API key for your Google Maps service, or stuff like button labels and URLs.

Configuring guinea pig facial expression using configuration objects.
Configuring guinea pig facial expression using configuration objects.

Conditioner gives you two ways to pass configuration options to your modules: page- and node-level options. On initialization of your module, it will automatically merge these two option levels and pass the resulting configuration to your module.

Setting Default Module Options

Defining a base options property on your module and setting the default options object are a good start, as in the following example. This way, some sort of default configuration is always available.

// Map module (based on AMD module pattern)
define(function(){

    // constructor
    // element: the node that the module is attached to
    // options: the merged options object
    var exports = function Map(element,options) {

    }

    // default options
    exports.options = {
        zoom:5,
        key:null
    }

    return exports;
});

By default, the map is set to zoom level 5 and has no API key. An API key is not something you’d want as a default setting because it’s kinda personal.

Defining Page-Wide Module Options

Page-level options are useful for overriding options for all modules on a page. This is useful for something like locale-related settings. You could define page-level options using the setOptions method that is available on the conditioner object, or you could pass them directly to the init method.

// Set default module options
conditioner.setOptions({
    modules:{
        'ui/Map':{
            options:{
                zoom:10,
                key:'012345ABCDEF'
            }
        }
    }
});

// Initialize Conditioner
conditioner.init();

In this case, we’ve set a default API key and increased the default zoom level to 10 for all maps on the page.

Overriding Options for a Particular Node

To alter options for one particular node on the page, use node-level options.

<a href="http://maps.google.com/?ll=51.741,3.822"
   data-module="ui/Map"
   data-options='{"zoom":15}'> … </a>

Codepen Example #2

For this single map, the zoom level will end up as 15. The API key will remain 012345ABCDEF because that’s what we set it to in the page-level options.

See the Pen ConditionerJS – Loading the Map Module and Passing Options14 by Rik Schennink (@rikschennink158) on CodePen169.

Note that the options are in JSON string format; therefore, the double quotes on the data-options attribute have been replaced by single quotes. Of course, you could also use double quotes and escape the double quotes in the JSON string.

Optimizing Your Build To Maximize Performance

As we discussed earlier, Conditioner relies on RequireJS1711 to load modules. With your modules carefully divided into various JavaScript files, one file per module, Conditioner can now load each of your modules separately. This means that your modules will be sent over the line and parsed only once they’re required to be shown to the user.

To maximize performance (and minimize HTTP requests), merge core modules together into one package using the RequireJS Optimizer18. The resulting minimized core package can then be dynamically enhanced with modules based on the state of the user’s active context.

Carefully balance what is contained in the core package and what’s loaded dynamically. Most of the time, you won’t want to include the more exotic modules or the very context-specific modules in your core package.

Try to keep your request count to a minimum — your users are known to be impatient.
Try to keep your request count to a minimum — your users are known to be impatient.

Keep in mind that the more modules you activate on page load, the greater the impact on the CPU and the longer the page will take to appear. On the other hand, loading scripts conditionally will increase the CPU load needed to measure context and will add additional requests; also, this could affect page-redrawing cycles later on. There’s no silver bullet here; you’ll have to determine the best approach for each website.

The Future Of Conditioner

A lot more functionality is contained in the library than we’ve discussed so far. Going into detail would require more in-depth code samples, but because the API is still changing, the samples would not stay up to date for long. Therefore, the focus of this article has been on the concept of the framework and its basic implementation.

I’m looking for people to critically comment on the concept, to test Conditioner19‘s performance and, of course, to contribute, so that we can build something together that will take the Web further!

(al, ml, il)

Footnotes

  1. 1 http://conditionerjs.com/
  2. 2 http://www.smashingmagazine.com/wp-content/uploads/2014/04/02-grouping-devices.jpg
  3. 3 http://www.thefreedictionary.com/context
  4. 4 http://www.cennydd.co.uk/2013/designing-with-context
  5. 5 http://en.wikipedia.org/wiki/Separation_of_concerns
  6. 6 http://alexkinnee.com/2013/11/binding-js-events-using-data-action-selectors/
  7. 7 http://codepen.io/rikschennink/pen/HiGEI/
  8. 8 http://codepen.io/rikschennink
  9. 9 http://codepen.io
  10. 10 https://developer.mozilla.org/en-US/docs/Web/API/Document.querySelectorAll
  11. 11 http://requirejs.org
  12. 12 http://conditionerjs.com/examples/custom/
  13. 13 http://www.w3.org/TR/battery-status/
  14. 14 http://codepen.io/rikschennink/pen/Iudth/
  15. 15 http://codepen.io/rikschennink
  16. 16 http://codepen.io
  17. 17 http://requirejs.org
  18. 18 http://requirejs.org/docs/optimization.html
  19. 19 http://conditionerjs.com/

↑ Back to topShare on Twitter

Rik is a Senior Front-end Developer working for Mirabeau in The Netherlands. He’s both a developer and a designer and loves to combine the two to create games, web products and other interactive experiences. Cycles to work each day while pondering the state and future of the web and has recently started to share his toughts with the rest of the community.

Advertising

Note: Our rating-system has caused errors, so it's disabled at the moment. It will be back the moment the problem has been resolved. We're very sorry. Happy Holidays!

  1. 1

    I know it may seem like a shameless plug but I think it’s worth mentioning.
    ConditionerJS is very similar to my library http://www.feaxures.com. Differences:
    1. ConditionerJS lets you specify the conditions in HTML through attributes, FeaxuresJS requires defining the condition as a function that returns true/false
    2. In ConditionerJS the module’s options are global (or that is my understanding of it), in FeaxuresJS each element may get it’s own options
    3. In ConditionerJS you can have only a module per element, with FeaxuresJS you can have as many as you want. I know that that’s usually not the case but…

    • 2

      No problem, I’ve stumbled upon feaxures a couple months back and I think it’s great more people are exploring how to tackle this problem.

      Conditioner’s options are defined on module level (for any default options) and can than be defined on a page level and a node level. They are not global, but page level overrides are expected to be passed to the conditioner setOptions or init method.

      Currently you can only bind one module to each DOM node, because of high demand the next version will support multiple modules. I do however find this to be something that people should be careful with. You could end up in a situation where modules are modifying each others nodes, which could result in strange behavior and makes modules less portable.

  2. 3

    Great article Rik! I’ve worked with ConditionerJS and its precursor (the BehaviourController) for almost two years now and I have to say that it has become an integral part of my work. I’d probably even pay for it!

  3. 5

    Yngve B. Nilsen

    April 4, 2014 9:36 pm

    Would be interesting to try and implement something like this as an AngularJS-module. Did you ever stumble upon something like that when writing this article?

    • 6

      I’ve played around with AngularJS a bit, but don’t have enough experience yet to determine if it would work with ConditionerJS. I have to say I targetted Conditioner towards websites not so much webapps, not saying it will not work, but that could be a problem.

      I build ConditionerJS because I could not find anything in that area. I’m afraid there’s not something readily available.

  4. 7

    Your examples didn’t work on my phone :(

    • 8

      The first example won’t load a map on small screens so that could be a reason it’s not working. The other one should definitely work.

      What phone are you on?

  5. 9

    nice plugin…. i am facing a problem with tinyscrollbar, dont want to show tinyscrollbar on mobile devices which are less then 480px, how i can use conditioner js with this….

    • 10

      You’d wrap tinyscrollbar in a requirejs module e.g. ui/Scroller in this module you initialise tinyscrollbar on the node the module is bound to $(element).tinyscrollbar();

      You can then bind the scroller using the data-module attribute and set conditions using the condition attribute data-condition="media:(min-width:480px)"

  6. 11

    Interesting concept, but the idea that the entire dom should be searched for the specific data-attribute, seems a cumbersome task. Loading conditionaljs, performing a huge Dom search, and then determine if a specific library should be loaded…?

    I would almost question the benefit, I mean, why not just load the libraries. And let smart use of CSS frameworks or/and (jquery/js) selectors determine whether or not to load the library. Ok, I must admit the google library from the example is not a library many users will have in their cache because of previous visited sites with CDN source calls, but many others are all ready cached.

    Don’t understand me wrong, I like the concept, but am surprised that it would make this huge difference, that we should apply these methods. What are the real world benefits, what does this mean for the CPU load client side, etc ?

    Would it be a good idea to use a certain class name (configurable), or a list of Ids to identify the targets with the required data attributes faster?

    • 12

      Well, historically you would do multiple queries on the DOM to find a specific node using getElementsByClassName. Depending on the size of your DOM and the amount of functionality you want to load doing a single query for [data-module] using the querySelector might be faster. As stated later on in the article, some scripts you might want to load conditionally, others might be contained in your core js file. So if your codebase is dependent on jQuery, you’d probably have jQuery already loaded.

      It’s not only about bytes going over the line, it’s also about parsing and executing scripts, which is a tedious task for slow devices. By carefully defining the right amount of functionality you should be able to keep your page load snappy. On top of this, it’s about code structure and keeping modules separated from each other (RequireJS is a huge help here).

      Right now I’ve got no data on what it exactly means for CPU load, I’ve gove some ideas but haven’t put it to test yet. That’s exactly why I’m searching for people to join in and shine their light on the concept, like you are doing right now, which is great!

  7. 13

    man these guinea pigs made my week lol

  8. 15

    Very nice post, i was looking for something like this!
    A small BUT though:

    I wonder how bad effect it will have on the application responsiveness (functionality), in other words, in slow connections, when the node element comes in the view port, it will need to be downloaded and processed before the user can interact with it. or maybe i am getting it wrong

    • 16

      Thanks!

      You’re right, you could end up in a situation where the Map won’t load due to an unstable connection, in that case you’ll be served a link to Google Maps.

      You still have access to the same information it’s just a click away. But because we did not load and render the Map on page load the initial load time of the page was probably faster.

      • 17

        I guess there is no one size fits all solution :)

        You might scroll and endup sending many requests (if you have a reach js application) then it will be slower to interract with. I guess there is many usecases for this but don’t thing this can be a main player in a site.

  9. 18

    This is a great article but what if i use browersify instead of requirejs ? ;)

    • 19

      Then, currently, it’s not gonna work out for you ;-)

      But, I’m looking into supporting multiple module loaders / frameworks using some sort of plugin architecture.

    • 20

      1.0 features browserify support, for conditional fetching of modules you’ll still need an AMD loader though.

Leave a Comment

Yay! You've decided to leave a comment. That's fantastic! Please keep in mind that comments are moderated and rel="nofollow" is in use. So, please do not use a spammy keyword or a domain as your name, or else it will be deleted. Let's have a personal and meaningful conversation instead. Thanks for dropping by!

↑ Back to top