From a0665e881278ba83280fca08229e7af5bec14336 Mon Sep 17 00:00:00 2001 From: Vince Speelman Date: Mon, 27 Feb 2017 21:54:40 -0500 Subject: [PATCH 01/10] Add test case for queries prop Add test case to check for matching `queries` prop --- modules/__tests__/Media-test.js | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/modules/__tests__/Media-test.js b/modules/__tests__/Media-test.js index 6bdaa41..ea2cccd 100644 --- a/modules/__tests__/Media-test.js +++ b/modules/__tests__/Media-test.js @@ -72,6 +72,47 @@ 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 = ( + ( + {md &&
goodbye
} + {sm &&
hello
} + )}/> + ) + + render(element, node, () => { + expect(node.firstChild.innerHTML).toMatch(/hello/) + }) + }) + }) + + }) describe('with a query that does not match', () => { From 8975a194b2589a42a8de846fcaece96b963b72e9 Mon Sep 17 00:00:00 2001 From: Vince Speelman Date: Tue, 28 Feb 2017 08:52:38 -0500 Subject: [PATCH 02/10] create matches from object of mqs - changes proptypes to make query optional and to accept the `queries` prop - creates a list of MatchMedia objects for the `queries` prop - update the `match` property of state to contain an object in the format of ``` { [mq name: string]: [matches: bool], } ``` --- modules/Media.js | 47 ++++++++++++++++++++++++++++----- modules/__tests__/Media-test.js | 6 +++-- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/modules/Media.js b/modules/Media.js index b16517f..6a1cc81 100644 --- a/modules/Media.js +++ b/modules/Media.js @@ -10,7 +10,14 @@ class Media extends React.Component { PropTypes.string, PropTypes.object, PropTypes.arrayOf(PropTypes.object.isRequired) - ]).isRequired, + ]), + queries: PropTypes.shape({ + [PropTypes.string]: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + PropTypes.arrayOf(PropTypes.object.isRequired) + ]), + }), render: PropTypes.func, children: PropTypes.oneOfType([ PropTypes.node, @@ -22,20 +29,46 @@ 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.mediaQueryList.matches }) + + if (queries) + this.setState({ + matches: this.mediaQueryList.reduce((accumulated, { name, mm }) => ({ + ...accumulated, + [name]: mm.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.mediaQueryList = window.matchMedia(query) + this.mediaQueryList.addListener(this.updateMatches) + } + + if (queries && typeof queries === 'object') { + queries = Object.keys(queries).map(mq => ({ + name: mq, + qs: json2mq(queries[mq]), + })) + this.mediaQueryList = queries.map(mq => ({ + name: mq.name, + mm: window.matchMedia(mq.qs), + })) + this.mediaQueryList.map(ql => ql.mm.addListener(this.updateMatches)) + } + this.updateMatches() } diff --git a/modules/__tests__/Media-test.js b/modules/__tests__/Media-test.js index ea2cccd..5e5c7c9 100644 --- a/modules/__tests__/Media-test.js +++ b/modules/__tests__/Media-test.js @@ -101,8 +101,10 @@ describe('A ', () => { } const element = ( ( - {md &&
goodbye
} - {sm &&
hello
} +
+ {md && 'goodbye'} + {sm && 'hello'} +
)}/> ) From 02fce3cfb7b0315a6a2391389aae65141451d78f Mon Sep 17 00:00:00 2001 From: Vince Speelman Date: Tue, 28 Feb 2017 09:05:15 -0500 Subject: [PATCH 03/10] remove all created listeners on `componentWillUnmount` --- modules/Media.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/Media.js b/modules/Media.js index 6a1cc81..cd82d70 100644 --- a/modules/Media.js +++ b/modules/Media.js @@ -57,7 +57,7 @@ class Media extends React.Component { this.mediaQueryList.addListener(this.updateMatches) } - if (queries && typeof queries === 'object') { + if (queries) { queries = Object.keys(queries).map(mq => ({ name: mq, qs: json2mq(queries[mq]), @@ -73,7 +73,11 @@ class Media extends React.Component { } componentWillUnmount() { - this.mediaQueryList.removeListener(this.updateMatches) + let { query, queries } = this.props + if (query) + this.mediaQueryList.removeListener(this.updateMatches) + if (queries) + this.mediaQueryList.map(ql => ql.mm.removeListener(this.updateMatches)) } render() { From 5d0787c0dd14d8a56066ca127d64fc02c0003bcf Mon Sep 17 00:00:00 2001 From: Vince Speelman Date: Tue, 28 Feb 2017 09:22:36 -0500 Subject: [PATCH 04/10] pass matches as prop tp child function --- modules/Media.js | 5 +++-- modules/__tests__/Media-test.js | 16 ++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/modules/Media.js b/modules/Media.js index cd82d70..b749864 100644 --- a/modules/Media.js +++ b/modules/Media.js @@ -81,7 +81,7 @@ class Media extends React.Component { } render() { - const { children, render } = this.props + const { children, render, queries, query } = this.props const { matches } = this.state return ( @@ -89,7 +89,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 5e5c7c9..8442e85 100644 --- a/modules/__tests__/Media-test.js +++ b/modules/__tests__/Media-test.js @@ -100,14 +100,18 @@ describe('A ', () => { }, } const element = ( - ( -
- {md && 'goodbye'} - {sm && 'hello'} -
- )}/> + + {({ sm, md }) => ( +
+ {md && 'goodbye'} + {sm && 'hello'} +
+ )} +
) + console.log(render(element, node)) + render(element, node, () => { expect(node.firstChild.innerHTML).toMatch(/hello/) }) From 1633031cec2da190eb11668a046d5a451819610e Mon Sep 17 00:00:00 2001 From: Vince Speelman Date: Tue, 28 Feb 2017 09:42:57 -0500 Subject: [PATCH 05/10] add to readme --- README.md | 41 +++++++++++++++++++++++++++++++-- modules/__tests__/Media-test.js | 2 -- 2 files changed, 39 insertions(+), 4 deletions(-) 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/__tests__/Media-test.js b/modules/__tests__/Media-test.js index 8442e85..e1ab3ef 100644 --- a/modules/__tests__/Media-test.js +++ b/modules/__tests__/Media-test.js @@ -110,8 +110,6 @@ describe('A ', () => { ) - console.log(render(element, node)) - render(element, node, () => { expect(node.firstChild.innerHTML).toMatch(/hello/) }) From db444334ec4af2f7c9722ad18cd5b8aac1861bb9 Mon Sep 17 00:00:00 2001 From: Vince Speelman Date: Sat, 8 Apr 2017 23:20:37 -0400 Subject: [PATCH 06/10] use arrays for MediaQueryList Previously, mediaQueryList could have multiple types. Now, it's always an array. --- modules/Media.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/modules/Media.js b/modules/Media.js index b749864..04b85c9 100644 --- a/modules/Media.js +++ b/modules/Media.js @@ -32,7 +32,12 @@ class Media extends React.Component { updateMatches = () => { let { query, queries } = this.props if (query) - this.setState({ matches: this.mediaQueryList.matches }) + this.setState({ + matches: this.mediaQueryList.reduce((accumulated, { name, mm }) => ({ + ...accumulated, + [name]: mm.matches, + }), {}).match, + }) if (queries) this.setState({ @@ -53,8 +58,12 @@ class Media extends React.Component { query = json2mq(query) if (query) { - this.mediaQueryList = window.matchMedia(query) - this.mediaQueryList.addListener(this.updateMatches) + this.mediaQueryList = [ + { + name: 'match', + mm: window.matchMedia(query), + } + ] } if (queries) { @@ -66,17 +75,15 @@ class Media extends React.Component { name: mq.name, mm: window.matchMedia(mq.qs), })) - this.mediaQueryList.map(ql => ql.mm.addListener(this.updateMatches)) } + this.mediaQueryList.map(ql => ql.mm.addListener(this.updateMatches)) this.updateMatches() } componentWillUnmount() { let { query, queries } = this.props - if (query) - this.mediaQueryList.removeListener(this.updateMatches) - if (queries) + if (query || queries) this.mediaQueryList.map(ql => ql.mm.removeListener(this.updateMatches)) } From 59bc44645f5785cadad532d6a1c48179b0b1d7bd Mon Sep 17 00:00:00 2001 From: Vince Speelman Date: Wed, 19 Apr 2017 15:27:30 -0400 Subject: [PATCH 07/10] create `queryType` and update proptypes --- modules/Media.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/Media.js b/modules/Media.js index 04b85c9..c46d47e 100644 --- a/modules/Media.js +++ b/modules/Media.js @@ -1,6 +1,12 @@ 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. */ @@ -11,13 +17,7 @@ class Media extends React.Component { PropTypes.object, PropTypes.arrayOf(PropTypes.object.isRequired) ]), - queries: PropTypes.shape({ - [PropTypes.string]: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.object, - PropTypes.arrayOf(PropTypes.object.isRequired) - ]), - }), + queries: PropTypes.objectOf(queryType), render: PropTypes.func, children: PropTypes.oneOfType([ PropTypes.node, From 04dc6001ad6b00a4304a894b9b70ec02916f7ab2 Mon Sep 17 00:00:00 2001 From: Vince Speelman Date: Wed, 19 Apr 2017 15:29:50 -0400 Subject: [PATCH 08/10] update query proptype to use `queryType` --- modules/Media.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/modules/Media.js b/modules/Media.js index c46d47e..d12493e 100644 --- a/modules/Media.js +++ b/modules/Media.js @@ -12,11 +12,7 @@ const queryType = PropTypes.oneOfType([ */ class Media extends React.Component { static propTypes = { - query: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.object, - PropTypes.arrayOf(PropTypes.object.isRequired) - ]), + query: queryType, queries: PropTypes.objectOf(queryType), render: PropTypes.func, children: PropTypes.oneOfType([ From bf347a59e42a536c76cb3646bbf18b772e318e06 Mon Sep 17 00:00:00 2001 From: Vince Speelman Date: Wed, 19 Apr 2017 15:32:47 -0400 Subject: [PATCH 09/10] rename `mediaQueryList` to `queries` `mediaQueryList` was not actually a `MediaQueryList`, so name it appropriately. --- modules/Media.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/Media.js b/modules/Media.js index d12493e..78a30fc 100644 --- a/modules/Media.js +++ b/modules/Media.js @@ -29,7 +29,7 @@ class Media extends React.Component { let { query, queries } = this.props if (query) this.setState({ - matches: this.mediaQueryList.reduce((accumulated, { name, mm }) => ({ + matches: this.queries.reduce((accumulated, { name, mm }) => ({ ...accumulated, [name]: mm.matches, }), {}).match, @@ -37,7 +37,7 @@ class Media extends React.Component { if (queries) this.setState({ - matches: this.mediaQueryList.reduce((accumulated, { name, mm }) => ({ + matches: this.queries.reduce((accumulated, { name, mm }) => ({ ...accumulated, [name]: mm.matches, }), {}), @@ -54,7 +54,7 @@ class Media extends React.Component { query = json2mq(query) if (query) { - this.mediaQueryList = [ + this.queries = [ { name: 'match', mm: window.matchMedia(query), @@ -67,20 +67,20 @@ class Media extends React.Component { name: mq, qs: json2mq(queries[mq]), })) - this.mediaQueryList = queries.map(mq => ({ + this.queries = queries.map(mq => ({ name: mq.name, mm: window.matchMedia(mq.qs), })) } - this.mediaQueryList.map(ql => ql.mm.addListener(this.updateMatches)) + this.queries.map(ql => ql.mm.addListener(this.updateMatches)) this.updateMatches() } componentWillUnmount() { let { query, queries } = this.props if (query || queries) - this.mediaQueryList.map(ql => ql.mm.removeListener(this.updateMatches)) + this.queries.map(ql => ql.mm.removeListener(this.updateMatches)) } render() { From 3cd10c713f487b688fe1cad787946541a407b068 Mon Sep 17 00:00:00 2001 From: Vince Speelman Date: Wed, 19 Apr 2017 15:33:56 -0400 Subject: [PATCH 10/10] rename `mm` to `mediaQueryList` `mm` _is_ an acutal `MediaQueryList`, so name it appropriately. --- modules/Media.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/Media.js b/modules/Media.js index 78a30fc..4fe0fd9 100644 --- a/modules/Media.js +++ b/modules/Media.js @@ -29,17 +29,17 @@ class Media extends React.Component { let { query, queries } = this.props if (query) this.setState({ - matches: this.queries.reduce((accumulated, { name, mm }) => ({ + matches: this.queries.reduce((accumulated, { name, mediaQueryList }) => ({ ...accumulated, - [name]: mm.matches, + [name]: mediaQueryList.matches, }), {}).match, }) if (queries) this.setState({ - matches: this.queries.reduce((accumulated, { name, mm }) => ({ + matches: this.queries.reduce((accumulated, { name, mediaQueryList }) => ({ ...accumulated, - [name]: mm.matches, + [name]: mediaQueryList.matches, }), {}), }) } @@ -57,7 +57,7 @@ class Media extends React.Component { this.queries = [ { name: 'match', - mm: window.matchMedia(query), + mediaQueryList: window.matchMedia(query), } ] } @@ -69,18 +69,18 @@ class Media extends React.Component { })) this.queries = queries.map(mq => ({ name: mq.name, - mm: window.matchMedia(mq.qs), + mediaQueryList: window.matchMedia(mq.qs), })) } - this.queries.map(ql => ql.mm.addListener(this.updateMatches)) + this.queries.map(ql => ql.mediaQueryList.addListener(this.updateMatches)) this.updateMatches() } componentWillUnmount() { let { query, queries } = this.props if (query || queries) - this.queries.map(ql => ql.mm.removeListener(this.updateMatches)) + this.queries.map(ql => ql.mediaQueryList.removeListener(this.updateMatches)) } render() {