FormatChange
... Smart window resize events for sites with responsive UI.
FormatChange makes it trivially easy to tie tailor-made scripting effects to (CSS defined) named @media query breakpoints – and respond intelligently when users tilt their phones and tablets, or resize their browser windows.
You can subscribe/unsubscribe to notifications (custom event firing) whenever
it detects that a new (named) CSS @media query breakpoint has become active.
HOWTO / API
0: Install
yarn add formatchange
npm install formatchange
1: Name your CSS breakpoints
FormatChange Monitors changes in a hidden Element's ::after { content: '' }
value, as defined in your page's CSS code.
So, first off, give a single name to each @media query breakpoint (format) you want your script to respond to.
@media screen {
#mediaformat::after {
content: "phone";
}
}
@media screen and (min-width: 500px) {
#mediaformat::after {
content: "phablet";
}
}
@media screen and (min-width: 700px) {
#mediaformat::after {
content: "tablet";
}
}
@media screen and (min-width: 950px) {
#mediaformat::after {
content: "netbook";
}
}
@media screen and (min-width: 1350px) {
#mediaformat::after {
content: "widescreen";
}
}
2: Configure and initialize FormatChange
FormatChange is a constructor, but is very understanding about being called as a normal function.
import { FormatChange } from "formatchange";
var formatMonitor = new FormatChange();
The constructor accepts two optional Object arguments: formatGroups
and options
.
formatGroups
allows you to optionally split the formats defined by your CSS into named groups – which can be convenient when handling format-change events (more on that below).
var formatGroups = {
Small: { phone: 1, phablet: 1 },
Large: { tablet: 1, netbook: 1, widescreen: 1 },
};
The available options
are as follows (showing default values):
var options = {
elm: null,
elmId: "mediaformat",
elmTagName: "del",
defer: false,
win: window,
manual: false,
};
Then this:
var formatMonitor = new FormatChange(formatGroups, options);
NOTE: All option and formatGroups defaults can be changed via FormatChange.prototype.*
3: Getting the current media format
As soon as FormatChange starts monitoring the viewport (on instantiation by default, or on .start()
if the defer
option is used) it writes information about the current media format into formatMonitor.media
.
var media = formatMonitor.media;
media.is
– contains the name of the current media format. E.g. 'tablet', 'phone' or 'widescreen', etc.
media.was
– starts out undefined
but once a format change is detected it contains the name of the last media format.
If you have defined any formatGroups
(as per example above) you'll also be provided with a set of dynamically defined boolean flags indicating if media.is
is part of that group.
So, using the above example settings and CSS, a 768px wide viewport would result in a media
object with these initial property values:
media.is === 'tablet',
media.was === undefined,
media.isSmall === false,
media.wasSmall === false,
media.becameSmall === false,
media.leftSmall === false,
media.isLarge === true,
media.wasLarge === false,
media.becameLarge === true,
media.leftLarge === false,
Then if the user resizes the viewport width down to 360px, the media
properties change to this:
media.is === 'phone',
media.was === 'tablet',
media.isSmall === true,
media.wasSmall === false,
media.becameSmall === true,
media.leftSmall === false,
media.isLarge === false,
media.wasLarge === true,
media.becameLarge === false,
media.leftLarge === true,
A second reszie, now to 550px wide viewport, results in this:
media.is === 'phablet',
media.was === 'phone',
media.isSmall === true,
media.wasSmall === true,
media.becameSmall === false,
media.leftSmall === false,
media.isLarge === false,
media.wasLarge === false,
media.becameLarge === false,
media.leftLarge === false,
If we now decide to add a new format group "Funky", the appropriate boolean flags for that group .(is|was|became|left)Funky
are created (either next time a format change is detected or once .refresh()
has been called), like so:
formatMonitor.formatGroups.Funky = { phone: 1, tablet: 1, widescreen: 1 };
formatMonitor.refresh();
alert(media.is);
alert(media.was);
alert(media.isFunky);
alert(media.wasFunky);
alert(media.becameFunky);
alert(media.leftFunky);
4: Subscribe to formatchange events.
Whenever FormatChange detects a new format
it runs any callbacks that have .subscribe()
d to be notified, passing them a reference to the formatMonitor.media
object.
formatMonitor.subscribe(myEventCallback);
function myEventCallback(media) {
if (media.is === "phone") {
}
if (media.was === "tablet") {
}
}
Each callback is immediately run upon subscription if formatMonitor.isRunning() === true
– so no separate "initialization" is required.
If the callback should not be run immediately, then pass false
as a second parameter to .subscribe()
– like so: formatMonitor.subscribe( myEventCallback, false )
Subscriptions can be cancelled any time:
formatMonitor.unsubscribe(myEventCallback);
5: Start, stop, refresh!
formatMonitor.isRunning()
tells you if the window.onresize
monitoring is active or not. If your monitor is set to manual
, it simply tells you if it has been started.
Call formatMonitor.stop()
any time to stop monitoring.
This does NOT unbind any subscribed "formatchange" event callbacks – only stops the onResize CSS-polling and triggering of events
formatMonitor.start()
Binds the window.onresize
event handler to poll the CSS and trigger event callbacks.
This method is called internally when a FormatChange
instance is created – unless the defer
option is passed.
Starting and stopping does not delete or reset the media
object. This means that restarting (i.e. .start()
after a .stop()
) will not re-trigger a 'formatchange' event – unless the window size (or CSS) changed in the meantime – or if if a "hard-refresh" argument is passed (i.e. .start(true)
).
formatMonitor.check()
quickly queries if the format has changed and triggers "formatchange" event if needed. This is the method to use with the manual
option.
formatMonitor.refresh()
refreshes the media
object and triggers "formatchange" event when appropriate – unless a "hard-refresh" boolean argument is passed (i.e. .refresh(true)
).
Helpers
FormatChange comes with a few helpers.
React makeFormatMonitorHook
A factory function that generates a react hook that is bound to a specific FormatChange
monitor instance.
import { FormatChange } from "formatchange";
import { makeFormatMonitorHook } from "formatchange/react";
var formatMonitor = new FormatChange();
export const useFormatMonitor = makeFormatMonitorHook(formatMonitor);
export const MyComponent = (props) => {
const [isPhone, setPhoneFormat] = React.useState(false);
useFormatMonitor((media) => {
setPhoneFormat(media.is === "phone");
});
return <div>Phone format: {String(isPhone)}</div>;
};
You can also pass a getter callback which returns the FormatChange
instance.
This may be the preferred signature for JS libraries that want to provide side-effect free imort
s.
let _formatMonitor;
export const useFormatMonitor = makeFormatMonitorHook(() => {
if (!_formatMonitor) {
_formatMonitor = new FormatChange();
}
return _formatMonitor;
});
makeGroups(normalizedCfg)
Helper
This opinionated helper takes a normalized config object and creates a formatGroup
object that fits into the FormatChange constructor.
This can be useful when your media-format config is stored in a .json file that is then read and interpreted by multiple sources.
Example use:
import { makeGroups } from "formatchange/makeGroups";
const mediaFormats = {
desktop: { minW: 900, group: "Large" },
tablet: { minW: 700, maxW: 900, group: ["Large", "Handheld"] },
phone: { maxW: 480, group: ["Small", "Handheld"] },
tablet_up: { minW: 700 },
phone_tablet: { maxW: 900 },
};
const groupConfig = makeGroups(mediaFormats);
console.log(groupConfig)
const myFormatMonitor = new FormatChange(groupConfig);
jQuery Plugin
FormatChange provides a convenient jQuery plugin.
import { jQueryPlugin } from "formatchange/jquery";
jQueryPlugin();
This adds a jQuery.formatChange()
utility method, that generates and returns new FormatChange()
instances, and allows you to bind formatchange
events handlers using jQuery's .on
and .off
methods. Like so:
var formatMonitor = $.formatChange(formatGroups, options);
$(window).on("formatchange", function (e, media) {
if (media.is === "phone") {
}
if (media.was === "tablet") {
}
});
(Note: Event handlers are auto-triggered upon binding – if formatMonitor.isRunning() === true
– so no separate "initialization" is required. The auto-triggering occurs after a setTimeout
of 0 ms, if the handler hasn't been triggerd manually in the meantime.)
jQuery.formatChange()
accepts the same arguments as the FormatChange
constructor.
In addition it accepts an eventName
option – which in turn results in the creation of a separate FormatChange
instance with its own hidden element, its own CSS breakpoint names and formatGroups
, etc...
#aspectformat::after {
content: "default";
}
@media screen and (max-width: 500px) and (min-height: 800px) {
#aspectformat::after {
content: "portrait";
}
}
@media screen and (min-width: 800px) and (max-height: 500px) {
#aspectformat::after {
content: "landscape";
}
}
var aspectMonitor = $.formatChange(null, {
elmId: "aspectformat",
eventName: "aspectchange",
});
$(window).on("aspectchange", function (e, aMedia) {
if (aMedia.is === "portrait") {
} else if (aMedia.was === "default") {
}
});
You can also pass custom jQuery instances and/or custom default event name to jQueryPlugin()
:
const myJQuery = jQuery.noConflict();
jQueryPlugin(myJQuery, "myDefaultEventName");
var formatMonitor = myJQuery.formatChange();
myJQuery(window).on("myDefaultEventName", function (e, media) {
});
React withMediaProps
HOC
Learn by example:
import { FormatChange } from "formatchange";
import { withMediaProps } from "formatchange/react";
const myMonitor = new FormatChange();
const media2Props = (media) => ({
isSmall: media.is === "phone" || media.is === "phablet",
});
class Foo extends React.Component {
static getPropsFromMedia(media) {
return media2Props(media);
}
}
const MonitoredFoo = withMediaProps(Foo, myMonitor);
class Bar extends React.Component {
}
const MonitoredBar = withMediaProps(Bar, myMonitor, media2Props);
class Baz extends React.Component {
}
const MonitoredBaz = withMediaProps(Baz, myMonitor);