├── .babelrc
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── README.md
├── package.json
├── src
├── form.js
└── index.js
├── test
├── entry.js
└── index.html
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react", "stage-2"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea/
3 | lib/
4 | local/
5 | node_modules/
6 | npm-debug.log
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .*
2 | src/
3 | local/
4 | node_modules/
5 | test/
6 | npm-debug.log
7 | webpack.config.js
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.5.0
2 | - now Group, Array and fields will receive validation state and message, thus creating more flexible components is possible.
3 |
4 | ## 0.4.2
5 | - update readme and keywords on npm
6 |
7 | ## 0.4.1
8 | - update readme on npm
9 |
10 | ## 0.4.0
11 | - build full value using cleanValue method of each field to prevent dirty value after schema changing
12 | - compute validation result before updating
13 | - return an editable value in onSubmit and submit methods
14 | - return validation summary and detail in onChange method of form
15 | - support set form type by props.type
16 |
17 | ## 0.3.0
18 | - support change schema
19 |
20 | ## 0.2.0
21 | - support full value cache
22 | - support validate against full form value, aka, change validate(value) to validate(value, formValue)
23 | - support ignore value of a field in a group
24 |
25 | ## 0.1.0
26 | first release
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RF Form
2 |
3 | build react form with validations in a better way.
4 |
5 | **Note: This project is inactively maintained, you can have a try to the great alternative [uniforms](https://github.com/vazco/uniforms)**
6 |
7 | ## Features
8 | - build React form with validations easily
9 | - schema based
10 | - support recursive structures
11 | - support circle schema definition
12 | - support connected forms
13 | - support manipulations of array of fields
14 | - support top level and individual controls of readOnly, disabled
15 | - enable validations of fields automatically
16 | - support extensions of form components (wrapper, group, array, fields)
17 |
18 | ## Demo
19 | [Demo Site](http://shakingmap.github.io/rf-form)
20 |
21 | ## Installation
22 | - install React via npm
23 | - install this package via npm: `npm install rf-form --save`
24 | - optionally install some [form components suit](https://github.com/ShakingMap/rf-form#components-suits-list)
25 |
26 | ## Basic Usage
27 |
28 | For more usages, please refer to the [Demo Site](http://shakingmap.github.io/rf-form).
29 |
30 | First, import *Form* and any *Form Components Suit* you choose.
31 |
32 | import Form from 'rf-form';
33 | import * as buildOptions from 'rf-bootstrap3';
34 |
35 | Then, you can optionally set the *Form Components Suit* as default props of the *Form*, or you can pass it as a prop when you render the form.
36 |
37 | Form.defaultProps.buildOptions = buildOptions;
38 |
39 | Then, define your [schema](https://github.com/ShakingMap/rf-form#field-schema).
40 |
41 | const schema = {
42 | name: {
43 | type: 'Text',
44 | label: 'Name',
45 | validate(v) {
46 | if (!v) return 'Name is required.'
47 | }
48 | },
49 | birthday: {
50 | type: 'Date',
51 | label: 'Birthday'
52 | },
53 | sex: {
54 | type: 'RadioGroup',
55 | label: 'Sex',
56 | options: {
57 | items: {
58 | male: 'Male',
59 | female: 'Female',
60 | unknown: {label: 'Unknown', disabled: true}
61 | }
62 | },
63 | validate(v) {
64 | if (!v) return 'Sex is required.'
65 | }
66 | },
67 | friends: {
68 | label: 'Friends',
69 | array: {
70 | type: 'Text',
71 | label: 'Friend Name'
72 | },
73 | validate(v) {
74 | if (v.length > 3) return 'You have too much friends.'
75 | }
76 | },
77 | account: {
78 | label: 'Account',
79 | group: {
80 | username: {
81 | type: 'Text',
82 | label: 'Username',
83 | validate(v) {if (!v) return 'Username is required.'}
84 | },
85 | password: {
86 | type: 'Password',
87 | label: 'Password',
88 | validate(v) {if (!v || v.length < 6) return 'Password length must be >= 6.'}
89 | },
90 | confirmPassword: {
91 | type: 'Password',
92 | label: 'Confirm Password',
93 | validate(v, fv /*form value*/) {
94 | if (v !== fv.account.password) return 'Password does not match.'
95 | }
96 | }
97 | }
98 | }
99 | };
100 |
101 | Finally, render the form
102 |
103 | class TestPage extends React.Component {
104 | render() {
105 | return
115 | }
116 | }
117 |
118 | ## APIs
119 |
120 | If you are not interested in creating form component, you can only have a look at the apis of *Form*.
121 |
122 | ### Common Concepts
123 | - validation state - one of 'success', 'warning', 'error' or falsy
124 |
125 | ### Form
126 | #### Props
127 | - schema - the *group field* of a [field schema](https://github.com/ShakingMap/rf-form#field-schema)
128 | - buildOptions - an object as {Wrapper, Group, Array, fields}, being used to build the form, often provided by some *form components suit package*.
129 | - value - if undefined, the form will be uncontrolled.
130 | - onChange - func(value, summary, details), summary is {success: count, warning: count, error: count}, details are all validation results.
131 | - onSubmit - func(value, summary, details), summary is {success: count, warning: count, error: count}, details are all validation results.
132 | - subForms - func(), return a group of sub forms in an object, such as {form1: this.refs.form1, form2: this.refs.form2, ...}.
133 | - readOnly - bool
134 | - disabled - bool
135 | - enableValidation - bool or 'auto', default to 'auto', which will enable validation of a field if the onChange of the field is triggered
136 | - type - string, default to 'form'
137 |
138 | ### Wrapper
139 | A Wrapper is a form component which is responsible for rendering label, field component and validation message.
140 | #### Props
141 | - id - field id, usually used as *htmlFor* prop of label
142 | - label - optional string
143 | - validationState - see *validation state* of common concepts
144 | - validationMessage - string
145 | - children - field
146 |
147 | ### Group
148 | A Group is a form component which is responsible for organizing a group of fields
149 | #### Props
150 | - children - field
151 | - validationState - see *validation state* of common concepts
152 | - validationMessage - string
153 | - readOnly - usually no effect
154 | - disabled - usually no effect
155 |
156 | ### Array
157 | An Array is a form component which is responsible for organizing an array of same fields
158 | #### Props
159 | - children - field
160 | - validationState - see *validation state* of common concepts
161 | - validationMessage - string
162 | - readOnly - usually no effect
163 | - disabled - the array manipulation operations should be disabled
164 | - onInsert - func(index)
165 | - onRemove - func(index)
166 | - onMove - func(from, to)
167 |
168 | ### Field
169 | A field is a form component which is responsible for managing the this field
170 | #### Props
171 | - id - field id, usually used as *id* props of inner input
172 | - validationState - see *validation state* of common concepts
173 | - validationMessage - string
174 | - value - any value this field needs.
175 | - onChange - func(value, event)
176 | - readOnly - bool, usually implemented by text field
177 | - disabled - bool, field should be disabled
178 | - ... - field component can define other props it needs, passed in from the corresponding schema.options
179 |
180 | #### Methods
181 | - cleanValue - func(value, options): compatibleValue. return a value that is compatible with this field.
182 |
183 | ## Field Schema
184 | A field schema is an object of following keys:
185 |
186 | - type - string or field component. if string, it will be mapped into a field component by form.props.buildOptions.fields. if this schema has array or group key, type key can be omitted
187 | - wrapper - optional string or wrapper component. if string, it will be mapped into a wrapper component by form.props.buildOptions.fields
188 | - label - optional string
189 | - options - all options will be spread to the props of corresponding form component. you should only set allowed options such as `readOnly`, `disabled` and any alllowed options defined by specific field component(usually addressed in its doc).
190 | - validate - optional func(value, formValue): result. standard result format is {state: validationState, message: validationMessage}. if result is not standard, it will be converted as below:
191 | - falsy -> {state: 'success', message: ''}
192 | - string -> {state: 'error', message: string}
193 | - array -> {state: array[0], message: array[1]}
194 | - array - optional *field schema*. if exists, this schema indicates an array field.
195 | - group - optional object as {key: *field schema*}. if exists, this schema indicates a group field.
196 | - ignoreValue - optional bool. only available for field in a group. if true, the value of this field won't be included in the form value and form validation on submit
197 |
198 | ## Components Suits List
199 | - [rf-bootstrap3](https://github.com/ShakingMap/rf-bootstrap3) - form components suit for bootstrap3
200 | - [rf-materialize](https://github.com/ShakingMap/rf-materialize) - form components suit for materialize
201 | - [rf-material-ui](https://github.com/ShakingMap/rf-material-ui) - form components suit for material-ui
202 |
203 | ## Q&A
204 | ### How to connect forms?
205 | For example
206 |
207 |
221 |
226 |
227 |
228 |
229 | You can separate your form into many sub forms and layout them in this way.
230 |
231 | ## License
232 | ISC
233 |
234 | ## Let's Build It Together!
235 | If you like this project, welcome to give any helps and supports! Thanks!
236 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rf-form",
3 | "version": "0.5.0",
4 | "description": "rf form: build react form with validations in a better way.",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "test": "node node_modules/webpack/bin/webpack.js --progress --colors --watch -d",
8 | "build": "node node_modules/babel-cli/bin/babel ./src --out-dir ./lib --source-maps",
9 | "autobuild": "node node_modules/babel-cli/bin/babel ./src --out-dir ./lib --source-maps --watch",
10 | "prepublish": "npm run build"
11 | },
12 | "author": "zhaoyao91 ",
13 | "license": "ISC",
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/ShakingMap/rf-form.git"
17 | },
18 | "dependencies": {
19 | "lodash": "^4.12.0"
20 | },
21 | "peerDependencies": {
22 | "react": ">=0.13.0"
23 | },
24 | "devDependencies": {
25 | "babel-cli": "^6.8.0",
26 | "babel-core": "^6.8.0",
27 | "babel-loader": "^6.2.4",
28 | "babel-preset-es2015": "^6.6.0",
29 | "babel-preset-react": "^6.5.0",
30 | "babel-preset-stage-2": "^6.5.0",
31 | "bootstrap": "^3.3.6",
32 | "css-loader": "^0.23.1",
33 | "file-loader": "^0.8.5",
34 | "react": "^15.0.2",
35 | "react-dom": "^15.0.2",
36 | "rf-bootstrap3": "^0.3.0",
37 | "source-map-loader": "^0.1.5",
38 | "style-loader": "^0.13.1",
39 | "url-loader": "^0.5.7",
40 | "webpack": "^1.13.0",
41 | "webpack-dev-server": "^1.14.1"
42 | },
43 | "keywords": ["react", "form", "rf-form"]
44 | }
45 |
--------------------------------------------------------------------------------
/src/form.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 |
4 | /**
5 | * Some Conceptions
6 | *
7 | * build options: {fields, Wrapper, Group, Array}
8 | *
9 | * validation: func(value, formValue, form): result
10 | * standard result format is {state, message}
11 | * if result is
12 | * falsy -> {state: 'success', message: ''}
13 | * string -> {state: 'error', message: string}
14 | * array -> {state: array[0], message: array[1]}
15 | */
16 |
17 | /**
18 | * Notes
19 | *
20 | * onChange is modified to pass enable validation state up.
21 | */
22 |
23 | const propTypes = {
24 | schema: React.PropTypes.object.isRequired,
25 | buildOptions: React.PropTypes.object.isRequired,
26 |
27 | value: React.PropTypes.object,
28 |
29 | // func(value, summary, detail)
30 | onChange: React.PropTypes.func,
31 |
32 | // func(value, summary, detail)
33 | onSubmit: React.PropTypes.func,
34 |
35 | // func(): {key: form}
36 | subForms: React.PropTypes.func,
37 |
38 | readOnly: React.PropTypes.bool,
39 |
40 | disabled: React.PropTypes.bool,
41 |
42 | enableValidation: React.PropTypes.any,
43 |
44 | type: React.PropTypes.string
45 | };
46 |
47 | const defaultProps = {
48 | schema: {},
49 | buildOptions: {},
50 | // value: {}, // to support uncontrolled form, default value should be disabled
51 | onChange(){},
52 | onSubmit(){},
53 | readOnly: false,
54 | disabled: false,
55 | enableValidation: 'auto',
56 | type: 'form'
57 | };
58 |
59 | class Form extends React.Component {
60 | constructor(props) {
61 | super(props);
62 | this.state = {
63 | enableValidation: false
64 | }
65 | }
66 |
67 | componentWillMount() {
68 | this.id = Math.random() + '';
69 | this.enableValidationState = null;
70 | this.fullValue = getFullValue(this.getFormSchema(), this.getValue(), this.props.buildOptions.fields);
71 | this.validationResult = getValidationResult(this.getFormSchema(), this.fullValue, this.fullValue);
72 | }
73 |
74 | componentWillUpdate(nextProps, nextState) {
75 | this.fullValue = getFullValue(this.getFormSchema(nextProps), this.getValue(nextProps, nextState), nextProps.buildOptions.fields);
76 | this.validationResult = getValidationResult(this.getFormSchema(nextProps), this.fullValue, this.fullValue)
77 | }
78 |
79 | render() {
80 | const {onChange, buildOptions, enableValidation, children, readOnly, disabled, type} = this.props;
81 | const schema = this.getFormSchema();
82 |
83 | return React.createElement(type, {
84 | onSubmit: this.onSubmit.bind(this)
85 | },
86 | this.getRenderNode({
87 | id: this.id,
88 | schema,
89 | value: this.fullValue,
90 | validation: this.validationResult.validation,
91 | enableValidationState: this.enableValidationState,
92 | onChange: (v, e, evs)=> {
93 | this.enableValidationState = evs;
94 | if (!this.isControlled()) this.setState({value: v});
95 | const validationResult = getValidationResult(this.getFormSchema(), v, v);
96 | onChange(v, validationResult.summary, validationResult.validation);
97 | },
98 | buildOptions,
99 | enableValidation,
100 | readOnly, disabled
101 | }),
102 | children
103 | );
104 | }
105 |
106 | getFormSchema(props) {
107 | props = props || this.props;
108 | return {
109 | type: EmptyGroup,
110 | wrapper: EmptyWrapper,
111 | group: props.schema
112 | }
113 | }
114 |
115 | getRenderNode({id, schema, value, validation, onChange, enableValidationState, buildOptions, enableValidation, readOnly, disabled}) {
116 | // pre-process options
117 | const Wrapper = typeof schema.wrapper === 'string' ? buildOptions.fields[schema.wrapper] : (schema.wrapper || buildOptions.Wrapper);
118 | const options = schema.options || {};
119 | enableValidationState = enableValidationState || {enabled: false, array: [], group: {}};
120 | const localEnableValidation = enableValidation === 'auto' ?
121 | (this.state.enableValidation || enableValidationState.enabled)
122 | :
123 | enableValidation;
124 | const localReadOnly = schema.readOnly || readOnly;
125 | const localDisabled = schema.disabled || disabled;
126 | const validationState = localEnableValidation ? validation.state : '';
127 | const validationMessage = localEnableValidation ? validation.message : '';
128 |
129 | // build node;
130 | let node = null;
131 | if (schema.array) {
132 | const Node = typeof schema.type === 'string' ? buildOptions.fields[schema.type] : (schema.type || buildOptions.Array);
133 | const validationStateForActiveArray = {
134 | enabled: true,
135 | array: enableValidationState.array || [],
136 | group: enableValidationState.group || {}
137 | };
138 | const children = _.map(value, (subValue, index)=>this.getRenderNode({
139 | id: id + '.' + index,
140 | schema: schema.array,
141 | value: subValue,
142 | validation: validation.array[index],
143 | enableValidationState: enableValidationState.array[index],
144 | buildOptions,
145 | onChange: (v, e, evs)=> onChange(
146 | value.slice(0, index).concat([v], value.slice(index + 1)),
147 | e,
148 | {
149 | enabled: true,
150 | array: enableValidationState.array.slice(0, index).concat([evs], enableValidationState.array.slice(index + 1)),
151 | group: enableValidationState.group || {}
152 | }
153 | ),
154 | enableValidation, readOnly, disabled
155 | }));
156 | node = onChange(value.slice(0, index).concat(null, value.slice(index)), null, validationStateForActiveArray),
161 | onRemove: (index)=> onChange(value.slice(0, index).concat(value.slice(index + 1)), null, validationStateForActiveArray),
162 | onMove: (from, to)=> onChange(
163 | from < to ?
164 | value.slice(0, from).concat(value.slice(from + 1, to + 1), [value[from]], value.slice(to + 1))
165 | :
166 | value.slice(0, to).concat([value[from]], value.slice(to, from), value.slice(from + 1))
167 | , null, validationStateForActiveArray),
168 | readOnly: localReadOnly,
169 | disabled: localDisabled
170 | }}/>
171 | }
172 | else if (schema.group) {
173 | const Node = typeof schema.type === 'string' ? buildOptions.fields[schema.type] : (schema.type || buildOptions.Group);
174 | const children = _.map(schema.group, (subSchema, key)=>this.getRenderNode({
175 | id: id + '.' + key,
176 | schema: subSchema,
177 | value: value[key],
178 | validation: validation.group[key],
179 | enableValidationState: enableValidationState.group[key],
180 | buildOptions,
181 | onChange: (v, e, evs)=> onChange(
182 | _.assign({}, value, {[key]: v}),
183 | e,
184 | {
185 | enabled: true,
186 | group: _.assign({}, enableValidationState.group, {[key]: evs}),
187 | array: enableValidationState.array || []
188 | }
189 | ),
190 | enableValidation, readOnly, disabled
191 | }));
192 | node =
199 | }
200 | else {
201 | const Node = typeof schema.type === 'string' ? buildOptions.fields[schema.type] : schema.type;
202 | node = onChange(v, e, {
205 | enabled: true,
206 | array: enableValidationState.array || [],
207 | group: enableValidationState.group || {}
208 | }),
209 | validationState,
210 | validationMessage,
211 | readOnly: localReadOnly,
212 | disabled: localDisabled
213 | }}/>
214 | }
215 |
216 | // build wrapper;
217 | return
225 | }
226 |
227 | onSubmit(e) {
228 | e.preventDefault();
229 | this.submit();
230 | }
231 |
232 | isControlled(props) {
233 | props = props || this.props;
234 | return props.value !== undefined;
235 | }
236 |
237 | getValue(props, state) {
238 | props = props || this.props;
239 | state = state || this.state;
240 | return this.isControlled(props) ? props.value : state.value;
241 | }
242 |
243 | // public
244 | submit() {
245 | if (!this.state.enableValidation) this.setState({enableValidation: true}); // update only if this state changed to prevent unused update
246 | let result = {
247 | value: getFullValue(this.getFormSchema(), this.getValue(), this.props.buildOptions.fields),
248 | summary: this.validationResult.summary,
249 | validation: this.validationResult.validation
250 | };
251 | if (this.props.subForms) {
252 | const subForms = this.props.subForms();
253 | result = _.reduce(subForms, (result, form, key)=> {
254 | const subResult = form.submit();
255 | return {
256 | value: _.assign({}, result.value, {[key]: subResult.value}),
257 | summary: _.assignWith({}, result.summary, subResult.summary, (v1, v2)=>(v1 || 0) + (v2 || 0)),
258 | validation: _.assign({}, result.validation, {group: _.assign({[key]: subResult.validation}, result.validation.group)})
259 | }
260 | }, result)
261 | }
262 | this.props.onSubmit(result.value, result.summary, result.validation);
263 | return result;
264 | }
265 | }
266 |
267 | Form.propTypes = propTypes;
268 | Form.defaultProps = defaultProps;
269 |
270 | export default Form;
271 |
272 | const EmptyWrapper = ({children})=> {
273 | return {children}
;
274 | };
275 |
276 | const EmptyGroup = ({children})=> {
277 | return {children}
;
278 | };
279 |
280 | const validate = (validate, value, formValue)=> {
281 | if (!validate) return {state: '', message: ''};
282 | else {
283 | const result = validate(value, formValue);
284 | if (!result) return {state: 'success', message: ''};
285 | else if (typeof result === 'string') return {state: 'error', message: result};
286 | else if (Array.isArray(result)) return {state: result[0], message: result[1]};
287 | else return result;
288 | }
289 | };
290 |
291 | /**
292 | * build value with prop default value
293 | * left node -> null
294 | * group -> {key: fullValue}
295 | * array -> []
296 | */
297 | const getFullValue = (schema, value, fields)=> {
298 | if (schema.group) {
299 | value = (typeof value === 'object' && value) || {};
300 | return _(schema.group).omitBy(subSchema=>subSchema.ignoreValue).mapValues((subSchema, key)=>getFullValue(subSchema, value[key], fields)).value();
301 | }
302 | else if (schema.array) {
303 | value = (Array.isArray(value) && value) || [];
304 | return _.map(value, (subValue, index)=>getFullValue(schema.array, subValue, fields))
305 | }
306 | else return getField(schema, fields).cleanValue(value === undefined ? null : value, schema.options || {});
307 | };
308 |
309 | const getValidationResult = (schema, value, formValue)=> {
310 | const validation = validate(schema.validate, value, formValue);
311 | const summary = validation.state ? {[validation.state]: 1} : {};
312 |
313 | if (schema.array) {
314 | value = value || [];
315 | return _.reduce(value, (result, subValue, index)=> {
316 | const subValidationData = getValidationResult(schema.array, subValue, formValue);
317 | return {
318 | summary: _.assignWith({}, result.summary, subValidationData.summary, (v1, v2)=> (v1 || 0) + (v2 || 0)),
319 | validation: _.assign({}, result.validation, {array: result.validation.array.concat([subValidationData.validation])})
320 | }
321 | }, {
322 | summary,
323 | validation: _.assign({array: []}, validation)
324 | })
325 | }
326 | else if (schema.group) {
327 | value = value || {};
328 | return _.reduce(schema.group, (result, subSchema, key)=> {
329 | if (subSchema.ignoreValue) return result;
330 | const subValidationData = getValidationResult(subSchema, value[key], formValue);
331 | return {
332 | summary: _.assignWith({}, result.summary, subValidationData.summary, (v1, v2)=> (v1 || 0) + (v2 || 0)),
333 | validation: _.assign({}, result.validation, {group: _.assign({[key]: subValidationData.validation}, result.validation.group)})
334 | }
335 | }, {
336 | summary,
337 | validation: _.assign({group: {}}, validation)
338 | })
339 | }
340 | else {
341 | return {
342 | summary,
343 | validation
344 | }
345 | }
346 | };
347 |
348 | const getField = (schema, fields)=> {
349 | if (typeof schema.type === 'string') return fields[schema.type];
350 | else return schema.type;
351 | };
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export {default} from './form';
--------------------------------------------------------------------------------
/test/entry.js:
--------------------------------------------------------------------------------
1 | require('bootstrap/dist/css/bootstrap.css');
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 |
6 | import * as buildOptions from 'rf-bootstrap3';
7 | import Form from '../lib';
8 |
9 | Form.defaultProps.buildOptions = buildOptions;
10 |
11 | const schema = {
12 | name: {
13 | type: 'Text',
14 | label: 'Name',
15 | validate(v) {
16 | if (!v) return 'Name is required.'
17 | }
18 | },
19 | birthday: {
20 | type: 'Date',
21 | label: 'Birthday'
22 | },
23 | sex: {
24 | type: 'RadioGroup',
25 | label: 'Sex',
26 | options: {
27 | items: {
28 | male: 'Male',
29 | female: 'Female',
30 | unknown: {label: 'Unknown', disabled: true}
31 | }
32 | },
33 | validate(v) {
34 | if (!v) return 'Sex is required.'
35 | }
36 | },
37 | friends: {
38 | label: 'Friends',
39 | array: {
40 | type: 'Text',
41 | label: 'Friend Name'
42 | },
43 | validate(v) {
44 | if (v.length > 3) return 'You have too much friends.'
45 | }
46 | },
47 | account: {
48 | label: 'Account',
49 | group: {
50 | username: {
51 | type: 'Text',
52 | label: 'Username',
53 | validate(v) {if (!v) return 'Username is required.'}
54 | },
55 | password: {
56 | type: 'Password',
57 | label: 'Password',
58 | validate(v) {if (!v || v.length < 6) return 'Password length must be >= 6.'}
59 | },
60 | confirmPassword: {
61 | type: 'Password',
62 | label: 'Confirm Password',
63 | validate(v, fv /*form value*/) {
64 | if (v !== fv.account.password) return 'Password does not match.'
65 | }
66 | }
67 | }
68 | }
69 | };
70 |
71 | class TestPage extends React.Component {
72 | render() {
73 | return
83 | }
84 | }
85 |
86 | let root = document.getElementById('root');
87 | if (!root) {
88 | root = document.createElement('div');
89 | root.id = 'root';
90 | document.body.appendChild(root);
91 | }
92 |
93 | ReactDOM.render(
94 | ,
95 | root
96 | );
97 |
98 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Title
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: "./test/entry.js",
5 | output: {
6 | path: path.resolve(__dirname, 'local'),
7 | filename: "bundle.js"
8 | },
9 | module: {
10 | preLoaders: [
11 | {
12 | test: /\.js$/,
13 | loader: "source-map-loader"
14 | }
15 | ],
16 | loaders: [
17 | {test: /\.css$/, loader: "style-loader!css-loader"},
18 | {test: /.(png|woff|woff2|eot|ttf|svg)$/, loader: 'url-loader?limit=100000'},
19 | {
20 | test: /\.jsx?$/,
21 | exclude: /(node_modules|bower_components|dist)/,
22 | loader: 'babel', // 'babel-loader' is also a legal name to reference
23 | query: {
24 | presets: ['es2015', 'react', 'stage-2'],
25 | cacheDirectory: true
26 | }
27 | }
28 | ]
29 | }
30 | };
--------------------------------------------------------------------------------