├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── dist.js ├── package.json └── src └── component.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | max_line_length = 80 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | 16 | [COMMIT_EDITMSG] 17 | max_line_length = 0 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | node_modules 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2015, Phil Plückthun 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Compatibility Component 2 | 3 | > The missing link! 4 | 5 | **Wrapper class, which adds a compatibility layer to React 0.13's ES6 Component 6 | class, to make it easier to use ES6 goodness without changing much code.** 7 | 8 | Do you want to use ES6 classes via React 0.13's Component class? 9 | 10 | Do you want to be able to keep your mixins? 11 | 12 | Do you miss autobinding `this` to your methods? 13 | 14 | *This is perfect for you!* 15 | 16 | `react-compat-component` allows you to use the new ES6 class structure for your 17 | components without changing much code. And it allows you to continue to use mixins 18 | without any special modifications. You don't need to code differently because of ES6. 19 | 20 | ## Installation 21 | 22 | ``` 23 | npm install --save react-compat-component 24 | ``` 25 | 26 | ## Example 27 | 28 | A modified example from the [React Docs](https://facebook.github.io/react/docs/reusable-components.html): 29 | 30 | ```javascript 31 | import React from "react"; 32 | import { CompatComponent } from "react-compat-component"; 33 | 34 | export class HelloMessage extends CompatComponent { 35 | getMixins() { 36 | return [ 37 | SetIntervalMixin 38 | ]; 39 | } 40 | 41 | getPropTypes() { 42 | return { 43 | example: React.PropTypes.number 44 | }; 45 | } 46 | 47 | getDefaultProps() { 48 | return { 49 | example: 0 50 | } 51 | } 52 | 53 | constructor(props) { 54 | super(props); 55 | // ... 56 | } 57 | 58 | getInitialState: function() { 59 | return { 60 | seconds: 0 61 | }; 62 | } 63 | 64 | render() { 65 | // ... 66 | } 67 | } 68 | ``` 69 | 70 | **In the future**, something like this might be possible: 71 | 72 | ```javascript 73 | import React from "react"; 74 | import { CompatComponent } from "react-compat-component"; 75 | 76 | export class HelloMessage extends CompatComponent { 77 | const mixins = [ 78 | SetIntervalMixin 79 | ]; 80 | defaultProps = { 81 | example: 0 82 | }; 83 | propTypes = { 84 | example: React.PropTypes.number 85 | }; 86 | 87 | constructor(props) { 88 | super(props); 89 | // ... 90 | } 91 | 92 | getInitialState: function() { 93 | return { 94 | seconds: 0 95 | }; 96 | } 97 | 98 | render() { 99 | // ... 100 | } 101 | } 102 | ``` 103 | 104 | ## Differences from createClass 105 | 106 | Originally React ditched two major features for ES6 classes: 107 | 108 | - Autobinding 109 | - Mixins 110 | - getInitialState() 111 | 112 | Also it deletes the following methods: 113 | 114 | - getInitialState() 115 | - getDefaultProps() 116 | 117 | `react-compat-component` restores all three features. 118 | 119 | Its "meta-constructor" modifies the component during instantiation. 120 | (Note that this might affect performance) 121 | 122 | Also as a workaround it has to introduce the `getMixins` and the 123 | `getPropTypes` method as in classes there isn't variable instantiation 124 | available as of now. 125 | 126 | ## Difference to react-mixin 127 | 128 | `react-mixin` implements a function to modify *arbitrary objects* to "import" 129 | mixins. It is an unnecessary hack and just doesn't feel as elegant as mixins 130 | were. It was a quick fix, but `react-compat-component` is the real deal. 131 | 132 | ```javascript 133 | reactMixin(Foo.prototype, someMixin); 134 | ``` 135 | 136 | ## Contribute 137 | 138 | Feel free to contribute! 139 | 140 | Open fork, open pull requests and issues and help out. Any feedback 141 | is greatly appreciated. 142 | 143 | ### Building 144 | 145 | You need to let Babel do its job to get a compiled version of this: 146 | 147 | ``` 148 | npm build 149 | ``` 150 | -------------------------------------------------------------------------------- /dist.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 12 | 13 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } } 14 | 15 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 16 | 17 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } 18 | 19 | var _react = require("react"); 20 | 21 | var _react2 = _interopRequireDefault(_react); 22 | 23 | var _reactLibInvariant = require("react/lib/invariant"); 24 | 25 | var _reactLibInvariant2 = _interopRequireDefault(_reactLibInvariant); 26 | 27 | var _objectAssign = require("object-assign"); 28 | 29 | var _objectAssign2 = _interopRequireDefault(_objectAssign); 30 | 31 | // Helper to mirror keys as values 32 | function keyMirror(obj) { 33 | for (var key in obj) { 34 | if (obj.hasOwnProperty(key)) { 35 | obj[key] = key; 36 | } 37 | } 38 | return obj; 39 | } 40 | 41 | // React lifecycle methods that can be defined multiple times 42 | var multiFunctions = keyMirror({ 43 | componentWillReceiveProps: null, 44 | componentWillMount: null, 45 | componentDidMount: null, 46 | componentWillUpdate: null, 47 | componentDidUpdate: null, 48 | componentWillUnmount: null 49 | }); 50 | 51 | var CompatComponent = (function (_React$Component) { 52 | function CompatComponent() { 53 | var _this = this; 54 | 55 | for (var _len = arguments.length, arrs = Array(_len), _key = 0; _key < _len; _key++) { 56 | arrs[_key] = arguments[_key]; 57 | } 58 | 59 | _classCallCheck(this, CompatComponent); 60 | 61 | _get(Object.getPrototypeOf(CompatComponent.prototype), "constructor", this).apply(this, arrs); 62 | 63 | // Make sure the mixins array is assigned 64 | if (!this.mixins) { 65 | this.mixins = []; 66 | } 67 | 68 | // Importing mixins from the getMixins method 69 | if (typeof this.getMixins === "function") { 70 | var _getMixins = this.getMixins(); 71 | if (this.mixins.length > 0) { 72 | this.mixins = this.mixins.concat(_getMixins); 73 | } else { 74 | this.mixins = _getMixins; 75 | } 76 | } 77 | 78 | if (!this.propTypes) { 79 | this.propTypes = {}; 80 | } 81 | 82 | // Importing propTypes from the getPropTypes method 83 | if (typeof this.getPropTypes === "function") { 84 | var _getPropTypes = this.getPropTypes(); 85 | for (var key in _getPropTypes) { 86 | this.propTypes[key] = _getPropTypes[key]; 87 | } 88 | } 89 | 90 | if (!this.defaultProps) { 91 | this.defaultProps = {}; 92 | } 93 | 94 | if (typeof this.getDefaultProps === "function") { 95 | var _getDefaultProps = this.getDefaultProps(); 96 | for (var key in _getDefaultProps) { 97 | this.defaultProps[key] = _getDefaultProps[key]; 98 | } 99 | } 100 | 101 | this._mixinImports = {}; 102 | // Overwrite the method to call all methods in the "holding" array 103 | for (var _key3 in multiFunctions) { 104 | if (multiFunctions.hasOwnProperty(_key3)) { 105 | (function () { 106 | var property = multiFunctions[_key3]; 107 | _this._mixinImports[property] = []; 108 | _this[property] = function () { 109 | for (var _len2 = arguments.length, arrs = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 110 | arrs[_key2] = arguments[_key2]; 111 | } 112 | 113 | if (_this._mixinImports[property] && _this._mixinImports[property].length) _this._mixinImports[property].forEach(function (func) { 114 | func.apply(undefined, arrs); 115 | }); 116 | }; 117 | })(); 118 | } 119 | } 120 | 121 | this._mixinImports["componentWillUpdate"].unshift(function () { 122 | _this._processProps(); 123 | }); 124 | 125 | this._processProps(); 126 | this._prePropsMixinFunctions(); 127 | 128 | // Reintroduce getInitialState() method 129 | var state = {}; 130 | if (typeof this.getInitialState === "function") { 131 | state = this.getInitialState(); 132 | } 133 | 134 | // Merge state with the mixins' getInitialState() output 135 | var arr = this.mixins.map(function (mixin) { 136 | if (typeof mixin.getInitialState === "function") { 137 | return mixin.getInitialState(); 138 | } else { 139 | return {}; 140 | } 141 | }); 142 | this.state = _objectAssign2["default"].apply(undefined, _toConsumableArray(arr).concat([state])); 143 | 144 | this._bindFunctions(); 145 | } 146 | 147 | _inherits(CompatComponent, _React$Component); 148 | 149 | _createClass(CompatComponent, [{ 150 | key: "_bindFunctions", 151 | 152 | // Reintroduce autobinding 153 | value: function _bindFunctions() { 154 | var _this2 = this; 155 | 156 | var childFunc = Object.getOwnPropertyNames(this.constructor.prototype); 157 | childFunc.forEach(function (func) { 158 | if (typeof _this2[func] === "function") { 159 | _this2[func] = _this2[func].bind(_this2); 160 | } 161 | }); 162 | } 163 | }, { 164 | key: "_processProps", 165 | 166 | // Process props to comply to propTypes and defaultProps 167 | value: function _processProps() { 168 | for (var key in this.defaultProps) { 169 | if (this.defaultProps.hasOwnProperty(key) && !this.props.hasOwnProperty(key)) { 170 | this.props[key] = this.defaultProps[key]; 171 | } 172 | } 173 | 174 | for (var key in this.propTypes) { 175 | if (this.propTypes.hasOwnProperty(key)) { 176 | var res = this.propTypes[key](this.props, key, this.constructor.name || ""); 177 | if (res) { 178 | console.warn("Warning: " + res.message); 179 | } 180 | } 181 | } 182 | } 183 | }, { 184 | key: "_prePropsMixinFunctions", 185 | 186 | // Import the mixins' properties 187 | value: function _prePropsMixinFunctions() { 188 | var _this3 = this; 189 | 190 | this.mixins.reverse().forEach(function (mixin) { 191 | Object.keys(mixin).forEach(function (property) { 192 | if (property === "propTypes") { 193 | _this3.propTypes = (0, _objectAssign2["default"])(mixin.propTypes || {}, _this3.propTypes || {}); 194 | } else if (property === "getDefaultProps" && typeof mixin.getDefaultProps === "function") { 195 | _this3.defaultProps = (0, _objectAssign2["default"])(mixin.getDefaultProps.call(_this3), _this3.defaultProps || {}); 196 | } else if (property === "statics") { 197 | _this3.statics = (0, _objectAssign2["default"])(mixin.statics, _this3.statics || {}); 198 | } else if (property === "getInitialState") {} else if (typeof mixin[property] === "function") { 199 | if (multiFunctions[property]) { 200 | if (!_this3._mixinImports.hasOwnProperty(property)) { 201 | _this3._mixinImports[property] = []; 202 | 203 | // Save the existing method in the parent class to the 204 | // "holding" array 205 | if (typeof _this3[property] === "function") { 206 | _this3._mixinImports[property].push(_this3[property].bind(_this3)); 207 | } 208 | } 209 | 210 | // Push the mixin's method into the "holding" array 211 | _this3._mixinImports[property].push(mixin[property].bind(_this3)); 212 | } else { 213 | // Check whether methods here can be imported, as they're supposed 214 | // to only be defined once 215 | (0, _reactLibInvariant2["default"])(!_this3.hasOwnProperty(property), "You are attempting to redefine '" + property + "' on your component. " + "This conflict may be due to a mixin."); 216 | _this3[property] = mixin[property]; 217 | } 218 | } 219 | }); 220 | }); 221 | } 222 | }]); 223 | 224 | return CompatComponent; 225 | })(_react2["default"].Component); 226 | 227 | exports.CompatComponent = CompatComponent; 228 | 229 | // This gets handled after the super call in the constructor 230 | // method to make props usable in f.e. getInitialState() 231 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-compat-component", 3 | "version": "1.3.1", 4 | "description": "Compatibility class for React's ES6 component class", 5 | "main": "dist.js", 6 | "engines": { 7 | "node": ">= 0.10.x" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/philplckthun/react-compat-component" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/philplckthun/react-compat-component/issues" 15 | }, 16 | "peerDependencies": { 17 | "react": "^0.13.0-beta.1" 18 | }, 19 | "scripts": { 20 | "prepublish": "./node_modules/.bin/babel ./src -o ./dist.js", 21 | "build": "./node_modules/.bin/babel ./src -o ./dist.js", 22 | "test": "echo \"Error: no test specified\" && exit 1" 23 | }, 24 | "author": "Phil Plückthun ", 25 | "license": "MIT", 26 | "keywords": [ 27 | "react", 28 | "compat", 29 | "component", 30 | "compatibility", 31 | "es6", 32 | "class", 33 | "mixin", 34 | "mixins" 35 | ], 36 | "devDependencies": { 37 | "babel": "^5.0.12" 38 | }, 39 | "dependencies": { 40 | "object-assign": "^2.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/component.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import invariant from "react/lib/invariant"; 3 | import assign from "object-assign"; 4 | 5 | // Helper to mirror keys as values 6 | function keyMirror(obj) { 7 | for(var key in obj) { 8 | if (obj.hasOwnProperty(key)) { 9 | obj[key] = key; 10 | } 11 | } 12 | return obj; 13 | } 14 | 15 | // React lifecycle methods that can be defined multiple times 16 | const multiFunctions = keyMirror({ 17 | componentWillReceiveProps: null, 18 | componentWillMount: null, 19 | componentDidMount: null, 20 | componentWillUpdate: null, 21 | componentDidUpdate: null, 22 | componentWillUnmount: null 23 | }); 24 | 25 | export class CompatComponent extends React.Component { 26 | constructor(...arrs) { 27 | super(...arrs); 28 | 29 | // Make sure the mixins array is assigned 30 | if (!this.mixins) { 31 | this.mixins = []; 32 | } 33 | 34 | // Importing mixins from the getMixins method 35 | if (typeof this.getMixins === "function") { 36 | const _getMixins = this.getMixins(); 37 | if (this.mixins.length > 0) { 38 | this.mixins = this.mixins.concat(_getMixins); 39 | } else { 40 | this.mixins = _getMixins; 41 | } 42 | } 43 | 44 | if (!this.propTypes) { 45 | this.propTypes = {}; 46 | } 47 | 48 | // Importing propTypes from the getPropTypes method 49 | if (typeof this.getPropTypes === "function") { 50 | const _getPropTypes = this.getPropTypes(); 51 | for (var key in _getPropTypes) { 52 | this.propTypes[key] = _getPropTypes[key]; 53 | } 54 | } 55 | 56 | if (!this.defaultProps) { 57 | this.defaultProps = {}; 58 | } 59 | 60 | if (typeof this.getDefaultProps === "function") { 61 | const _getDefaultProps = this.getDefaultProps(); 62 | for (var key in _getDefaultProps) { 63 | this.defaultProps[key] = _getDefaultProps[key]; 64 | } 65 | } 66 | 67 | this._mixinImports = {}; 68 | // Overwrite the method to call all methods in the "holding" array 69 | for (let key in multiFunctions) { 70 | if (multiFunctions.hasOwnProperty(key)) { 71 | const property = multiFunctions[key]; 72 | this._mixinImports[property] = []; 73 | 74 | // Save the existing method in the parent class to the 75 | // "holding" array 76 | if (typeof this[property] === "function") { 77 | this._mixinImports[property].push( 78 | this[property].bind(this) 79 | ); 80 | } 81 | 82 | this[property] = (...arrs) => { 83 | if (this._mixinImports[property] && this._mixinImports[property].length) 84 | this._mixinImports[property].forEach(func => { 85 | func(...arrs); 86 | }); 87 | }; 88 | } 89 | } 90 | 91 | this._mixinImports["componentWillUpdate"].unshift(() => { 92 | this._processProps(); 93 | }); 94 | 95 | this._processProps(); 96 | this._prePropsMixinFunctions(); 97 | 98 | // Reintroduce getInitialState() method 99 | let state = {}; 100 | if (typeof this.getInitialState === "function") { 101 | state = this.getInitialState(); 102 | } 103 | 104 | // Merge state with the mixins' getInitialState() output 105 | const arr = this.mixins.map(mixin => { 106 | if (typeof mixin.getInitialState === "function") { 107 | return mixin.getInitialState(); 108 | } else { 109 | return {}; 110 | } 111 | }); 112 | this.state = assign(...arr, state); 113 | 114 | this._bindFunctions(); 115 | } 116 | 117 | // Reintroduce autobinding 118 | _bindFunctions() { 119 | const childFunc = Object.getOwnPropertyNames(this.constructor.prototype); 120 | childFunc.forEach(func => { 121 | if (typeof this[func] === "function") { 122 | this[func] = this[func].bind(this); 123 | } 124 | }); 125 | } 126 | 127 | // Process props to comply to propTypes and defaultProps 128 | _processProps() { 129 | for (let key in this.defaultProps) { 130 | if ( 131 | this.defaultProps.hasOwnProperty(key) && 132 | !this.props.hasOwnProperty(key) 133 | ) { 134 | this.props[key] = this.defaultProps[key]; 135 | } 136 | } 137 | 138 | for (let key in this.propTypes) { 139 | if (this.propTypes.hasOwnProperty(key)) { 140 | const res = this.propTypes[key]( 141 | this.props, 142 | key, 143 | this.constructor.name || "" 144 | ); 145 | if (res) { 146 | console.warn("Warning: " + res.message); 147 | } 148 | } 149 | } 150 | } 151 | 152 | // Import the mixins' properties 153 | _prePropsMixinFunctions() { 154 | this.mixins.reverse().forEach(mixin => { 155 | Object.keys(mixin).forEach(property => { 156 | if ( 157 | property === "propTypes" 158 | ) { 159 | this.propTypes = assign( 160 | mixin.propTypes || {}, 161 | this.propTypes || {} 162 | ); 163 | } else if ( 164 | property === "getDefaultProps" && 165 | typeof mixin.getDefaultProps === "function" 166 | ) { 167 | this.defaultProps = assign( 168 | mixin.getDefaultProps.call(this), 169 | this.defaultProps || {} 170 | ); 171 | } else if ( 172 | property === "statics" 173 | ) { 174 | this.statics = assign( 175 | mixin.statics, 176 | this.statics || {} 177 | ); 178 | } else if ( 179 | property === "getInitialState" 180 | ) { 181 | // This gets handled after the super call in the constructor 182 | // method to make props usable in f.e. getInitialState() 183 | } else if ( 184 | typeof mixin[property] === "function" 185 | ) { 186 | if (multiFunctions[property]) { 187 | // Push the mixin's method into the "holding" array 188 | this._mixinImports[property].push(mixin[property].bind(this)); 189 | } else { 190 | // Check whether methods here can be imported, as they're supposed 191 | // to only be defined once 192 | invariant( 193 | !this.hasOwnProperty(property), 194 | "You are attempting to redefine '"+property+"' on your component. " + 195 | "This conflict may be due to a mixin." 196 | ); 197 | this[property] = mixin[property]; 198 | } 199 | } 200 | }); 201 | }); 202 | } 203 | } 204 | --------------------------------------------------------------------------------