diff --git a/README.md b/README.md index cbaa688..f501069 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,9 @@ You can find the library on `window.ReactMedia`. ## Usage -Render a `` component with a `query` prop whose value is a valid [CSS media query](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries). The `children` prop should be a function whose only argument will be a boolean flag that indicates whether the media query matches or not. +Render a `` component with a `query` prop whose value is a valid [CSS media query](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries) or a `queries` prop whose value is an object with keys as the name of your query and values as a vali [CSS media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries). The `children` prop should be a function whose only argument will be a boolean flag that indicates whether the media query matches or not. + +with `query`: ```js import React from 'react' @@ -59,6 +61,41 @@ class App extends React.Component { } ``` +with `queries`: + +```js +import React from 'react' +import Media from 'react-media' + +class App extends React.Component { + render() { + return ( +
+ + {({ small, medium }) => ( +
+

This always shows.

+ small && ( +

The document is at least 300px wide.

+ ) + medium && ( +

The document is at least 600px wide.

+ ) +
+ )} +
+
+ ) + } +} +``` + + If you render a `` component on the server, it always matches. If you use a regular React element as `children` (i.e. ``) it will be rendered if the query matches. However, *you may end up creating a bunch of elements that won't ever actually be rendered to the page* (i.e. you'll do a lot of unnecessary `createElement`s on each `render`). Thus, a `children` **function** (i.e. `{matches => ...}`) is the preferred API. Then you can decide in the callback which elements to create based on the result of the query. @@ -84,7 +121,7 @@ class App extends React.Component { The `render` prop is never called if the query does not match. -`` also accepts an object, similar to [React's built-in support for inline style objects](https://facebook.github.io/react/tips/inline-styles.html) in e.g. `
`. These objects are converted to CSS media queries via [json2mq](https://github.com/akiran/json2mq/blob/master/README.md#usage). +`` and `` also accepts an object, similar to [React's built-in support for inline style objects](https://facebook.github.io/react/tips/inline-styles.html) in e.g. `
`. These objects are converted to CSS media queries via [json2mq](https://github.com/akiran/json2mq/blob/master/README.md#usage). ```js import React from 'react' diff --git a/modules/Media.js b/modules/Media.js index b16517f..4fe0fd9 100644 --- a/modules/Media.js +++ b/modules/Media.js @@ -1,16 +1,19 @@ import React, { PropTypes } from 'react' import json2mq from 'json2mq' +const queryType = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + PropTypes.arrayOf(PropTypes.object.isRequired) +]) + /** * Conditionally renders based on whether or not a media query matches. */ class Media extends React.Component { static propTypes = { - query: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.object, - PropTypes.arrayOf(PropTypes.object.isRequired) - ]).isRequired, + query: queryType, + queries: PropTypes.objectOf(queryType), render: PropTypes.func, children: PropTypes.oneOfType([ PropTypes.node, @@ -22,29 +25,66 @@ class Media extends React.Component { matches: true } - updateMatches = () => - this.setState({ matches: this.mediaQueryList.matches }) + updateMatches = () => { + let { query, queries } = this.props + if (query) + this.setState({ + matches: this.queries.reduce((accumulated, { name, mediaQueryList }) => ({ + ...accumulated, + [name]: mediaQueryList.matches, + }), {}).match, + }) + + if (queries) + this.setState({ + matches: this.queries.reduce((accumulated, { name, mediaQueryList }) => ({ + ...accumulated, + [name]: mediaQueryList.matches, + }), {}), + }) + } componentWillMount() { if (typeof window !== 'object') return - let { query } = this.props + let { query, queries } = this.props - if (typeof query !== 'string') + if (query && typeof query !== 'string') query = json2mq(query) - this.mediaQueryList = window.matchMedia(query) - this.mediaQueryList.addListener(this.updateMatches) + if (query) { + this.queries = [ + { + name: 'match', + mediaQueryList: window.matchMedia(query), + } + ] + } + + if (queries) { + queries = Object.keys(queries).map(mq => ({ + name: mq, + qs: json2mq(queries[mq]), + })) + this.queries = queries.map(mq => ({ + name: mq.name, + mediaQueryList: window.matchMedia(mq.qs), + })) + } + + this.queries.map(ql => ql.mediaQueryList.addListener(this.updateMatches)) this.updateMatches() } componentWillUnmount() { - this.mediaQueryList.removeListener(this.updateMatches) + let { query, queries } = this.props + if (query || queries) + this.queries.map(ql => ql.mediaQueryList.removeListener(this.updateMatches)) } render() { - const { children, render } = this.props + const { children, render, queries, query } = this.props const { matches } = this.state return ( @@ -52,7 +92,8 @@ class Media extends React.Component { matches ? render() : null ) : children ? ( typeof children === 'function' ? ( - children(matches) + query && children(matches) || + queries && children({ ...matches }) ) : !Array.isArray(children) || children.length ? ( // Preact defaults to empty children array matches ? React.Children.only(children) : null ) : ( diff --git a/modules/__tests__/Media-test.js b/modules/__tests__/Media-test.js index 6bdaa41..e1ab3ef 100644 --- a/modules/__tests__/Media-test.js +++ b/modules/__tests__/Media-test.js @@ -72,6 +72,51 @@ describe('A ', () => { }) }) + describe('and a queries object', () => { + it('renders its child', () => { + const queries = { + sm: { + maxWidth: window.innerWidth, + }, + } + const element = ( + ( +
hello
+ )}/> + ) + + render(element, node, () => { + expect(node.firstChild.innerHTML).toMatch(/hello/) + }) + }) + + it('passes matches for each key', () => { + const queries = { + sm: { + maxWidth: window.innerWidth, + }, + md: { + maxWidth: window.innerWidth - 1, + }, + } + const element = ( + + {({ sm, md }) => ( +
+ {md && 'goodbye'} + {sm && 'hello'} +
+ )} +
+ ) + + render(element, node, () => { + expect(node.firstChild.innerHTML).toMatch(/hello/) + }) + }) + }) + + }) describe('with a query that does not match', () => {