├── .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 |
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 |
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 | ``
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 |
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 |
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 |
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 |
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 |
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