├── .babelrc ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── README.md ├── dist └── form.js ├── package.json └── src └── form.jsx /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .babelrc 3 | README.md 4 | node_modules 5 | src -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.1 2 | deprecate this project 3 | 4 | ## 1.1.0 5 | support readOnly and disabled 6 | 7 | ## 1.0.1 8 | 9 | ## 1.0.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shaking React Form 2 | build and use extensible react forms easily. 3 | 4 | ## Deprecated 5 | This project is deprecated. Please refer to the replacement project [rf-form](https://github.com/ShakingMap/rf-form). 6 | 7 | ## Activation 8 | Writing forms is boring, writing forms with validations is extremely boring. We want to build forms with validations 9 | easily, meanwhile, we want to use different styles. By searching and learning some existing react form projects, I found 10 | them difficult to use or hard to extend... 11 | 12 | ## Design Goals 13 | - based on react 14 | - schema driven 15 | - least dependencies 16 | - simple and robust 17 | - easy to use 18 | - easy to extend with different layouts, styles, input types 19 | 20 | ## Installation 21 | - install React 22 | - install core form by `npm install shaking-react-form --save` 23 | - write your form field class or install some existing field packages listed below. 24 | 25 | ## Available field packages 26 | - [raw-shaking-react-form-field](https://github.com/ShakingMap/raw-shaking-react-form-field) 27 | - [react-bootstrap-shaking-react-form-field](https://github.com/ShakingMap/react-bootstrap-shaking-react-form-field) 28 | 29 | ## Demo 30 | [More demos](https://github.com/ShakingMap/shaking-react-form-demo) 31 | 32 | Brief demo: 33 | 34 | import ShackingForm from 'shaking-react-form'; 35 | import RawShakingReactFormField from 'raw-shaking-react-form-field'; 36 | 37 | const schemas = { 38 | username: { 39 | label: 'Username', 40 | validate(v){ 41 | if (!v) return 'username is required' 42 | }, 43 | options: { 44 | placeholder: 'input username' 45 | } 46 | }, 47 | 48 | password: { 49 | label: 'Password', 50 | type: 'password', 51 | validate(v){ 52 | if (!v || v.length < 6) return 'password cannot be less than 6 letters' 53 | } 54 | }, 55 | } 56 | 57 | class SomeReactComponent extends React.Component { 58 | // you can save form values as you like, here is just for convenience saving them directly into state 59 | constructor(props) { 60 | super(props); 61 | this.state = {}; 62 | Object.keys(schemas).forEach(key=>this.state[key] = null); 63 | } 64 | 65 | render() { 66 | return
67 | {this.setState(values)}} 71 | onSubmit={(values)=>console.log('submit', values)} 72 | onErrors={(errors)=>console.log('errors', errors)} 73 | fieldClass={RawShakingReactFormField} 74 | > 75 | 76 | 77 |
78 | } 79 | } 80 | 81 | ## Apis 82 | ### props 83 | - {Array | Object} **schemas** - schemas for form fields. a schema is like 84 | 85 | { 86 | type: '...', // defined by the field class you use 87 | label: '...', 88 | validate: function(value){...}, // validation function for value of this field, return a string to indicate an validation error 89 | options: ..., // any thing a specific field needs, defined by the field class you use 90 | fieldClass: ..., // you can specify a the field class for this filed individually 91 | readOnly: bool, 92 | disabled: bool 93 | } 94 | 95 | - {ReactComponent} **[fieldClass]** - the field class the form will use to render the fields 96 | - {Array | Object} **[values]** - values for form fields. if not defined, the form will be uncontrolled 97 | - {Func} **[onChange]** - function(values), callback for change events of fields, only changed values are passed in 98 | - {Func} **[onSubmit]** - function(values), callback for submit event of the form, won't be triggered if there are validation errors 99 | - {Func} **[onErrors]** - function(errors), callback for submit event of the form, will be triggered only if there are validation errors 100 | - {Boolean} **[readOnly]** 101 | - {Boolean} **[disabled]** 102 | 103 | ### settings 104 | - **defaultFieldClass** - you can set a field class for all forms in your app by `ShackingForm.defaultFieldClass = ...` 105 | 106 | ## Advanced 107 | You can create your own field classes, it's not hard, and we hope you can contribute them to the society :) 108 | 109 | A field class is nothing but a react component which is responsible to **properly** render the field with several props: 110 | 111 | - {String} label 112 | - {String} type 113 | - {Any} options 114 | - {Any} value 115 | - {Func(value)} onChange 116 | - {String} validationState - null, 'success' or 'error' 117 | - {String} validationError 118 | 119 | there are no limit about type, options, and value, you can define and describe them clearly in your docs. 120 | 121 | it's helpful to have a look at [how we do](https://github.com/ShakingMap/raw-shaking-react-form-field/blob/master/src/field.jsx). 122 | 123 | ## Q&A 124 | ### How to customize style or layout of a field? 125 | You can create your own field class, use it for a individual field or compose it into default field class. 126 | see [how to create field class](https://github.com/ShakingMap/shaking-react-form#advanced) and [how to compose field classes](https://github.com/ShakingMap/shaking-react-form#how-to-compose-field-classes) 127 | 128 | ### How to compose field classes 129 | A field class can be just a swicher which renders other field class by type. So you can write such a swicher field class to compose many other field classes. [react-mapper](https://github.com/zhaoyao91/react-mapper) is a good helper for you to do this. see this [demo](https://github.com/ShakingMap/shaking-react-form-demo/tree/master/compose-field-class). 130 | -------------------------------------------------------------------------------- /dist/form.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 _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 14 | 15 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 16 | 17 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 18 | 19 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 20 | 21 | 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) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 22 | 23 | var Form = function (_React$Component) { 24 | _inherits(Form, _React$Component); 25 | 26 | function Form() { 27 | _classCallCheck(this, Form); 28 | 29 | return _possibleConstructorReturn(this, Object.getPrototypeOf(Form).apply(this, arguments)); 30 | } 31 | 32 | _createClass(Form, [{ 33 | key: 'render', 34 | value: function render() { 35 | return _react2.default.createElement( 36 | 'form', 37 | { onSubmit: this.onFormSubmit.bind(this) }, 38 | this.getFields(), 39 | this.props.children 40 | ); 41 | } 42 | }, { 43 | key: 'onFormSubmit', 44 | value: function onFormSubmit(e) { 45 | var _this2 = this; 46 | 47 | e.preventDefault(); 48 | var _props = this.props; 49 | var onSubmit = _props.onSubmit; 50 | var onErrors = _props.onErrors; 51 | 52 | var values = {}; 53 | var errors = {}; 54 | this.forEachKey(function (key) { 55 | var field = _this2.refs[key]; 56 | field.enableValidation(); 57 | values[key] = field.getValue(); 58 | var error = field.getValidationError(); 59 | if (error) errors[key] = error; 60 | }); 61 | Object.keys(errors).length > 0 ? onErrors(errors) : onSubmit(values); 62 | } 63 | }, { 64 | key: 'forEachKey', 65 | value: function forEachKey(callback) { 66 | for (var key in this.props.schemas) { 67 | if (this.props.schemas.hasOwnProperty(key)) { 68 | callback(key); 69 | } 70 | } 71 | } 72 | }, { 73 | key: 'getFields', 74 | value: function getFields() { 75 | var _this3 = this; 76 | 77 | var _props2 = this.props; 78 | var schemas = _props2.schemas; 79 | var readOnly = _props2.readOnly; 80 | var disabled = _props2.disabled; 81 | 82 | return Object.keys(schemas).map(function (key) { 83 | var fieldClass = schemas[key].fieldClass || _this3.props.fieldClass || Form.defaultFieldClass; 84 | return _react2.default.createElement(Field, { 85 | ref: key, 86 | key: key, 87 | schema: schemas[key], 88 | fieldClass: fieldClass, 89 | value: _this3.props.values && _this3.props.values[key], 90 | onChange: function onChange(value) { 91 | return _this3.props.onChange(_defineProperty({}, key, value)); 92 | }, 93 | validate: schemas[key].validate || Form.defaultValidate, 94 | readOnly: readOnly, 95 | disabled: disabled 96 | }); 97 | }); 98 | } 99 | }]); 100 | 101 | return Form; 102 | }(_react2.default.Component); 103 | 104 | exports.default = Form; 105 | 106 | 107 | Form.propTypes = { 108 | schemas: _react2.default.PropTypes.oneOfType([_react2.default.PropTypes.object, _react2.default.PropTypes.array]).isRequired, 109 | 110 | fieldClass: _react2.default.PropTypes.any, 111 | 112 | values: _react2.default.PropTypes.oneOfType([_react2.default.PropTypes.object, _react2.default.PropTypes.array]), 113 | 114 | // func(values), called with only changed values 115 | onChange: _react2.default.PropTypes.func, 116 | 117 | // func(values) 118 | onSubmit: _react2.default.PropTypes.func, 119 | 120 | // func(errors) 121 | onErrors: _react2.default.PropTypes.func, 122 | 123 | readOnly: _react2.default.PropTypes.bool, 124 | 125 | disabled: _react2.default.PropTypes.bool 126 | }; 127 | 128 | Form.defaultProps = { 129 | onChange: function onChange() {}, 130 | onSubmit: function onSubmit() {}, 131 | onValues: function onValues() {}, 132 | onErrors: function onErrors() {} 133 | }; 134 | 135 | Form.defaultFieldClass = null; 136 | Form.defaultValidate = function () {}; 137 | 138 | var Field = function (_React$Component2) { 139 | _inherits(Field, _React$Component2); 140 | 141 | function Field(props) { 142 | _classCallCheck(this, Field); 143 | 144 | var _this4 = _possibleConstructorReturn(this, Object.getPrototypeOf(Field).call(this, props)); 145 | 146 | var state = { 147 | enableValidation: false 148 | }; 149 | 150 | // if there are no values, this is an uncontrolled form 151 | // but the fields are controlled by form 152 | if (props.value === undefined) state.value = null; 153 | 154 | var value = _this4.getValue(props, state); 155 | var result = _this4.validate(value); 156 | state.validationError = result.validationError; 157 | state.validationState = result.validationState; 158 | 159 | _this4.state = state; 160 | return _this4; 161 | } 162 | 163 | _createClass(Field, [{ 164 | key: 'componentWillUpdate', 165 | value: function componentWillUpdate(nextProps, nextState) { 166 | var oldValue = this.getValue(); 167 | var newValue = this.getValue(nextProps, nextState); 168 | if (oldValue !== newValue) { 169 | this.enableValidation(); 170 | this.setState(this.validate(newValue)); 171 | } 172 | } 173 | }, { 174 | key: 'render', 175 | value: function render() { 176 | var _this5 = this; 177 | 178 | var _props3 = this.props; 179 | var schema = _props3.schema; 180 | var onChange = _props3.onChange; 181 | var label = schema.label; 182 | var type = schema.type; 183 | var options = schema.options; 184 | 185 | var FieldClass = this.props.fieldClass; 186 | var validationState = this.state.enableValidation ? this.state.validationState : null; 187 | var validationError = this.state.enableValidation ? this.state.validationError : ''; 188 | var onFieldChange = this.props.value === undefined ? function (value) { 189 | return _this5.setState({ value: value }); 190 | } : onChange; 191 | 192 | var readOnly = this.props.readOnly || schema.readOnly; 193 | var disabled = this.props.disabled || schema.disabled; 194 | 195 | return _react2.default.createElement(FieldClass, { 196 | label: label, 197 | type: type, 198 | options: options, 199 | value: this.getValue(), 200 | onChange: onFieldChange, 201 | validationState: validationState, 202 | validationError: validationError, 203 | readOnly: readOnly, 204 | disabled: disabled 205 | }); 206 | } 207 | }, { 208 | key: 'getValue', 209 | value: function getValue(props, state) { 210 | if (!props || !state) { 211 | props = this.props; 212 | state = this.state; 213 | } 214 | return props.value !== undefined ? props.value : state.value; 215 | } 216 | }, { 217 | key: 'getValidationError', 218 | value: function getValidationError() { 219 | return this.state.validationError; 220 | } 221 | }, { 222 | key: 'enableValidation', 223 | value: function enableValidation() { 224 | var flag = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0]; 225 | 226 | this.setState({ enableValidation: flag }); 227 | } 228 | }, { 229 | key: 'validate', 230 | value: function validate(value) { 231 | var validationError = this.props.validate(value) || ''; 232 | var validationState = validationError ? 'error' : 'success'; 233 | return { validationError: validationError, validationState: validationState }; 234 | } 235 | }]); 236 | 237 | return Field; 238 | }(_react2.default.Component); 239 | 240 | Field.propTypes = { 241 | schema: _react2.default.PropTypes.object.isRequired, 242 | fieldClass: _react2.default.PropTypes.any.isRequired, 243 | value: _react2.default.PropTypes.any, 244 | onChange: _react2.default.PropTypes.func, 245 | validate: _react2.default.PropTypes.func, 246 | readOnly: _react2.default.PropTypes.bool, 247 | disabled: _react2.default.PropTypes.bool 248 | }; 249 | 250 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shaking-react-form", 3 | "version": "1.1.1", 4 | "description": "build and use extensible react forms easily.", 5 | "main": "dist/form.js", 6 | "scripts": { 7 | "build": "node_modules/.bin/babel src/form.jsx > dist/form.js", 8 | "prepublish": "npm run build" 9 | }, 10 | "author": "zhaoyao91", 11 | "license": "ISC", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/ShakingMap/shaking-react-form.git" 15 | }, 16 | "devDependencies": { 17 | "babel-cli": "^6.7.7", 18 | "babel-preset-es2015": "^6.6.0", 19 | "babel-preset-react": "^6.5.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Form extends React.Component { 4 | render() { 5 | return
6 | {this.getFields()} 7 | {this.props.children} 8 |
9 | } 10 | 11 | onFormSubmit(e) { 12 | e.preventDefault(); 13 | const {onSubmit, onErrors} = this.props; 14 | let values = {}; 15 | let errors = {}; 16 | this.forEachKey(key=> { 17 | const field = this.refs[key]; 18 | field.enableValidation(); 19 | values[key] = field.getValue(); 20 | const error = field.getValidationError(); 21 | if (error) errors[key] = error; 22 | }); 23 | Object.keys(errors).length > 0 ? onErrors(errors) : onSubmit(values); 24 | } 25 | 26 | forEachKey(callback) { 27 | for (let key in this.props.schemas) { 28 | if (this.props.schemas.hasOwnProperty(key)) { 29 | callback(key); 30 | } 31 | } 32 | } 33 | 34 | getFields() { 35 | const {schemas, readOnly, disabled} = this.props; 36 | return Object.keys(schemas).map((key=> { 37 | const fieldClass = schemas[key].fieldClass || this.props.fieldClass || Form.defaultFieldClass; 38 | return this.props.onChange({[key]:value})} 45 | validate={schemas[key].validate || Form.defaultValidate} 46 | readOnly={readOnly} 47 | disabled={disabled} 48 | /> 49 | })) 50 | } 51 | } 52 | 53 | Form.propTypes = { 54 | schemas: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.array]).isRequired, 55 | 56 | fieldClass: React.PropTypes.any, 57 | 58 | values: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.array]), 59 | 60 | // func(values), called with only changed values 61 | onChange: React.PropTypes.func, 62 | 63 | // func(values) 64 | onSubmit: React.PropTypes.func, 65 | 66 | // func(errors) 67 | onErrors: React.PropTypes.func, 68 | 69 | readOnly: React.PropTypes.bool, 70 | 71 | disabled: React.PropTypes.bool 72 | }; 73 | 74 | Form.defaultProps = { 75 | onChange(){}, 76 | onSubmit(){}, 77 | onValues(){}, 78 | onErrors(){} 79 | }; 80 | 81 | Form.defaultFieldClass = null; 82 | Form.defaultValidate = function () {}; 83 | 84 | class Field extends React.Component { 85 | constructor(props) { 86 | super(props); 87 | 88 | const state = { 89 | enableValidation: false 90 | }; 91 | 92 | // if there are no values, this is an uncontrolled form 93 | // but the fields are controlled by form 94 | if (props.value === undefined) state.value = null; 95 | 96 | const value = this.getValue(props, state); 97 | const result = this.validate(value); 98 | state.validationError = result.validationError; 99 | state.validationState = result.validationState; 100 | 101 | this.state = state; 102 | } 103 | 104 | componentWillUpdate(nextProps, nextState) { 105 | const oldValue = this.getValue(); 106 | const newValue = this.getValue(nextProps, nextState); 107 | if (oldValue !== newValue) { 108 | this.enableValidation(); 109 | this.setState(this.validate(newValue)) 110 | } 111 | } 112 | 113 | render() { 114 | const {schema, onChange} = this.props; 115 | const {label, type, options} = schema; 116 | const FieldClass = this.props.fieldClass; 117 | const validationState = this.state.enableValidation ? this.state.validationState : null; 118 | const validationError = this.state.enableValidation ? this.state.validationError : ''; 119 | const onFieldChange = this.props.value === undefined ? (value)=>this.setState({value}) : onChange; 120 | 121 | const readOnly = this.props.readOnly || schema.readOnly; 122 | const disabled = this.props.disabled || schema.disabled; 123 | 124 | return 135 | } 136 | 137 | getValue(props, state) { 138 | if (!props || !state) { 139 | props = this.props; 140 | state = this.state; 141 | } 142 | return props.value !== undefined ? props.value : state.value; 143 | } 144 | 145 | getValidationError() { 146 | return this.state.validationError; 147 | } 148 | 149 | enableValidation(flag = true) { 150 | this.setState({enableValidation: flag}); 151 | } 152 | 153 | validate(value) { 154 | const validationError = this.props.validate(value) || ''; 155 | const validationState = validationError ? 'error' : 'success'; 156 | return {validationError, validationState}; 157 | } 158 | } 159 | 160 | Field.propTypes = { 161 | schema: React.PropTypes.object.isRequired, 162 | fieldClass: React.PropTypes.any.isRequired, 163 | value: React.PropTypes.any, 164 | onChange: React.PropTypes.func, 165 | validate: React.PropTypes.func, 166 | readOnly: React.PropTypes.bool, 167 | disabled: React.PropTypes.bool 168 | }; --------------------------------------------------------------------------------