Skip to content
This repository has been archived by the owner on Jul 8, 2023. It is now read-only.

Commit

Permalink
➕ add withMatchMediaProps
Browse files Browse the repository at this point in the history
  • Loading branch information
deepsweet authored and Kir Belevich committed Aug 4, 2017
1 parent 0c0e88a commit aef4070
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 4 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"devDependencies": {
"lerna": "^2.0.0",
"start-babel-cli": "^4.0.2",
"start-deepsweet-react-components-monorepo-preset": "^0.0.1"
"start-deepsweet-react-components-monorepo-preset": "^0.0.2"
},
"scripts": {
"start": "start-runner --preset start-deepsweet-react-components-monorepo-preset",
Expand Down
16 changes: 16 additions & 0 deletions packages/with-match-media-props/demo/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';

import withMatchMediaProps from '../src/';

const Demo = (props) => (
<h1>props: {JSON.stringify(props)}</h1>
);

export default withMatchMediaProps({
isSmallScreen: {
maxWidth: 500
},
isHighDpiScreen: {
minResolution: '192dpi'
}
})(Demo);
28 changes: 28 additions & 0 deletions packages/with-match-media-props/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@hocs/with-match-media-props",
"library": "withMatchMediaProps",
"version": "0.1.0",
"description": "CSS Media Queries HOC for React",
"keywords": [
"react",
"hoc",
"recompose"
],
"main": "lib/index.js",
"module": "es/index.js",
"files": [
"dist/",
"es/",
"lib/"
],
"repository": "deepsweet/hocs",
"author": "Kir Belevich <[email protected]> (https://github.com/deepsweet)",
"license": "MIT",
"dependencies": {
"json2mq": "^0.2.0"
},
"peerDependencies": {
"react": "^15.6.1",
"recompose": "^0.24.0"
}
}
61 changes: 61 additions & 0 deletions packages/with-match-media-props/src/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { Component } from 'react';
import { setDisplayName, wrapDisplayName } from 'recompose';
import json2mq from 'json2mq';

const queryToMql = (query) => global.matchMedia(json2mq(query));
const createMediaMatcher = (query) => {
const mql = queryToMql(query);

return {
matches: mql.matches,
subscribe(handler) {
mql.addListener(handler);

return () => mql.removeListener(handler);
}
};
};

const withMatchMediaProps = (propsQieries = {}) => (Target) => {
class WithMatchMediaProps extends Component {
constructor(props, context) {
super(props, context);

this.propsMatchers = Object.keys(propsQieries).map((prop) => ({
prop,
...createMediaMatcher(propsQieries[prop])
}));

this.state = this.propsMatchers.reduce((result, propMatcher) => ({
...result,
[propMatcher.prop]: propMatcher.matches
}), {});
}

componentDidMount() {
this.unsubscribers = this.propsMatchers.map((propMatcher) => propMatcher.subscribe((e) => {
this.setState({
[propMatcher.prop]: e.matches
});
}));
}

componentWillUnmount() {
this.unsubscribers.forEach((unsubscribe) => unsubscribe());
}

render() {
return (
<Target {...this.props} {...this.state}/>
);
}
}

if (process.env.NODE_ENV !== 'production') {
return setDisplayName(wrapDisplayName(Target, 'withMatchMediaProps'))(WithMatchMediaProps);
}

return WithMatchMediaProps;
};

export default withMatchMediaProps;
164 changes: 164 additions & 0 deletions packages/with-match-media-props/test/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import React from 'react';
import { mount } from 'enzyme';

const Target = () => null;

describe('withMatchMediaProps', () => {
const mockJson2mq = jest.fn();
let withMatchMediaProps = null;

beforeAll(() => {
withMatchMediaProps = require('../src/').default;

jest.mock('json2mq', () => mockJson2mq);
});

beforeEach(() => {
mockJson2mq.mockClear();
});

afterAll(() => {
jest.unmock('json2mq');
});

it('should just pass props through when called without arguments', () => {
const EnchancedTarget = withMatchMediaProps()(Target);
const wrapper = mount(
<EnchancedTarget a={1} b={2} c={3}/>
);

expect(wrapper.find(Target).props()).toEqual({ a: 1, b: 2, c: 3 });
});

describe('`window.matchMedia`', () => {
let originMatchMedia = null;

beforeAll(() => {
originMatchMedia = global.matchMedia;
});

afterAll(() => {
global.matchMedia = originMatchMedia;
});

it('should pass query object to json2mq', () => {
global.matchMedia = jest.fn(() => ({
addListener: () => {},
removeListener: () => {},
matches: true
}));

const EnchancedTarget = withMatchMediaProps({
test: {
maxWidth: 300
}
})(Target);

mount(
<EnchancedTarget/>
);

expect(mockJson2mq).toHaveBeenCalledTimes(1);
expect(mockJson2mq).toHaveBeenCalledWith({ maxWidth: 300 });
});

it('should set initial state and provide props with matched queries', () => {
global.matchMedia = jest.fn(() => ({
addListener: () => {},
removeListener: () => {},
matches: true
}));

const EnchancedTarget = withMatchMediaProps({
test: {
maxWidth: 300
}
})(Target);
const wrapper = mount(
<EnchancedTarget/>
);

expect(wrapper.find(Target).prop('test')).toBe(true);
});

it('should subscribe on mount and unsubscribe on unmount', () => {
const mockAddListener = jest.fn();
const mockRemoveListener = jest.fn();

global.matchMedia = jest.fn(() => ({
addListener: mockAddListener,
removeListener: mockRemoveListener
}));

const EnchancedTarget = withMatchMediaProps({
test: {
maxWidth: 300
}
})(Target);
const wrapper = mount(
<EnchancedTarget/>
);

expect(mockAddListener).toHaveBeenCalledTimes(1);
wrapper.unmount();
expect(mockRemoveListener).toHaveBeenCalledTimes(1);
expect(mockRemoveListener).toHaveBeenCalledWith(mockAddListener.mock.calls[0][0]);
});

it('should change state and provide props when query has been matched', () => {
const mockAddListener = jest.fn();

global.matchMedia = jest.fn(() => ({
addListener: mockAddListener,
removeListener: () => {},
matches: false
}));

const EnchancedTarget = withMatchMediaProps({
test: {
maxWidth: 300
}
})(Target);
const wrapper = mount(
<EnchancedTarget/>
);

mockAddListener.mock.calls[0][0]({ matches: true });
expect(wrapper.find(Target).prop('test')).toBe(true);
});
});

describe('display name', () => {
let origNodeEnv = null;

beforeAll(() => {
origNodeEnv = process.env.NODE_ENV;
});

afterAll(() => {
process.env.NODE_ENV = origNodeEnv;
});

it('should wrap display name in non-production env', () => {
process.env.NODE_ENV = 'test';

const EnchancedTarget = withMatchMediaProps()(Target);
const wrapper = mount(
<EnchancedTarget/>
);

expect(wrapper.name()).toBe('withMatchMediaProps(Target)');
});

it('should not wrap display name in production env', () => {
process.env.NODE_ENV = 'production';

const EnchancedTarget = withMatchMediaProps()(Target);
const wrapper = mount(
<EnchancedTarget/>
);

expect(wrapper.name()).toBe('WithMatchMediaProps');
});
});
});
13 changes: 13 additions & 0 deletions packages/with-match-media-props/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


json2mq@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/json2mq/-/json2mq-0.2.0.tgz#b637bd3ba9eabe122c83e9720483aeb10d2c904a"
dependencies:
string-convert "^0.2.0"

string-convert@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97"
43 changes: 43 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ A collection of [Higher-Order Components](https://facebook.github.io/react/docs/
* [Packages](#packages)
* :non-potable_water: [`omitProps`](#non-potable_water-omitprops--)
* :recycle: [`withLifecycle`](#recycle-withlifecycle--)
* :left_right_arrow: [`withMatchMediaProps`](#left_right_arrow-withmatchmediaprops--)
* :hourglass: [`debounceHandler`](#hourglass-debouncehandler--)
* :hourglass: [`throttleHandler`](#hourglass-throttlehandler--)
* …and more to come, you can [follow me on Twitter](https://twitter.com/deepsweet) for updates
Expand All @@ -35,6 +36,7 @@ import omitProps from '@hocs/omit-props';

const Demo = (props) => (
<h1>props: {JSON.stringify(props)}</h1>
// props: {"c":3}
);

export default compose(
Expand Down Expand Up @@ -124,6 +126,47 @@ yarn start demo with-lifecycle

As a bonus you can "share" stuff across different lifecycle methods in that factory scope with `let mySharedStuff`, just like you did before with `this.mySharedStuff` using a class instance.

### :left_right_arrow: [`withMatchMediaProps`](packages/with-match-media-props) [![npm](https://img.shields.io/npm/v/@hocs/with-match-media-props.svg?style=flat-square)](https://www.npmjs.com/package/@hocs/with-match-media-props) [![deps](https://david-dm.org/deepsweet/hocs.svg?path=packages/with-match-media-props&style=flat-square)](https://david-dm.org/deepsweet/hocs?path=packages/with-match-media-props)

Dynamically map [CSS Media Queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries) matches to boolean props using [`window.matchMedia`](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia).

```
yarn add recompose @hocs/with-match-media-props
```

```js
withMatchMediaProps(
mediaMatchers: {
[propName: string]: Object
}
): HigherOrderComponent
```

```js
import React from 'react';
import withMatchMediaProps from '@hocs/with-match-media-props';

const Demo = (props) => (
<h1>props: {JSON.stringify(props)}</h1>
// props: {"isSmallScreen":false,"isRetina":true}
);

export default withMatchMediaProps({
isSmallScreen: {
maxWidth: 500
},
isHighDpiScreen: {
minResolution: '192dpi'
}
})(Demo);
```

```
yarn start demo with-match-media-props
```

Check [json2mq](https://github.com/akiran/json2mq) for query syntax details.

### :hourglass: [`debounceHandler`](packages/debounce-handler) [![npm](https://img.shields.io/npm/v/@hocs/debounce-handler.svg?style=flat-square)](https://www.npmjs.com/package/@hocs/debounce-handler) [![deps](https://david-dm.org/deepsweet/hocs.svg?path=packages/debounce-handler&style=flat-square)](https://david-dm.org/deepsweet/hocs?path=packages/debounce-handler)

```
Expand Down
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5610,9 +5610,9 @@ start-codecov@^2.0.0:
dependencies:
codecov-lite "^0.1.2"

start-deepsweet-react-components-monorepo-preset@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/start-deepsweet-react-components-monorepo-preset/-/start-deepsweet-react-components-monorepo-preset-0.0.1.tgz#56f7f324e26a83dbe46abd21d7b1ba35a8c89167"
start-deepsweet-react-components-monorepo-preset@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/start-deepsweet-react-components-monorepo-preset/-/start-deepsweet-react-components-monorepo-preset-0.0.2.tgz#f890db520bf552fe8f47d07c1c93b782cca481dc"
dependencies:
babel-jest "^20.0.3"
babel-loader "^7.1.1"
Expand Down

0 comments on commit aef4070

Please sign in to comment.