├── .eslintrc
├── .gitignore
├── LICENSE
├── README.md
├── dist
├── At.js
├── Respond.js
└── index.js
├── example
├── base.jsx
├── index.jsx
└── js
│ └── index.js
├── gulpfile.js
├── package.json
└── src
├── At.jsx
├── Respond.jsx
└── index.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "ecmaFeatures": {
3 | "arrowFunctions": true,
4 | "binaryLiterals": false,
5 | "blockBindings": true,
6 | "classes": true,
7 | "defaultParams": true,
8 | "destructuring": true,
9 | "forOf": false,
10 | "generators": true,
11 | "modules": true,
12 | "objectLiteralComputedProperties": true,
13 | "objectLiteralDuplicateProperties": false,
14 | "objectLiteralShorthandMethods": true,
15 | "objectLiteralShorthandProperties": true,
16 | "octalLiterals": true,
17 | "regexUFlag": true,
18 | "regexYFlag": true,
19 | "superInFunctions": false,
20 | "templateStrings": true,
21 | "unicodeCodePointEscapes": false,
22 | "globalReturn": false,
23 | "jsx": true
24 | },
25 |
26 | "parser": "babel-eslint",
27 |
28 | "plugins": [
29 | "react"
30 | ],
31 |
32 | "env": {
33 | "browser": true,
34 | "node": true,
35 | "es6": true
36 | },
37 |
38 | "rules": {
39 | "no-alert": 2,
40 | "no-array-constructor": 2,
41 | "no-bitwise": 0,
42 | "no-caller": 2,
43 | "no-catch-shadow": 0,
44 | "no-cond-assign": 2,
45 | "no-console": 1,
46 | "no-constant-condition": 2,
47 | "no-control-regex": 2,
48 | "no-debugger": 2,
49 | "no-delete-var": 2,
50 | "no-div-regex": 0,
51 | "no-dupe-keys": 2,
52 | "no-dupe-args": 2,
53 | "no-else-return": 1,
54 | "no-empty": 2,
55 | "no-empty-class": 2,
56 | "no-empty-label": 2,
57 | "no-eq-null": 0,
58 | "no-eval": 2,
59 | "no-ex-assign": 2,
60 | "no-extend-native": 2,
61 | "no-extra-bind": 2,
62 | "no-extra-boolean-cast": 2,
63 | "no-extra-parens": 0,
64 | "no-extra-semi": 2,
65 | "no-extra-strict": 2,
66 | "no-fallthrough": 2,
67 | "no-floating-decimal": 0,
68 | "no-func-assign": 2,
69 | "no-implied-eval": 2,
70 | "no-inline-comments": 0,
71 | "no-inner-declarations": [2, "functions"],
72 | "no-invalid-regexp": 2,
73 | "no-irregular-whitespace": 2,
74 | "no-iterator": 2,
75 | "no-label-var": 2,
76 | "no-labels": 2,
77 | "no-lone-blocks": 2,
78 | "no-lonely-if": 0,
79 | "no-loop-func": 2,
80 | "no-mixed-requires": [1, true],
81 | "no-mixed-spaces-and-tabs": [2, false],
82 | "no-multi-spaces": [2, { exceptions: { "VariableDeclarator": true } }],
83 | "no-multi-str": 2,
84 | "no-multiple-empty-lines": [0, {"max": 2}],
85 | "no-native-reassign": 2,
86 | "no-negated-in-lhs": 2,
87 | "no-nested-ternary": 1,
88 | "no-new": 2,
89 | "no-new-func": 2,
90 | "no-new-object": 2,
91 | "no-new-require": 0,
92 | "no-new-wrappers": 2,
93 | "no-obj-calls": 2,
94 | "no-octal": 2,
95 | "no-octal-escape": 2,
96 | "no-path-concat": 0,
97 | "no-plusplus": 0,
98 | "no-process-env": 0,
99 | "no-process-exit": 2,
100 | "no-proto": 2,
101 | "no-redeclare": 2,
102 | "no-regex-spaces": 2,
103 | "no-reserved-keys": 0,
104 | "no-restricted-modules": 0,
105 | "no-return-assign": 2,
106 | "no-script-url": 2,
107 | "no-self-compare": 0,
108 | "no-sequences": 2,
109 | "no-shadow": 2,
110 | "no-shadow-restricted-names": 2,
111 | "no-space-before-semi": 0,
112 | "no-spaced-func": 2,
113 | "no-sparse-arrays": 2,
114 | "no-sync": 0,
115 | "no-ternary": 0,
116 | "no-trailing-spaces": 2,
117 | "no-throw-literal": 0,
118 | "no-undef": 2,
119 | "no-undef-init": 2,
120 | "no-undefined": 0,
121 | "no-underscore-dangle": 0,
122 | "no-unreachable": 2,
123 | "no-unused-expressions": 2,
124 | "no-unused-vars": [1, {"vars": "all", "args": "after-used"}],
125 | "no-use-before-define": 2,
126 | "no-void": 0,
127 | "no-var": 0,
128 | "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }],
129 | "no-with": 2,
130 | "no-wrap-func": 2,
131 |
132 | "block-scoped-var": 0,
133 | "brace-style": [0, "1tbs"],
134 | "camelcase": 2,
135 | "comma-dangle": [2, always-multiline],
136 | "comma-spacing": 2,
137 | "comma-style": 0,
138 | "complexity": [0, 11],
139 | "consistent-return": 2,
140 | "consistent-this": [0, "that"],
141 | "curly": [2, "all"],
142 | "default-case": 0,
143 | "dot-notation": [2, { "allowKeywords": true, "allowPattern": "^[a-zA-Z/d]+(_[a-zA-Z/d]+)+$" }],
144 | "eol-last": 2,
145 | "eqeqeq": 2,
146 | "func-names": 0,
147 | "func-style": [0, "declaration"],
148 | "generator-star": 0,
149 | "global-strict": [0, "never"],
150 | "guard-for-in": 0,
151 | "handle-callback-err": 0,
152 | "indent": [2, 2],
153 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }],
154 | "max-depth": [0, 4],
155 | "max-len": [0, 80, 4],
156 | "max-nested-callbacks": [0, 2],
157 | "max-params": [0, 3],
158 | "max-statements": [0, 10],
159 | "new-cap": 0,
160 | "new-parens": 2,
161 | "one-var": 0,
162 | "operator-assignment": [0, "always"],
163 | "padded-blocks": 0,
164 | "quote-props": 0,
165 | "quotes": [0, "double"],
166 | "radix": 0,
167 | "semi": 2,
168 | "semi-spacing": [2, {"before": false, "after": true}],
169 | "sort-vars": 0,
170 | "space-after-function-name": [2, "never"],
171 | "space-after-keywords": [2, "always"],
172 | "space-before-blocks": [0, "always"],
173 | "space-before-function-parentheses": [0, "always"],
174 | "space-in-brackets": [0, "never"],
175 | "space-in-parens": [0, "never"],
176 | "space-infix-ops": 2,
177 | "space-return-throw-case": 2,
178 | "space-unary-ops": [2, { "words": true, "nonwords": false }],
179 | "spaced-line-comment": [0, "always"],
180 | "strict": 2,
181 | "use-isnan": 2,
182 | "valid-jsdoc": 0,
183 | "valid-typeof": 2,
184 | "vars-on-top": 0,
185 | "wrap-iife": 0,
186 | "wrap-regex": 0,
187 | "yoda": [2, "never"],
188 |
189 | // eslint-plugin-react rules
190 | "react/jsx-boolean-value": [1, "always"],
191 | "react/jsx-uses-react": 1,
192 | "react/jsx-uses-vars": 1,
193 | "react/jsx-no-undef": 2,
194 | "react/no-did-mount-set-state": 1,
195 | "react/no-did-update-set-state": 1,
196 | "react/no-multi-comp": 1,
197 | "react/no-unknown-property": 1,
198 | "react/prop-types": 1,
199 | "react/react-in-jsx-scope": 2,
200 | "react/self-closing-comp": 1,
201 | "react/wrap-multilines": 2
202 | }
203 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .sass-cache/
2 | node_modules/
3 | .module-cache/
4 | example/index.html
5 | example/build/
6 | example/css/*.css
7 | .idea
8 | .DS_Store
9 | *.sublime-*
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2015 Likealike, Ltd. DBA onefinestay
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-respond-to
2 |
3 | Responsive Component for React
4 |
5 | [Demo](http://onefinestay.github.io/react-respond-to/)
6 |
7 |
8 | ## Installation
9 |
10 | ```shell
11 | npm install --save react-respond-to
12 | ```
13 |
14 | Note: This library assumes you have the necessary polyfills in place for `Array.prototype.find` and `matchMedia`.
15 |
16 | ## Why another responsive component?
17 |
18 | Simply put, we looked at what was available and didn't find an API to our liking. We had a few goals:
19 |
20 | * Abstract away the syntax of media queries a bit, even if it means adding restrictions (we don't allow you to test against multiple features at the same time).
21 | * The API should be obvious without explanation
22 | * Avoid having to specify both min-width and max-width when you have multiple breakpoint. We do this by always picking the last matching child, we should feel intuitive to anyone familiar with the mobile-up pattern for CSS.
23 |
24 |
25 | ## Basic Usage
26 |
27 | Each `Respond` element can query a single media feature (if you want more complex queries, you can nest things). The list of available features is browser-dependent, but they are well-listed elsewhere.
28 |
29 | Each `At` element can specify a value to test. The order here is important, because only one child will ever be rendered. Either the last match will be used (look at min-width to understand why this makes sense), or if nothing matches, default will be used. If you don't provide a default, nothing will be rendered.
30 |
31 | If you only want to check against a single value to decide whether to display a child or not, there's a short-hand where you can specify an `at` property on the `Response` element itself.
32 |
33 |
34 | ```javascript
35 | import {Respond, At} from 'react-respond-to';
36 |
37 | class MyComponent extends React.Component {
38 | render() {
39 | return (
40 |
41 |
Width
42 |
43 |
44 | Up to 479px
45 | 480px – 849px
46 | 850px – 1023px
47 | 1024px – 1399px
48 | 1400px upwards
49 |
50 |
51 |
Orientation
52 |
53 |
54 | Landscape
55 | Portrait
56 |
57 |
58 |
Pixel Density
59 |
60 |
61 | (1x) Old-fashioned
62 | (2x) Retina-ish
63 | (3x) The future
64 |
65 |
66 |
67 | Only visible up to 850px
68 |
69 |
70 | );
71 | }
72 | }
73 | ```
74 |
75 | ## Todo
76 |
77 | * Server-side rendering, specifying the server case. We can't assume it's the same as default
78 | * Battle-hardening
79 | * Tests (obviously)
--------------------------------------------------------------------------------
/dist/At.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, '__esModule', {
4 | value: true
5 | });
6 |
7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
8 |
9 | var _react = require('react');
10 |
11 | var _react2 = _interopRequireDefault(_react);
12 |
13 | var At = _react2['default'].createClass({
14 | displayName: 'At',
15 |
16 | propTypes: {
17 | 'default': function _default(props, propName) {
18 | if (props.value || props[propName]) {
19 | return null;
20 | }
21 | return new Error('Must have either a \'value\' or \'default\' prop');
22 | },
23 | value: _react2['default'].PropTypes.any
24 | },
25 |
26 | getDefaultProps: function getDefaultProps() {
27 | return {
28 | initial: false,
29 | 'default': false,
30 | value: null
31 | };
32 | },
33 |
34 | render: function render() {
35 | var result = this.props.children;
36 |
37 | if (typeof result === 'string') {
38 | result = _react2['default'].createElement(
39 | 'span',
40 | null,
41 | result
42 | );
43 | }
44 |
45 | return result;
46 | }
47 | });
48 |
49 | exports['default'] = At;
50 | module.exports = exports['default'];
--------------------------------------------------------------------------------
/dist/Respond.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, '__esModule', {
4 | value: true
5 | });
6 |
7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
8 |
9 | var _react = require('react');
10 |
11 | var _react2 = _interopRequireDefault(_react);
12 |
13 | // Convert a value that potentially isn't an array into one
14 | // If it is already an array just return it
15 | function valueToArray(value) {
16 | if (Array.isArray(value)) {
17 | return value;
18 | }
19 | return [value];
20 | }
21 |
22 | var Respond = _react2['default'].createClass({
23 | displayName: 'Respond',
24 |
25 | propTypes: {
26 | to: _react2['default'].PropTypes.string.isRequired,
27 | children: _react2['default'].PropTypes.any.isRequired,
28 | at: _react2['default'].PropTypes.any,
29 | initial: _react2['default'].PropTypes.bool
30 | },
31 |
32 | getInitialState: function getInitialState() {
33 | return {
34 | mounted: false
35 | };
36 | },
37 |
38 | componentWillMount: function componentWillMount() {
39 | this.updateQueries(this.props);
40 | },
41 |
42 | componentWillReceiveProps: function componentWillReceiveProps(props) {
43 | this.updateQueries(props);
44 | },
45 |
46 | componentDidMount: function componentDidMount() {
47 | this.setState({ mounted: true });
48 | },
49 |
50 | componentWillUnmount: function componentWillUnmount() {
51 | var _this = this;
52 |
53 | this.queries.forEach(function (q) {
54 | return q[0].removeListener(_this.onMatch);
55 | });
56 | },
57 |
58 | onMatch: function onMatch() {
59 | this.forceUpdate();
60 | },
61 |
62 | updateQueries: function updateQueries(props) {
63 | var _this2 = this;
64 |
65 | var to = props.to;
66 | var children = props.children;
67 | var at = props.at;
68 |
69 | if (this.queries) {
70 | this.queries.forEach(function (q) {
71 | return q[0].removeListener(_this2.onMatch);
72 | });
73 | }
74 |
75 | if (at) {
76 | var queryString = '(' + to + ': ' + at + ')';
77 |
78 | var q = matchMedia(queryString);
79 | q.addListener(this.onMatch);
80 |
81 | this.queries = [[q, at]];
82 | } else {
83 | this.queries = valueToArray(children).filter(function (c) {
84 | return !c.props['default'];
85 | }).map(function (c) {
86 | var v = c.props.value;
87 | var queryString = '(' + to + ': ' + c.props.value + ')';
88 |
89 | var q = matchMedia(queryString);
90 | q.addListener(_this2.onMatch);
91 | return [q, v];
92 | });
93 | }
94 | },
95 |
96 | render: function render() {
97 | var _props = this.props;
98 | var children = _props.children;
99 | var at = _props.at;
100 | var initial = _props.initial;
101 | var mounted = this.state.mounted;
102 |
103 | var matches = this.queries.filter(function (q) {
104 | return q[0].matches;
105 | });
106 |
107 | if (at) {
108 | // Shortcut case
109 | if (!mounted && initial || mounted && matches.length) {
110 | var result = children;
111 |
112 | if (typeof result === 'string') {
113 | result = _react2['default'].createElement(
114 | 'span',
115 | null,
116 | result
117 | );
118 | }
119 |
120 | return result;
121 | }
122 | } else {
123 | var defaultChild = valueToArray(children).find(function (c) {
124 | return c.props['default'];
125 | });
126 |
127 | if (matches.length) {
128 | var _ret = (function () {
129 | var val = matches[matches.length - 1][1];
130 | var child = valueToArray(children).find(function (c) {
131 | return c.props.value === val;
132 | });
133 |
134 | if (!mounted && child.props.initial || mounted) {
135 | return {
136 | v: child
137 | };
138 | }
139 | })();
140 |
141 | if (typeof _ret === 'object') return _ret.v;
142 | } else if (defaultChild) {
143 | if (!mounted && defaultChild.props.initial || mounted) {
144 | return defaultChild;
145 | }
146 | }
147 | }
148 |
149 | return null;
150 | }
151 | });
152 |
153 | exports['default'] = Respond;
154 | module.exports = exports['default'];
155 |
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, '__esModule', {
4 | value: true
5 | });
6 |
7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
8 |
9 | var _At = require('./At');
10 |
11 | var _At2 = _interopRequireDefault(_At);
12 |
13 | var _Respond = require('./Respond');
14 |
15 | var _Respond2 = _interopRequireDefault(_Respond);
16 |
17 | exports.At = _At2['default'];
18 | exports.Respond = _Respond2['default'];
--------------------------------------------------------------------------------
/example/base.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons';
2 |
3 |
4 | class Base extends React.Component {
5 | render() {
6 | return (
7 |
8 |
9 | react-respond-to demo
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 | }
21 |
22 | export default Base;
23 |
--------------------------------------------------------------------------------
/example/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons';
2 |
3 | import {Respond, At} from '../src';
4 |
5 |
6 | const Index = React.createClass({
7 |
8 |
9 | render() {
10 | return (
11 |
12 | Width
13 |
14 |
15 | Up to 479px
16 | 480px – 849px
17 | 850px – 1023px
18 | 1024px – 1399px
19 | 1400px upwards
20 |
21 |
22 | Orientation
23 |
24 |
25 | Landscape
26 | Portrait
27 |
28 |
29 | Pixel Density
30 |
31 |
32 | (1x) Old-fashioned
33 | (2x) Retina-ish
34 | (3x) The future
35 |
36 |
37 | Max width (uses short-hand syntax)
38 |
39 |
40 | Only visible up to 850px
41 |
42 |
43 |
44 | );
45 | },
46 | });
47 |
48 | export default Index;
49 |
--------------------------------------------------------------------------------
/example/js/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react/addons';
2 | import Index from '../index';
3 |
4 | const IndexFactory = React.createFactory(Index);
5 |
6 | window.React = React;
7 |
8 | React.render(
9 | IndexFactory(),
10 | document.getElementById('app')
11 | );
12 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.assign = require('object.assign');
4 |
5 | var fs = require('fs');
6 | var path = require('path');
7 | var gulp = require('gulp');
8 | var autoprefixer = require('gulp-autoprefixer');
9 | var extReplace = require('gulp-ext-replace');
10 | var watch = require('gulp-watch');
11 | var babel = require('gulp-babel');
12 | var connect = require('gulp-connect');
13 | var sass = require('gulp-sass');
14 | var deploy = require('gulp-gh-pages');
15 | var React = require('react');
16 | var webpack = require('webpack');
17 | var gulpWebpack = require('gulp-webpack');
18 |
19 | var PRODUCTION = (process.env.NODE_ENV === 'production');
20 |
21 | var gulpPlugins = [];
22 |
23 | if (PRODUCTION) {
24 | gulpPlugins.push(new webpack.DefinePlugin({
25 | "process.env": {
26 | NODE_ENV: JSON.stringify("production")
27 | }
28 | }));
29 | gulpPlugins.push(new webpack.optimize.DedupePlugin());
30 | gulpPlugins.push(new webpack.optimize.UglifyJsPlugin({
31 | compress: true,
32 | mangle: true,
33 | sourceMap: true
34 | }));
35 | }
36 |
37 | var webpackConfig = {
38 | cache: true,
39 | debug: !PRODUCTION,
40 | devtool: PRODUCTION ? 'source-map' : 'eval-source-map',
41 | context: __dirname,
42 | output: {
43 | path: path.resolve('./example/build/'),
44 | filename: 'index.js'
45 | },
46 | module: {
47 | loaders: [
48 | {
49 | test: /\.jsx|.js$/,
50 | exclude: /node_modules\//,
51 | loaders: [
52 | 'babel-loader?stage=1'
53 | ]
54 | },
55 | ]
56 | },
57 | resolve: {
58 | extensions: ['', '.js', '.jsx']
59 | },
60 | plugins: gulpPlugins
61 | };
62 |
63 | gulp.task('build-dist-js', function() {
64 | // build javascript files
65 | return gulp.src('src/**/*.{js,jsx}')
66 | .pipe(babel({
67 | stage: 1
68 | }))
69 | .pipe(extReplace('.js'))
70 | .pipe(gulp.dest('dist'));
71 | });
72 |
73 | gulp.task('build-example-js', function() {
74 | var compiler = gulpWebpack(webpackConfig, webpack);
75 |
76 | return gulp.src('./example/js/index.js')
77 | .pipe(compiler)
78 | .pipe(gulp.dest('./example/build'));
79 | });
80 |
81 | gulp.task('watch-example-js', function() {
82 | var compiler = gulpWebpack(Object.assign({}, {watch: true}, webpackConfig), webpack);
83 | return gulp.src('./example/js/index.js')
84 | .pipe(compiler)
85 | .pipe(gulp.dest('./example/build'));
86 | });
87 |
88 | gulp.task('build-example', function() {
89 | // setup babel hook
90 | require("babel/register")({
91 | stage: 1
92 | });
93 |
94 | var Index = React.createFactory(require('./example/base.jsx'));
95 | var markup = '' + React.renderToString(Index());
96 |
97 | // write file
98 | fs.writeFileSync('./example/index.html', markup);
99 | });
100 |
101 | gulp.task('example-server', function() {
102 | connect.server({
103 | root: 'example',
104 | port: '9989'
105 | });
106 | });
107 |
108 | gulp.task('build', ['build-dist-js', 'build-example', 'build-example-js']);
109 | gulp.task('develop', ['build-example', 'watch-example-js', 'example-server']);
110 |
111 | gulp.task('deploy-example', ['build'], function() {
112 | return gulp.src('./example/**/*')
113 | .pipe(deploy());
114 | });
115 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-respond-to",
3 | "version": "0.4.1",
4 | "description": "Responsive Component for React",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/onefinestay/react-respond-to.git"
12 | },
13 | "author": "Andrew Ingram ",
14 | "license": "Apache 2.0",
15 | "bugs": {
16 | "url": "https://github.com/onefinestay/react-respond-to/issues"
17 | },
18 | "keywords": [
19 | "matchMedia",
20 | "react",
21 | "react-component",
22 | "responsive"
23 | ],
24 | "homepage": "https://github.com/onefinestay/react-respond-to#readme",
25 | "peerDependencies": {
26 | "react": ">=0.12.0"
27 | },
28 | "dependencies": {},
29 | "devDependencies": {
30 | "babel": "^5.2.16",
31 | "babel-core": "^5.2.6",
32 | "babel-loader": "^5.0.0",
33 | "brfs": "^1.2.0",
34 | "gulp": "^3.8.9",
35 | "gulp-autoprefixer": "^2.1.0",
36 | "gulp-babel": "^5.1.0",
37 | "gulp-connect": "^2.2.0",
38 | "gulp-ext-replace": "^0.1.0",
39 | "gulp-gh-pages": "^0.4.0",
40 | "gulp-sass": "^1.2.0",
41 | "gulp-sourcemaps": "^1.2.4",
42 | "gulp-util": "^3.0.4",
43 | "gulp-watch": "^1.1.0",
44 | "gulp-webpack": "^1.4.0",
45 | "object.assign": "^1.1.1",
46 | "transform-loader": "^0.2.1",
47 | "webpack": "^1.5.3"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/At.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 |
4 | const At = React.createClass({
5 | propTypes: {
6 | default: (props, propName) => {
7 | if (props.value || props[propName]) {
8 | return null;
9 | }
10 | return new Error(`Must have either a 'value' or 'default' prop`);
11 | },
12 | value: React.PropTypes.any,
13 | },
14 |
15 | getDefaultProps() {
16 | return {
17 | initial: false,
18 | default: false,
19 | value: null,
20 | };
21 | },
22 |
23 | render() {
24 | let result = this.props.children;
25 |
26 | if (typeof result === 'string') {
27 | result = {result};
28 | }
29 |
30 | return result;
31 | },
32 | });
33 |
34 | export default At;
35 |
--------------------------------------------------------------------------------
/src/Respond.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // Convert a value that potentially isn't an array into one
4 | // If it is already an array just return it
5 | function valueToArray(value) {
6 | if (Array.isArray(value)) {
7 | return value;
8 | }
9 | return [value];
10 | }
11 |
12 | const Respond = React.createClass({
13 | propTypes: {
14 | to: React.PropTypes.string.isRequired,
15 | children: React.PropTypes.any.isRequired,
16 | at: React.PropTypes.any,
17 | initial: React.PropTypes.bool,
18 | },
19 |
20 | getInitialState() {
21 | return {
22 | mounted: false,
23 | };
24 | },
25 |
26 | componentWillMount() {
27 | this.updateQueries(this.props);
28 | },
29 |
30 | componentWillReceiveProps(props) {
31 | this.updateQueries(props);
32 | },
33 |
34 | componentDidMount() {
35 | this.setState({mounted: true});
36 | },
37 |
38 | componentWillUnmount() {
39 | this.queries.forEach(q => q[0].removeListener(this.onMatch));
40 | },
41 |
42 | onMatch() {
43 | this.forceUpdate();
44 | },
45 |
46 | updateQueries(props) {
47 | const {to, children, at} = props;
48 |
49 | if (this.queries) {
50 | this.queries.forEach(q => q[0].removeListener(this.onMatch));
51 | }
52 |
53 | if (at) {
54 | const queryString = `(${ to }: ${ at })`;
55 |
56 | let q = matchMedia(queryString);
57 | q.addListener(this.onMatch);
58 |
59 | this.queries = [[q, at]];
60 | } else {
61 | this.queries = valueToArray(children).filter(c => !c.props.default).map((c) => {
62 | const v = c.props.value;
63 | const queryString = `(${ to }: ${ c.props.value })`;
64 |
65 | let q = matchMedia(queryString);
66 | q.addListener(this.onMatch);
67 | return [q, v];
68 | });
69 | }
70 | },
71 |
72 | render() {
73 | const {children, at, initial} = this.props;
74 | const {mounted} = this.state;
75 | const matches = this.queries.filter(q => q[0].matches);
76 |
77 | if (at) {
78 | // Shortcut case
79 | if ((!mounted && initial) || (mounted && matches.length)) {
80 | let result = children;
81 |
82 | if (typeof result === 'string') {
83 | result = {result};
84 | }
85 |
86 | return result;
87 | }
88 | } else {
89 | const defaultChild = valueToArray(children).find(c => c.props.default);
90 |
91 | if (matches.length) {
92 | let val = matches[matches.length - 1][1];
93 | let child = valueToArray(children).find(c => c.props.value === val);
94 |
95 | if ((!mounted && child.props.initial) || mounted) {
96 | return child;
97 | }
98 | } else if (defaultChild) {
99 | if ((!mounted && defaultChild.props.initial) || mounted) {
100 | return defaultChild;
101 | }
102 | }
103 | }
104 |
105 | return null;
106 | },
107 | });
108 |
109 |
110 | export default Respond;
111 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import At from './At';
2 | import Respond from './Respond';
3 |
4 | export {At, Respond};
5 |
--------------------------------------------------------------------------------