Skip to content

Commit 0908a23

Browse files
committed
add support for inline forms
- add ability to render inline forms by passing renderer function as Form's only child - update README: move most of the content to the project's Wiki, fix spelling issues - minor lint-related updates in demo app
1 parent 02bed8d commit 0908a23

File tree

6 files changed

+75
-151
lines changed

6 files changed

+75
-151
lines changed

README.md

Lines changed: 39 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ Base Form component for building convenient forms for [React](https://facebook.g
88

99
## Features Overview
1010

11-
- Controlled Form, i.e. it works with input values passed in props as JS object.
11+
- Controlled Form, i.e. it accepts input values as a JSON object.
1212
- Simple API that handles deeply nested values and collections.
13-
- Flexible and conventient validation that allows to validate inputs as user types.
13+
- Flexible and convenient validation that allows to validate inputs as user types.
1414
- Allows to easily turn any existing component into a Form Input component.
1515

1616
## Installation
@@ -26,177 +26,69 @@ npm install --save react-form-base
2626
`react-form-base` provides a `Form` base class which expects to work together
2727
with **Input** components. An **Input** is any component that consumes three
2828
properties: `value`, `error` and `onChange`. It also has to provide it's
29-
`value` as first argument to `onChange` function supplied in props. For
30-
existing ready-for-use input bindings take a look on [react-form-js](https://github.com/akuzko/react-form-js)
31-
and [react-form-material-ui](https://github.com/akuzko/react-form-material-ui).
29+
`value` as first argument to `onChange` function supplied in props.
30+
*For existing ready-for-use input bindings take a look on:*
31+
- [react-form-js](https://github.com/akuzko/react-form-js)
32+
- [react-form-material-ui](https://github.com/akuzko/react-form-material-ui)
3233

3334
### Form Usage
3435

3536
Most of form use-cases with examples are revealed in [**Demo Application**](https://akuzko.github.io/react-form-base/).
3637
Details on how to run it locally are at the end of README.
3738

38-
Bellow you can take a glance on main aspects of form usage: general API,
39-
custom on-change handlers, validation and `$render` helper function.
39+
#### Dedicated Forms
4040

41-
#### Basic example
41+
Most of forms developers deal with are quite complicated and encapsulate
42+
vast amount of validation and rendering logic. After some basic setup described
43+
in the [Wiki](https://github.com/akuzko/react-form-base/wiki) your form may
44+
look like following:
4245

4346
```js
44-
import Form from 'react-form-base';
45-
import { TextField } from 'your-inputs'; // read on inputs in the beginning of README
46-
47-
class MyForm extends Form {
48-
render() {
49-
return (
50-
<div>
51-
<TextField {...this.$('firstName')} />
52-
<TextField {...this.$('lastName')} />
53-
54-
<button onClick={this.save.bind(this)}>Save</button>
55-
</div>
56-
);
57-
}
58-
}
59-
```
60-
61-
#### Nested fields example
62-
63-
```js
64-
import Form from 'react-form-base';
65-
import { TextField, Select } from 'your-inputs'; // read on inputs in the beginning of README
66-
import countries from 'utils/countries'; // it's just a stub
67-
68-
class MyForm extends Form {
69-
render() {
70-
return (
71-
<div>
72-
<TextField {...this.$('email')} />
73-
74-
<Select {...this.$('address.country')} options={countries} />
75-
<TextField {...this.$('address.city')} />
76-
<TextField {...this.$('address.streetLine')} />
77-
78-
<button onClick={this.save.bind(this)}>Save</button>
79-
</div>
80-
);
81-
}
82-
}
83-
```
84-
85-
#### Custom on-change handler
86-
87-
```js
88-
import Form from 'react-form-base';
89-
import { Select } from 'your-inputs'; // read on inputs in the beginning of README
90-
91-
class MyForm extends Form {
92-
changeItem(value) {
93-
this.set({
94-
item: value,
95-
amount: null
96-
});
97-
}
98-
99-
render() {
100-
return (
101-
<div>
102-
<Select {...this.$('item')(this.changeItem)} options={['Item 1', 'Item 2']} />
103-
<Select {...this.$('amount')} options={['10', '50', '100']} />
104-
105-
<button onClick={this.save.bind(this)}>Save</button>
106-
</div>
107-
);
108-
}
109-
}
110-
```
111-
112-
#### Validation
113-
114-
```js
115-
import Form from 'react-form-base';
116-
import { TextField } from 'your-inputs'; // read on inputs in the beginning of README
117-
118-
class MyForm extends Form {
119-
// static validations are common validation rules. it's best to define them
120-
// in your top-level application form that is a base class for other forms.
121-
static validations = {
122-
presence: function(value) {
123-
if (!value) return 'cannot be blank';
124-
},
125-
numericality: function(value, options) {
126-
const { greaterThan } = options;
127-
const fValue = parseFloat(value);
128-
129-
if (isNaN(fValue)) return 'should be a number';
130-
if (greaterThan != undefined && fValue <= greaterThan) {
131-
return `should be greater than ${greaterThan}`;
132-
}
133-
}
134-
};
135-
136-
// per-form input validations
47+
class UserForm extends Form {
13748
validations = {
138-
// firstName: 'presence' from static validation rules
139-
firstName: 'presence',
140-
// email: 'presence' validation from rules and custom regexp validation
141-
// for this specific form
142-
email: ['presence', function(value) {
143-
if (!/^[\w\d\.]+@[\w\d]+\.[\w\d]{2,}$/.test(value)) {
144-
return 'should be an email';
145-
}
146-
}],
147-
// validation with options
148-
amount: { presence: true, numericality: { greaterThan: 10 } }
49+
'email': ['presence', 'email'],
50+
'fullName': 'presence',
51+
'address.city': 'presence',
52+
'address.line': { presence: true, format: /^[\w\s\d\.,]+$/ }
14953
};
15054

151-
render() {
55+
$render($) {
15256
return (
15357
<div>
154-
<TextField {...this.$('firstName')} />
155-
<TextField {...this.$('email')} />
156-
<TextField {...this.$('amount')} />
157-
158-
<button onClick={this.performValidation.bind(this)}>Validate</button>
159-
</div>
160-
);
161-
}
162-
}
163-
```
58+
<TextField {...$('email')} label="Email" />
59+
<TextField {...$('fullName')} label="Full Name" />
16460

165-
#### $render($) method
61+
<Select {...$('address.countryId') options={countryOptions} label="Country" />
62+
<TextField {...$('address.city')} label="City" />
63+
<TextField {...$('address.line')} label="Address" />
16664

167-
If you don't have extra logic based on render method (such as implementing
168-
rendering in base form and calling `super.render(someContent)` from child
169-
forms), and you want to make things a little bit more DRY, you may declare
170-
your form's rendering using `$render` method that accepts input-generation
171-
function as argument. Thus, removing the `this.` prefix in inputs:
172-
173-
```js
174-
class MyForm extends Form {
175-
$render($) {
176-
return (
177-
<div>
178-
<TextField {...$('firstName')} />
179-
<TextField {...$('lastName')} />
180-
<TextField {...$('email')} />
65+
<button onClick={this.save.bind(this)}>Submit</button>
18166
</div>
18267
);
18368
}
18469
}
18570
```
18671
187-
This form of rendering declaration is also very useful when working with
188-
nested forms, since it has a special `nested` method that will generate
189-
onChange handler for nested form for you:
72+
#### Inline Forms
73+
74+
If your form is small enough, you might want to render it inline instead of
75+
defining separate form component. In this case you may pass renderer function
76+
as only form's child. This function takes form's `$` function as argument for
77+
convenience. Note that you still need to define static `validation` rules
78+
for the Form to be able to use validations.
19079
19180
```js
192-
{this.map('items', (_item, i) =>
193-
<ItemForm key={i} {...$.nested(`items.${i}`)} />
194-
)}
81+
<Form {...bindState(this)} validations={{ email: ['presence', 'email'], fullName: 'presence' }}>
82+
{$ => (
83+
<div>
84+
<TextField {...$('email')} label="Email" />
85+
<TextField {...$('fullName')} label="FullName" />
86+
<button onClick={this.registerUser}>Register</button>
87+
</div>
88+
)}
89+
</Form>
19590
```
19691
197-
Of course, since `$` is argument in this method, you may use any name for
198-
this variable that you find suitable.
199-
20092
#### API and helper methods
20193
20294
- `$(name)`, `input(name)` - returns a set of properties for input with a given

demo/src/App.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* global Promise */
12
import React, { PureComponent } from 'react';
23
import { Intro, InputPrerequisites } from './components';
34
import * as Forms from './forms';

demo/src/components/Source.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* global Prism */
12
import React, { PropTypes, PureComponent } from 'react';
23

34
export default class Source extends PureComponent {

src/Form.jsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ export default class Form extends PureComponent {
1414
clearErrorsOnChange: PropTypes.bool,
1515
validateOnChange: PropTypes.bool,
1616
validateOnSave: PropTypes.bool,
17-
onRequestSave: PropTypes.func
17+
onRequestSave: PropTypes.func,
18+
validations: PropTypes.object,
19+
children: PropTypes.func
1820
};
1921

2022
static defaultProps = {
@@ -128,6 +130,10 @@ export default class Form extends PureComponent {
128130
return this.props.validateOnChange && this.state.hadErrors;
129131
}
130132

133+
get _validations() {
134+
return this.props.validations || this.validations;
135+
}
136+
131137
ifValid(callback) {
132138
const errors = this.getValidationErrors();
133139

@@ -150,7 +156,7 @@ export default class Form extends PureComponent {
150156
}
151157

152158
validate(validate) {
153-
for (const name in this.validations) {
159+
for (const name in this._validations) {
154160
validate(name);
155161
}
156162

@@ -213,6 +219,17 @@ export default class Form extends PureComponent {
213219
}
214220

215221
render() {
222+
const $bound = this._bind$();
223+
const { children: renderer } = this.props;
224+
225+
if (typeof renderer === 'function') {
226+
return renderer($bound);
227+
}
228+
229+
return this.$render($bound);
230+
}
231+
232+
_bind$() {
216233
const $bound = this.$.bind(this);
217234

218235
Object.defineProperty($bound, 'nested', {
@@ -226,7 +243,7 @@ export default class Form extends PureComponent {
226243
enumerable: false
227244
});
228245

229-
return this.$render($bound);
246+
return $bound;
230247
}
231248

232249
$render() {

src/utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function wildcard(name) {
1919
export function buildFormValidator(form) {
2020
function validate(name, options = {}) {
2121
const value = options.hasOwnProperty('value') ? options.value : form.get(name);
22-
const validator = options['with'] || form.validations[name] || form.validations[wildcard(name)];
22+
const validator = options['with'] || form._validations[name] || form._validations[wildcard(name)];
2323

2424
if (!validator) return null;
2525

test/Form.test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,4 +559,17 @@ describe('<Form />', function() {
559559
expect(this.test.wrapper.state('form').foo).toEqual('new value');
560560
});
561561
});
562+
563+
describe('render', function() {
564+
context('when function is passed', function() {
565+
it('uses it as renderer function', function() {
566+
const wrapper = shallow(
567+
<Form attrs={{ foo: 'bar' }}>
568+
{$ => <Input {...$('foo')} className="bar" />}
569+
</Form>
570+
);
571+
expect(wrapper.containsMatchingElement(<Input name="foo" value="bar" className="bar" />)).toEqual(true);
572+
});
573+
});
574+
});
562575
});

0 commit comments

Comments
 (0)