├── .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 | 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 | --------------------------------------------------------------------------------