Skip to content

Multiple Queries #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 20, 2017
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ You can find the library on `window.ReactMedia`.

## Usage

Render a `<Media>` 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 `<Media>` 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'
Expand All @@ -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 (
<div>
<Media
queries={{
small: "(min-width: 300px)"
medium: "(min-width: 600px)"
}}
>
{({ small, medium }) => (
<div>
<p>This always shows.</p>
small && (
<p>The document is at least 300px wide.</p>
)
medium && (
<p>The document is at least 600px wide.</p>
)
</div>
)}
</Media>
</div>
)
}
}
```


If you render a `<Media>` component on the server, it always matches.

If you use a regular React element as `children` (i.e. `<Media><SomethingHere/></Media>`) 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. `<Media>{matches => ...}</Media>`) is the preferred API. Then you can decide in the callback which elements to create based on the result of the query.
Expand All @@ -84,7 +121,7 @@ class App extends React.Component {

The `render` prop is never called if the query does not match.

`<Media query>` 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. `<div style>`. These objects are converted to CSS media queries via [json2mq](https://github.com/akiran/json2mq/blob/master/README.md#usage).
`<Media query>` and `<Media queries>` 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. `<div style>`. 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'
Expand Down
65 changes: 55 additions & 10 deletions modules/Media.js
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't valid. You can't use PropTypes.string as the key of an object because it will just be converted to a string. Instead, let's use:

queries: PropTypes.objectOf(
  PropTypes.oneOfType(...)
)

To reduce on code duplication, let's also create a type for the oneOfType and call it queryType.

const queryType = PropTypes.oneOfType(...)

Then we can reuse that in both propTypes.query and propTypes.queries.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

understood, I've been using flow too much 🙊

PropTypes.string,
PropTypes.object,
PropTypes.arrayOf(PropTypes.object.isRequired)
]),
}),
render: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.node,
Expand All @@ -22,37 +29,75 @@ 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.reduce((accumulated, { name, mm }) => ({
...accumulated,
[name]: mm.matches,
}), {}).match,
})

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 = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an array that we created. It's not really a MediaQueryList, just something we made to hold a bunch of media queries. Let's call it this.queries.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, updated

{
name: 'match',
mm: window.matchMedia(query),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The object returned from matchMedia is a MediaQueryList, so let's call it something like mediaQueryList or queryList instead of mm.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opted for mediaQueryList. updated.

}
]
}

if (queries) {
queries = Object.keys(queries).map(mq => ({
name: mq,
qs: json2mq(queries[mq]),
}))
this.mediaQueryList = queries.map(mq => ({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case of a single query, this.mediaQueryList is an actual MediaQueryList object. In the case of multiple queries, it's an array. I'd prefer it to be consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

understood and updated. Thanks!

name: mq.name,
mm: window.matchMedia(mq.qs),
}))
}

this.mediaQueryList.map(ql => ql.mm.addListener(this.updateMatches))
this.updateMatches()
}

componentWillUnmount() {
this.mediaQueryList.removeListener(this.updateMatches)
let { query, queries } = this.props
if (query || queries)
this.mediaQueryList.map(ql => ql.mm.removeListener(this.updateMatches))
}

render() {
const { children, render } = this.props
const { children, render, queries, query } = this.props
const { matches } = this.state

return (
render ? (
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
) : (
Expand Down
45 changes: 45 additions & 0 deletions modules/__tests__/Media-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,51 @@ describe('A <Media>', () => {
})
})

describe('and a queries object', () => {
it('renders its child', () => {
const queries = {
sm: {
maxWidth: window.innerWidth,
},
}
const element = (
<Media queries={queries} render={() => (
<div>hello</div>
)}/>
)

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 = (
<Media queries={queries}>
{({ sm, md }) => (
<div>
{md && 'goodbye'}
{sm && 'hello'}
</div>
)}
</Media>
)

render(element, node, () => {
expect(node.firstChild.innerHTML).toMatch(/hello/)
})
})
})


})

describe('with a query that does not match', () => {
Expand Down