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 all 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
69 changes: 55 additions & 14 deletions modules/Media.js
Original file line number Diff line number Diff line change
@@ -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),
Copy link
Member

Choose a reason for hiding this comment

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

👍

render: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.node,
Expand All @@ -22,37 +25,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.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 (
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