Responsive web-sites use so-called "CSS media queries" to adapt dynamically to the screen dimension they are rendered on. These "media queries" define conditions about the screen, with subsequent blocks of CSS-rules to be applied when the condition matches. Conditions sound like "When the device's width is smaller than 768 pixels". In this condition, 768 pixels would be called a "breakpoint" (has nothing to do with a debugger-breakpoint!).
Such a layout adaption is not always an easy task. In many cases also JavaScript code must be executed. In this Blog I will introduce a short and simple solution for JS "breakpoint" notifications.
Listening for Resize
This browser window is currently
pixels wide.
Try to resize your browser window, the above pixel amount will update each time you do it. Here is the way how to listen for browser resizes:
<i><p> This browser window is currently </p> <blockquote> <span id="width-output"></span> </blockquote> <p> pixels wide. </p></i> <script type="text/javascript"> var outputWidth = function() { var element = document.getElementById("width-output"); element.innerHTML = ''+window.innerWidth; }; outputWidth(); window.addEventListener("resize", outputWidth); </script>
This won't work on older exotic browsers, but essentially
window.addEventListener("resize", listenerFunction)
is all you need on modern HTML-5 browsers.
Provide the API
How do I want to register and receive breakpoint notifications?
I want to pass an arbitrary pixel amount to the API,
and a function to execute when the browser is resized across that breakpoint.
Something like function addBreakpointListener(breakpoint, listenerFunction) { ... };
should do it.
The second thing the API must provide is the resize-listener function to install to the browser window. This could also be done internally, but leaving the resize-listener installation to the caller allows it also to de-install it temporarily, or install it at a certain time.
Here is the outline of the module to implement:
/** * JS screen-width breakpoint manager that notifies * when a breakpoint is hit. */ var screenWidthEventNotifier = function() { var that = {}; /** Install this to receive browser-resize events. */ that.resizeListener = function() { .... }; /** Call this to add listeners to certain breakpoints. */ that.addBreakpointListener = function(breakpoint, listenerFunction) { .... }; return that; };
This API can then be used like the following:
// create a breakpoint-notifier var breakpointNotifier = screenWidthEventNotifier(); // install breakpoint-notifier as resize-listener window.addEventListener("resize", breakpointNotifier.resizeListener); // example breakpoint-listener implementation var breakpoint1024Listener = function(breakpoint, currentBrowserWidth) { var output = document.getElementById("output"); output.innerHTML += "<p>Received breakpoint event "+breakpoint+ " at browser width "+currentBrowserWidth+", "+new Date()+"</p>"; }; // add example implementation as listener breakpointNotifier.addBreakpointListener(1024, breakpoint1024Listener);
This implementation anticipates that somewhere in the HTML page there is an element with id = "output"
.
Every breakpoint-event would append a paragraph to it.
Detecting and Notifying Breakpoints
I will need a map that contains breakpoints as keys, and lists of listener-functions as value. That way I can check for all breakpoints, at any resize-event, whether the related listeners have to be called.
For detecting that a breakpoint is trespassed I will need a field that holds the previous screen width. Using this I can check whether the breakpoint is between current and previous width.
For determining the current browser width I will use window.innerWidth
. This works on all modern browsers.
So here is my API implementation:
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 | var screenWidthEventNotifier = function() { var breakpoint2ListenerLists = {}; var browserWindowWidth = function() { return window.innerWidth; } var fireEvent = function(breakpoint, currentWidth) { var listeners = breakpoint2ListenerLists[breakpoint]; for (var i = 0; i < listeners.length; i++) { var listener = listeners[i]; listener(breakpoint, currentWidth); } }; var previousWidth = browserWindowWidth(); var that = {}; /** Install this to receive browser-resize events. */ that.resizeListener = function() { var currentWidth = browserWindowWidth(); for (var breakpoint in breakpoint2ListenerLists) { if (breakpoint2ListenerLists.hasOwnProperty(breakpoint)) { // JS converts every map key to a string, so convert it back to integer var intBreakpoint = window.parseInt(breakpoint); if (intBreakpoint != currentWidth && (previousWidth <= intBreakpoint && intBreakpoint < currentWidth || previousWidth >= intBreakpoint && intBreakpoint > currentWidth)) { fireEvent(intBreakpoint, currentWidth); } } } previousWidth = currentWidth; }; /** Call this to add listeners to certain breakpoints. */ that.addBreakpointListener = function(breakpoint, listenerFunction) { var listenersForBreakpoint = breakpoint2ListenerLists[breakpoint]; if ( ! listenersForBreakpoint ) { listenersForBreakpoint = []; breakpoint2ListenerLists[breakpoint] = listenersForBreakpoint; } listenersForBreakpoint.push(listenerFunction); }; return that; }; |
The line var breakpoint2ListenerLists = {};
sets up my breakpoint-to-listener map.
As you might know, in JS there is no difference between a map and an object.
For determining the browser's window width I use window.innerWidth
.
This is encapsulated to be used
once-and-only-once
on (1) initialize and (2) every resize-event.
The fireEvent()
function is private, it is not visible outside the module.
I do not want any code to call this function except the internal resize-listener.
The implementation receives the breakpoint that was hit, and the current browser width, as parameters.
It then gets the listener array for the breakpoint out of the map, and loops its listeners,
passing them the breakpoint and the current width. That way a listener can decide
whether it wants to work below or above the breakpoint, or on both sides.
The private previousWidth
field always will hold the width of the previous resize-event.
Initially I assign the current browser width to it.
After the private part, I allocate a return-object called that
where I will put my public functions into.
The most complex part is the resize-event-dispatch in resizeListener()
.
Essentially this functions loops all breakpoints on every resize-event,
and checks whether the breakpoint is hit by the current change.
Do not pay attention to breakpoint2ListenerLists.hasOwnProperty(breakpoint)
,
this is "boiler-plate code"
to circumvent JavaScript for-in-loop problems with maps.
Assuming that my breakpoint is at 1024, my previous width at 1023, and my current width at 1024,
I will NOT fire an event. When my previous width is at 1024 then, and my current width at 1025 or more,
I will fire an event. That way I avoid that the listener receives a currentWidth
being the same as the browser width, and thus can not decide whether the browser is now bigger or smaller than the breakpoint.
The public addBreakpointListener()
implementation uses the breakpoint as key,
and puts an empty array as value when there is no value yet.
It adds the given listener to the array under that breakpoint.
Example Breakpoint Listener
Here is the Live-Example. Try it out by resizing your browser window across 1024 pixels in both directions. The JS listener working here is the one from "Provide the API" chapter.
Currently your browser is pixels wide.
Example listener output: