├── .flowconfig ├── .gitignore ├── .npmignore ├── Makefile ├── README.md ├── bower.json ├── dist └── react-form-for.js ├── example ├── __tests__ │ └── form-example-test.js ├── array-example.js ├── bootstrap-form-example.js ├── demo.js ├── form-example.coffee ├── form-example.js ├── index.html └── readme-example.js ├── jest-preprocessor.js ├── jest-setup.js ├── package.json ├── src ├── FieldProxy.js ├── FieldProxyMixin.js ├── FormContext.js ├── FormContextMixin.js ├── FormProxy.js ├── FormProxyMixin.js ├── ListProxy.js ├── ReactFormFor.js ├── __tests__ │ └── ListProxy-test.js ├── components │ ├── Field.js │ └── ListEditor.js ├── deepCloneElementWithFormContext.js ├── inferSchemaFromProxy.js ├── isProxyOfType.js ├── makeDefaultValueFor.js └── util │ ├── Inflection.js │ ├── React-browser.js │ ├── React.js │ ├── cloneElement-browser.js │ ├── cloneElement.js │ ├── createElementFrom.js │ ├── getElementType.js │ ├── isElement.js │ └── util.js └── tasks └── watch.js /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/lib/.* 3 | .*/dist/.* 4 | .*/example/.* 5 | [include] 6 | ./src 7 | ./node_modules/ 8 | [libs] 9 | 10 | [options] 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore docs files 2 | _gh_pages 3 | _site 4 | .ruby-version 5 | 6 | # Numerous always-ignore extensions 7 | *.diff 8 | *.err 9 | *.orig 10 | *.log 11 | *.rej 12 | *.swo 13 | *.swp 14 | *.zip 15 | *.vi 16 | *~ 17 | 18 | # OS or Editor folders 19 | .DS_Store 20 | ._* 21 | Thumbs.db 22 | .cache 23 | .project 24 | .settings 25 | .tmproj 26 | *.esproj 27 | nbproject 28 | *.sublime-project 29 | *.sublime-workspace 30 | .idea 31 | 32 | # Komodo 33 | *.komodoproject 34 | .komodotools 35 | 36 | # Folders to ignore 37 | node_modules 38 | bower_components 39 | 40 | lib/ 41 | example/output/ 42 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | example/ 4 | tasks/ 5 | jest-preprocessor.js 6 | Makefile 7 | *.sh 8 | .module-cache 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | JSX=$(shell npm root)/.bin/jsx 2 | BROWSERIFY=$(shell npm root)/.bin/browserify 3 | BOOTSTRAP=$(shell node -e "process.stdout.write(require.resolve('bootstrap/dist/css/bootstrap.css'))") 4 | SOURCEFILES=lib/*.js 5 | 6 | all: build 7 | 8 | build: 9 | $(JSX) --harmony src/ lib/ 10 | 11 | demo: build 12 | mkdir -p example/output/ 13 | 14 | $(BROWSERIFY) -t [ reactify --harmony ] example/demo.js > example/output/bundle.js 15 | cp $(BOOTSTRAP) example/output/ 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-form-for 2 | 3 | An expressive and intuitive form builder for React, in the style of Rails' `form_for` 4 | 5 | ### example 6 | 7 | ```js 8 | var React = require('react') 9 | var {Form, Fields, Field} = require('react-form-for') 10 | var {ListEditor} = require('react-form-for').Components 11 | 12 | var DateField = require('./date-field') 13 | var languages = require('./languages') 14 | 15 | var ExampleForm = React.createClass({ 16 | getInitialState: function() { 17 | return {value: {}} 18 | }, 19 | handleChange: function(updatedValue) { 20 | this.setState({value: updatedValue}) 21 | }, 22 | renderLanguageSelectOptions: function() { 23 | return languages.map((name) => 24 | 25 | ) 26 | }, 27 | render: function() { 28 | var {value} = this.state 29 | var onChange = this.handleChange 30 | 31 | return ( 32 |
33 |

A Beautiful Form

34 | 35 | } 38 | help="Choose a date" 39 | /> 40 | 41 | {this.renderLanguageSelectOptions()} 42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | ) 57 | } 58 | }) 59 | 60 | React.render(, document.body) 61 | ``` 62 | 63 | #### Custom field components 64 | A possible implementation of the `DateField` from the example above: 65 | ```js 66 | var React = require('react') 67 | 68 | var DateField = React.createClass({ 69 | render: function() { 70 | return ( 71 |
72 | 80 | {this.props.help} 81 |
82 | ) 83 | } 84 | }) 85 | 86 | module.exports = DateField 87 | ``` 88 | Note the use of the important props `value`, `onChange` and `label` which are 89 | provided by the form builder. Other props such as `help` are passed through from 90 | the `` proxy components used above. 91 | 92 | #### Overriding the default field component 93 | ```js 94 | // as long as a component takes a `value` prop (and ideally a `label` prop) 95 | // and an `onChange` callback prop, it can be used as a react-form-for field 96 | var Input = require('react-bootstrap/Input') 97 | var {Form, Fields, Field} = require('react-form-for') 98 | 99 | var ExampleForm = React.createClass({ 100 | handleChange: function(updatedValue) { 101 | this.setState({value: updatedValue}) 102 | }, 103 | // the checkbox Field gets an Input component with different layout classes 104 | getCheckboxComponent: function() { 105 | return ( 106 | 107 | ) 108 | }, 109 | render: function() { 110 | var formOpts = { 111 | onChange: this.handleChange, 112 | fieldComponent: ( 113 | 114 | ) 115 | } 116 | // all of these fields will be rendered as a react-bootstrap/Input 117 |
118 | 119 | 120 | 122 | 123 | 124 | ) 125 | } 126 | }) 127 | ``` 128 | 129 | ##### Warning 130 | :warning: This module is pretty new and might have some bugs, please [file an issue](https://github.com/jsdf/react-form-for/issues) 131 | if you encounter any problems. 132 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-form-for", 3 | "version": "1.1.1", 4 | "homepage": "https://github.com/jsdf/react-form-for", 5 | "authors": [ 6 | "James Friend " 7 | ], 8 | "description": "A simple form builder for React in the style of Rails' form_for", 9 | "main": "./dist/react-form-for.js", 10 | "keywords": [ 11 | "react", 12 | "form-builder", 13 | "react-component", 14 | "forms" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "test", 22 | "src", 23 | "example", 24 | "tasks" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /dist/react-form-for.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var o;"undefined"!=typeof window?o=window:"undefined"!=typeof global?o=global:"undefined"!=typeof self&&(o=self),o.ReactFormFor=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o -1; 728 | } 729 | 730 | function pick(obj) { 731 | for (var _len = arguments.length, rest = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 732 | rest[_key - 1] = arguments[_key]; 733 | } 734 | 735 | var iteratee = rest[0]; 736 | var result = {}, 737 | key; 738 | if (obj == null) { 739 | return result; 740 | }if (iteratee instanceof Function) { 741 | for (key in obj) { 742 | var value = obj[key]; 743 | if (iteratee(value, key, obj)) result[key] = value; 744 | } 745 | } else { 746 | var keys = concat.apply([], rest); 747 | obj = new Object(obj); 748 | for (var i = 0, length = keys.length; i < length; i++) { 749 | key = keys[i]; 750 | if (key in obj) result[key] = obj[key]; 751 | } 752 | } 753 | return result; 754 | } 755 | 756 | function omit(obj) { 757 | var keys = concat.apply([], slice.call(arguments, 1)).map(String); 758 | return pick(obj, function (value, key) { 759 | return !contains(keys, key); 760 | }); 761 | } 762 | 763 | var idCounter = 0; 764 | function uniqueId(prefix) { 765 | var id = ++idCounter + ""; 766 | return typeof prefix == "string" ? prefix + id : id; 767 | } 768 | 769 | function isArray(arr) { 770 | return toString.call(arr) == "[object Array]"; 771 | } 772 | 773 | function arrayCopy(arr) { 774 | return slice.call(arr); 775 | } 776 | 777 | // update nested object structure via copying 778 | function updateIn(object, path, value) { 779 | if (!path || !path.length) throw new Error("invalid path"); 780 | 781 | var updated; 782 | if (isArray(object)) { 783 | updated = arrayCopy(object); 784 | } else { 785 | updated = extend({}, object); 786 | } 787 | var name = path[0]; 788 | 789 | if (path.length === 1) { 790 | updated[name] = value; 791 | } else { 792 | updated[name] = updateIn(updated[name] || {}, path.slice(1), value); 793 | } 794 | return updated; 795 | } 796 | 797 | module.exports = { updateIn: updateIn, clone: clone, extend: extend, merge: merge, omit: omit, pick: pick, contains: contains, uniqueId: uniqueId, isArray: isArray, arrayCopy: arrayCopy }; 798 | },{"xtend/mutable":20}],15:[function(require,module,exports){ 799 | function classNames() { 800 | var args = arguments; 801 | var classes = []; 802 | 803 | for (var i = 0; i < args.length; i++) { 804 | var arg = args[i]; 805 | if (!arg) { 806 | continue; 807 | } 808 | 809 | if ('string' === typeof arg || 'number' === typeof arg) { 810 | classes.push(arg); 811 | } else if ('object' === typeof arg) { 812 | for (var key in arg) { 813 | if (!arg.hasOwnProperty(key) || !arg[key]) { 814 | continue; 815 | } 816 | classes.push(key); 817 | } 818 | } 819 | } 820 | return classes.join(' '); 821 | } 822 | 823 | // safely export classNames in case the script is included directly on a page 824 | if (typeof module !== 'undefined' && module.exports) { 825 | module.exports = classNames; 826 | } 827 | 828 | },{}],16:[function(require,module,exports){ 829 | /** 830 | * Lo-Dash 2.4.1 (Custom Build) 831 | * Build: `lodash modularize modern exports="npm" -o ./npm/` 832 | * Copyright 2012-2013 The Dojo Foundation 833 | * Based on Underscore.js 1.5.2 834 | * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 835 | * Available under MIT license 836 | */ 837 | var isFunction = require('lodash.isfunction'), 838 | keyPrefix = require('lodash._keyprefix'); 839 | 840 | /** Used for native method references */ 841 | var objectProto = Object.prototype; 842 | 843 | /** Native method shortcuts */ 844 | var hasOwnProperty = objectProto.hasOwnProperty; 845 | 846 | /** 847 | * Creates a function that memoizes the result of `func`. If `resolver` is 848 | * provided it will be used to determine the cache key for storing the result 849 | * based on the arguments provided to the memoized function. By default, the 850 | * first argument provided to the memoized function is used as the cache key. 851 | * The `func` is executed with the `this` binding of the memoized function. 852 | * The result cache is exposed as the `cache` property on the memoized function. 853 | * 854 | * @static 855 | * @memberOf _ 856 | * @category Functions 857 | * @param {Function} func The function to have its output memoized. 858 | * @param {Function} [resolver] A function used to resolve the cache key. 859 | * @returns {Function} Returns the new memoizing function. 860 | * @example 861 | * 862 | * var fibonacci = _.memoize(function(n) { 863 | * return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); 864 | * }); 865 | * 866 | * fibonacci(9) 867 | * // => 34 868 | * 869 | * var data = { 870 | * 'fred': { 'name': 'fred', 'age': 40 }, 871 | * 'pebbles': { 'name': 'pebbles', 'age': 1 } 872 | * }; 873 | * 874 | * // modifying the result cache 875 | * var get = _.memoize(function(name) { return data[name]; }, _.identity); 876 | * get('pebbles'); 877 | * // => { 'name': 'pebbles', 'age': 1 } 878 | * 879 | * get.cache.pebbles.name = 'penelope'; 880 | * get('pebbles'); 881 | * // => { 'name': 'penelope', 'age': 1 } 882 | */ 883 | function memoize(func, resolver) { 884 | if (!isFunction(func)) { 885 | throw new TypeError; 886 | } 887 | var memoized = function() { 888 | var cache = memoized.cache, 889 | key = resolver ? resolver.apply(this, arguments) : keyPrefix + arguments[0]; 890 | 891 | return hasOwnProperty.call(cache, key) 892 | ? cache[key] 893 | : (cache[key] = func.apply(this, arguments)); 894 | } 895 | memoized.cache = {}; 896 | return memoized; 897 | } 898 | 899 | module.exports = memoize; 900 | 901 | },{"lodash._keyprefix":17,"lodash.isfunction":18}],17:[function(require,module,exports){ 902 | /** 903 | * Lo-Dash 2.4.2 (Custom Build) 904 | * Build: `lodash modularize modern exports="npm" -o ./npm/` 905 | * Copyright 2012-2014 The Dojo Foundation 906 | * Based on Underscore.js 1.5.2 907 | * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 908 | * Available under MIT license 909 | */ 910 | 911 | /** Used to prefix keys to avoid issues with `__proto__` and properties on `Object.prototype` */ 912 | var keyPrefix = '__1335248838000__'; 913 | 914 | module.exports = keyPrefix; 915 | 916 | },{}],18:[function(require,module,exports){ 917 | /** 918 | * Lo-Dash 2.4.1 (Custom Build) 919 | * Build: `lodash modularize modern exports="npm" -o ./npm/` 920 | * Copyright 2012-2013 The Dojo Foundation 921 | * Based on Underscore.js 1.5.2 922 | * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 923 | * Available under MIT license 924 | */ 925 | 926 | /** 927 | * Checks if `value` is a function. 928 | * 929 | * @static 930 | * @memberOf _ 931 | * @category Objects 932 | * @param {*} value The value to check. 933 | * @returns {boolean} Returns `true` if the `value` is a function, else `false`. 934 | * @example 935 | * 936 | * _.isFunction(_); 937 | * // => true 938 | */ 939 | function isFunction(value) { 940 | return typeof value == 'function'; 941 | } 942 | 943 | module.exports = isFunction; 944 | 945 | },{}],19:[function(require,module,exports){ 946 | var has = Object.hasOwnProperty 947 | var proto = Object.getPrototypeOf 948 | var trace = Error.captureStackTrace 949 | module.exports = StandardError 950 | 951 | function StandardError(msg, props) { 952 | // Let all properties be enumerable for easier serialization. 953 | if (msg && typeof msg == "object") props = msg, msg = undefined 954 | else this.message = msg 955 | 956 | // Name has to be an own property (or on the prototype a single step up) for 957 | // the stack to be printed with the correct name. 958 | if (props) for (var key in props) this[key] = props[key] 959 | if (!has.call(this, "name")) 960 | this.name = has.call(proto(this), "name")? this.name : this.constructor.name 961 | 962 | if (trace && !("stack" in this)) trace(this, this.constructor) 963 | } 964 | 965 | StandardError.prototype = Object.create(Error.prototype, { 966 | constructor: {value: StandardError, configurable: true, writable: true} 967 | }) 968 | 969 | // Set name explicitly for when the code gets minified. 970 | StandardError.prototype.name = "StandardError" 971 | 972 | },{}],20:[function(require,module,exports){ 973 | module.exports = extend 974 | 975 | function extend(target) { 976 | for (var i = 1; i < arguments.length; i++) { 977 | var source = arguments[i] 978 | 979 | for (var key in source) { 980 | if (source.hasOwnProperty(key)) { 981 | target[key] = source[key] 982 | } 983 | } 984 | } 985 | 986 | return target 987 | } 988 | 989 | },{}]},{},[8])(8) 990 | }); -------------------------------------------------------------------------------- /example/__tests__/form-example-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff() 2 | var jQuery = require('jquery') 3 | var beautify = require('js-beautify') 4 | 5 | // integration test for the whole thing 6 | describe('form-example', function() { 7 | it('renders the form', function() { 8 | var React = require('react/addons') 9 | var {TestUtils} = React.addons 10 | var ExampleForm = require('../form-example.js') 11 | 12 | var formComponent = TestUtils.renderIntoDocument() 13 | var form = formComponent.getDOMNode() 14 | 15 | assertHasLabelAndInputWithValue(form, 'Name', 'James') 16 | assertHasLabelAndInputWithValue(form, 'From date', '2012-1-1') 17 | assertHasLabelAndInputWithValue(form, 'SomeThing', '1') 18 | 19 | var inputToUpdate = inputForLabel(form, 'Something else') 20 | 21 | TestUtils.Simulate.change(inputToUpdate, {target: {value: '2'}}) 22 | 23 | expect(formComponent.state.value['related']['something_else']).toEqual('2') 24 | 25 | var updatedInput = inputForLabel(form, 'Something else') 26 | expect(updatedInput.value).toEqual('2') 27 | }) 28 | 29 | it('produces expected output', function() { 30 | var React = require('react/addons') 31 | var ExampleForm = require('../form-example.js') 32 | 33 | // expected output formatted for readability with variable ids stripped 34 | var expectedFormatted = ( 35 | `
36 |
37 | 38 | 39 |
40 |
41 | 42 | 43 |
44 |
45 | 46 | 47 |
48 |
49 | 59 |
60 |
61 |
62 |
63 |
64 |
65 | 66 | 67 |
68 |
69 | 70 |
71 |
72 |
73 |
74 | 75 | 76 |
77 |
78 | 79 |
80 |
81 | 82 |
83 |
` 84 | ) 85 | 86 | var result = React.renderToStaticMarkup(React.createElement(ExampleForm)) 87 | var resultFormatted = beautify.html(result, {indent_size: 2}) 88 | expect(resultFormatted).toEqual(expectedFormatted) 89 | }) 90 | }) 91 | 92 | function assertHasLabelAndInputWithValue(tree, label, value) { 93 | var input = inputForLabel(tree, label) 94 | expect(input.value).toEqual(value) 95 | } 96 | 97 | function inputForLabel(tree, labelText) { 98 | var label = jQuery(tree).find(`label:contains("${labelText}")`).get(0) 99 | expect(label).not.toBeNull() 100 | var labelForId = label.htmlFor 101 | var input = tree.querySelector(`#${labelForId}`) 102 | expect(input).not.toBeNull() 103 | return input 104 | } 105 | -------------------------------------------------------------------------------- /example/array-example.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var FormFor = require('../') 3 | var {Form, Fields, Field} = require('../') 4 | 5 | var TeamForm = React.createClass({ 6 | getInitialState: function() { 7 | return { 8 | value: { 9 | name: "Awesome Team", 10 | members: [ 11 | { 12 | name: "Jean", 13 | age: 21, 14 | interests: ['yachting', 'hunting', 'exploring'], 15 | }, 16 | { 17 | name: "Billie", 18 | age: 21, 19 | interests: ['riding', 'calligraphy', 'sculpture'], 20 | }, 21 | { 22 | name: "Alex", 23 | age: 22, 24 | interests: ['writing', 'viticulture', 'typeography'], 25 | }, 26 | { 27 | name: "Jo", 28 | age: 24, 29 | interests: ['combinators', 'set theory', 'monads'], 30 | }, 31 | ] 32 | }, 33 | } 34 | }, 35 | handleChange: function(updatedValue) { 36 | this.setState({value: updatedValue}) 37 | }, 38 | render: function() { 39 | var {value} = this.state 40 | var onChange = this.handleChange 41 | 42 | return ( 43 |
44 |

A Team Called

45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ) 53 | } 54 | }) 55 | 56 | React.render(, document.body) 57 | -------------------------------------------------------------------------------- /example/bootstrap-form-example.js: -------------------------------------------------------------------------------- 1 | var {Form, Fields, Field} = require('react-form-for') 2 | // any component which takes a `value` prop (and ideally a `label` prop) 3 | // and an `onChange` callback prop, can be used as a react-form-for field 4 | var Input = require('react-bootstrap/Input') 5 | 6 | var ExampleForm = React.createClass({ 7 | handleChange: function(updatedValue) { 8 | this.setState({value: updatedValue}) 9 | }, 10 | // the checkbox Field gets an Input component with different layout classes 11 | getCheckboxComponent: function() { 12 | return ( 13 | 14 | ) 15 | }, 16 | render: function() { 17 | var formOpts = { 18 | onChange: this.handleChange, 19 | fieldComponent: ( 20 | 21 | ) 22 | } 23 | // all of these fields will be rendered as a react-bootstrap/Input 24 | return ( 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | } 34 | }) 35 | 36 | module.exports = ExampleForm 37 | -------------------------------------------------------------------------------- /example/demo.js: -------------------------------------------------------------------------------- 1 | window.React = require('react/addons') 2 | window.$ = require('jquery') 3 | var ExampleForm = require('./form-example.js') 4 | React.render(, document.body) 5 | 6 | -------------------------------------------------------------------------------- /example/form-example.coffee: -------------------------------------------------------------------------------- 1 | {Form, Fields, Field} = require 'react-form-for' 2 | ExampleForm = React.createClass 3 | handleChange: (updatedValue) -> @setState value: updatedValue 4 | render: -> 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | module.exports = ExampleForm 15 | -------------------------------------------------------------------------------- /example/form-example.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var {Form, Fields, Field, List} = require('../lib/ReactFormFor') 3 | var {ListEditor} = require('../lib/ReactFormFor').Components 4 | var ExampleForm = React.createClass({ 5 | getInitialState: function(){ 6 | return { value: { 7 | name: "James", 8 | from_date: "2012-1-1", 9 | to_date: "2012-21-31", 10 | related: { 11 | something: 1, 12 | something_else: 3, 13 | }, 14 | members: [ 15 | { 16 | name: "Jean", 17 | }, 18 | { 19 | name: "Billie", 20 | }, 21 | ], 22 | } 23 | } 24 | }, 25 | handleChange: function(updatedValue) { 26 | this.setState({value: updatedValue}) 27 | }, 28 | render: function() { 29 | return ( 30 |
31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | 39 |
40 | }> 41 | 42 | 43 | 44 | ) 45 | } 46 | }) 47 | 48 | module.exports = ExampleForm 49 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/readme-example.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var FormFor = require('../') 3 | var {Form, Fields, Field} = require('../') 4 | var languages = [ 5 | 'English', 6 | 'Spanish', 7 | 'German', 8 | 'Italian', 9 | 'Japanese', 10 | ] 11 | 12 | var PersonForm = React.createClass({ 13 | getInitialState: function() { 14 | return {value: {}} 15 | }, 16 | handleChange: function(updatedValue) { 17 | this.setState({value: updatedValue}) 18 | }, 19 | renderLanguageSelectOptions: function() { 20 | return languages.map((name) => 21 | 22 | ) 23 | }, 24 | render: function() { 25 | var {value} = this.state 26 | var onChange = this.handleChange 27 | 28 | return ( 29 |
30 |

A Beautiful Form

31 | 32 | 33 | 34 | {this.renderLanguageSelectOptions()} 35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 | ) 45 | } 46 | }) 47 | 48 | React.render(, document.body) 49 | -------------------------------------------------------------------------------- /jest-preprocessor.js: -------------------------------------------------------------------------------- 1 | var babel = require('babel') 2 | 3 | module.exports = { 4 | process: function (src, filename) { 5 | // Ignore all files within node_modules 6 | // babel files can be .js, .es, .jsx or .es6 7 | if (filename.indexOf("node_modules") === -1 && babel.canCompile(filename)) { 8 | return babel.transform(src, { filename: filename, stage: 0 }).code 9 | } 10 | return src 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | require('object.assign').shim() 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-form-for", 3 | "version": "1.1.0", 4 | "description": "A simple form builder for React in the style of Rails' form_for", 5 | "main": "./lib/ReactFormFor.js", 6 | "author": "James Friend ", 7 | "license": "MIT", 8 | "homepage": "https://github.com/jsdf/react-form-for", 9 | "bugs": "https://github.com/jsdf/react-form-for/issues", 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/jsdf/react-form-for.git" 13 | }, 14 | "keywords": [ 15 | "react", 16 | "form-builder", 17 | "react-component", 18 | "forms" 19 | ], 20 | "scripts": { 21 | "test": "npm run prepublish && jest", 22 | "jest": "jest", 23 | "flow": "flow check", 24 | "watch": "node tasks/watch test src/ example/", 25 | "demo": "npm run prepublish && mkdir -p example/output/ && browserify -t [ babelify --stage 0 ] example/demo.js > example/output/bundle.js && cp ./node_modules/bootstrap/dist/css/bootstrap.css example/output/", 26 | "demo-run": "open http://0.0.0.0:8080/ && http-server example/", 27 | "umd": "browserify ./lib/index.js --external react/addons --standalone ReactFormFor > dist/react-form-for.js", 28 | "dist-umd": "npm run umd; git add dist/; git commit -m 'umd distribution rebuild'", 29 | "dist-version": "npm version $V; git tag -d v$V; bower version $V; git push origin --tags", 30 | "dist": "npm run dist-umd; echo 'version:'; read version; V=$version npm run dist-version; git push origin master", 31 | "prepublish": "babel --stage 0 src/ --out-dir lib/" 32 | }, 33 | "jest": { 34 | "scriptPreprocessor": "/jest-preprocessor.js", 35 | "setupEnvScriptFile": "/jest-setup.js", 36 | "testPathIgnorePatterns": [ 37 | "/node_modules/", 38 | "/lib/" 39 | ], 40 | "unmockedModulePathPatterns": [ 41 | "/node_modules/react", 42 | "/src/util", 43 | "/node_modules" 44 | ] 45 | }, 46 | "browser": { 47 | "./lib/util/cloneElement": "./lib/util/cloneElement-browser", 48 | "./lib/util/React": "./lib/util/React-browser" 49 | }, 50 | "dependencies": { 51 | "classnames": "^1.1.4", 52 | "lodash.memoize": "^2.4.1" 53 | }, 54 | "devDependencies": { 55 | "babel": "^5.1.9", 56 | "babelify": "^6.0.2", 57 | "browserify": "^7.1.0", 58 | "chokidar": "^0.11.1", 59 | "jest-cli": "^0.4.0", 60 | "jquery": "^2.1.1", 61 | "js-beautify": "^1.5.4", 62 | "object.assign": "^2.0.1", 63 | "react": "^0.13.1" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/FieldProxy.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var React = require('./util/React') 3 | var createElementFrom = require('./util/createElementFrom') 4 | var FieldProxyMixin = require('./FieldProxyMixin') 5 | 6 | var FieldProxy:any = React.createClass({ 7 | mixins: [ 8 | FieldProxyMixin, 9 | ], 10 | render() { 11 | var parentContext = this.getParentFormContext() 12 | if (!parentContext) throw new Error(`no parent FormContext for ${this.getName()}`) 13 | return createElementFrom(this.getFieldComponent(parentContext), this.getFieldProps(parentContext)) 14 | } 15 | }) 16 | 17 | module.exports = FieldProxy 18 | -------------------------------------------------------------------------------- /src/FieldProxyMixin.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var React = require('./util/React') 3 | var {omit, uniqueId} = require('./util/util') 4 | var memoize = require('lodash.memoize') 5 | var {humanize} = require('./util/Inflection') 6 | var FormContextMixin = require('./FormContextMixin') 7 | 8 | // a memoized inflection of the field name 9 | var getLabelForFieldName = memoize(humanize) 10 | 11 | var FieldProxyMixin:any = { 12 | statics: { 13 | isFieldProxy: true, 14 | }, 15 | mixins: [ 16 | FormContextMixin, 17 | ], 18 | getDefaultProps():Object { 19 | return { 20 | type: 'text', 21 | } 22 | }, 23 | // TODO: DRY up to somewhere else 24 | getName():string { 25 | return this.props.for || this.props.name 26 | }, 27 | getPathWithName(parentContext:?Object):Array { 28 | if (parentContext == null) parentContext = this.getParentFormContext() 29 | return parentContext.path.concat(this.getName()) 30 | }, 31 | handleChange(e:any, parentContext) { 32 | var updatedValue 33 | var name = this.getName() 34 | if (e && typeof e == 'object' && e.target) { 35 | if (e.stopPropagation) e.stopPropagation() 36 | updatedValue = e.target.value 37 | } else { 38 | updatedValue = e 39 | } 40 | 41 | this.applyUpdate(parentContext, updatedValue, parentContext.path.concat(name)) 42 | }, 43 | getParentFormContext() { 44 | return this.getFormContext() 45 | }, 46 | getFieldProps(parentContext:?Object):Object { 47 | if (parentContext == null) parentContext = this.getParentFormContext() 48 | var name = this.getName() 49 | 50 | // TODO: move blacklisted props somewhere DRY 51 | return Object.assign(omit(this.props, 'for'), { 52 | name, 53 | type: this.props.inputType || this.props.type, 54 | label: this.props.label || parentContext.getChildContextProp('labels', name) || getLabelForFieldName(name), 55 | value: parentContext.getChildContextProp('value', name), 56 | validation: parentContext.getChildContextProp('externalValidation', name), 57 | hint: parentContext.getChildContextProp('hints', name), 58 | id: `rff-field-input-${uniqueId(null)}`, 59 | className: `field-${this.getPathWithName(parentContext).join('-')}`, 60 | onChange: (e) => this.handleChange(e, parentContext), 61 | }) 62 | }, 63 | getFieldComponent(parentContext:?Object):ReactClass|ReactComponent { 64 | if (parentContext == null) parentContext = this.getParentFormContext() 65 | return this.props.component || parentContext && parentContext.props.fieldComponent 66 | }, 67 | } 68 | 69 | module.exports = FieldProxyMixin 70 | -------------------------------------------------------------------------------- /src/FormContext.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var React = require('./util/React') 3 | var {pick} = require('./util/util') 4 | var makeDefaultValueFor = require('./makeDefaultValueFor') 5 | var Field = require('./components/Field') 6 | 7 | function contextChildPropFor(formContext:Object, propName:?string, childName:?string):any { 8 | var contextProp = formContext[propName] 9 | return contextProp instanceof Object ? contextProp[childName] : null 10 | } 11 | 12 | function contextChildProps(parentContext:Object, childName:?string, propNames:Array) { 13 | return propNames.reduce((childProps, propName) => { 14 | childProps[propName] = contextChildPropFor(parentContext, propName, childName) 15 | return childProps 16 | }, {}) 17 | } 18 | 19 | var INHERITED_CONTEXT_PROPNAMES = [ 20 | 'value', 21 | 'labels', 22 | 'externalValidation', 23 | 'hints', 24 | ] 25 | 26 | var INHERITED_COMPONENT_PROPNAMES = [ 27 | 'onChange', 28 | 'labels', 29 | 'externalValidation', 30 | 'hints', 31 | 'fieldComponent', 32 | ] 33 | 34 | type FormState = { 35 | value: Object|Array; 36 | onChange: Function; 37 | } 38 | 39 | type FormContextProps = { 40 | // value: Object; 41 | // labels: Object; 42 | // externalValidation: Object; 43 | // hints: Object; 44 | // fieldComponent: ReactClass|ReactComponent; 45 | [key: string]: any; 46 | } 47 | 48 | class FormContext { 49 | state: FormState; 50 | props: FormContextProps = {}; 51 | parentContext: ?FormContext; 52 | path: Array; 53 | proxyComponent: ReactComponent; 54 | 55 | initFromRootProxy(proxyComponent:ReactComponent) { 56 | this.proxyComponent = proxyComponent 57 | 58 | this.initAsRoot() 59 | } 60 | initFromChildProxy(proxyComponent:ReactComponent) { 61 | this.proxyComponent = proxyComponent 62 | 63 | var parentContext = FormContext.getParentFromProxy(proxyComponent) 64 | // a nested form fieldset, delegates to the top level form 65 | if (parentContext != null) { 66 | this.initAsChild(parentContext) 67 | } else { 68 | throw new Error('missing parentContext') 69 | } 70 | } 71 | initAsRoot() { 72 | this.path = [] 73 | this.initStateFromRootProxy(this.proxyComponent) 74 | this.initPropsFromProxy(this.proxyComponent) 75 | } 76 | initAsChild(parentContext:FormContext) { 77 | var name = FormContext.getNameFromProxy(this.proxyComponent) 78 | if (!(parentContext instanceof FormContext)) throw new Error('invalid parentContext') 79 | if (!(typeof name == 'string' || typeof name == 'number')) { 80 | throw new Error('name required when parentContext provided') 81 | } 82 | 83 | this.initFromParent(parentContext, name) 84 | 85 | // extra stuff from proxyComponent 86 | this.initPropsFromProxy(this.proxyComponent) 87 | 88 | } 89 | getChildContextProp(propName:string, childName:?string):any { 90 | return contextChildPropFor(this, propName, childName) 91 | } 92 | initFromParent(parentContext:Object, childName:string|number) { 93 | this.parentContext = parentContext 94 | this.state = parentContext.state 95 | this.path = parentContext.path.concat(childName) 96 | 97 | var propsInheritedFromParent = contextChildProps(parentContext, String(childName), INHERITED_CONTEXT_PROPNAMES) 98 | Object.assign(this.props, propsInheritedFromParent) 99 | 100 | if (this.props.value == null) { 101 | if (parentContext.state.value instanceof Object) { 102 | this.props.value = parentContext.state.value[childName] 103 | } else { 104 | this.props.value = makeDefaultValueFor(this) 105 | } 106 | } 107 | 108 | // defaults 109 | this.props.fieldComponent = parentContext.fieldComponent 110 | } 111 | initStateFromRootProxy(proxyComponent:ReactComponent) { 112 | var value = FormContext.getValueFromProxy(proxyComponent) 113 | 114 | // root FormContext owns FormState, child contexts only hold a reference to it 115 | this.state = { 116 | value: value != null ? value : makeDefaultValueFor(this), 117 | onChange: proxyComponent.props.onChange, 118 | } 119 | } 120 | initPropsFromProxy(proxyComponent:ReactComponent) { 121 | Object.assign(this.props, pick(proxyComponent.props, INHERITED_COMPONENT_PROPNAMES)) 122 | 123 | this.props.fieldComponent = this.props.fieldComponent || Field 124 | } 125 | static getValueFromProxy(proxyComponent:ReactComponent):?Object { 126 | var {props} = proxyComponent 127 | if (props.value instanceof Object) { 128 | return props.value 129 | } else if (props.for instanceof Object) { 130 | return props.for 131 | } else { 132 | return null 133 | } 134 | } 135 | static getNameFromProxy(proxyComponent:ReactComponent):?string|number { 136 | var {props} = proxyComponent 137 | if (typeof props.name == 'string' || typeof props.name == 'number') { 138 | return props.name 139 | } else if (typeof props.for == 'string') { 140 | return props.for 141 | } else { 142 | return null 143 | } 144 | } 145 | static getParentFromProxy(proxyComponent:ReactComponent):?FormContext { 146 | var {props} = proxyComponent 147 | if (props.parentFormContext instanceof FormContext) { 148 | return props.parentFormContext 149 | } else { 150 | return null 151 | } 152 | } 153 | } 154 | 155 | module.exports = FormContext 156 | -------------------------------------------------------------------------------- /src/FormContextMixin.js: -------------------------------------------------------------------------------- 1 | var FormContext = require('./FormContext') 2 | var deepCloneElementWithFormContext = require('./deepCloneElementWithFormContext') 3 | var {updateIn} = require('./util/util') 4 | 5 | var FormContextMixin = { 6 | isFormRoot():boolean { 7 | var parentContext = FormContext.getParentFromProxy(this) 8 | if (!parentContext instanceof FormContext) return false 9 | 10 | // if this form proxy has been provided with a 'value' prop, it could become 11 | // the root of a new form structure. not yet supported. 12 | return Boolean(FormContext.getValueFromProxy(this)) 13 | }, 14 | getFormContext():FormContext { 15 | var formContext = new FormContext() 16 | 17 | if (this.isFormRoot()) { 18 | formContext.initFromRootProxy(this) 19 | } else { 20 | formContext.initFromChildProxy(this) 21 | } 22 | return formContext 23 | }, 24 | renderChildrenForFormContext(formContext:FormContext) { 25 | if (formContext.proxyComponent == null) throw new Error('formContext missing proxyComponent') 26 | if (formContext.proxyComponent.props == null || formContext.proxyComponent.props.children == null) { 27 | throw new Error('No children') 28 | } else { 29 | // traverse proxyComponent children and inject form prop 30 | return deepCloneElementWithFormContext(formContext.proxyComponent, formContext) 31 | } 32 | }, 33 | // TODO: maybe move the guts of this elsewhere 34 | applyUpdate(formContext:FormContext, value:Object, path:Array) { 35 | if (formContext.state.onChange instanceof Function) { 36 | formContext.state.onChange(updateIn(formContext.state.value, path, value)) 37 | } else { 38 | console && (console.warn || console.log)('value update occured but onChange handler not set') 39 | } 40 | }, 41 | } 42 | 43 | module.exports = FormContextMixin 44 | -------------------------------------------------------------------------------- /src/FormProxy.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var React = require('./util/React') 3 | var classSet = require('classnames') 4 | var FormProxyMixin = require('./FormProxyMixin') 5 | var createElementFrom = require('./util/createElementFrom') 6 | 7 | var FormProxy = React.createClass({ 8 | mixins: [ 9 | FormProxyMixin, 10 | ], 11 | render() { 12 | var formContext = this.getFormContext() 13 | var formProps = this.getFormProps(formContext) 14 | formProps.children = this.renderFormChildren(formContext) 15 | 16 | var defaultComponent 17 | if (this.isFormRoot()) { 18 | formProps.className = classSet(this.props.className, 'rff-form') 19 | defaultComponent = React.DOM.form 20 | } else { 21 | formProps.className = classSet(this.props.className, 'rff-fieldset') 22 | defaultComponent = React.DOM.div 23 | } 24 | return this.props.component ? createElementFrom(this.props.component, formProps) : defaultComponent(formProps) 25 | }, 26 | }) 27 | 28 | module.exports = FormProxy 29 | -------------------------------------------------------------------------------- /src/FormProxyMixin.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var React = require('./util/React') 3 | var {omit} = require('./util/util') 4 | var createElementFrom = require('./util/createElementFrom') 5 | var FormContextMixin = require('./FormContextMixin') 6 | 7 | // TODO: move somewhere DRY 8 | var API_PROPS = ['for', 'name', 'value', 'formDelegate'] 9 | 10 | var FormProxyMixin = { 11 | statics: { 12 | isFormProxy: true, 13 | }, 14 | mixins: [ 15 | FormContextMixin, 16 | ], 17 | renderFormChildren(formContext:?Object):any { 18 | if (formContext == null) formContext = this.getFormContext() 19 | return this.renderChildrenForFormContext(formContext) 20 | }, 21 | getFormProps(formContext:?Object):Object { 22 | if (formContext == null) formContext = this.getFormContext() 23 | var formProps = omit(this.props, API_PROPS) 24 | return formProps 25 | }, 26 | } 27 | 28 | module.exports = FormProxyMixin 29 | -------------------------------------------------------------------------------- /src/ListProxy.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var React = require('./util/React') 3 | var classSet = require('classnames') 4 | var FormProxyMixin = require('./FormProxyMixin') 5 | var FieldProxyMixin = require('./FieldProxyMixin') 6 | var FormProxy = require('./FormProxy') 7 | var createElementFrom = require('./util/createElementFrom') 8 | var {omit} = require('./util/util') 9 | 10 | var ListProxy = React.createClass({ 11 | mixins: [ 12 | FormProxyMixin, 13 | // avoid mixin-mixins conflict 14 | omit(FieldProxyMixin, 'mixins') 15 | ], 16 | statics: { 17 | isListProxy: true, 18 | }, 19 | renderListChildren(parentContext:?Object):any { 20 | if (parentContext == null) parentContext = this.getParentFormContext() 21 | // note that is effectively creating a fieldset for each item in the array 22 | // and using that item in the array as the value for the fieldset, with the 23 | // child FormProxy elements passed into the ListProxy as the fields 24 | return (parentContext.props.value||[]).map((item, index) => { 25 | // note: children are passed to new FormProxy 26 | // this is important as a ListProxy is basically a FormProxy, but repeated 27 | // TODO: investigate whether child elements should be cloned 28 | var inherited = omit(this.props, 'for', 'name', 'component') 29 | return 30 | }) 31 | }, 32 | render() { 33 | var parentContext = this.getParentFormContext() 34 | var formProps = this.getFieldProps(parentContext.parentContext) 35 | Object.assign(formProps, this.getFormProps(parentContext)) 36 | formProps.className = classSet(this.props.className, 'rff-list') 37 | formProps.children = this.renderListChildren(null) 38 | return this.props.component ? createElementFrom(this.props.component, formProps) : React.DOM.div(formProps) 39 | }, 40 | }) 41 | 42 | module.exports = ListProxy 43 | -------------------------------------------------------------------------------- /src/ReactFormFor.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | var ReactFormFor = { 4 | Form: require('./FormProxy'), 5 | Fields: require('./FormProxy'), // alias 6 | Fieldset: require('./FormProxy'), // alias 7 | Field: require('./FieldProxy'), 8 | List: require('./ListProxy'), 9 | Components: { 10 | ListEditor: require('./components/ListEditor'), 11 | } 12 | } 13 | 14 | module.exports = ReactFormFor 15 | -------------------------------------------------------------------------------- /src/__tests__/ListProxy-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../ListProxy') 2 | 3 | jest.dontMock('../FormContext') 4 | jest.dontMock('../FormContextMixin') 5 | jest.dontMock('../FormProxyMixin') 6 | jest.dontMock('../FieldProxyMixin') 7 | jest.dontMock('../makeDefaultValueFor') 8 | jest.dontMock('../components/Field') 9 | 10 | describe('ListProxy', function() { 11 | it('changes the text after click', function() { 12 | var React = require('react/addons') 13 | var ListProxy = require('../ListProxy') 14 | var FieldProxy = require('../FieldProxy') 15 | var FormContext = require('../FormContext') 16 | var TestUtils = React.addons.TestUtils 17 | 18 | // TODO: decouple FormContext initialisation from components 19 | var rootContext = new FormContext() 20 | return 21 | rootContext.props = { 22 | value: {members: [{name: "Jean"}, {name: "Billie"}]}, 23 | parentFormContext: rootContext, 24 | } 25 | var membersContext = new FormContext() 26 | membersContext.props = { 27 | value: rootContext.props.value.members, 28 | parentFormContext: rootContext, 29 | } 30 | 31 | var listProxy = TestUtils.renderIntoDocument( 32 | 33 | 34 | 35 | ) 36 | 37 | console.log(React.findDOMNode(listProxy)) 38 | 39 | // // Verify that it's Off by default 40 | // var label = TestUtils.findRenderedDOMComponentWithTag( 41 | // listProxy, 'label') 42 | // expect(label.getDOMNode().textContent).toEqual('Off') 43 | 44 | // // Simulate a click and verify that it is now On 45 | // var input = TestUtils.findRenderedDOMComponentWithTag( 46 | // listProxy, 'input') 47 | // TestUtils.Simulate.change(input) 48 | // expect(label.getDOMNode().textContent).toEqual('On') 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/components/Field.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var React = require('../util/React') 3 | var {omit} = require('../util/util') 4 | var classSet = require('classnames') 5 | 6 | // a subset of react-bootstrap/Input, without any bootstrapisms 7 | // most importantly it accepts value and label props and an onChange callback 8 | var Field = React.createClass({ 9 | propTypes: { 10 | type: React.PropTypes.string, 11 | label: React.PropTypes.any, 12 | validation: React.PropTypes.any, 13 | help: React.PropTypes.any, 14 | hint: React.PropTypes.any, 15 | groupClassName: React.PropTypes.string, 16 | fieldClassName: React.PropTypes.string, 17 | wrapperClassName: React.PropTypes.string, 18 | labelClassName: React.PropTypes.string, 19 | onChange: React.PropTypes.func 20 | }, 21 | getInputDOMNode():any { 22 | return this.refs.input.getDOMNode() 23 | }, 24 | getValue():string { 25 | if (typeof this.props.type == 'string') return this.getInputDOMNode().value 26 | else throw new Error('Cannot use getValue without specifying input type.') 27 | }, 28 | getChecked():boolean { 29 | return Boolean(this.getInputDOMNode().checked) 30 | }, 31 | renderInput():any { 32 | var input = null 33 | 34 | if (!this.props.type) { 35 | return this.props.children 36 | } 37 | 38 | var propsForInput = Object.assign(omit(this.props, 'form', 'name'), {ref: "input", key: "input"}) 39 | 40 | switch (this.props.type) { 41 | case 'select': 42 | input = React.DOM.select(Object.assign({children: this.props.children}, propsForInput)) 43 | break 44 | case 'textarea': 45 | input = React.DOM.textarea(propsForInput) 46 | break 47 | case 'submit': 48 | input = React.DOM.input(Object.assign({type: "submit"}, propsForInput)) 49 | break 50 | default: 51 | input = React.DOM.input(propsForInput) 52 | } 53 | 54 | return input 55 | }, 56 | renderHint():any { 57 | var hint = this.props.help || this.props.hint 58 | return hint ? ( 59 | 60 | {hint} 61 | 62 | ) : null 63 | }, 64 | renderErrorMessage():any { 65 | var errorMessage = this.props.validation 66 | return errorMessage ? ( 67 | 68 | {errorMessage} 69 | 70 | ) : null 71 | }, 72 | renderWrapper(children:any):any { 73 | return this.props.wrapperClassName ? ( 74 |
75 | {children} 76 |
77 | ) : children 78 | }, 79 | renderLabel(children:any):any { 80 | return this.props.label ? ( 81 | 85 | ) : children 86 | }, 87 | renderFieldWrapper(children:any):any { 88 | var fieldClassName = this.props.groupClassName || this.props.fieldClassName 89 | var fieldClassSet:{[key:string]:string|boolean} = { 90 | 'rff-field': true, 91 | 'rff-field-with-errors': this.props.validation, 92 | } 93 | if (fieldClassName) fieldClassSet[fieldClassName] = true 94 | return
95 | }, 96 | render():any { 97 | return this.renderFieldWrapper([ 98 | this.renderLabel(null), 99 | this.renderWrapper([ 100 | this.renderInput(), 101 | this.renderHint(), 102 | this.renderErrorMessage(), 103 | ]) 104 | ]) 105 | } 106 | }) 107 | 108 | module.exports = Field 109 | -------------------------------------------------------------------------------- /src/components/ListEditor.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var React = require('../util/React') 3 | var {omit} = require('../util/util') 4 | var classSet = require('classnames') 5 | 6 | var ListEditor = React.createClass({ 7 | handleChange(update:any) { 8 | this.props.onChange(update) 9 | }, 10 | handleAddItem() { 11 | var {value} = this.props 12 | this.handleChange(value.concat(null)) 13 | }, 14 | handleRemoveItem(index:number) { 15 | var {value} = this.props 16 | if (index === 0 && value.length === 1) { 17 | // replace with single null item 18 | this.handleChange([null]) 19 | } else { 20 | // remove item by index 21 | this.handleChange(value.filter((v, i) => index !== i)) 22 | } 23 | }, 24 | renderItemWrapper(item:ReactElement):ReactElement { 25 | return ( 26 |
27 | {item} 28 | 35 |
36 | ) 37 | }, 38 | render():ReactElement { 39 | var items = React.Children.map(this.props.children, (item) => this.renderItemWrapper(item)) 40 | var inherited = omit(this.props, 'for', 'name', 'label', 'value', 'type', 'id') 41 | return ( 42 |
43 |
44 | {items} 45 |
46 | 53 |
54 | ) 55 | } 56 | }) 57 | 58 | module.exports = ListEditor 59 | -------------------------------------------------------------------------------- /src/deepCloneElementWithFormContext.js: -------------------------------------------------------------------------------- 1 | var React = require('./util/React') 2 | var cloneElement = require('./util/cloneElement') 3 | var isElement = require('./util/isElement') 4 | var isProxyOfType = require('./isProxyOfType') 5 | 6 | function hasChildren(element):boolean { 7 | if (element != null && element.props != null) { 8 | return element.props.children != null 9 | } 10 | } 11 | 12 | // recursive map over children and inject form prop 13 | // TODO: replace this with parent-child context if/when it becomes a public API 14 | function deepCloneElementWithFormContext(element, parentContext) { 15 | return React.Children.map(element.props.children, function(child) { 16 | if ( 17 | !isElement(child) 18 | || typeof child == 'string' 19 | || typeof child.props == 'string' 20 | || (child.props && typeof child.props.children == 'string') 21 | ) { 22 | return child 23 | } 24 | 25 | var updatedProps = {} 26 | 27 | if (isProxyOfType('FormProxy', child)) { 28 | if (!hasChildren(child)) throw new Error('No children') 29 | // stop recursion, just inject form parentContext 30 | updatedProps.parentFormContext = parentContext 31 | } else { 32 | if (isProxyOfType('FieldProxy', child)) { 33 | updatedProps.parentFormContext = parentContext 34 | } 35 | // recurse to update grandchildren 36 | updatedProps.children = deepCloneElementWithFormContext(child, parentContext) 37 | } 38 | 39 | return cloneElement(child, updatedProps) 40 | }) 41 | } 42 | 43 | module.exports = deepCloneElementWithFormContext 44 | -------------------------------------------------------------------------------- /src/inferSchemaFromProxy.js: -------------------------------------------------------------------------------- 1 | var isProxyOfType = require('./isProxyOfType') 2 | 3 | function inferTypeFromFieldProxy(proxyComponent:ReactComponent) { 4 | if (typeof proxyComponent.props.type == 'undefined') { 5 | return 'string' 6 | } 7 | 8 | switch (proxyComponent.props.type) { 9 | case 'number': 10 | return 'number' 11 | case 'checkbox': 12 | return 'boolean' 13 | } 14 | return 'string' 15 | } 16 | 17 | function inferSchemaFromProxy(proxyComponent:ReactComponent) { 18 | var type 19 | 20 | if (isProxyOfType('FormProxy', proxyComponent)) { 21 | type = 'object' 22 | } else if (isProxyOfType('FieldProxy', proxyComponent)) { 23 | type = inferTypeFromFieldProxy(proxyComponent) 24 | } else if (isProxyOfType('ListProxy', proxyComponent)) { 25 | type = 'array' 26 | } else { 27 | type = 'object' 28 | } 29 | 30 | switch (type) { 31 | case 'object': 32 | return { 33 | type: 'object', 34 | properties: {}, 35 | } 36 | case 'array': 37 | return { 38 | type: 'array', 39 | items: { 40 | type: 'object', 41 | properties: {}, 42 | }, 43 | } 44 | } 45 | return { 46 | type: type, 47 | } 48 | } 49 | 50 | module.exports = inferSchemaFromProxy 51 | -------------------------------------------------------------------------------- /src/isProxyOfType.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var getElementType = require('./util/getElementType') 3 | 4 | function isProxyOfType(matchTypeName:string, element:?Object):boolean { 5 | var type = getElementType(element) 6 | if (type != null) { 7 | switch (matchTypeName) { 8 | case 'FieldProxy': 9 | return Boolean(type.isFieldProxy) 10 | case 'FormProxy': 11 | return Boolean(type.isFormProxy) 12 | case 'ListProxy': 13 | return Boolean(type.isListProxy) 14 | } 15 | return false 16 | } else { 17 | return false 18 | } 19 | } 20 | 21 | module.exports = isProxyOfType 22 | -------------------------------------------------------------------------------- /src/makeDefaultValueFor.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var isProxyOfType = require('./isProxyOfType') 3 | var FormContext = require('./FormContext') 4 | 5 | function makeDefaultValueFor(formContext:FormContext):Object|Array { 6 | // TODO: make default value from schema node 7 | return isProxyOfType('ListProxy', formContext.proxyComponent) ? [] : {} 8 | } 9 | 10 | module.exports = makeDefaultValueFor 11 | -------------------------------------------------------------------------------- /src/util/Inflection.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var ID_SUFFIX = new RegExp('(_ids|_id)$', 'g') 3 | var UNDERBAR = new RegExp('_', 'g') 4 | 5 | var Inflection = { 6 | capitalize(str:string):string { 7 | var lowerStr = str.toLowerCase() 8 | return lowerStr.substring(0, 1).toUpperCase() + lowerStr.substring(1) 9 | }, 10 | humanize(str:string):string { 11 | return Inflection.capitalize( 12 | str 13 | .toLowerCase() 14 | .replace(ID_SUFFIX, '') 15 | .replace(UNDERBAR, ' ') 16 | ) 17 | }, 18 | } 19 | 20 | module.exports = Inflection 21 | -------------------------------------------------------------------------------- /src/util/React-browser.js: -------------------------------------------------------------------------------- 1 | if (typeof React == 'undefined') { 2 | module.exports = require('react/addons') 3 | } else { 4 | if (!React.addons) { 5 | throw new Error('React addons build is required to use react-form-for') 6 | } 7 | module.exports = React 8 | } 9 | -------------------------------------------------------------------------------- /src/util/React.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | module.exports = require('react/addons') 3 | -------------------------------------------------------------------------------- /src/util/cloneElement-browser.js: -------------------------------------------------------------------------------- 1 | var React = require('./React') 2 | 3 | var cloneElement = React.cloneElement || React.addons && React.addons.cloneWithProps 4 | 5 | if (!cloneElement) { 6 | if (!React.addons) { 7 | throw new Error('React.addons build required for cloneWithProps') 8 | } else { 9 | throw new Error('unsupported') 10 | } 11 | } 12 | 13 | module.exports = cloneElement 14 | -------------------------------------------------------------------------------- /src/util/cloneElement.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var React = require('./React') 3 | 4 | module.exports = React.addons.cloneWithProps 5 | -------------------------------------------------------------------------------- /src/util/createElementFrom.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var React = require('./React') 3 | var cloneElement = require('./cloneElement') 4 | var isElement = require('./isElement') 5 | 6 | function createElementFrom(component:any, props:any):Object { 7 | if (isElement(component)) { 8 | return cloneElement(component, props) 9 | } else { 10 | return React.createElement(component, props) 11 | } 12 | } 13 | 14 | module.exports = createElementFrom 15 | -------------------------------------------------------------------------------- /src/util/getElementType.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | function getElementType(node:?Object):?Function { 3 | if (node != null) { 4 | if (node.type != null) return node.type 5 | else if (node.constructor != null) return node.constructor 6 | else return null 7 | } else { 8 | return null 9 | } 10 | } 11 | 12 | module.exports = getElementType 13 | -------------------------------------------------------------------------------- /src/util/isElement.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var React = require('./React') 3 | 4 | var isElement = React.isValidElement || React.isValidComponent 5 | 6 | module.exports = isElement 7 | -------------------------------------------------------------------------------- /src/util/util.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // subset of underscore methods for our purposes 4 | 5 | var slice = Array.prototype.slice 6 | var concat = Array.prototype.concat 7 | var toString = Object.prototype.toString 8 | 9 | function contains(haystack:any, needle:any):any { 10 | return haystack.indexOf(needle) > -1 11 | } 12 | 13 | function pick(obj:?Object, ...rest:Array):Object { 14 | var iteratee:any = rest[0] 15 | var result = {}, key 16 | if (obj == null) return result 17 | if (iteratee instanceof Function) { 18 | for (key in obj) { 19 | var value = obj[key] 20 | if (iteratee(value, key, obj)) result[key] = value 21 | } 22 | } else { 23 | var keys = concat.apply([], rest) 24 | obj = new Object(obj) 25 | for (var i = 0, length = keys.length; i < length; i++) { 26 | key = keys[i] 27 | if (key in obj) result[key] = obj[key] 28 | } 29 | } 30 | return result 31 | } 32 | 33 | function omit(obj:Object):any { 34 | var keys = concat.apply([], slice.call(arguments, 1)).map(String) 35 | return pick(obj, (value, key) => !contains(keys, key)) 36 | } 37 | 38 | var idCounter = 0 39 | function uniqueId(prefix:?string):string { 40 | var id = ++idCounter + '' 41 | return typeof prefix == 'string' ? prefix + id : id 42 | } 43 | 44 | function isArray(arr:any):boolean { 45 | return toString.call(arr) == '[object Array]' 46 | } 47 | 48 | function arrayCopy(arr:Array):Array { 49 | return slice.call(arr) 50 | } 51 | 52 | // update nested object structure via copying 53 | function updateIn(object:any, path:any, value:any):any { 54 | if (!path || !path.length) throw new Error('invalid path') 55 | 56 | var updated 57 | if (isArray(object)) { 58 | updated = arrayCopy(object) 59 | } else { 60 | updated = Object.assign({}, object) 61 | } 62 | var [name] = path 63 | if (path.length === 1) { 64 | updated[name] = value 65 | } else { 66 | updated[name] = updateIn((updated[name] || {}), path.slice(1), value) 67 | } 68 | return updated 69 | } 70 | 71 | module.exports = {updateIn, omit, pick, contains, uniqueId, isArray, arrayCopy} 72 | -------------------------------------------------------------------------------- /tasks/watch.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | spawn = require('child_process').spawn 3 | exec = require('child_process').exec 4 | chokidar = require('chokidar') 5 | 6 | command = process.argv[2] 7 | pathsToWatch = process.argv.slice(3) 8 | bellOnError = process.argv.indexOf('--bell') < -1 9 | 10 | runningCommand = false 11 | 12 | exec('git rev-parse --show-toplevel', function(err, stdout, stderr) { 13 | // set working directory to git repo root 14 | rootDir = stdout.split('\n')[0] 15 | process.chdir(rootDir) 16 | 17 | console.log('watching', pathsToWatch.join(' ')) 18 | chokidar.watch(pathsToWatch, {ignored: /[\/\\]\./, persistent: true}) 19 | .on('change', function(path, stats) { 20 | console.log('change', path) 21 | if (runningCommand) return 22 | 23 | console.log('running npm run '+command) 24 | runningCommand = true 25 | spawn('npm', ['run', command], {stdio: 'inherit'}) 26 | .on('exit', function(code) { 27 | if (code != 0) bell() 28 | runningCommand = false 29 | }) 30 | .on('error', function(err) { 31 | runningCommand = false 32 | bell() 33 | console.error(err) 34 | }) 35 | }) 36 | }) 37 | 38 | function bell() { 39 | if (bellOnError) process.stdout.write('\x07') 40 | } 41 | --------------------------------------------------------------------------------