UNPKG

formatchange

Version:

Utility that monitors named CSS media-query breakpoints and triggers event callbacks when a media-format change occurs.

441 lines (334 loc) 13.6 kB
# 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. <!-- prettier-ignore-start --> - [HOWTO / API](#howto--api) - [0: Install](#0-install) - [1: Name your CSS breakpoints](#1-name-your-css-breakpoints) - [2: Configure and initialize FormatChange](#2-configure-and-initialize-formatchange) - [3: Getting the current media format](#3-getting-the-current-media-format) - [4: Subscribe to formatchange events.](#4-subscribe-to-formatchange-events) - [5: Start, stop, refresh!](#5-start-stop-refresh) - [Helpers](#helpers) - [React `makeFormatMonitorHook`](#react-makeformatmonitorhook) - [`makeGroups` Helper](#makegroups-helper) - [jQuery Plugin](#jquery-plugin) - [React `withMediaProps` HOC](#react-withmediaprops-hoc) <!-- prettier-ignore-end --> ## HOWTO / API ### 0: Install ```sh yarn add formatchange # or... 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. ```css @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. ```js 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). ```js var formatGroups = { Small: { phone: 1, phablet: 1 }, Large: { tablet: 1, netbook: 1, widescreen: 1 }, }; ``` The available `options` are as follows (showing default values): ```js var options = { // Optionally supply a pre-existing element to query elm: null, // DOM id of the element to query. (Used if `elm` is missing) elmId: "mediaformat", // Tag-name used when auto-generating an element to query elmTagName: "del", // Set to `true` if you want to `.start()` manually defer: false, // A custom `window` object/scope to monitor. win: window, // Set to `true` to disable `window.onresize` evend binding // and run `.check()` manually manual: false, }; ``` Then this: ```js 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`. ```js 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: ```js media.is === 'tablet', media.was === undefined, // for the 'Small' group: media.isSmall === false, media.wasSmall === false, media.becameSmall === false, media.leftSmall === false, // for the 'Large' group: 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: ```js media.is === 'phone', media.was === 'tablet', // for the 'Small' group: media.isSmall === true, media.wasSmall === false, media.becameSmall === true, media.leftSmall === false, // for the 'Large' group: media.isLarge === false, media.wasLarge === true, media.becameLarge === false, media.leftLarge === true, ``` A second reszie, now to 550px wide viewport, results in this: ```js media.is === 'phablet', media.was === 'phone', // for the 'Small' group: media.isSmall === true, media.wasSmall === true, media.becameSmall === false, media.leftSmall === false, // for the 'Large' group: 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: ```js formatMonitor.formatGroups.Funky = { phone: 1, tablet: 1, widescreen: 1 }; formatMonitor.refresh(); // formatMonitor.refresh(true); // to force-trigger a "formatchange" event. alert(media.is); // --> "phablet" alert(media.was); // --> "phone" alert(media.isFunky); // --> false alert(media.wasFunky); // --> true alert(media.becameFunky); // --> false alert(media.leftFunky); // --> true ``` ### 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. ```js formatMonitor.subscribe(myEventCallback); function myEventCallback(media) { // media === formatMonitor.media if (media.is === "phone") { // init mobile menu } if (media.was === "tablet") { // tear down tablet UI } } ``` 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: ```js 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. ```js import { FormatChange } from "formatchange"; import { makeFormatMonitorHook } from "formatchange/react"; var formatMonitor = new FormatChange(/* groups, options */); export const useFormatMonitor = makeFormatMonitorHook(formatMonitor); // elsewhere off in some React component file export const MyComponent = (props) => { const [isPhone, setPhoneFormat] = React.useState(false); useFormatMonitor((media) => { setPhoneFormat(media.is === "phone"); }); return <div>Phone format: {String(isPhone)}</div>; }; ``` The generated hook returns `FormatChange` instance's `media` object, in case you want to use it directly. (NOTE: The object may or may not be initialized yet.) 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. ```js let _formatMonitor; export const useFormatMonitor = makeFormatMonitorHook(() => { if (!_formatMonitor) { _formatMonitor = new FormatChange(/* groups, options */); } return _formatMonitor; }); ``` ### `makeGroups` Helper This opinionated/esoteric 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: ```js 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"] }, // Formats without `group` are ignored by `makeGroups` tablet_up: { minW: 700 }, phone_tablet: { maxW: 900 }, }; const groupConfig = makeGroups(mediaFormats); console.log(groupConfig); /* { Small: { phone: true }, Large: { tablet: true, desktop: true }, Handheld: { tablet: true, phone: true }, } */ const myFormatMonitor = new FormatChange(groupConfig); ``` ### jQuery Plugin FormatChange provides a convenient jQuery plugin. ```js 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: ```js // initialize/instantiate FormatChange var formatMonitor = $.formatChange(formatGroups, options); $(window).on("formatchange", function (e, media) { // media === formatMonitor.media if (media.is === "phone") { // init mobile menu } if (media.was === "tablet") { // tear down tablet UI } }); ``` (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... ```css #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"; } } ``` ```js var aspectMonitor = $.formatChange(null, { elmId: "aspectformat", eventName: "aspectchange", }); $(window).on("aspectchange", function (e, aMedia) { // aMedia === aspectMonitor.media; if (aMedia.is === "portrait") { // do stuff... } else if (aMedia.was === "default") { // do stuff... } }); ``` You can also pass custom jQuery instances and/or custom default event name to `jQueryPlugin()`: ```js const myJQuery = jQuery.noConflict(); jQueryPlugin(myJQuery, "myDefaultEventName"); var formatMonitor = myJQuery.formatChange(); myJQuery(window).on("myDefaultEventName", function (e, media) { // ... }); ``` ### React `withMediaProps` HOC Learn by example: ```js import { FormatChange } from "formatchange"; import { withMediaProps } from "formatchange/react"; const myMonitor = new FormatChange(); // Mapper function that returns an object with media-related // props to be spread on the wrapped component const media2Props = (media) => ({ isSmall: media.is === "phone" || media.is === "phablet", }); // With static class-method class Foo extends React.Component { static getPropsFromMedia(media) { return media2Props(media); } /* ... */ } const MonitoredFoo = withMediaProps(Foo, myMonitor); // With mapper as a third HOC parameter class Bar extends React.Component { /* ... */ } const MonitoredBar = withMediaProps(Bar, myMonitor, media2Props); // With dumb default mapper `(media) => media` class Baz extends React.Component { /* ... */ } const MonitoredBaz = withMediaProps(Baz, myMonitor); ```