├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── native.js ├── package.json ├── src ├── components │ ├── createAll.js │ ├── createInject.js │ └── createProvider.js ├── index.js ├── native.js └── utils │ ├── hasEmptyIntersection.js │ ├── isPlainObject.js │ ├── shallowEqual.js │ └── sharedKeys.js ├── test ├── components │ ├── Provider.spec.js │ ├── inject.spec.js │ └── jsdomReact.js └── utils │ ├── hasEmptyIntersection.spec.js │ ├── isPlainObject.spec.js │ ├── shallowEqual.spec.js │ └── sharedKeys.spec.js ├── webpack.config.base.js ├── webpack.config.development.js └── webpack.config.production.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "loose": "all" 4 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "react/jsx-uses-react": 2, 10 | "react/jsx-uses-vars": 2, 11 | "react/react-in-jsx-scope": 2, 12 | 13 | //Temporarirly disabled due to a possible bug in babel-eslint (todomvc example) 14 | "block-scoped-var": 0, 15 | // Temporarily disabled for test/* until babel/babel-eslint#33 is resolved 16 | "padded-blocks": 0 17 | }, 18 | "plugins": [ 19 | "react" 20 | ] 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | dist 5 | lib 6 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | examples -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.0.0" 4 | script: 5 | - npm test 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Josh Story 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-tunnel 2 | React components and decorators for putting context-like values into context and pulling them out as props 3 | 4 | Heavily copied/modeled off the code used in [react-redux](https://github.com/gaearon/react-redux/) by @gaearon 5 | 6 | [![build status](https://img.shields.io/travis/gnoff/react-tunnel/master.svg?style=flat-square)](https://travis-ci.org/gnoff/react-tunnel) 7 | [![npm version](https://img.shields.io/npm/v/react-tunnel.svg?style=flat-square)](https://www.npmjs.com/package/react-tunnel) 8 | [![npm downloads](https://img.shields.io/npm/dm/react-tunnel.svg?style=flat-square)](https://www.npmjs.com/package/react-tunnel) 9 | 10 | ## Table of Contents 11 | 12 | - [Installation](#installation) 13 | - [React Native](#react-native) 14 | - [Quick Start](#quick-start) 15 | - [Best Practices](#bestpractices) 16 | - [API](#api) 17 | - [``](#provider-provide) 18 | - [`inject([mapProvidedToProps])`](#injectmapprovidedtoprops) 19 | - [Thanks](#thanks) 20 | - [License](#license) 21 | 22 | ## installation 23 | 24 | `npm install --save react-tunnel`. 25 | 26 | ## React Native 27 | 28 | for React: require/import from `react-tunnel`. 29 | For React Native: require/import from `react-tunnel/native`. 30 | 31 | ## Quick Start 32 | 33 | `react-tunnel` helps you provide injectable props to child components to help avoid deep chains of prop passing 34 | 35 | - install with `npm install react-tunnel` 36 | - import or require `Provider` and `inject` by 37 | ```js 38 | 39 | //es5 40 | var Provider = require('react-tunnel').Provider; 41 | var inject = require('react-tunnel').inject; 42 | 43 | //es6 44 | import { Provider, inject } from 'react-tunnel' 45 | 46 | ``` 47 | 48 | - wrap a `Component` tree with `` like 49 | ```js 50 | //using object provide 51 | render() { 52 | return ( 53 | 54 | 55 | 56 | ) 57 | } 58 | 59 | //or as a function 60 | function provider () { 61 | return { 62 | thing: "one", 63 | anotherThing: 2g 64 | } 65 | } 66 | 67 | render() { 68 | return ( 69 | 70 | 71 | 72 | ) 73 | } 74 | ``` 75 | 76 | - decorate a child component with `inject` and provide a mapping function determine which provided props to inject into the decorated component 77 | ```js 78 | var SomeChild = require('./SomeChildOfAnything') //a react Component 79 | 80 | function mapProvidedToProps(provided) { 81 | return { 82 | that: provided.thing 83 | } 84 | } 85 | 86 | //notice that inject returns a wrapping function 87 | var InjectedChild = inject(mapProvidedToProps)(SomeChild); 88 | 89 | // now in InjectedChild props will have `that` 90 | ... 91 | render() { 92 | var injectedProp = this.props.that; 93 | return {injectedProp}; 94 | //will render as one 95 | } 96 | ``` 97 | 98 | ## Best Practices 99 | 100 | `react-tunnel` uses React's context feature to make provided props available to children regardless of how deep they are. While this is powerful it also can be abused and make for a nightmare to manage. 101 | 102 | It is reccommended that this functionality be used to provide generally static properties that don't change much if at all based on the local conditions of the injecting component. Examples might include 103 | 104 | - Providing viewport dimensions to arbitrarily deep Components 105 | - Providing [redux](https://github.com/rackt/redux) action creators from a parent `connected` Component to deep children 106 | 107 | Also please consider that the context api for React has PropType checking for a reason and that by using this library and opting out of that stronger contract has costs and you may want to utilize the base context features rather than this library 108 | 109 | ## API 110 | 111 | ### `` 112 | 113 | makes `provide` available via `context.provided` to children of Provider. use `inject` to access them easily 114 | 115 | #### Props 116 | 117 | - `provide {fn | object}`: 118 | - `provide: function(parentProvided) { return provided{object} }`: will provide the return value of `provide` prop. Function takes in any provided values from parent providers if any. If none, an empty object is passed. 119 | - `provide: object`: provides any parent provided values if nested along with `provide` object properties. if there is a key collision the properties of the `provide` prop are used and mask similarly named properties from any parent provided objects 120 | 121 | #### Nesting 122 | 123 | `Provider`s are nestable and if using the object version of `provide` will automatically reprovide any values provided in the immediate parent `Provider`. Use this if you want to say Provide some truly global props at the root of your App but also use `Provider`s for [Redux action creators](https://github.com/gaearon/react-redux) produced via `connect` to the local render tree. 124 | 125 | If you nest `Provider`s but use the function form of `provide` you will need to forward any desired parent provided values using the function forms argument `parentProvided`. 126 | 127 | ### `inject([mapProvidedToProps])` 128 | 129 | Creates a decorator which injects props from `Provider` into the decorated component according to the `mapProvidedToProps` function. 130 | 131 | #### Arguments 132 | 133 | - `mapProvidedToProps(provided)? returns object`: called when decorated Component mounts and when it receives new context. the return object of this call is added to the underlying Component as props 134 | - `default`: if `mapProvidedToProps` is not passed to `inject` then all `Provider` values are passed to underlying component. 135 | 136 | 137 | ## Thanks 138 | - [@gaearon](https://github.com/gaearon) for inspiring this API with the more specialized [react-redux](https://github.com/gaearon/react-redux) 139 | - [@rt2zz](https://www.github.com/rt2zz) for helping flesh out the design and API 140 | 141 | ## License 142 | 143 | MIT 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /native.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/native'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tunnel", 3 | "version": "0.1.0", 4 | "description": "React component and decorator for prop providing and prop injection", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "build:lib": "babel src --out-dir lib", 8 | "build:umd": "webpack src/index.js dist/react-tunnel.js --config webpack.config.development.js", 9 | "build:umd:min": "webpack src/index.js dist/react-tunnel.min.js --config webpack.config.production.js", 10 | "build": "npm run build:lib && npm run build:umd && npm run build:umd:min", 11 | "clean": "rimraf lib dist coverage", 12 | "lint": "eslint src test", 13 | "prepublish": "npm run clean && npm run build", 14 | "test": "mocha --compilers js:babel/register --recursive", 15 | "test:watch": "npm test -- --watch", 16 | "test:cov": "babel-node ./node_modules/isparta/bin/isparta cover ./node_modules/mocha/bin/_mocha -- --recursive" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/gnoff/react-tunnel.git" 21 | }, 22 | "keywords": [ 23 | "react", 24 | "tunnel", 25 | "context", 26 | "provide", 27 | "provider", 28 | "inject", 29 | "injection" 30 | ], 31 | "author": "Josh Story (http://github.com/gnoff)", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/gnoff/react-tunnel/issues" 35 | }, 36 | "homepage": "https://github.com/gnoff/react-tunnel", 37 | "devDependencies": { 38 | "babel": "5.x.x", 39 | "babel-core": "5.x.x", 40 | "babel-eslint": "4.x.x", 41 | "babel-loader": "5.x.x", 42 | "eslint": "1.x.x", 43 | "eslint-config-airbnb": "0.0.7", 44 | "eslint-plugin-react": "3.x.x", 45 | "expect": "1.x.x", 46 | "exenv": "1.x.x", 47 | "isparta": "3.x.x", 48 | "istanbul": "0.3.x", 49 | "jsdom": "6.x.x", 50 | "mocha": "2.x.x", 51 | "mocha-jsdom": "1.x.x", 52 | "react": "0.14.x", 53 | "rimraf": "2.x.x", 54 | "webpack": "1.x.x", 55 | "webpack-dev-server": "1.x.x" 56 | }, 57 | "dependencies": { 58 | "invariant": "2.x.x", 59 | "prop-types": "^15.5.10" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/createAll.js: -------------------------------------------------------------------------------- 1 | import createProvider from './createProvider'; 2 | import createInject from './createInject'; 3 | 4 | export default function createAll(React) { 5 | const Provider = createProvider(React); 6 | const inject = createInject(React); 7 | 8 | return { Provider, inject }; 9 | } -------------------------------------------------------------------------------- /src/components/createInject.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import shallowEqual from '../utils/shallowEqual'; 3 | import isPlainObject from '../utils/isPlainObject'; 4 | import { object } from 'prop-types'; 5 | 6 | const defaultMapProvidedToProps = (provided) => ({...provided}); 7 | 8 | function getDisplayName(Component) { 9 | return Component.displayName || Component.name || 'Component'; 10 | } 11 | 12 | export default function createInject(React) { 13 | const { Component } = React; 14 | 15 | //@TODO have not tested nextVersion stuff 16 | var nextVersion = 0; 17 | return function inject(mapProvidedToProps) { 18 | const finalMapProvidedToProps = mapProvidedToProps || defaultMapProvidedToProps; 19 | 20 | // Helps track hot reloading. 21 | const version = nextVersion++; 22 | 23 | function computeProvidedProps(provided) { 24 | const providedProps = finalMapProvidedToProps(provided); 25 | invariant( 26 | isPlainObject(providedProps), 27 | '`mapProvidedToProps` must return an object. Instead received %s.', 28 | providedProps 29 | ); 30 | return providedProps; 31 | } 32 | 33 | return function wrapWithInject(WrappedComponent) { 34 | class Inject extends Component { 35 | static displayName = `inject(${getDisplayName(WrappedComponent)})`; 36 | static WrappedComponent = WrappedComponent; 37 | 38 | static contextTypes = { 39 | provided: object 40 | }; 41 | 42 | shouldComponentUpdate(nextProps, nextState, nextContext) { 43 | return !shallowEqual(this.state.provided, nextState.provided) || 44 | !shallowEqual(this.props, nextProps); 45 | } 46 | 47 | constructor(props, context) { 48 | super(props, context); 49 | this.version = version; 50 | this.provided = context.provided; 51 | 52 | invariant(this.provided, 53 | `Could not find "provided" in context ` + 54 | `of "${this.constructor.displayName}". ` + 55 | `Wrap a higher component in a . ` 56 | ); 57 | 58 | this.state = { 59 | provided: computeProvidedProps(this.provided) 60 | }; 61 | } 62 | 63 | componentWillReceiveProps(nextProps, nextContext) { 64 | if (!shallowEqual(this.provided, nextContext.provided)) { 65 | this.provided = nextContext.provided; 66 | this.recomputeProvidedProps(nextContext); 67 | } 68 | } 69 | 70 | recomputeProvidedProps(context = this.context) { 71 | const nextProvidedProps = computeProvidedProps(context.provided); 72 | if (!shallowEqual(nextProvidedProps, this.state.provided)) { 73 | this.setState({provided: nextProvidedProps}); 74 | } 75 | } 76 | 77 | getWrappedInstance() { 78 | return this.refs.wrappedInstance; 79 | } 80 | 81 | render() { 82 | return ( 83 | 85 | ); 86 | } 87 | } 88 | 89 | if (( 90 | // Node-like CommonJS environments (Browserify, Webpack) 91 | typeof process !== 'undefined' && 92 | typeof process.env !== 'undefined' && 93 | process.env.NODE_ENV !== 'production' 94 | ) || 95 | // React Native 96 | typeof __DEV__ !== 'undefined' && 97 | __DEV__ //eslint-disable-line no-undef 98 | ) { 99 | Inject.prototype.componentWillUpdate = function componentWillUpdate() { 100 | if (this.version === version) { 101 | return; 102 | } 103 | 104 | // We are hot reloading! 105 | this.version = version; 106 | 107 | // Update the state and bindings. 108 | this.recomputeProvidedProps(); 109 | }; 110 | } 111 | 112 | return Inject; 113 | }; 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /src/components/createProvider.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import shallowEqual from '../utils/shallowEqual'; 3 | import isPlainObject from '../utils/isPlainObject'; 4 | import hasEmptyIntersection from '../utils/hasEmptyIntersection'; 5 | import sharedKeys from '../utils/sharedKeys'; 6 | import { object, func, oneOfType, element } from 'prop-types'; 7 | 8 | export default function createProvider(React) { 9 | const { Component, Children } = React; 10 | 11 | return class Provider extends Component { 12 | static contextTypes = { 13 | provided: object 14 | }; 15 | 16 | static childContextTypes = { 17 | provided: object.isRequired 18 | }; 19 | 20 | static propTypes = { 21 | children: element.isRequired, 22 | provide: oneOfType([ 23 | object, 24 | func, 25 | ]), 26 | }; 27 | 28 | getChildContext() { 29 | return { provided: this.state.provided }; 30 | } 31 | 32 | constructor(props, context) { 33 | super(props, context); 34 | const provided = this.providedFromPropsAndContext(props, context); 35 | this.state = { provided: provided}; 36 | } 37 | 38 | componentWillReceiveProps(nextProps, nextContext) { 39 | const { provided } = this.state; 40 | const nextProvided = this.providedFromPropsAndContext(nextProps, nextContext); 41 | if (shallowEqual(provided, nextProvided)) { 42 | return; 43 | } 44 | this.setState({ provided: nextProvided }); 45 | } 46 | 47 | providedFromPropsAndContext(props, context) { 48 | const isNestedProvider = isPlainObject(context.provided); 49 | const parentProvided = isNestedProvider ? context.provided : {}; 50 | 51 | if (isNestedProvider) { 52 | invariant( 53 | isPlainObject(parentProvided), 54 | 'This Provider appears to be nested inside another provider but received a parent `provided` ' + 55 | 'is not a plain Object. `provided` must be always be a plain Object. %s', 56 | parentProvided 57 | ); 58 | } 59 | 60 | 61 | const { provide } = props; 62 | let provider = provide; 63 | 64 | if (isPlainObject(provide)) { 65 | provider = (parentProvided) => ({...parentProvided, ...provide}) 66 | } 67 | 68 | let provided = provider(parentProvided); 69 | 70 | invariant( 71 | isPlainObject(provided), 72 | 'This Provider is attempting to provide something other than a plain Object. ' + 73 | 'the `provide` prop must either be a plain object itself or a function that returns ' + 74 | 'a plain Object. `provide` is or returned %s', 75 | provided 76 | ); 77 | 78 | return provided; 79 | } 80 | 81 | isNested() { 82 | 83 | } 84 | 85 | render() { 86 | return Children.only(this.props.children); 87 | } 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createAll from './components/createAll'; 3 | 4 | export const { Provider, inject } = createAll(React); 5 | -------------------------------------------------------------------------------- /src/native.js: -------------------------------------------------------------------------------- 1 | import React from 'react-native'; 2 | import createAll from './components/createAll'; 3 | 4 | export const { Provider, inject } = createAll(React); 5 | -------------------------------------------------------------------------------- /src/utils/hasEmptyIntersection.js: -------------------------------------------------------------------------------- 1 | export default function hasEmptyIntersection(objA, objB) { 2 | if (!objA || !objB) { 3 | return true; 4 | } 5 | 6 | const keysA = Object.keys(objA); 7 | const keysB = Object.keys(objB); 8 | 9 | if (keysA.length === 0 || keysB.length === 0) { 10 | return true; 11 | } 12 | 13 | if (objA === objB) { 14 | return false; 15 | } 16 | 17 | const objCombined = {...objA, ...objB}; 18 | const keysCombined = Object.keys(objCombined); 19 | 20 | if (keysA.length + keysB.length === keysCombined.length) { 21 | return true; 22 | } 23 | 24 | return false; 25 | } -------------------------------------------------------------------------------- /src/utils/isPlainObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * copied from https://github.com/gaearon/react-redux/blob/master/src/utils/isPlainObject.js authored by @gaearon 3 | */ 4 | 5 | const fnToString = (fn) => Function.prototype.toString.call(fn); 6 | 7 | /** 8 | * @param {any} obj The object to inspect. 9 | * @returns {boolean} True if the argument appears to be a plain object. 10 | */ 11 | export default function isPlainObject(obj) { 12 | if (!obj || typeof obj !== 'object') { 13 | return false; 14 | } 15 | 16 | const proto = typeof obj.constructor === 'function' ? 17 | Object.getPrototypeOf(obj) : 18 | Object.prototype; 19 | 20 | if (proto === null) { 21 | return true; 22 | } 23 | 24 | const constructor = proto.constructor; 25 | 26 | return typeof constructor === 'function' 27 | && constructor instanceof constructor 28 | && fnToString(constructor) === fnToString(Object); 29 | } -------------------------------------------------------------------------------- /src/utils/shallowEqual.js: -------------------------------------------------------------------------------- 1 | /** 2 | * copied from https://github.com/gaearon/react-redux/blob/master/src/utils/isPlainObject.js authored by @gaearon 3 | */ 4 | 5 | export default function shallowEqual(objA, objB) { 6 | if (objA === objB) { 7 | return true; 8 | } 9 | 10 | const keysA = Object.keys(objA); 11 | const keysB = Object.keys(objB); 12 | 13 | if (keysA.length !== keysB.length) { 14 | return false; 15 | } 16 | 17 | // Test for A's keys different from B. 18 | const hasOwn = Object.prototype.hasOwnProperty; 19 | for (let i = 0; i < keysA.length; i++) { 20 | if (!hasOwn.call(objB, keysA[i]) || 21 | objA[keysA[i]] !== objB[keysA[i]]) { 22 | return false; 23 | } 24 | } 25 | 26 | return true; 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/sharedKeys.js: -------------------------------------------------------------------------------- 1 | export default function sharedKeys(objA, objB) { 2 | 3 | if (!objA || !objB) { 4 | return []; 5 | } 6 | 7 | const keysA = Object.keys(objA); 8 | const keysB = Object.keys(objB); 9 | 10 | if (keysA.length === 0 || keysB.length === 0) { 11 | return []; 12 | } 13 | 14 | if (objA === objB) { 15 | return keysA; 16 | } 17 | 18 | let sharedKeys = []; 19 | 20 | const hasOwn = Object.prototype.hasOwnProperty; 21 | for (let keyA of keysA) { 22 | if (hasOwn.call(objB, keyA)) { 23 | sharedKeys.push(keyA); 24 | } 25 | } 26 | 27 | return sharedKeys; 28 | } 29 | -------------------------------------------------------------------------------- /test/components/Provider.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import jsdomReact from './jsdomReact'; 3 | import React, { PropTypes, Component } from 'react/addons'; 4 | import { Provider } from '../../src/index'; 5 | 6 | const { TestUtils } = React.addons; 7 | 8 | describe('React', () => { 9 | describe('Provider', () => { 10 | jsdomReact(); 11 | 12 | class Child extends Component { 13 | static contextTypes = { 14 | provided: PropTypes.object.isRequired 15 | } 16 | 17 | render() { 18 | return
{this.props.children}
; 19 | } 20 | } 21 | 22 | class DeepChild extends Component { 23 | static contextTypes = { 24 | provided: PropTypes.object.isRequired 25 | } 26 | 27 | render() { 28 | return
{this.props.children}
; 29 | } 30 | } 31 | 32 | const obj = {a: 1}; 33 | const fn = () => obj; 34 | 35 | it('should add the `provide` prop to the child context, if plain Object', () => { 36 | 37 | const targetProvided = { 38 | string: "a string", 39 | number: 1, 40 | func: fn, 41 | object: obj, 42 | } 43 | 44 | const tree = TestUtils.renderIntoDocument( 45 | 46 | 47 | 48 | ); 49 | 50 | const child = TestUtils.findRenderedComponentWithType(tree, Child); 51 | expect(child.context.provided).toEqual(targetProvided); 52 | expect(child.context.provided.func()).toBe(obj); 53 | expect(child.context.provided.object).toBe(obj); 54 | }); 55 | 56 | it('should add the `provide` prop return value to the child context, if function', () => { 57 | 58 | const tree = TestUtils.renderIntoDocument( 59 | ({any: 'thing'})}> 60 | 61 | 62 | ); 63 | 64 | const child = TestUtils.findRenderedComponentWithType(tree, Child); 65 | expect(child.context.provided.any).toBe('thing'); 66 | }); 67 | 68 | it('should forward parent provided values if nested Provider', () => { 69 | 70 | const forwardingTree = TestUtils.renderIntoDocument( 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ) 79 | 80 | const forwardingTreeWithFn = TestUtils.renderIntoDocument( 81 | 82 | 83 | ({...provided, two: 2})}> 84 | 85 | 86 | 87 | 88 | ) 89 | 90 | const forwardingTreeWithFns = TestUtils.renderIntoDocument( 91 | ({one: 1})}> 92 | 93 | ({...provided, two: 2})}> 94 | 95 | 96 | 97 | 98 | ) 99 | 100 | const deepChild = TestUtils.findRenderedComponentWithType(forwardingTree, DeepChild); 101 | expect(deepChild.context.provided).toEqual({one: 1, two: 2}); 102 | 103 | const deepChildWithFn = TestUtils.findRenderedComponentWithType(forwardingTreeWithFn, DeepChild); 104 | expect(deepChildWithFn.context.provided).toEqual({one: 1, two: 2}); 105 | 106 | const deepChildWithFns = TestUtils.findRenderedComponentWithType(forwardingTreeWithFns, DeepChild); 107 | expect(deepChildWithFns.context.provided).toEqual({one: 1, two: 2}); 108 | 109 | }); 110 | 111 | it('should mask parent provided values if nested Provider with overloaded keys', () => { 112 | 113 | const forwardingTree = TestUtils.renderIntoDocument( 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | ) 122 | 123 | const deepChild = TestUtils.findRenderedComponentWithType(forwardingTree, DeepChild); 124 | expect(deepChild.context.provided.thing).toBe(2); 125 | expect(deepChild.context.provided.other).toBe(3); 126 | expect(deepChild.context.provided.andAnother).toBe(4); 127 | 128 | }); 129 | 130 | it('should require `provide` prop of type object or function', () => { 131 | 132 | expect(() => TestUtils.renderIntoDocument( 133 | 134 | 135 | 136 | )).toThrow(/provide/); 137 | 138 | expect(() => TestUtils.renderIntoDocument( 139 | 140 | 141 | 142 | )).toThrow(/provide/); 143 | 144 | expect(() => TestUtils.renderIntoDocument( 145 | 146 | 147 | 148 | )).toThrow(/provide/); 149 | 150 | expect(() => TestUtils.renderIntoDocument( 151 | 152 | 153 | 154 | )).toThrow(/provide/); 155 | 156 | expect(() => TestUtils.renderIntoDocument( 157 | 158 | 159 | 160 | )).toNotThrow(); 161 | 162 | expect(() => TestUtils.renderIntoDocument( 163 | ({})}> 164 | 165 | 166 | )).toNotThrow(); 167 | 168 | expect(() => TestUtils.renderIntoDocument( 169 | 1}> 170 | 171 | 172 | )).toThrow(/provide/); 173 | 174 | expect(() => TestUtils.renderIntoDocument( 175 | "test"}> 176 | 177 | 178 | )).toThrow(/provide/); 179 | 180 | expect(() => TestUtils.renderIntoDocument( 181 | () => ({})}> 182 | 183 | 184 | )).toThrow(/provide/); 185 | 186 | }); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /test/components/inject.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import jsdomReact from './jsdomReact'; 3 | import React, { PropTypes, Component, Children} from 'react/addons'; 4 | import { inject } from '../../src/index'; 5 | 6 | const { TestUtils } = React.addons; 7 | 8 | describe('React', () => { 9 | describe('inject', () => { 10 | jsdomReact(); 11 | 12 | class Foil extends Component { 13 | render() { 14 | return this.props.children; 15 | } 16 | } 17 | 18 | class DeepFoil extends Component { 19 | render() { 20 | return this.props.children; 21 | } 22 | } 23 | 24 | class Child extends Component { 25 | static contextTypes = { 26 | provided: PropTypes.object.isRequired 27 | } 28 | 29 | render() { 30 | return
{this.props.children}
; 31 | } 32 | } 33 | 34 | class DeepChild extends Component { 35 | static contextTypes = { 36 | provided: PropTypes.object.isRequired 37 | } 38 | 39 | render() { 40 | return
{this.props.children}
; 41 | } 42 | } 43 | 44 | const obj = {a: 1}; 45 | const fn = () => obj; 46 | 47 | class SimpleProvider extends Component { 48 | static contextTypes = { 49 | provided: PropTypes.object 50 | }; 51 | 52 | static childContextTypes = { 53 | provided: PropTypes.object.isRequired 54 | }; 55 | 56 | getChildContext() { 57 | return { provided: this.state.provided }; 58 | } 59 | 60 | constructor(props, context) { 61 | super(props, context); 62 | const { children, ...rest} = props; 63 | this.state = { provided: {...context.provided, ...rest}}; 64 | } 65 | 66 | componentWillReceiveProps(nextProps, nextContext) { 67 | const { children, ...rest} = nextProps; 68 | this.setState({ provided: {...nextContext.provided, ...rest}}); 69 | } 70 | 71 | render() { 72 | return Children.only(this.props.children); 73 | } 74 | 75 | } 76 | 77 | it('should add provided props to inject wrapped Component', () => { 78 | 79 | const targetProvided = { 80 | string: "a string", 81 | number: 1, 82 | func: fn, 83 | object: obj, 84 | } 85 | 86 | const InjectedChild = inject()(Child); 87 | 88 | const tree = TestUtils.renderIntoDocument( 89 | 90 | 91 | 92 | ); 93 | 94 | const child = TestUtils.findRenderedComponentWithType(tree, Foil); 95 | expect(child.props.string).toBe("a string"); 96 | expect(child.props.number).toBe(1); 97 | expect(child.props.func).toBe(fn); 98 | expect(child.props.object).toBe(obj); 99 | }); 100 | 101 | it('should inject return value of `mapProvidedToProps` into wrapped Component', () => { 102 | 103 | const targetProvided = { 104 | string: "a string", 105 | number: 1, 106 | func: fn, 107 | object: obj, 108 | } 109 | 110 | function mapProvidedToProps (provided) { 111 | return { 112 | newNumber: provided.number + 3, 113 | longerString: provided.string + ' plus some', 114 | wrappedFn: () => provided.func, 115 | } 116 | } 117 | 118 | const InjectedChild = inject(mapProvidedToProps)(Child); 119 | 120 | const tree = TestUtils.renderIntoDocument( 121 | 122 | 123 | 124 | ); 125 | 126 | const child = TestUtils.findRenderedComponentWithType(tree, Foil); 127 | expect(child.props.string).toBe(undefined); 128 | expect(child.props.number).toBe(undefined); 129 | expect(child.props.func).toBe(undefined); 130 | expect(child.props.object).toBe(undefined); 131 | expect(child.props.newNumber).toBe(4); 132 | expect(child.props.longerString).toBe('a string plus some'); 133 | expect(child.props.wrappedFn()()).toBe(obj); 134 | 135 | }); 136 | 137 | it('should reflect changes in provided values', () => { 138 | const spy = expect.createSpy(() => ({})); 139 | function render({ saying }) { 140 | spy(); 141 | return
; 142 | } 143 | 144 | const InjectedChild = inject()(Child); 145 | 146 | @inject() 147 | class InjectedContainer extends Component { 148 | render() { 149 | return render(this.props); 150 | } 151 | } 152 | 153 | class ProviderContainer extends Component { 154 | constructor(props, context) { 155 | super(props, context); 156 | this.state = {saying: "hello"}; 157 | } 158 | render() { 159 | return ( 160 | 161 | 162 | 163 | ); 164 | } 165 | } 166 | 167 | const tree = TestUtils.renderIntoDocument(); 168 | 169 | const child = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); 170 | expect(spy.calls.length).toBe(1); 171 | expect(child.props.saying).toBe('hello'); 172 | 173 | tree.setState({saying: 'goodbye'}); 174 | 175 | expect(spy.calls.length).toBe(2); 176 | expect(child.props.saying).toBe('goodbye'); 177 | 178 | }); 179 | 180 | it('should rerender component if props change even if provided does not.', () => { 181 | const spy = expect.createSpy(() => ({})); 182 | function render({ saying, changingProp }) { 183 | spy(); 184 | return
; 185 | } 186 | 187 | const InjectedChild = inject()(Child); 188 | 189 | @inject() 190 | class InjectedContainer extends Component { 191 | render() { 192 | return render(this.props); 193 | } 194 | } 195 | 196 | class ProviderContainer extends Component { 197 | constructor(props, context) { 198 | super(props, context); 199 | this.state = { 200 | saying: "hello", 201 | otherProp: "A", 202 | }; 203 | } 204 | render() { 205 | return ( 206 | 207 | 208 | 209 | ); 210 | } 211 | } 212 | 213 | const tree = TestUtils.renderIntoDocument(); 214 | 215 | const child = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); 216 | expect(spy.calls.length).toBe(1); 217 | expect(child.props.saying).toBe('hello'); 218 | expect(child.props.changingProp).toBe('A'); 219 | 220 | tree.setState({otherProp: "B"}); 221 | 222 | expect(spy.calls.length).toBe(2); 223 | expect(child.props.saying).toBe('hello'); 224 | expect(child.props.changingProp).toBe('B'); 225 | 226 | }); 227 | 228 | it('should not rerender if injected props remain the same.', () => { 229 | const spy = expect.createSpy(() => ({})); 230 | function render({ otherProp }) { 231 | spy(); 232 | return
; 233 | } 234 | 235 | const InjectedChild = inject()(Child); 236 | 237 | @inject(provided => ({otherProp: provided.otherProp})) 238 | class InjectedContainer extends Component { 239 | render() { 240 | return render(this.props); 241 | } 242 | } 243 | 244 | class ProviderContainer extends Component { 245 | constructor(props, context) { 246 | super(props, context); 247 | this.state = { 248 | saying: "hello", 249 | otherProp: "A", 250 | }; 251 | } 252 | render() { 253 | return ( 254 | 255 | 256 | 257 | ); 258 | } 259 | } 260 | 261 | const tree = TestUtils.renderIntoDocument(); 262 | 263 | const child = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); 264 | expect(spy.calls.length).toBe(1); 265 | expect(child.props.otherProp).toBe('A'); 266 | 267 | tree.setState({saying: "goodbye"}); 268 | 269 | expect(spy.calls.length).toBe(1); 270 | expect(child.props.otherProp).toBe('A'); 271 | 272 | }); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /test/components/jsdomReact.js: -------------------------------------------------------------------------------- 1 | import ExecutionEnvironment from 'exenv'; 2 | import jsdom from 'mocha-jsdom'; 3 | 4 | export default function jsdomReact() { 5 | jsdom(); 6 | ExecutionEnvironment.canUseDOM = true; 7 | } -------------------------------------------------------------------------------- /test/utils/hasEmptyIntersection.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import hasEmptyIntersection from '../../src/utils/hasEmptyIntersection'; 3 | 4 | describe('Utils', () => { 5 | describe('hasEmptyIntersection', () => { 6 | it('should return true if one or the other object is empty, null, or undefined', () => { 7 | expect( 8 | hasEmptyIntersection( 9 | {}, 10 | { a: 1, b: undefined, c: {}, d: 'd' } 11 | ) 12 | ).toBe(true); 13 | 14 | expect( 15 | hasEmptyIntersection( 16 | { a: 1, b: undefined, c: {}, d: 'd' }, 17 | {} 18 | ) 19 | ).toBe(true); 20 | 21 | expect( 22 | hasEmptyIntersection( 23 | null, 24 | { a: 1, b: undefined, c: {}, d: 'd' } 25 | ) 26 | ).toBe(true); 27 | 28 | expect( 29 | hasEmptyIntersection( 30 | { a: 1, b: undefined, c: {}, d: 'd' }, 31 | null 32 | ) 33 | ).toBe(true); 34 | 35 | expect( 36 | hasEmptyIntersection( 37 | undefined, 38 | { a: 1, b: undefined, c: {}, d: 'd' } 39 | ) 40 | ).toBe(true); 41 | 42 | expect( 43 | hasEmptyIntersection( 44 | { a: 1, b: undefined, c: {}, d: 'd' }, 45 | undefined 46 | ) 47 | ).toBe(true); 48 | }); 49 | 50 | it('should return false if the arguments are the same non-empty object', () => { 51 | const emptyObj = {}; 52 | const fullObj = { a: 1 }; 53 | expect( 54 | hasEmptyIntersection( 55 | emptyObj, 56 | emptyObj 57 | ) 58 | ).toBe(true); 59 | 60 | expect( 61 | hasEmptyIntersection( 62 | fullObj, 63 | fullObj 64 | ) 65 | ).toBe(false); 66 | }); 67 | 68 | it('should return false if objects are different but have the same keys', () => { 69 | expect( 70 | hasEmptyIntersection( 71 | { a: 1, b: 2, c: 3 }, 72 | { a: 'a', b: 'b', c: 'c' } 73 | ) 74 | ).toBe(false); 75 | }); 76 | 77 | it('should return false if objects share a single key', () => { 78 | expect( 79 | hasEmptyIntersection( 80 | { x: 1, y: 2, z: 3 }, 81 | { a: 1, b: 2, c: 3 } 82 | ) 83 | ).toBe(true); 84 | 85 | expect( 86 | hasEmptyIntersection( 87 | { x: 1, y: 4, z: 3, b: 'blah' }, 88 | { a: 1, b: 2, c: 3 } 89 | ) 90 | ).toBe(false); 91 | }); 92 | }); 93 | }); -------------------------------------------------------------------------------- /test/utils/isPlainObject.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * copied from https://github.com/gaearon/react-redux/blob/master/src/utils/isPlainObject.js authored by @gaearon 3 | */ 4 | 5 | import expect from 'expect'; 6 | import isPlainObject from '../../src/utils/isPlainObject'; 7 | 8 | describe('Utils', () => { 9 | describe('isPlainObject', () => { 10 | it('should return true only if plain object', () => { 11 | function Test() { 12 | this.prop = 1; 13 | } 14 | 15 | expect(isPlainObject(new Test())).toBe(false); 16 | expect(isPlainObject(new Date())).toBe(false); 17 | expect(isPlainObject([1, 2, 3])).toBe(false); 18 | expect(isPlainObject(null)).toBe(false); 19 | expect(isPlainObject()).toBe(false); 20 | expect(isPlainObject({ 'x': 1, 'y': 2 })).toBe(true); 21 | }); 22 | }); 23 | }); -------------------------------------------------------------------------------- /test/utils/shallowEqual.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * copied from https://github.com/gaearon/react-redux/blob/master/src/utils/isPlainObject.js authored by @gaearon 3 | */ 4 | 5 | import expect from 'expect'; 6 | import shallowEqual from '../../src/utils/shallowEqual'; 7 | 8 | describe('Utils', () => { 9 | describe('shallowEqual', () => { 10 | it('should return true if arguments fields are equal', () => { 11 | expect( 12 | shallowEqual( 13 | { a: 1, b: 2, c: undefined }, 14 | { a: 1, b: 2, c: undefined } 15 | ) 16 | ).toBe(true); 17 | 18 | expect( 19 | shallowEqual( 20 | { a: 1, b: 2, c: 3 }, 21 | { a: 1, b: 2, c: 3 } 22 | ) 23 | ).toBe(true); 24 | 25 | const o = {}; 26 | expect( 27 | shallowEqual( 28 | { a: 1, b: 2, c: o }, 29 | { a: 1, b: 2, c: o } 30 | ) 31 | ).toBe(true); 32 | }); 33 | 34 | it('should return false if first argument has too many keys', () => { 35 | expect( 36 | shallowEqual( 37 | { a: 1, b: 2, c: 3 }, 38 | { a: 1, b: 2 } 39 | ) 40 | ).toBe(false); 41 | }); 42 | 43 | it('should return false if second argument has too many keys', () => { 44 | expect( 45 | shallowEqual( 46 | { a: 1, b: 2 }, 47 | { a: 1, b: 2, c: 3 } 48 | ) 49 | ).toBe(false); 50 | }); 51 | 52 | it('should return false if arguments have different keys', () => { 53 | expect( 54 | shallowEqual( 55 | { a: 1, b: 2, c: undefined }, 56 | { a: 1, bb: 2, c: undefined } 57 | ) 58 | ).toBe(false); 59 | }); 60 | }); 61 | }); -------------------------------------------------------------------------------- /test/utils/sharedKeys.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import sharedKeys from '../../src/utils/sharedKeys'; 3 | 4 | describe('Utils', () => { 5 | describe('sharedKeys', () => { 6 | it('should return an empty Array if one or the other object is empty, null, or undefined', () => { 7 | expect( 8 | sharedKeys( 9 | {}, 10 | { a: 1, b: undefined, c: {}, d: 'd' } 11 | ) 12 | ).toEqual([]); 13 | 14 | expect( 15 | sharedKeys( 16 | { a: 1, b: undefined, c: {}, d: 'd' }, 17 | {} 18 | ) 19 | ).toEqual([]); 20 | 21 | expect( 22 | sharedKeys( 23 | null, 24 | { a: 1, b: undefined, c: {}, d: 'd' } 25 | ) 26 | ).toEqual([]); 27 | 28 | expect( 29 | sharedKeys( 30 | { a: 1, b: undefined, c: {}, d: 'd' }, 31 | null 32 | ) 33 | ).toEqual([]); 34 | 35 | expect( 36 | sharedKeys( 37 | undefined, 38 | { a: 1, b: undefined, c: {}, d: 'd' } 39 | ) 40 | ).toEqual([]); 41 | 42 | expect( 43 | sharedKeys( 44 | { a: 1, b: undefined, c: {}, d: 'd' }, 45 | undefined 46 | ) 47 | ).toEqual([]); 48 | }); 49 | 50 | it('should return equivalent of Object.keys if the arguments are the same non-empty object', () => { 51 | const emptyObj = {}; 52 | const fullObj = { a: 1 }; 53 | expect( 54 | sharedKeys( 55 | emptyObj, 56 | emptyObj 57 | ) 58 | ).toEqual(Object.keys(emptyObj)); 59 | 60 | expect( 61 | sharedKeys( 62 | fullObj, 63 | fullObj 64 | ) 65 | ).toEqual(Object.keys(fullObj)); 66 | }); 67 | 68 | it('should return equivalent of Object.keys of either argument if objects are different but have the same keys', () => { 69 | const objA = { a: 1, b: 2, c: 3 }; 70 | const objB = { a: 'a', b: 'b', c: 'c' }; 71 | 72 | expect( 73 | sharedKeys( 74 | objA, 75 | objB 76 | ) 77 | ).toEqual(Object.keys(objA)); 78 | 79 | expect( 80 | sharedKeys( 81 | objA, 82 | objB 83 | ) 84 | ).toEqual(Object.keys(objB)); 85 | }); 86 | 87 | it('should return array containing all keys found in both objects', () => { 88 | expect( 89 | sharedKeys( 90 | { x: 1, y: 2, z: 3 }, 91 | { a: 1, b: 2, c: 3 } 92 | ) 93 | ).toEqual([]); 94 | 95 | expect( 96 | sharedKeys( 97 | { x: 1, y: 4, z: 3, b: 'blah' }, 98 | { a: 1, b: 2, c: 3 } 99 | ) 100 | ).toEqual(['b']); 101 | 102 | expect( 103 | sharedKeys( 104 | { x: 1, y: 4, z: 3, b: 'blah' }, 105 | { a: 1, b: 2, c: 3, y: 'z', x: 'b'} 106 | ).sort() 107 | ).toEqual(['b', 'x', 'y'].sort()); 108 | }); 109 | }); 110 | }); -------------------------------------------------------------------------------- /webpack.config.base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | 5 | var reactExternal = { 6 | root: 'React', 7 | commonjs2: 'react', 8 | commonjs: 'react', 9 | amd: 'react' 10 | }; 11 | 12 | module.exports = { 13 | externals: { 14 | 'react': reactExternal, 15 | 'react-native': reactExternal, 16 | }, 17 | module: { 18 | loaders: [ 19 | { test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ } 20 | ] 21 | }, 22 | output: { 23 | library: 'ReactTunnel', 24 | libraryTarget: 'umd' 25 | }, 26 | resolve: { 27 | extensions: ['', '.js'] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /webpack.config.development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | var baseConfig = require('./webpack.config.base'); 5 | 6 | var config = Object.create(baseConfig); 7 | config.plugins = [ 8 | new webpack.optimize.OccurenceOrderPlugin(), 9 | new webpack.DefinePlugin({ 10 | 'process.env.NODE_ENV': JSON.stringify('development') 11 | }) 12 | ]; 13 | 14 | module.exports = config; -------------------------------------------------------------------------------- /webpack.config.production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | var baseConfig = require('./webpack.config.base'); 5 | 6 | var config = Object.create(baseConfig); 7 | config.plugins = [ 8 | new webpack.optimize.OccurenceOrderPlugin(), 9 | new webpack.DefinePlugin({ 10 | 'process.env.NODE_ENV': JSON.stringify('production') 11 | }), 12 | new webpack.optimize.UglifyJsPlugin({ 13 | compressor: { 14 | screw_ie8: true, 15 | warnings: false 16 | } 17 | }) 18 | ]; 19 | 20 | module.exports = config; --------------------------------------------------------------------------------