├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE.md
├── README.md
├── examples
├── bootstrap
│ ├── components
│ │ ├── App.js
│ │ └── bootstrap
│ │ │ ├── Checkbox.js
│ │ │ ├── Form.js
│ │ │ ├── FormGroup.js
│ │ │ └── RadioGroup.js
│ ├── index.html
│ ├── index.js
│ ├── package.json
│ ├── server.js
│ └── webpack.config.js
└── simple
│ ├── components
│ └── App.js
│ ├── index.html
│ ├── index.js
│ ├── package.json
│ ├── server.js
│ └── webpack.config.js
├── karma.conf.js
├── package.json
├── src
├── components
│ ├── Checkbox.js
│ ├── Form.js
│ ├── Message.js
│ ├── RadioGroup.js
│ ├── Select.js
│ ├── Text.js
│ └── TextArea.js
├── hoc
│ ├── connect.js
│ ├── connectCheckbox.js
│ ├── connectInput.js
│ ├── connectMessage.js
│ ├── connectSelect.js
│ └── createForm.js
├── index.js
└── utils
│ ├── formShape.js
│ ├── makePath.js
│ └── shallowEqual.js
├── test.js
├── test
├── .eslintrc
├── components
│ ├── Checkbox.spec.js
│ ├── Form.js
│ ├── RadioGroup.spec.js
│ ├── Select.spec.js
│ ├── Text.spec.js
│ └── TextArea.spec.js
└── hoc
│ ├── connectInput.spec.js
│ └── createMessage.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015", "stage-1"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | **/node_modules
3 | **/webpack.config.js
4 | examples/**/server.js
5 | karma.conf.js
6 | tests.webpack.js
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "eslint-config-airbnb",
4 | "env": {
5 | "browser": true,
6 | "mocha": true,
7 | "node": true
8 | },
9 | "ecmaFeatures": {
10 | "experimentalObjectRestSpread": true
11 | },
12 | "rules": {
13 | "react/jsx-uses-react": 2,
14 | "react/jsx-uses-vars": 2,
15 | "react/jsx-indent-props": 0,
16 | "react/jsx-no-bind": 0,
17 | "react/jsx-closing-bracket-location": 0,
18 | "react/react-in-jsx-scope": 2,
19 | "react/no-multi-comp": 0,
20 | "react/prefer-es6-class": 0,
21 | "max-len": 0,
22 | "indent": [0, 4],
23 | "new-cap": 0,
24 | "comma-dangle": 0,
25 | "camelcase": 0,
26 | "id-length": 0,
27 | "no-nested-ternary": 0,
28 | "no-param-reassign": 2,
29 | "prefer-arrow-callback": 0,
30 | "arrow-body-style": 0
31 | },
32 | "plugins": [
33 | "react"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .DS_Store
4 | dist
5 | lib
6 | coverage
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | src
4 | test
5 | examples
6 | coverage
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "iojs"
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change log
2 |
3 | All notable changes to this project will be documented in this file.
4 | This project adheres to [Semantic Versioning](http://semver.org/).
5 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4 |
5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
6 |
7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8 |
9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10 |
11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12 |
13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
14 |
15 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Malte Wessel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | react-formalize
2 | =========================
3 |
4 | []()
5 | [](https://www.npmjs.com/package/react-formalize)
6 | [](https://www.npmjs.com/package/react-formalize)
7 |
8 | * serialize forms with react
9 | * pass defaults to form or input
10 | * easy two-way data binding
11 | * validation messages
12 | * works great with flux, redux and friends
13 | * fully customizable
14 |
15 | ### Demos
16 | * **[Simple example](http://malte-wessel.github.io/react-formalize/simple.html)**
17 | * **[Bootstrap integration](http://malte-wessel.github.io/react-formalize/bootstrap.html)**
18 |
19 | ## Table of Contents
20 |
21 | - [Installation](#installation)
22 | - [Usage](#usage)
23 | - [Customization](#customization)
24 | - [API](#api)
25 | - [Examples](#examples)
26 | - [License](#license)
27 |
28 | ## Installation
29 | ```bash
30 | npm install react-formalize --save
31 | ```
32 |
33 | ## Usage
34 | ```javascript
35 | import { Component } from 'react';
36 | import { Form, Text, Select } from 'react-formalize';
37 |
38 | export default class MyForm extends Component {
39 |
40 | handleSubmit(values) {
41 | console.info('Submit', values);
42 | // {
43 | // title: 'Lorem ipsum dolor ist',
44 | // category: 'news'
45 | // };
46 | }
47 |
48 | render() {
49 | const post = {
50 | title: 'Lorem ipsum dolor ist',
51 | category: 'news'
52 | };
53 |
54 | return (
55 |
76 | );
77 | }
78 | }
79 | ```
80 |
81 | ## API
82 |
83 | ### Primitives
84 |
85 | #### `
111 | ```
112 |
113 | #### ``
114 |
115 | Input component wrapper, connects to `Form` component, receives and propagates data, **do not use directly**.
116 |
117 | ##### Props
118 |
119 | * `name`: *(String)* name of the input field
120 | * `value`: *(Array|Boolean|Number|Object|String)* value of the input field
121 | * `serialize`: *(Function)* function that extracts the input's data from the change event
122 | * `children`: *(Component)* children components
123 |
124 | **[Input component source](https://github.com/malte-wessel/react-formalize/blob/master/src/components/Form.js)**
125 |
126 | ##### Example
127 | ```javascript
128 | import React, { PropTypes, Component } from 'react';
129 | import { Input } from 'react-formalize';
130 |
131 | export default class MyCustomTextField extends Component {
132 |
133 | renderInput(props) {
134 | return ;
135 | }
136 |
137 | render() {
138 | return (
139 |
140 | {props => }
141 |
142 | );
143 | }
144 | }
145 |
146 | ```
147 |
148 | #### ``
149 |
150 | Message component, connects to `Form` component, receives messages
151 |
152 | ##### Props
153 |
154 | * `name`: *(String)* name of the related input field
155 | * `renderMessage`: *(Function)* render a custom message
156 | * `children`: *(Function)* children components
157 |
158 | **[Message component source](https://github.com/malte-wessel/react-formalize/blob/master/src/components/Form.js)**
159 |
160 | ##### Example
161 | ```javascript
162 |
166 | ```
167 |
168 | ### Build in input components
169 |
170 |
171 | #### ``
172 |
173 | Native text input component
174 |
175 | ##### Props
176 |
177 | * `name`: *(String)* name of the input field
178 | * `type`: *(String)* One of: `text`, `date`, `datetime`, `datetime-local`, `email`, `month`, `number`, `password`, `tel`, `time`, `search`, `url`, `week`. Default is text
179 |
180 | **[Text component source](https://github.com/malte-wessel/react-formalize/blob/master/src/components/inputs/Text.js)**
181 |
182 | ##### Example
183 | ```javascript
184 |
188 | ```
189 |
190 | * Text ([Source](https://github.com/malte-wessel/react-formalize/blob/master/src/components/inputs/Text.js))
191 | * TextArea ([Source](https://github.com/malte-wessel/react-formalize/blob/master/src/components/inputs/TextArea.js))
192 | * Checkbox ([Source](https://github.com/malte-wessel/react-formalize/blob/master/src/components/inputs/Checkbox.js))
193 | * RadioGroup ([Source](https://github.com/malte-wessel/react-formalize/blob/master/src/components/inputs/RadioGroup.js))
194 | * Select ([Source](https://github.com/malte-wessel/react-formalize/blob/master/src/components/inputs/Select.js))
195 |
196 | ## Examples
197 |
198 | Run the simple example:
199 | ```bash
200 | cd react-formalize
201 | npm install
202 | cd react-formalize/examples/simple
203 | npm install
204 | npm start
205 | ```
206 |
207 | ## License
208 |
209 | MIT
210 |
--------------------------------------------------------------------------------
/examples/bootstrap/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { createClass } from 'react';
2 |
3 | import { Text, Select, TextArea } from 'react-formalize';
4 |
5 | import Form from './bootstrap/Form';
6 | import FormGroup from './bootstrap/FormGroup';
7 | import Checkbox from './bootstrap/Checkbox';
8 | import RadioGroup from './bootstrap/RadioGroup';
9 |
10 | export default createClass({
11 |
12 | displayName: 'App',
13 |
14 | getInitialState() {
15 | return {
16 | messages: {},
17 | saving: false,
18 | values: {
19 | categories: ['articles', 'react']
20 | }
21 | };
22 | },
23 |
24 | onChange(values) {
25 | this.setState({ values });
26 | console.info('onChange', values);
27 | },
28 |
29 | onSubmit(values) {
30 | console.info('onSubmit', values);
31 | const messages = this.validate(values);
32 | let saving = false;
33 | if (!messages) {
34 | saving = true;
35 | setTimeout(() => this.setState({ saving: false }), 2000);
36 | }
37 | this.setState({ messages, saving });
38 | },
39 |
40 | validate(values) {
41 | const { title, categories, text } = values;
42 | const errors = {};
43 | if (!title) errors.title = 'Title is required';
44 | if (categories.length < 1) errors.categories = 'Please select at least one category';
45 | if (!text) errors.text = 'Text is required';
46 |
47 | if (Object.keys(errors).length > 0) {
48 | return errors;
49 | }
50 | return undefined;
51 | },
52 |
53 | render() {
54 | const { values, messages, saving } = this.state;
55 | const categories = {
56 | articles: 'Articles',
57 | react: 'React',
58 | reactNative: 'React Native',
59 | flux: 'Flux',
60 | bootstrap: 'Bootstrap'
61 | };
62 | const ads = {
63 | no: 'Don\'t show ads',
64 | yes: 'Shou ads'
65 | };
66 | return (
67 |
68 |
69 |
70 |
71 |
Bootstrap Form Example
72 |
73 |
104 |
105 |
106 |
107 | );
108 | }
109 | });
110 |
--------------------------------------------------------------------------------
/examples/bootstrap/components/bootstrap/Checkbox.js:
--------------------------------------------------------------------------------
1 | import { Checkbox } from 'react-formalize';
2 | import React, { createClass, PropTypes } from 'react';
3 |
4 | export default createClass({
5 |
6 | displayName: 'Checkbox',
7 |
8 | propTypes: {
9 | name: PropTypes.string.isRequired,
10 | label: PropTypes.string.isRequired
11 | },
12 |
13 | render() {
14 | const { name, label, ...props } = this.props;
15 | return (
16 |
17 |
20 |
21 | );
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/examples/bootstrap/components/bootstrap/Form.js:
--------------------------------------------------------------------------------
1 | import { createForm } from 'react-formalize';
2 | import React, { createClass, PropTypes } from 'react';
3 |
4 | const Form = createClass({
5 |
6 | displayName: 'Form',
7 |
8 | propTypes: {
9 | messages: PropTypes.object,
10 | children: PropTypes.node,
11 | },
12 |
13 | render() {
14 | const { messages, children, ...props } = this.props;
15 | return (
16 |
24 | );
25 | }
26 | });
27 |
28 | export default createForm(Form);
29 |
--------------------------------------------------------------------------------
/examples/bootstrap/components/bootstrap/FormGroup.js:
--------------------------------------------------------------------------------
1 | import { connectMessage } from 'react-formalize';
2 | import React, { createClass, PropTypes } from 'react';
3 |
4 | const FormGroup = createClass({
5 |
6 | displayName: 'FormGroup',
7 |
8 | propTypes: {
9 | name: PropTypes.string,
10 | message: PropTypes.string,
11 | label: PropTypes.string,
12 | children: PropTypes.node
13 | },
14 |
15 | render() {
16 | const { name, message, label, children, ...props } = this.props;
17 | return (
18 |
19 |
20 |
21 | {children}
22 | {message
23 | ? {message}
24 | : null
25 | }
26 |
27 |
28 | );
29 | }
30 | });
31 |
32 | export default connectMessage(FormGroup);
33 |
--------------------------------------------------------------------------------
/examples/bootstrap/components/bootstrap/RadioGroup.js:
--------------------------------------------------------------------------------
1 | import { RadioGroup } from 'react-formalize';
2 | import React, { createClass, PropTypes } from 'react';
3 |
4 | export default createClass({
5 |
6 | displayName: 'RadioGroup',
7 |
8 | propTypes: {
9 | name: PropTypes.string.isRequired,
10 | options: PropTypes.object.isRequired
11 | },
12 |
13 | renderOptions(options, Radio) {
14 | const children = [];
15 | for (const value in options) {
16 | if (!options.hasOwnProperty(value)) continue;
17 | const label = options[value];
18 | children.push(
19 |
20 |
25 |
26 | );
27 | }
28 | return {children}
;
29 | },
30 |
31 | render() {
32 | const { name, options, ...props } = this.props;
33 | return (
34 |
35 | {this.renderOptions.bind(this, options)}
36 |
37 | );
38 | }
39 | });
40 |
--------------------------------------------------------------------------------
/examples/bootstrap/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | react-formalize bootstrap example
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/bootstrap/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import App from './components/App';
4 |
5 | render(, document.getElementById('root'));
6 |
--------------------------------------------------------------------------------
/examples/bootstrap/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-formalize-example-bootstrap",
3 | "version": "0.1.0",
4 | "description": "Simple example",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "NODE_ENV=development node server.js",
8 | "build": "NODE_ENV=production ./node_modules/.bin/webpack",
9 | "build:pages": "npm run build && cp index.html ../../ && rm -rf ../../static && mv static ../../"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/malte-wessel/react-formalize.git"
14 | },
15 | "license": "MIT",
16 | "bugs": {
17 | "url": "https://github.com/malte-wessel/react-formalize/issues"
18 | },
19 | "homepage": "https://github.com/malte-wessel/react-formalize",
20 | "dependencies": {
21 | "react": "^15.0.0-rc.2",
22 | "react-dom": "^15.0.0-rc.2"
23 | },
24 | "devDependencies": {
25 | "babel-core": "^6.2.1",
26 | "babel-loader": "^6.2.0",
27 | "node-libs-browser": "^0.5.2",
28 | "webpack": "^1.9.11",
29 | "webpack-dev-server": "^1.9.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/examples/bootstrap/server.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var WebpackDevServer = require('webpack-dev-server');
3 | var config = require('./webpack.config');
4 |
5 | new WebpackDevServer(webpack(config), {
6 | publicPath: config.output.publicPath,
7 | watch: true,
8 | stats: {
9 | colors: true
10 | }
11 | }).listen(3000, 'localhost', function (err) {
12 | if (err) console.log(err);
13 | console.log('Listening at localhost:3000');
14 | });
15 |
--------------------------------------------------------------------------------
/examples/bootstrap/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | var entry = [];
5 | if(process.env.NODE_ENV === 'development') {
6 | entry.push(
7 | 'webpack-dev-server/client?http://localhost:3000'
8 | );
9 | }
10 |
11 | var plugins = [
12 | new webpack.NoErrorsPlugin()
13 | ];
14 |
15 | if(process.env.NODE_ENV === 'production') {
16 | plugins.push(
17 | new webpack.optimize.UglifyJsPlugin({
18 | compress: {
19 | warnings: false
20 | }
21 | })
22 | );
23 | }
24 |
25 | var loaders = [];
26 | if(process.env.NODE_ENV === 'development') {
27 | loaders.push({
28 | test: /\.js$/,
29 | loaders: ['babel'],
30 | exclude: /node_modules/,
31 | include: __dirname
32 | });
33 | } else {
34 | loaders.push({
35 | test: /\.js$/,
36 | loaders: ['babel'],
37 | exclude: /node_modules/,
38 | include: __dirname
39 | });
40 | }
41 |
42 | loaders.push({
43 | test: /\.js$/,
44 | loaders: ['babel'],
45 | include: path.join(__dirname, '..', '..', 'src')
46 | });
47 |
48 | module.exports = {
49 | devtool: 'eval',
50 | entry: entry.concat('./index'),
51 | output: {
52 | path: path.join(__dirname, 'static'),
53 | filename: 'bundle.js',
54 | publicPath: '/static/'
55 | },
56 | plugins: plugins,
57 | resolve: {
58 | alias: {
59 | 'react-formalize': path.join(__dirname, '..', '..', 'src')
60 | },
61 | extensions: ['', '.js']
62 | },
63 | module: {
64 | loaders: loaders
65 | },
66 | sassLoader: {
67 | includePaths: [path.resolve(__dirname, './node_modules')]
68 | }
69 | };
70 |
--------------------------------------------------------------------------------
/examples/simple/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { createClass } from 'react';
2 | import {
3 | Form,
4 | Text,
5 | TextArea,
6 | Checkbox,
7 | RadioGroup,
8 | Select
9 | } from 'react-formalize';
10 |
11 | export default createClass({
12 |
13 | displayName: 'App',
14 |
15 | getInitialState() {
16 | return {
17 | title: 'Hello World'
18 | };
19 | },
20 |
21 | handleChange(values) {
22 | this.setState(values);
23 | console.info('Change', values);
24 | },
25 |
26 | handleSubmit(values) {
27 | console.info('Submit', values);
28 | },
29 |
30 | render() {
31 | return (
32 |
33 |
Simple example
34 |
75 |
76 | );
77 | }
78 | });
79 |
--------------------------------------------------------------------------------
/examples/simple/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | react-formalize simple example
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/simple/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import App from './components/App';
4 |
5 | render(, document.getElementById('root'));
6 |
--------------------------------------------------------------------------------
/examples/simple/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-formalize-example-simple",
3 | "version": "0.1.0",
4 | "description": "Simple example",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "NODE_ENV=development node server.js",
8 | "build": "NODE_ENV=production ./node_modules/.bin/webpack",
9 | "build:pages": "npm run build && cp index.html ../../ && rm -rf ../../static && mv static ../../"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/malte-wessel/react-formalize.git"
14 | },
15 | "license": "MIT",
16 | "bugs": {
17 | "url": "https://github.com/malte-wessel/react-formalize/issues"
18 | },
19 | "homepage": "https://github.com/malte-wessel/react-formalize",
20 | "dependencies": {
21 | "react": "^15.0.0-rc.2",
22 | "react-dom": "^15.0.0-rc.2"
23 | },
24 | "devDependencies": {
25 | "babel-core": "^6.2.1",
26 | "babel-loader": "^6.2.0",
27 | "node-libs-browser": "^0.5.2",
28 | "webpack": "^1.9.11",
29 | "webpack-dev-server": "^1.9.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/examples/simple/server.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var WebpackDevServer = require('webpack-dev-server');
3 | var config = require('./webpack.config');
4 |
5 | new WebpackDevServer(webpack(config), {
6 | publicPath: config.output.publicPath,
7 | watch: true,
8 | stats: {
9 | colors: true
10 | }
11 | }).listen(3000, 'localhost', function (err) {
12 | if (err) console.log(err);
13 | console.log('Listening at localhost:3000');
14 | });
15 |
--------------------------------------------------------------------------------
/examples/simple/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | var entry = [];
5 | if(process.env.NODE_ENV === 'development') {
6 | entry.push(
7 | 'webpack-dev-server/client?http://localhost:3000'
8 | );
9 | }
10 |
11 | var plugins = [
12 | new webpack.NoErrorsPlugin()
13 | ];
14 |
15 | if(process.env.NODE_ENV === 'production') {
16 | plugins.push(
17 | new webpack.optimize.UglifyJsPlugin({
18 | compress: {
19 | warnings: false
20 | }
21 | })
22 | );
23 | }
24 |
25 | var loaders = [];
26 | if(process.env.NODE_ENV === 'development') {
27 | loaders.push({
28 | test: /\.js$/,
29 | loaders: ['babel'],
30 | exclude: /node_modules/,
31 | include: __dirname
32 | });
33 | } else {
34 | loaders.push({
35 | test: /\.js$/,
36 | loaders: ['babel'],
37 | exclude: /node_modules/,
38 | include: __dirname
39 | });
40 | }
41 |
42 | loaders.push({
43 | test: /\.js$/,
44 | loaders: ['babel'],
45 | include: path.join(__dirname, '..', '..', 'src')
46 | });
47 |
48 | module.exports = {
49 | devtool: 'eval',
50 | entry: entry.concat('./index'),
51 | output: {
52 | path: path.join(__dirname, 'static'),
53 | filename: 'bundle.js',
54 | publicPath: '/static/'
55 | },
56 | plugins: plugins,
57 | resolve: {
58 | alias: {
59 | 'react-formalize': path.join(__dirname, '..', '..', 'src')
60 | },
61 | extensions: ['', '.js']
62 | },
63 | module: {
64 | loaders: loaders
65 | },
66 | sassLoader: {
67 | includePaths: [path.resolve(__dirname, './node_modules')]
68 | }
69 | };
70 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | /* eslint no-var: 0, no-unused-vars: 0 */
2 | var path = require('path');
3 | var webpack = require('webpack');
4 | var runCoverage = process.env.COVERAGE === 'true';
5 |
6 | var coverageLoaders = [];
7 | var coverageReporters = [];
8 |
9 | if (runCoverage) {
10 | coverageLoaders.push({
11 | test: /\.js$/,
12 | include: path.resolve('src/'),
13 | loader: 'isparta'
14 | });
15 | coverageReporters.push('coverage');
16 | }
17 |
18 | module.exports = function karmaConfig(config) {
19 | config.set({
20 | browsers: ['Chrome'],
21 | singleRun: true,
22 | frameworks: ['mocha'],
23 | files: ['./test.js'],
24 | preprocessors: {
25 | './test.js': ['webpack', 'sourcemap']
26 | },
27 | reporters: ['mocha'].concat(coverageReporters),
28 | webpack: {
29 | devtool: 'inline-source-map',
30 | resolve: {
31 | alias: {
32 | 'react-formalize': path.resolve(__dirname, './src')
33 | }
34 | },
35 | module: {
36 | loaders: [{
37 | test: /\.js$/,
38 | loader: 'babel',
39 | exclude: /(node_modules)/
40 | }].concat(coverageLoaders)
41 | }
42 | },
43 | coverageReporter: {
44 | dir: 'coverage/',
45 | reporters: [
46 | { type: 'html', subdir: 'report-html' },
47 | { type: 'text', subdir: '.', file: 'text.txt' },
48 | { type: 'text-summary', subdir: '.', file: 'text-summary.txt' },
49 | ]
50 | }
51 | });
52 | };
53 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-formalize",
3 | "version": "2.0.0-beta.3",
4 | "description": "Serialize react forms",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "clean": "rimraf lib dist",
8 | "build": "babel src --out-dir lib",
9 | "build:umd": "NODE_ENV=development webpack src/index.js dist/react-formalize.js",
10 | "build:umd:min": "NODE_ENV=production webpack src/index.js dist/react-formalize.min.js",
11 | "lint": "eslint src test examples",
12 | "test": "NODE_ENV=test karma start",
13 | "test:watch": "NODE_ENV=test karma start --auto-watch --no-single-run",
14 | "test:cov": "NODE_ENV=test COVERAGE=true karma start --single-run",
15 | "prepublish": "npm run lint && npm run test && npm run clean && npm run build && npm run build:umd && npm run build:umd:min"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/malte-wessel/react-formalize.git"
20 | },
21 | "keywords": [
22 | "react",
23 | "form",
24 | "serialze",
25 | "submit"
26 | ],
27 | "author": "Malte Wessel",
28 | "license": "MIT",
29 | "bugs": {
30 | "url": "https://github.com/malte-wessel/react-formalize/issues"
31 | },
32 | "homepage": "https://github.com/malte-wessel/react-formalize",
33 | "devDependencies": {
34 | "babel-cli": "^6.2.0",
35 | "babel-core": "^6.2.1",
36 | "babel-eslint": "4.*",
37 | "babel-loader": "^6.2.0",
38 | "babel-preset-es2015": "^6.1.18",
39 | "babel-preset-react": "^6.3.13",
40 | "babel-preset-stage-1": "^6.1.18",
41 | "babel-register": "^6.3.13",
42 | "babel-runtime": "^6.3.19",
43 | "eslint": "^1.8.0",
44 | "eslint-config-airbnb": "^5.0.0",
45 | "eslint-plugin-react": "^3.5.1",
46 | "expect": "^1.16.0",
47 | "isparta-loader": "^2.0.0",
48 | "karma": "^0.13.10",
49 | "karma-chrome-launcher": "^0.2.1",
50 | "karma-cli": "^0.1.1",
51 | "karma-coverage": "^0.5.3",
52 | "karma-mocha": "^0.2.0",
53 | "karma-mocha-reporter": "^1.0.3",
54 | "karma-sourcemap-loader": "^0.3.6",
55 | "karma-webpack": "^1.6.0",
56 | "mocha": "^2.2.5",
57 | "react": "^15.0.0",
58 | "react-addons-test-utils": "^15.0.0",
59 | "react-addons-update": "^15.0.0",
60 | "react-dom": "^15.0.0",
61 | "rimraf": "^2.3.4",
62 | "webpack": "^1.9.6",
63 | "webpack-dev-server": "^1.8.2"
64 | },
65 | "peerDependencies": {
66 | "react": "^15.0.0",
67 | "react-dom": "^15.0.0",
68 | "react-addons-update": "^15.0.0"
69 | },
70 | "dependencies": {
71 | "invariant": "^2.0.0",
72 | "object-path": "^0.9.2"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/Checkbox.js:
--------------------------------------------------------------------------------
1 | import React, { createClass, PropTypes } from 'react';
2 | import connectCheckbox from '../hoc/connectCheckbox';
3 |
4 | const Checkbox = createClass({
5 | displayName: 'Checkbox',
6 | propTypes: {
7 | name: PropTypes.string.isRequired,
8 | checked: PropTypes.bool.isRequired,
9 | onChange: PropTypes.func.isRequired
10 | },
11 | getDefaultProps() {
12 | return { type: 'text' };
13 | },
14 | render() {
15 | return (
16 |
19 | );
20 | }
21 | });
22 |
23 | export default connectCheckbox(Checkbox);
24 |
--------------------------------------------------------------------------------
/src/components/Form.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import createForm from '../hoc/createForm';
3 |
4 | const Form = props => ;
5 | export default createForm(Form);
6 |
--------------------------------------------------------------------------------
/src/components/Message.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, createClass } from 'react';
2 | import connectMessage from '../hoc/connectMessage';
3 |
4 | const Message = createClass({
5 | displayName: 'Message',
6 | propTypes: {
7 | message: PropTypes.node
8 | },
9 | render() {
10 | const { message, ...props } = this.props;
11 | return {message}
;
12 | }
13 | });
14 |
15 | export default connectMessage(Message);
16 |
--------------------------------------------------------------------------------
/src/components/RadioGroup.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, createClass } from 'react';
2 | import connectInput from '../hoc/connectInput';
3 |
4 | const createRadio = (name, selectedValue, disabled, onChange) => createClass({
5 | displayName: 'Radio',
6 | propTypes: {
7 | value: PropTypes.string.isRequired
8 | },
9 | handleChange() {
10 | const { value } = this.props;
11 | onChange(value);
12 | },
13 | render() {
14 | const { value } = this.props;
15 | return (
16 |
23 | );
24 | }
25 | });
26 |
27 | const RadioGroup = createClass({
28 | displayName: 'RadioGroup',
29 | propTypes: {
30 | name: PropTypes.string.isRequired,
31 | value: PropTypes.string.isRequired,
32 | options: PropTypes.object,
33 | disabled: PropTypes.bool,
34 | onChange: PropTypes.func.isRequired,
35 | children: PropTypes.func,
36 | },
37 | renderOptions(Radio, options) {
38 | const children = [];
39 | for (const value in options) {
40 | if (!options.hasOwnProperty(value)) continue;
41 | const label = options[value];
42 | children.push(
43 |
44 | {label}
45 |
46 | );
47 | }
48 | return children;
49 | },
50 | render() {
51 | const { name, options, value, disabled, onChange, children, ...props } = this.props;
52 | const Radio = createRadio(name, value, disabled, onChange);
53 | if (options) return {this.renderOptions(Radio, options)}
;
54 | return children(Radio, props);
55 | }
56 | });
57 |
58 | const serialize = value => value;
59 |
60 | export default connectInput(
61 | RadioGroup,
62 | { serialize }
63 | );
64 |
--------------------------------------------------------------------------------
/src/components/Select.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, createClass } from 'react';
2 | import connectSelect from '../hoc/connectSelect';
3 |
4 | const defaultRenderOption = props => ;
5 |
6 | const Select = createClass({
7 |
8 | displayName: 'Select',
9 |
10 | propTypes: {
11 | name: PropTypes.string.isRequired,
12 | options: PropTypes.object,
13 | value: PropTypes.oneOfType([
14 | PropTypes.string,
15 | PropTypes.number,
16 | PropTypes.array
17 | ]).isRequired,
18 | multiple: PropTypes.bool,
19 | placeholder: PropTypes.any,
20 | renderOption: PropTypes.func,
21 | onChange: PropTypes.func.isRequired,
22 | children: PropTypes.node,
23 | },
24 |
25 | getDefaultProps() {
26 | return {
27 | renderOption: defaultRenderOption
28 | };
29 | },
30 |
31 | renderOptions(options, multiple, placeholder) {
32 | const { renderOption } = this.props;
33 | const children = [];
34 |
35 | if (!multiple && placeholder) {
36 | children.push(
37 | renderOption({
38 | key: 'placeholder',
39 | value: '',
40 | disabled: true,
41 | children: placeholder
42 | })
43 | );
44 | }
45 |
46 | for (const value in options) {
47 | if (!options.hasOwnProperty(value)) continue;
48 | const label = options[value];
49 | children.push(
50 | renderOption({
51 | key: value,
52 | value,
53 | children: label
54 | })
55 | );
56 | }
57 | return children;
58 | },
59 |
60 | render() {
61 | const {
62 | name,
63 | options,
64 | value,
65 | multiple,
66 | placeholder,
67 | renderOption,
68 | onChange,
69 | children,
70 | ...props
71 | } = this.props;
72 |
73 | let finalValue = value;
74 | if (placeholder && !value) {
75 | // Set empty string as default value.
76 | // This will show up the placeholder option, when no value is set.
77 | finalValue = '';
78 | }
79 |
80 | return (
81 |
90 | );
91 | }
92 | });
93 |
94 | export default connectSelect(Select);
95 |
--------------------------------------------------------------------------------
/src/components/Text.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, createClass } from 'react';
2 | import connectInput from '../hoc/connectInput';
3 |
4 | const types = [
5 | 'date',
6 | 'datetime',
7 | 'datetime-local',
8 | 'email',
9 | 'month',
10 | 'number',
11 | 'password',
12 | 'tel',
13 | 'text',
14 | 'time',
15 | 'search',
16 | 'url',
17 | 'week'
18 | ];
19 |
20 | const Text = createClass({
21 | displayName: 'Text',
22 | propTypes: {
23 | type: PropTypes.oneOf(types),
24 | name: PropTypes.string.isRequired,
25 | value: PropTypes.string.isRequired,
26 | onChange: PropTypes.func.isRequired,
27 | },
28 | getDefaultProps() {
29 | return { type: 'text' };
30 | },
31 | render() {
32 | const { onChange } = this.props;
33 | return (
34 |
38 | );
39 | }
40 | });
41 |
42 | export default connectInput(Text);
43 |
--------------------------------------------------------------------------------
/src/components/TextArea.js:
--------------------------------------------------------------------------------
1 | import React, { createClass, PropTypes } from 'react';
2 | import connectInput from '../hoc/connectInput';
3 |
4 | const TextArea = createClass({
5 | displayName: 'TextArea',
6 | propTypes: {
7 | name: PropTypes.string.isRequired,
8 | value: PropTypes.string.isRequired,
9 | onChange: PropTypes.func.isRequired,
10 | },
11 | render() {
12 | const { onChange } = this.props;
13 | return (
14 |
18 | );
19 | }
20 | });
21 |
22 | export default connectInput(TextArea);
23 |
--------------------------------------------------------------------------------
/src/hoc/connect.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, createClass } from 'react';
2 | import formShape from '../utils/formShape';
3 | import shallowEqual from '../utils/shallowEqual';
4 |
5 | function defaultMapFormPropsToState(wrapperProps, formProps) {
6 | const { disabled } = formProps;
7 | return { disabled };
8 | }
9 |
10 | function defaultMapStateToProps(state) {
11 | return state;
12 | }
13 |
14 | export default function connect(Component, options = {}) {
15 | const {
16 | mapFormPropsToState = defaultMapFormPropsToState,
17 | mapStateToProps = defaultMapStateToProps,
18 | pure = true
19 | } = options;
20 |
21 | return createClass({
22 |
23 | displayName: 'Connected',
24 |
25 | propTypes: {
26 | name: PropTypes.string
27 | },
28 |
29 | contextTypes: {
30 | form: formShape
31 | },
32 |
33 | getInitialState() {
34 | return this.getStateFromForm();
35 | },
36 |
37 | componentWillMount() {
38 | // @TODO: Check for value property and warn if a value was given
39 | const { form } = this.context;
40 | const { subscribe } = form;
41 | this.unsubscribe = subscribe(this.handleFormDataChange);
42 | },
43 |
44 | componentWillReceiveProps(nextProps) {
45 | const { name: prevName } = this.props;
46 | const { name: nextName } = nextProps;
47 | if (!prevName || prevName === nextName) return;
48 | const nextState = this.getStateFromForm(nextProps);
49 | this.setState(nextState);
50 | },
51 |
52 | shouldComponentUpdate(nextProps, nextState) {
53 | if (!pure) return true;
54 | return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
55 | },
56 |
57 | componentWillUnmount() {
58 | this.unsubscribe();
59 | },
60 |
61 | getStateFromForm(wrapperProps = this.props, formProps = this.context.form.getFormProps()) {
62 | return mapFormPropsToState(wrapperProps, formProps);
63 | },
64 |
65 | setStateFromForm(wrapperProps, formProps) {
66 | const nextState = this.getStateFromForm(wrapperProps, formProps);
67 | this.setState(nextState);
68 | },
69 |
70 | handleFormDataChange(formProps) {
71 | this.setStateFromForm(this.props, formProps);
72 | },
73 |
74 | render() {
75 | const state = mapStateToProps(this.state, this.props);
76 | const props = this.props;
77 | return ;
78 | }
79 | });
80 | }
81 |
--------------------------------------------------------------------------------
/src/hoc/connectCheckbox.js:
--------------------------------------------------------------------------------
1 | import connectInput from './connectInput';
2 |
3 | const serialize = event => {
4 | const target = event.target;
5 | const { checked } = target;
6 | return !!checked;
7 | };
8 |
9 | const mapStateToProps = ({ value, disabled }) => ({
10 | value: true,
11 | checked: !!value,
12 | disabled
13 | });
14 |
15 | export default function connectCheckbox(Component, options = {}) {
16 | return connectInput(Component, {
17 | serialize,
18 | mapStateToProps,
19 | ...options
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/hoc/connectInput.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, createClass } from 'react';
2 | import { get as getPath } from 'object-path';
3 | import formShape from '../utils/formShape';
4 | import connect from './connect';
5 |
6 | function defaultSerialize(event) {
7 | const target = event.target;
8 | const { value } = target;
9 | return value;
10 | }
11 |
12 | function defaultMapFormPropsToState(wrapperProps, formProps) {
13 | const { values, disabled } = formProps;
14 | const { name } = wrapperProps;
15 | const value = getPath(values, name) || '';
16 | return { value, disabled };
17 | }
18 |
19 | export default function connectInput(Component, options = {}) {
20 | const {
21 | serialize = defaultSerialize,
22 | mapFormPropsToState = defaultMapFormPropsToState,
23 | ...restOptions
24 | } = options;
25 |
26 | const Wrapper = createClass({
27 |
28 | displayName: 'ConnectedInput',
29 |
30 | propTypes: {
31 | name: PropTypes.string.isRequired
32 | },
33 |
34 | contextTypes: {
35 | form: formShape
36 | },
37 |
38 | handleChange(...args) {
39 | const { name } = this.props;
40 | const { form } = this.context;
41 | const { handleChange } = form;
42 | const value = serialize(...args);
43 | handleChange(name, value);
44 | },
45 |
46 | render() {
47 | return (
48 |
51 | );
52 | }
53 | });
54 |
55 | return connect(Wrapper, {
56 | mapFormPropsToState,
57 | ...restOptions
58 | });
59 | }
60 |
--------------------------------------------------------------------------------
/src/hoc/connectMessage.js:
--------------------------------------------------------------------------------
1 | import { get as getPath } from 'object-path';
2 | import connect from './connect';
3 |
4 | const mapFormPropsToState = (wrapperProps, formProps) => {
5 | const { messages, disabled } = formProps;
6 | const { name } = wrapperProps;
7 | const message = name && messages && (getPath(messages, name) || messages[name]);
8 | return { message, disabled };
9 | };
10 |
11 | export default function connectMessage(Component, options = {}) {
12 | return connect(Component, {
13 | mapFormPropsToState,
14 | ...options
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/src/hoc/connectSelect.js:
--------------------------------------------------------------------------------
1 | import connectInput from './connectInput';
2 |
3 | const serialize = event => {
4 | const target = event.target;
5 | const { value, type } = target;
6 | if (type === 'select-multiple') {
7 | const values = [];
8 | const { options } = target;
9 | for (let i = 0, l = options.length; i < l; i++) {
10 | const option = options[i];
11 | if (option.selected) values.push(option.value);
12 | }
13 | return values;
14 | }
15 | return value;
16 | };
17 |
18 | const mapStateToProps = ({ value, disabled }, { multiple }) => ({
19 | value: !value && multiple ? [] : value,
20 | disabled
21 | });
22 |
23 | export default function connectSelect(Component, options = {}) {
24 | return connectInput(Component, {
25 | serialize,
26 | mapStateToProps,
27 | ...options
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/src/hoc/createForm.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, createClass } from 'react';
2 | import update from 'react-addons-update';
3 | import formShape from '../utils/formShape';
4 | import makePath from '../utils/makePath';
5 |
6 | function cleanProps(props) {
7 | const { children, ...rest } = props;
8 | return rest;
9 | }
10 |
11 | function defaultUpdateValue(name, value, values) {
12 | const mutation = makePath(`${name}.$set`, value);
13 | return update(values, mutation);
14 | }
15 |
16 | export default function createFormProvider(Component, options = {}) {
17 | const {
18 | updateValue = defaultUpdateValue
19 | } = options;
20 | return createClass({
21 |
22 | displayName: 'FormProvider',
23 |
24 | propTypes: {
25 | values: PropTypes.object.isRequired,
26 | messages: PropTypes.object,
27 | disabled: PropTypes.bool,
28 | onChange: PropTypes.func.isRequired,
29 | onSubmit: PropTypes.func
30 | },
31 |
32 | childContextTypes: {
33 | form: formShape
34 | },
35 |
36 | getDefaultProps() {
37 | return {
38 | messages: {},
39 | disabled: false
40 | };
41 | },
42 |
43 | getChildContext() {
44 | return {
45 | form: {
46 | subscribe: this.subscribe,
47 | handleChange: this.handleInputChange,
48 | getFormProps: this.getFormProps
49 | }
50 | };
51 | },
52 |
53 | componentWillMount() {
54 | this.listeners = [];
55 | },
56 |
57 | componentWillUpdate(nextProps) {
58 | this.notify(nextProps);
59 | },
60 |
61 | componentWillUnmount() {
62 | this.listeners = [];
63 | },
64 |
65 | getFormProps() {
66 | return cleanProps(this.props);
67 | },
68 |
69 | handleInputChange(name, value) {
70 | const { values, onChange } = this.props;
71 | const nextValues = updateValue(name, value, values);
72 | if (onChange) onChange(nextValues);
73 | },
74 |
75 | subscribe(listener) {
76 | this.listeners.push(listener);
77 | return () => {
78 | const index = this.listeners.indexOf(listener);
79 | this.listeners.splice(index, 1);
80 | };
81 | },
82 |
83 | notify(props) {
84 | const cleanedProps = cleanProps(props);
85 | this.listeners.forEach(listener => listener(cleanedProps));
86 | },
87 |
88 | handleSubmit(event) {
89 | const { onSubmit, values } = this.props;
90 | if (onSubmit) {
91 | event.preventDefault();
92 | onSubmit(values);
93 | }
94 | },
95 |
96 | render() {
97 | const {
98 | values,
99 | messages,
100 | disabled,
101 | onChange,
102 | onSubmit,
103 | ...props
104 | } = this.props;
105 | return (
106 |
109 | );
110 | }
111 | });
112 | }
113 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import connect from './hoc/connect';
2 | import connectInput from './hoc/connectInput';
3 | import connectSelect from './hoc/connectSelect';
4 | import connectCheckbox from './hoc/connectCheckbox';
5 | import connectMessage from './hoc/connectMessage';
6 | import createForm from './hoc/createForm';
7 |
8 | import Form from './components/Form';
9 | import Message from './components/Message';
10 | import Text from './components/Text';
11 | import TextArea from './components/TextArea';
12 | import Checkbox from './components/Checkbox';
13 | import Select from './components/Select';
14 | import RadioGroup from './components/RadioGroup';
15 |
16 | export {
17 | // higher order functions
18 | connect,
19 | connectInput,
20 | connectSelect,
21 | connectCheckbox,
22 | connectMessage,
23 | createForm,
24 | // components
25 | Form,
26 | Message,
27 | Text,
28 | TextArea,
29 | Checkbox,
30 | Select,
31 | RadioGroup
32 | };
33 |
--------------------------------------------------------------------------------
/src/utils/formShape.js:
--------------------------------------------------------------------------------
1 | import { PropTypes } from 'react';
2 |
3 | export default PropTypes.shape({
4 | subscribe: PropTypes.func.isRequired,
5 | handleChange: PropTypes.func.isRequired,
6 | getFormProps: PropTypes.func.isRequired
7 | }).isRequired;
8 |
--------------------------------------------------------------------------------
/src/utils/makePath.js:
--------------------------------------------------------------------------------
1 | export default function makePath(path, value) {
2 | const fragments = path.split('.');
3 | const obj = {};
4 | let tmp = obj;
5 |
6 | for (let i = 0, l = fragments.length; i < l; i++) {
7 | const fragment = fragments[i];
8 | tmp[fragment] = i === l - 1 ? value : {};
9 | tmp = tmp[fragment];
10 | }
11 |
12 | return obj;
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/shallowEqual.js:
--------------------------------------------------------------------------------
1 | export default function shallowEqual(objA, objB) {
2 | if (objA === objB) {
3 | return true;
4 | }
5 |
6 | if (typeof objA !== 'object' || objA === null ||
7 | typeof objB !== 'object' || objB === null) {
8 | return false;
9 | }
10 |
11 | const keysA = Object.keys(objA);
12 | const keysB = Object.keys(objB);
13 |
14 | if (keysA.length !== keysB.length) {
15 | return false;
16 | }
17 |
18 | const bHasOwnProperty = hasOwnProperty.bind(objB);
19 | for (let i = 0, l = keysA.length; i < l; i++) {
20 | if (!bHasOwnProperty(keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
21 | return false;
22 | }
23 | }
24 |
25 | return true;
26 | }
27 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | window.expect = expect;
3 | window.createSpy = expect.createSpy;
4 | window.spyOn = expect.spyOn;
5 | window.isSpy = expect.isSpy;
6 |
7 | const context = require.context('./test', true, /\.spec\.js$/);
8 | context.keys().forEach(context);
9 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "describe": true,
4 | "it": true,
5 | "expect": true,
6 | "before": true,
7 | "beforeEach": true,
8 | "after": true,
9 | "afterEach": true,
10 | "createSpy": true,
11 | "spyOn": true,
12 | "isSpy": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/test/components/Checkbox.spec.js:
--------------------------------------------------------------------------------
1 | import React, { createClass } from 'react';
2 | import { findDOMNode } from 'react-dom';
3 | import {
4 | renderIntoDocument,
5 | findRenderedComponentWithType,
6 | scryRenderedComponentsWithType,
7 | scryRenderedDOMComponentsWithTag,
8 | Simulate
9 | } from 'react/lib/ReactTestUtils';
10 |
11 | import Form from '../../src/components/Form';
12 | import Checkbox from '../../src/components/Checkbox';
13 |
14 | const noop = () => {};
15 |
16 | describe('Checkbox', () => {
17 | describe('when rendering for the first time', () => {
18 | it('should take given value', done => {
19 | const values = { foo: true, qux: false };
20 | const tree = renderIntoDocument(
21 |
27 | );
28 |
29 | const inputs = scryRenderedComponentsWithType(tree, Checkbox);
30 | const $foo = findDOMNode(inputs[0]);
31 | const $qux = findDOMNode(inputs[1]);
32 | expect($foo.value).toEqual('true');
33 | expect($foo.checked).toEqual(true);
34 | expect($qux.value).toEqual('true');
35 | expect($qux.checked).toEqual(false);
36 | done();
37 | });
38 | });
39 | describe('when values change', () => {
40 | it('should update value', done => {
41 | const Wrapper = createClass({
42 | getInitialState() {
43 | return {
44 | foo: true,
45 | qux: false
46 | };
47 | },
48 | render() {
49 | return (
50 |
56 | );
57 | }
58 | });
59 | const tree = renderIntoDocument();
60 | const wrapper = findRenderedComponentWithType(tree, Wrapper);
61 | const inputs = scryRenderedComponentsWithType(tree, Checkbox);
62 | const $foo = findDOMNode(inputs[0]);
63 | const $qux = findDOMNode(inputs[1]);
64 | expect($foo.value).toEqual('true');
65 | expect($foo.checked).toEqual(true);
66 | expect($qux.value).toEqual('true');
67 | expect($qux.checked).toEqual(false);
68 |
69 | wrapper.setState({ qux: true }, () => {
70 | expect($foo.checked).toEqual(true);
71 | expect($qux.checked).toEqual(true);
72 | done();
73 | });
74 | });
75 | });
76 | describe('when input changes', () => {
77 | it('should propagate changes', done => {
78 | const spy = createSpy();
79 | const values = {
80 | foo: true,
81 | qux: false,
82 | doo: {
83 | foo: true
84 | }
85 | };
86 | const tree = renderIntoDocument(
87 |
94 | );
95 | const inputs = scryRenderedDOMComponentsWithTag(tree, 'input');
96 | const input = inputs[0];
97 | const $input = findDOMNode(input);
98 | Simulate.change($input, { target: { checked: false } });
99 |
100 | const nextValues = spy.calls[0].arguments[0];
101 | expect(spy.calls.length).toEqual(1);
102 | expect(nextValues).toEqual({
103 | foo: false,
104 | qux: false,
105 | doo: {
106 | foo: true
107 | }
108 | });
109 | expect(values === nextValues).toEqual(false);
110 | expect(values.doo === nextValues.doo).toEqual(true);
111 | done();
112 | });
113 | });
114 | describe('when form is disabled', () => {
115 | it('should be disabled too', done => {
116 | const values = { foo: true };
117 | const tree = renderIntoDocument(
118 |
124 | );
125 |
126 | const input = findRenderedComponentWithType(tree, Checkbox);
127 | expect(findDOMNode(input).disabled).toEqual(true);
128 | done();
129 | });
130 | });
131 | });
132 |
--------------------------------------------------------------------------------
/test/components/Form.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import { findDOMNode } from 'react-dom';
3 | import {
4 | renderIntoDocument,
5 | findRenderedComponentWithType,
6 | scryRenderedComponentsWithType,
7 | Simulate
8 | } from 'react/lib/ReactTestUtils';
9 |
10 | import Form from '../../src/components/Form';
11 | import Text from '../../src/components/inputs/Text';
12 |
13 | describe('Form', () => {
14 | it('should add form to child context', () => {
15 | class Child extends Component {
16 | static contextTypes = {
17 | form: PropTypes.any
18 | };
19 |
20 | render() {
21 | return ;
22 | }
23 | }
24 |
25 | const tree = renderIntoDocument(
26 |
31 | );
32 |
33 | const child = findRenderedComponentWithType(tree, Child);
34 | expect(child.context.form).toBeA('object');
35 | expect(child.context.form.getValue).toBeA('function');
36 | expect(child.context.form.setValue).toBeA('function');
37 | });
38 |
39 | it('should add the passed values to its state', () => {
40 | const values = { foo: 'bar' };
41 |
42 | const tree = renderIntoDocument(
43 |
59 | );
60 |
61 | const form = findRenderedComponentWithType(tree, Form);
62 | expect(form.values).toEqual({
63 | foo: 'faz',
64 | boo: 'baz',
65 | qux: {
66 | boo: 'bar',
67 | qoo: ''
68 | }
69 | });
70 | });
71 |
72 | it('should call `onChange` when a value changes', () => {
73 | let onChangeResult;
74 | function onChange(values) { onChangeResult = values; }
75 |
76 | const tree = renderIntoDocument(
77 |
81 | );
82 |
83 | const inputs = scryRenderedComponentsWithType(tree, Text);
84 | const input = findDOMNode(inputs[0]);
85 | Simulate.change(input, { target: { value: 'baz' } });
86 | expect(onChangeResult).toEqual({
87 | foo: 'baz',
88 | bar: 'foo'
89 | });
90 | });
91 |
92 | it('should not mutate the state when a value changes', () => {
93 | const tree = renderIntoDocument(
94 |
99 | );
100 |
101 | const form = findRenderedComponentWithType(tree, Form);
102 | const inputs = scryRenderedComponentsWithType(tree, Text);
103 | const input = findDOMNode(inputs[0]);
104 |
105 | const before = form.values;
106 | Simulate.change(input, { target: { value: 'baz' } });
107 | const after = form.values;
108 |
109 | expect(before === after).toBe(false);
110 | expect(before.qux === after.qux).toBe(true);
111 | });
112 |
113 | it('should propagate changes, when values property changes', (done) => {
114 | class Root extends Component {
115 | constructor(props, context) {
116 | super(props, context);
117 | this.state = {};
118 | }
119 | render() {
120 | return (
121 |
124 | );
125 | }
126 | }
127 |
128 | const tree = renderIntoDocument();
129 | const root = findRenderedComponentWithType(tree, Root);
130 | const form = findRenderedComponentWithType(tree, Form);
131 |
132 | root.setState({ foo: 'baz' }, () => {
133 | expect(form.values).toEqual({ foo: 'baz' });
134 | done();
135 | });
136 | });
137 |
138 | it('should support children as function', () => {
139 | const values = {
140 | foo: 'bar'
141 | };
142 | const tree = renderIntoDocument(
143 |
147 | );
148 |
149 | const input = findRenderedComponentWithType(tree, Text);
150 | const $input = findDOMNode(input);
151 | expect($input.value).toEqual('bar');
152 | });
153 |
154 | it('should reset registered inputs on rerendering', done => {
155 | class Root extends Component {
156 | constructor(props, context) {
157 | super(props, context);
158 | this.state = {};
159 | }
160 | render() {
161 | const { odd } = this.state;
162 |
163 | const values = odd
164 | ? { foo: 'bar', bar: 'doo' }
165 | : { qux: 'fax', bar: 'noo' };
166 |
167 | return (
168 |
175 | );
176 | }
177 | }
178 |
179 | const tree = renderIntoDocument();
180 | const root = findRenderedComponentWithType(tree, Root);
181 | const form = findRenderedComponentWithType(tree, Form);
182 | const inputs = scryRenderedComponentsWithType(tree, Text);
183 | const text1 = findDOMNode(inputs[0]);
184 | const text2 = findDOMNode(inputs[1]);
185 |
186 | expect(form.values).toEqual({ qux: 'fax', bar: 'noo' });
187 | expect(form.inputs).toEqual({ qux: 'fax2', bar: null });
188 | expect(text1.value).toEqual('fax');
189 | expect(text2.value).toEqual('noo');
190 |
191 | root.setState({ odd: true }, () => {
192 | expect(form.values).toEqual({ foo: 'bar', bar: 'doo' });
193 | expect(form.inputs).toEqual({ foo: 'bar2', bar: 'noo' });
194 | expect(text1.value).toEqual('bar');
195 | expect(text2.value).toEqual('doo');
196 | done();
197 | });
198 | });
199 | });
200 |
--------------------------------------------------------------------------------
/test/components/RadioGroup.spec.js:
--------------------------------------------------------------------------------
1 | import React, { createClass } from 'react';
2 | import { findDOMNode } from 'react-dom';
3 | import {
4 | renderIntoDocument,
5 | findRenderedComponentWithType,
6 | scryRenderedDOMComponentsWithTag,
7 | Simulate
8 | } from 'react/lib/ReactTestUtils';
9 |
10 | import Form from '../../src/components/Form';
11 | import RadioGroup from '../../src/components/RadioGroup';
12 |
13 | const noop = () => {};
14 |
15 | describe('RadioGroup', () => {
16 | describe('options', () => {
17 | it('should accept options property', done => {
18 | const values = {
19 | foo: 'bux'
20 | };
21 | const options = {
22 | bux: 'bar',
23 | qux: 'qax'
24 | };
25 | const tree = renderIntoDocument(
26 |
31 | );
32 |
33 | const inputs = scryRenderedDOMComponentsWithTag(tree, 'input');
34 | expect(inputs[0].name).toEqual('foo');
35 | expect(inputs[0].value).toEqual('bux');
36 | expect(inputs[1].name).toEqual('foo');
37 | expect(inputs[1].value).toEqual('qux');
38 | done();
39 | });
40 | it('should accept options as childs', done => {
41 | const values = {
42 | foo: 'bux'
43 | };
44 | const tree = renderIntoDocument(
45 |
57 | );
58 |
59 | const inputs = scryRenderedDOMComponentsWithTag(tree, 'input');
60 | expect(inputs[0].name).toEqual('foo');
61 | expect(inputs[0].value).toEqual('bux');
62 | expect(inputs[1].name).toEqual('foo');
63 | expect(inputs[1].value).toEqual('qux');
64 | done();
65 | });
66 | });
67 | describe('when rendering for the first time', () => {
68 | it('should take given value', done => {
69 | const values = {
70 | foo: 'bux'
71 | };
72 | const options = {
73 | bux: 'bar',
74 | qux: 'qax'
75 | };
76 | const tree = renderIntoDocument(
77 |
82 | );
83 |
84 | const inputs = scryRenderedDOMComponentsWithTag(tree, 'input');
85 | expect(inputs[0].checked).toEqual(true);
86 | expect(inputs[1].checked).toEqual(false);
87 | done();
88 | });
89 | });
90 | describe('when values change', () => {
91 | it('should update value', done => {
92 | const options = {
93 | bux: 'bar',
94 | qux: 'qax'
95 | };
96 | const Wrapper = createClass({
97 | getInitialState() {
98 | return {
99 | foo: 'bux'
100 | };
101 | },
102 | render() {
103 | return (
104 |
109 | );
110 | }
111 | });
112 | const tree = renderIntoDocument();
113 | const wrapper = findRenderedComponentWithType(tree, Wrapper);
114 | const inputs = scryRenderedDOMComponentsWithTag(tree, 'input');
115 | expect(inputs[0].checked).toEqual(true);
116 | expect(inputs[1].checked).toEqual(false);
117 |
118 | wrapper.setState({ foo: 'qux' }, () => {
119 | // Not sure why, but we need to grab the inputs again
120 | // in order to receive the actual checked state.
121 | // See: https://github.com/facebook/react/issues/6321
122 | const updatedInputs = scryRenderedDOMComponentsWithTag(tree, 'input');
123 | expect(updatedInputs[0].checked).toEqual(false);
124 | expect(updatedInputs[1].checked).toEqual(true);
125 | done();
126 | });
127 | });
128 | });
129 | describe('when input changes', () => {
130 | it('should propagate changes', done => {
131 | const spy = createSpy();
132 | const options = {
133 | bux: 'bar',
134 | qux: 'qax'
135 | };
136 | const values = {
137 | foo: 'bux',
138 | qux: 'qux',
139 | doo: {
140 | foo: 'bux'
141 | }
142 | };
143 | const tree = renderIntoDocument(
144 |
151 | );
152 | const inputs = scryRenderedDOMComponentsWithTag(tree, 'input');
153 | const input = inputs[1];
154 | const $input = findDOMNode(input);
155 | Simulate.change($input, { target: { checked: true } });
156 |
157 | const nextValues = spy.calls[0].arguments[0];
158 | expect(spy.calls.length).toEqual(1);
159 | expect(nextValues).toEqual({
160 | foo: 'qux',
161 | qux: 'qux',
162 | doo: {
163 | foo: 'bux'
164 | }
165 | });
166 | expect(values === nextValues).toEqual(false);
167 | expect(values.doo === nextValues.doo).toEqual(true);
168 | done();
169 | });
170 | });
171 | describe('when form is disabled', () => {
172 | it('should be disabled too', done => {
173 | const values = {
174 | foo: 'bux'
175 | };
176 | const options = {
177 | bux: 'bar',
178 | qux: 'qax'
179 | };
180 | const tree = renderIntoDocument(
181 |
187 | );
188 |
189 | const inputs = scryRenderedDOMComponentsWithTag(tree, 'input');
190 | expect(inputs[0].disabled).toEqual(true);
191 | expect(inputs[1].disabled).toEqual(true);
192 | done();
193 | });
194 | });
195 | });
196 |
--------------------------------------------------------------------------------
/test/components/Select.spec.js:
--------------------------------------------------------------------------------
1 | import React, { createClass } from 'react';
2 | import { findDOMNode } from 'react-dom';
3 | import {
4 | renderIntoDocument,
5 | findRenderedComponentWithType,
6 | findRenderedDOMComponentWithTag,
7 | scryRenderedDOMComponentsWithTag,
8 | Simulate
9 | } from 'react/lib/ReactTestUtils';
10 |
11 | import Form from '../../src/components/Form';
12 | import Select from '../../src/components/Select';
13 |
14 | const noop = () => {};
15 |
16 | describe('Select', () => {
17 | describe('when form is disabled', () => {
18 | it('should be disabled too', () => {
19 | const values = {
20 | foo: 'bux'
21 | };
22 | const options = {
23 | bux: 'bar',
24 | qux: 'qax'
25 | };
26 | const tree = renderIntoDocument(
27 |
33 | );
34 |
35 | const $select = findRenderedDOMComponentWithTag(tree, 'select');
36 | expect($select.disabled).toEqual(true);
37 | });
38 | });
39 |
40 | describe('when a placeholder is given', () => {
41 | it.skip('should add a placeholder option', () => {
42 | const tree = renderIntoDocument(
43 |
52 | );
53 | const options = scryRenderedDOMComponentsWithTag(tree, 'option');
54 | console.log(options);
55 | expect(options.length).toEqual(3);
56 | expect(options[0].value).toEqual('');
57 | expect(options[0].disabled).toEqual(true);
58 | });
59 | });
60 |
61 | describe('single select', () => {
62 | describe('options', () => {
63 | it('should accept options property', () => {
64 | const values = {
65 | foo: 'bux'
66 | };
67 | const options = {
68 | bux: 'bar',
69 | qux: 'qax'
70 | };
71 | const tree = renderIntoDocument(
72 |
77 | );
78 |
79 | const input = findRenderedComponentWithType(tree, Select);
80 | const $input = findDOMNode(input);
81 | expect($input.value).toEqual('bux');
82 | expect($input.childNodes[0].value).toEqual('bux');
83 | expect($input.childNodes[1].value).toEqual('qux');
84 | });
85 | it('should accept options as childs', () => {
86 | const values = {
87 | foo: 'qax'
88 | };
89 | const tree = renderIntoDocument(
90 |
98 | );
99 |
100 | const input = findRenderedDOMComponentWithTag(tree, 'select');
101 | const $input = findDOMNode(input);
102 | expect($input.value).toEqual('bux');
103 | expect($input.childNodes[0].value).toEqual('bux');
104 | expect($input.childNodes[1].value).toEqual('qux');
105 | });
106 | });
107 | describe('when rendering for the first time', () => {
108 | it('should take given value', done => {
109 | const values = {
110 | foo: 'bux'
111 | };
112 | const options = {
113 | bux: 'bar',
114 | qux: 'qax'
115 | };
116 | const tree = renderIntoDocument(
117 |
122 | );
123 |
124 | const $select = findRenderedDOMComponentWithTag(tree, 'select');
125 | const $options = scryRenderedDOMComponentsWithTag(tree, 'option');
126 | expect($select.value).toEqual('bux');
127 | expect($options[0].selected).toEqual(true);
128 | expect($options[1].selected).toEqual(false);
129 | done();
130 | });
131 | });
132 | describe('when values change', () => {
133 | it('should update value', done => {
134 | const options = {
135 | bux: 'bar',
136 | qux: 'qax'
137 | };
138 | const Wrapper = createClass({
139 | getInitialState() {
140 | return {
141 | foo: 'bux'
142 | };
143 | },
144 | render() {
145 | return (
146 |
151 | );
152 | }
153 | });
154 | const tree = renderIntoDocument();
155 | const wrapper = findRenderedComponentWithType(tree, Wrapper);
156 | const $options = scryRenderedDOMComponentsWithTag(tree, 'option');
157 | expect($options[0].selected).toEqual(true);
158 | expect($options[1].selected).toEqual(false);
159 |
160 | wrapper.setState({ foo: 'qux' }, () => {
161 | expect($options[0].selected).toEqual(false);
162 | expect($options[1].selected).toEqual(true);
163 | done();
164 | });
165 | });
166 | });
167 | describe('when input changes', () => {
168 | it('should propagate changes', done => {
169 | const spy = createSpy();
170 | const options = {
171 | bux: 'bar',
172 | qux: 'qax'
173 | };
174 | const values = {
175 | foo: 'bux',
176 | qux: 'qux',
177 | doo: {
178 | foo: 'bux'
179 | }
180 | };
181 | const tree = renderIntoDocument(
182 |
189 | );
190 | const selects = scryRenderedDOMComponentsWithTag(tree, 'select');
191 | const select = selects[0];
192 | const $select = findDOMNode(select);
193 | Simulate.change($select, { target: { value: 'qux' } });
194 |
195 | const nextValues = spy.calls[0].arguments[0];
196 | expect(spy.calls.length).toEqual(1);
197 | expect(nextValues).toEqual({
198 | foo: 'qux',
199 | qux: 'qux',
200 | doo: {
201 | foo: 'bux'
202 | }
203 | });
204 | expect(values === nextValues).toEqual(false);
205 | expect(values.doo === nextValues.doo).toEqual(true);
206 | done();
207 | });
208 | });
209 | });
210 |
211 | describe('multi select', () => {
212 | describe('options', () => {
213 | it('should accept options property', () => {
214 | const values = {
215 | foo: ['bux']
216 | };
217 | const options = {
218 | bux: 'bar',
219 | qux: 'qax'
220 | };
221 | const tree = renderIntoDocument(
222 |
230 | );
231 |
232 | const input = findRenderedComponentWithType(tree, Select);
233 | const $input = findDOMNode(input);
234 | expect($input.childNodes[0].value).toEqual('bux');
235 | expect($input.childNodes[0].selected).toEqual(true);
236 | expect($input.childNodes[1].value).toEqual('qux');
237 | expect($input.childNodes[1].selected).toEqual(false);
238 | });
239 | it('should accept options as childs', () => {
240 | const values = {
241 | foo: ['bux']
242 | };
243 | const tree = renderIntoDocument(
244 |
254 | );
255 |
256 | const input = findRenderedComponentWithType(tree, Select);
257 | const $input = findDOMNode(input);
258 | expect($input.childNodes[0].value).toEqual('bux');
259 | expect($input.childNodes[0].selected).toEqual(true);
260 | expect($input.childNodes[1].value).toEqual('qux');
261 | expect($input.childNodes[1].selected).toEqual(false);
262 | });
263 | });
264 | describe('when rendering for the first time', () => {
265 | it('should take given value', done => {
266 | const values = {
267 | foo: ['bux', 'qux']
268 | };
269 | const options = {
270 | bux: 'bar',
271 | qux: 'qax'
272 | };
273 | const tree = renderIntoDocument(
274 |
282 | );
283 | const $options = scryRenderedDOMComponentsWithTag(tree, 'option');
284 | expect($options[0].selected).toEqual(true);
285 | expect($options[1].selected).toEqual(true);
286 | done();
287 | });
288 | });
289 | describe('when values change', () => {
290 | it('should update value', done => {
291 | const options = {
292 | bux: 'bar',
293 | qux: 'qax'
294 | };
295 | const Wrapper = createClass({
296 | getInitialState() {
297 | return {
298 | foo: ['bux']
299 | };
300 | },
301 | render() {
302 | return (
303 |
311 | );
312 | }
313 | });
314 | const tree = renderIntoDocument();
315 | const wrapper = findRenderedComponentWithType(tree, Wrapper);
316 | const $options = scryRenderedDOMComponentsWithTag(tree, 'option');
317 | expect($options[0].selected).toEqual(true);
318 | expect($options[1].selected).toEqual(false);
319 |
320 | wrapper.setState({ foo: ['bux', 'qux'] }, () => {
321 | expect($options[0].selected).toEqual(true);
322 | expect($options[1].selected).toEqual(true);
323 | done();
324 | });
325 | });
326 | });
327 | describe('when input changes', () => {
328 | it('should propagate changes', done => {
329 | const spy = createSpy();
330 | const options = {
331 | bux: 'bar',
332 | qux: 'qax'
333 | };
334 | const values = {
335 | foo: ['bux'],
336 | qux: 'qux',
337 | doo: {
338 | foo: 'bux'
339 | }
340 | };
341 | const tree = renderIntoDocument(
342 |
349 | );
350 | const selects = scryRenderedDOMComponentsWithTag(tree, 'select');
351 | const select = selects[0];
352 | const $select = findDOMNode(select);
353 | $select.options[0].selected = true;
354 | $select.options[1].selected = true;
355 | Simulate.change($select, { target: $select });
356 |
357 | const nextValues = spy.calls[0].arguments[0];
358 | expect(spy.calls.length).toEqual(1);
359 | expect(nextValues).toEqual({
360 | foo: ['bux', 'qux'],
361 | qux: 'qux',
362 | doo: {
363 | foo: 'bux'
364 | }
365 | });
366 | expect(values === nextValues).toEqual(false);
367 | expect(values.doo === nextValues.doo).toEqual(true);
368 | done();
369 | });
370 | });
371 | });
372 |
373 | // it('should rerender when options change', done => {
374 | // class Root extends Component {
375 | // constructor(props, context) {
376 | // super(props, context);
377 | // this.state = {};
378 | // }
379 | // render() {
380 | // const { odd } = this.state;
381 | // const options = odd
382 | // ? { foo: 'bar' }
383 | // : { qux: 'fax' };
384 | // return (
385 | //
391 | // );
392 | // }
393 | // }
394 | //
395 | // const tree = renderIntoDocument();
396 | // const root = findRenderedComponentWithType(tree, Root);
397 | // const option = scryRenderedDOMComponentsWithTag(tree, 'option');
398 | // expect(option[0].value).toEqual('');
399 | // expect(option[1].value).toEqual('qux');
400 | // root.setState({ odd: true}, () => {
401 | // const option2 = scryRenderedDOMComponentsWithTag(tree, 'option');
402 | // expect(option2[0].value).toEqual('');
403 | // expect(option2[1].value).toEqual('foo');
404 | // done();
405 | // });
406 | // });
407 | // it('should accept placeholder option', () => {
408 | // const tree = renderIntoDocument(
409 | //
415 | // );
416 | // const options = scryRenderedDOMComponentsWithTag(tree, 'option');
417 | // expect(options.length).toEqual(3);
418 | // expect(options[0].value).toEqual('');
419 | // expect(options[0].disabled).toEqual(true);
420 | // });
421 | });
422 |
--------------------------------------------------------------------------------
/test/components/Text.spec.js:
--------------------------------------------------------------------------------
1 | import React, { createClass } from 'react';
2 | import { findDOMNode } from 'react-dom';
3 | import {
4 | renderIntoDocument,
5 | findRenderedComponentWithType,
6 | scryRenderedDOMComponentsWithTag,
7 | Simulate
8 | } from 'react/lib/ReactTestUtils';
9 |
10 | import Form from '../../src/components/Form';
11 | import Text from '../../src/components/Text';
12 |
13 | const noop = () => {};
14 |
15 | describe('Text', () => {
16 | describe('when rendering for the first time', () => {
17 | it('should take given value', done => {
18 | const values = { foo: 'bar' };
19 | const tree = renderIntoDocument(
20 |
25 | );
26 |
27 | const input = findRenderedComponentWithType(tree, Text);
28 | const $input = findDOMNode(input);
29 | expect($input.value).toEqual('bar');
30 | done();
31 | });
32 | });
33 | describe('when values change', () => {
34 | it('should update value', done => {
35 | const Wrapper = createClass({
36 | getInitialState() {
37 | return {
38 | foo: 'bar',
39 | qux: 'qax'
40 | };
41 | },
42 | render() {
43 | return (
44 |
50 | );
51 | }
52 | });
53 | const tree = renderIntoDocument();
54 | const wrapper = findRenderedComponentWithType(tree, Wrapper);
55 | const inputs = scryRenderedDOMComponentsWithTag(tree, 'input');
56 | expect(inputs[0].value).toEqual('bar');
57 | expect(inputs[1].value).toEqual('qax');
58 |
59 | wrapper.setState({ foo: 'doo' }, () => {
60 | expect(inputs[0].value).toEqual('doo');
61 | expect(inputs[1].value).toEqual('qax');
62 | done();
63 | });
64 | });
65 | });
66 | describe('when input changes', () => {
67 | it('should propagate changes', done => {
68 | const spy = createSpy();
69 | const values = {
70 | foo: 'bar',
71 | qux: 'qax',
72 | doo: {
73 | foo: 'bar'
74 | }
75 | };
76 | const tree = renderIntoDocument(
77 |
84 | );
85 | const inputs = scryRenderedDOMComponentsWithTag(tree, 'input');
86 | const input = inputs[0];
87 | const $input = findDOMNode(input);
88 | Simulate.change($input, { target: { value: 'doo' } });
89 |
90 | const nextValues = spy.calls[0].arguments[0];
91 | expect(spy.calls.length).toEqual(1);
92 | expect(nextValues).toEqual({
93 | foo: 'doo',
94 | qux: 'qax',
95 | doo: {
96 | foo: 'bar'
97 | }
98 | });
99 | expect(values === nextValues).toEqual(false);
100 | expect(values.doo === nextValues.doo).toEqual(true);
101 | done();
102 | });
103 | });
104 | describe('when form is disabled', () => {
105 | it('should be disabled too', done => {
106 | const values = { foo: 'bar' };
107 | const tree = renderIntoDocument(
108 |
114 | );
115 |
116 | const input = findRenderedComponentWithType(tree, Text);
117 | expect(findDOMNode(input).disabled).toEqual(true);
118 | done();
119 | });
120 | });
121 | });
122 |
--------------------------------------------------------------------------------
/test/components/TextArea.spec.js:
--------------------------------------------------------------------------------
1 | import React, { createClass } from 'react';
2 | import { findDOMNode } from 'react-dom';
3 | import {
4 | renderIntoDocument,
5 | findRenderedComponentWithType,
6 | scryRenderedDOMComponentsWithTag,
7 | Simulate
8 | } from 'react/lib/ReactTestUtils';
9 |
10 | import Form from '../../src/components/Form';
11 | import TextArea from '../../src/components/TextArea';
12 |
13 | const noop = () => {};
14 |
15 | describe('TextArea', () => {
16 | describe('when rendering for the first time', () => {
17 | it('should take given value', done => {
18 | const values = { foo: 'bar' };
19 | const tree = renderIntoDocument(
20 |
25 | );
26 |
27 | const input = findRenderedComponentWithType(tree, TextArea);
28 | const $input = findDOMNode(input);
29 | expect($input.value).toEqual('bar');
30 | done();
31 | });
32 | });
33 | describe('when values change', () => {
34 | it('should update value', done => {
35 | const Wrapper = createClass({
36 | getInitialState() {
37 | return {
38 | foo: 'bar',
39 | qux: 'qax'
40 | };
41 | },
42 | render() {
43 | return (
44 |
50 | );
51 | }
52 | });
53 | const tree = renderIntoDocument();
54 | const wrapper = findRenderedComponentWithType(tree, Wrapper);
55 | const inputs = scryRenderedDOMComponentsWithTag(tree, 'input');
56 | expect(inputs[0].value).toEqual('bar');
57 | expect(inputs[1].value).toEqual('qax');
58 |
59 | wrapper.setState({ foo: 'doo' }, () => {
60 | expect(inputs[0].value).toEqual('doo');
61 | expect(inputs[1].value).toEqual('qax');
62 | done();
63 | });
64 | });
65 | });
66 | describe('when input changes', () => {
67 | it('should propagate changes', done => {
68 | const spy = createSpy();
69 | const values = {
70 | foo: 'bar',
71 | qux: 'qax',
72 | doo: {
73 | foo: 'bar'
74 | }
75 | };
76 | const tree = renderIntoDocument(
77 |
84 | );
85 | const inputs = scryRenderedDOMComponentsWithTag(tree, 'input');
86 | const input = inputs[0];
87 | const $input = findDOMNode(input);
88 | Simulate.change($input, { target: { value: 'doo' } });
89 |
90 | const nextValues = spy.calls[0].arguments[0];
91 | expect(spy.calls.length).toEqual(1);
92 | expect(nextValues).toEqual({
93 | foo: 'doo',
94 | qux: 'qax',
95 | doo: {
96 | foo: 'bar'
97 | }
98 | });
99 | expect(values === nextValues).toEqual(false);
100 | expect(values.doo === nextValues.doo).toEqual(true);
101 | done();
102 | });
103 | });
104 | describe('when form is disabled', () => {
105 | it('should be disabled too', done => {
106 | const values = { foo: 'bar' };
107 | const tree = renderIntoDocument(
108 |
114 | );
115 |
116 | const input = findRenderedComponentWithType(tree, TextArea);
117 | expect(findDOMNode(input).disabled).toEqual(true);
118 | done();
119 | });
120 | });
121 | });
122 |
--------------------------------------------------------------------------------
/test/hoc/connectInput.spec.js:
--------------------------------------------------------------------------------
1 | import React, { createClass } from 'react';
2 | import { findDOMNode } from 'react-dom';
3 | import {
4 | renderIntoDocument,
5 | findRenderedComponentWithType,
6 | findRenderedDOMComponentWithTag,
7 | scryRenderedDOMComponentsWithTag,
8 | Simulate
9 | } from 'react/lib/ReactTestUtils';
10 |
11 | import Form from '../../src/components/Form';
12 | import connectInput from '../../src/hoc/connectInput';
13 |
14 | const Component = props => ;
15 | const Input = connectInput(Component);
16 | const noop = () => {};
17 |
18 | describe('connectInput', () => {
19 | describe('when rendering for the first time', () => {
20 | it('should receive given value', done => {
21 | const values = { foo: 'bar' };
22 | const tree = renderIntoDocument(
23 |
28 | );
29 | const input = findRenderedDOMComponentWithTag(tree, 'input');
30 | expect(input.value).toEqual('bar');
31 | done();
32 | });
33 | });
34 |
35 | describe('when values change', () => {
36 | it('should update respective components', done => {
37 | const Wrapper = createClass({
38 | getInitialState() {
39 | return {
40 | foo: 'bar',
41 | qux: 'qax'
42 | };
43 | },
44 | render() {
45 | return (
46 |
52 | );
53 | }
54 | });
55 | const tree = renderIntoDocument();
56 | const wrapper = findRenderedComponentWithType(tree, Wrapper);
57 | const inputs = scryRenderedDOMComponentsWithTag(tree, 'input');
58 | expect(inputs[0].value).toEqual('bar');
59 | expect(inputs[1].value).toEqual('qax');
60 |
61 | wrapper.setState({ foo: 'doo' }, () => {
62 | expect(inputs[0].value).toEqual('doo');
63 | expect(inputs[1].value).toEqual('qax');
64 | done();
65 | });
66 | });
67 | });
68 |
69 | describe('when an input changes', () => {
70 | it('should propagate changes', done => {
71 | const spy = createSpy();
72 | const values = {
73 | foo: 'bar',
74 | qux: 'qax',
75 | doo: {
76 | foo: 'bar'
77 | }
78 | };
79 | const tree = renderIntoDocument(
80 |
87 | );
88 | const inputs = scryRenderedDOMComponentsWithTag(tree, 'input');
89 | const input = inputs[0];
90 | const $input = findDOMNode(input);
91 | Simulate.change($input, { target: { value: 'doo' } });
92 |
93 | const nextValues = spy.calls[0].arguments[0];
94 | expect(spy.calls.length).toEqual(1);
95 | expect(nextValues).toEqual({
96 | foo: 'doo',
97 | qux: 'qax',
98 | doo: {
99 | foo: 'bar'
100 | }
101 | });
102 | expect(values === nextValues).toEqual(false);
103 | expect(values.doo === nextValues.doo).toEqual(true);
104 | done();
105 | });
106 | });
107 |
108 | describe('when the name of an input is changed', () => {
109 | it('should update the value', done => {
110 | const values = {
111 | foo: 'bar',
112 | qux: 'qax'
113 | };
114 | const Wrapper = createClass({
115 | getInitialState() {
116 | return {
117 | name: 'foo'
118 | };
119 | },
120 | render() {
121 | const { name } = this.state;
122 | return (
123 |
128 | );
129 | }
130 | });
131 | const tree = renderIntoDocument();
132 | const wrapper = findRenderedComponentWithType(tree, Wrapper);
133 | const input = findRenderedDOMComponentWithTag(tree, 'input');
134 | expect(input.value).toEqual('bar');
135 | wrapper.setState({ name: 'qux' }, () => {
136 | expect(input.value).toEqual('qax');
137 | done();
138 | });
139 | });
140 | });
141 | });
142 |
--------------------------------------------------------------------------------
/test/hoc/createMessage.js:
--------------------------------------------------------------------------------
1 | // import React, { createClass } from 'react';
2 | // import { findDOMNode } from 'react-dom';
3 | // import {
4 | // renderIntoDocument,
5 | // findRenderedComponentWithType
6 | // } from 'react/lib/ReactTestUtils';
7 | //
8 | // import createMessage from '../../src/components/createMessage';
9 | // import Form from '../../src/components/Form';
10 | //
11 | // const Message = createMessage(createClass({
12 | // render() {
13 | // const { message } = this.props;
14 | // return {message}
;
15 | // }
16 | // }));
17 | //
18 | // describe('createMessage', () => {
19 | // it('should pass message', () => {
20 | // const messages = {
21 | // foo: 'bar'
22 | // };
23 | // const tree = renderIntoDocument(
24 | //
27 | // );
28 | //
29 | // const message = findRenderedComponentWithType(tree, Message);
30 | // const $message = findDOMNode(message);
31 | // expect($message.childNodes[0].nodeValue).toEqual('bar');
32 | // });
33 | //
34 | // it('should pass nested messages', () => {
35 | // const messages = {
36 | // foo: {
37 | // boo: 'bar'
38 | // }
39 | // };
40 | // const tree = renderIntoDocument(
41 | //
44 | // );
45 | //
46 | // const message = findRenderedComponentWithType(tree, Message);
47 | // const $message = findDOMNode(message);
48 | // expect($message.childNodes[0].nodeValue).toEqual('bar');
49 | // });
50 | //
51 | // it('should pass nested messages (path keys)', () => {
52 | // const messages = {
53 | // 'foo.boo': 'bar'
54 | // };
55 | // const tree = renderIntoDocument(
56 | //
59 | // );
60 | //
61 | // const message = findRenderedComponentWithType(tree, Message);
62 | // const $message = findDOMNode(message);
63 | // expect($message.childNodes[0].nodeValue).toEqual('bar');
64 | // });
65 | // });
66 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var webpack = require('webpack');
4 |
5 | var plugins = [
6 | new webpack.optimize.OccurenceOrderPlugin(),
7 | new webpack.DefinePlugin({
8 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
9 | })
10 | ];
11 |
12 | if (process.env.NODE_ENV === 'production') {
13 | plugins.push(
14 | new webpack.optimize.UglifyJsPlugin({
15 | compressor: {
16 | screw_ie8: true,
17 | warnings: false
18 | }
19 | })
20 | );
21 | }
22 |
23 | module.exports = {
24 | externals: {
25 | react: {
26 | root: 'React',
27 | commonjs2: 'react',
28 | commonjs: 'react',
29 | amd: 'react'
30 | }
31 | },
32 | module: {
33 | loaders: [{
34 | test: /\.js$/,
35 | loaders: ['babel-loader'],
36 | exclude: /node_modules/
37 | }]
38 | },
39 | output: {
40 | library: 'ReactFormalize',
41 | libraryTarget: 'umd'
42 | },
43 | plugins: plugins,
44 | resolve: {
45 | extensions: ['', '.js']
46 | }
47 | };
48 |
--------------------------------------------------------------------------------