Skip to content

Commit a567747

Browse files
committed
Added server side validation
1 parent c3bfdc1 commit a567747

File tree

8 files changed

+217
-134
lines changed

8 files changed

+217
-134
lines changed

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Isomorphic React and Express
22

3-
This repository shows how React can be used with the Express framework isomorphically; that is to use the same code on both the server and browser. This uses the comment box example from the [React tutorial](http://facebook.github.io/react/docs/tutorial.html) but in addition to rendering the comment box in the browser, it pre-renders it on the server, using the same code.
3+
This repository shows how React can be used with the Express framework isomorphically; that is to use the same code on both the server and browser to render the same sections of pages. This uses the comment box example from the [React tutorial](http://facebook.github.io/react/docs/tutorial.html) but in addition to rendering the comment box in the browser, it pre-renders it on the server, using the same code.
44

55
There are also a few other additions. I've set up `webpack` so that it bundles up the code to be used in the browser. I also didn't particularly like the `JSX` template render methods being in the same file as the controller / component code (despite what React says, they are not just "views"; they behave very similar to Angular's controllers). And lastly since I had to server-side rendering, I've had to use a view engine. I've chose `Swig` as unlike `Jade` it uses proper HTML and so means I have one fewer language to learn (which fits into the philosophy of isomorphism).
66

7+
Isomorphic validation is also available: see more information below under `Validation`.
8+
79
Naturally this means all of the other server language implementations have been removed - Python, PHP etc.
810

911
## Implementation
@@ -14,13 +16,19 @@ In the browser, the main entry point method which calls `ReactDOM.render`, is ad
1416

1517
On the browser, `components/server.js` is `require`d, after the `node-jsx` module is set up, in order that the JSX syntax can be understood. This exports a `renderCommentBox` method which returns a static string after calling `ReactDOMServer.renderToString` from the `react-dom/server` module. The route for `/` simply calls this method and passes it as a variable to the `views/index.html` view.
1618

19+
## Validation
20+
21+
There is also client-side and server-side validation which again is isomorphic. This uses `react-validation-mixin` for React from which the rules are defined by `validatorjs`. These are connected using a "strategy" defined in `components/strategy.js` where more information can be found. The schemas are defined in `components/schemas.js`.
22+
23+
Extra functionality has been added to `components/templates.jsx` and `components/components.js` to handle the validation and server-side to the `/api/comments` POST end point in `server.js`.
24+
1725
## To use
1826

1927
npm install
2028

2129
Next, to compile the browser code into `public/scripts/bundle.js`:
2230

23-
npm run webpack-dev
31+
npm run webpack:dev
2432

2533
Or if you want to compile it to a minified version without the source map:
2634

@@ -30,4 +38,4 @@ And then start the server:
3038

3139
npm start
3240

33-
And visit <http://localhost:3000/>.
41+
And visit <http://127.0.0.1:3000/>.

comments.json

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,5 @@
88
"id": 1420070400000,
99
"author": "Paul O’Shannessy",
1010
"text": "React is *great*!"
11-
},
12-
{
13-
"id": 1455145027806,
14-
"author": "dfgh",
15-
"text": "dgh"
16-
},
17-
{
18-
"id": 1455145275989,
19-
"author": "dfghdf",
20-
"text": "ghfgjh"
21-
},
22-
{
23-
"id": 1455145280188,
24-
"author": "fghjfgj",
25-
"text": "fgjh"
26-
},
27-
{
28-
"id": 1455152836500,
29-
"author": "sdfgsd",
30-
31-
},
32-
{
33-
"id": 1455154018698,
34-
"author": "sdfg",
35-
36-
},
37-
{
38-
"id": 1455154150577,
39-
"author": "sdfgsdfg",
40-
"text": "sdsdfgsdfgsdf"
4111
}
4212
]

components/components.js

Lines changed: 23 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@
1010
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1111
*/
1212

13-
'use strict';
1413

15-
var templates = require('./templates.jsx');
1614
var React = require('react');
1715
var marked = require('marked');
18-
var $ = require('jquery');
1916
var classNames = require('classnames');
20-
2117
var validation = require('react-validation-mixin');
22-
var Validator = require('validatorjs');
18+
var $ = require('jquery');
19+
20+
var templates = require('./templates.jsx');
21+
var strategy = require('./strategy');
22+
var schemas = require('./schemas');
2323

2424
var Comment = React.createClass({
2525
displayName: 'Comment',
@@ -89,23 +89,24 @@ var CommentForm = React.createClass({
8989
displayName: 'CommentForm',
9090
getInitialState: function () {
9191
// Define the rules and custom messages for each field.
92-
this.validatorTypes = strategy.createInactiveSchema(
93-
{
94-
author: 'required',
95-
text: 'required|min:10|max:50'
96-
},
97-
{
98-
//'min.text': 'Enter a message between 10 and 50 characters',
99-
//'max.text': 'Enter a message between 10 and 50 characters'
100-
}
101-
);
92+
this.validatorTypes = schemas.commentForm;
10293

10394
return {author: '', text: ''};
10495
},
105-
addValidation: function(e) {
96+
/**
97+
* Activate the validation rule for the element on blur
98+
*
99+
* @param {Event} e
100+
*/
101+
activateValidation: function(e) {
106102
strategy.activateRule(this.validatorTypes, e.target.name);
107103
this.props.handleValidation(e.target.name)(e);
108104
},
105+
/**
106+
* Set the state of the changed variable and then when set, call validator
107+
*
108+
* @param {Event} e
109+
*/
109110
handleChange: function (e) {
110111
var state = {};
111112
state[e.target.name] = e.target.value;
@@ -114,12 +115,17 @@ var CommentForm = React.createClass({
114115
this.props.handleValidation(e.target.name)(e);
115116
});
116117
},
118+
/**
119+
* Validate the form and if valid, submit the comment
120+
*
121+
* @param {Event} e
122+
*/
117123
handleSubmit: function (e) {
118124
e.preventDefault();
119125

120-
// If the form is valid, then submit the comment
121126
this.props.validate((error) => {
122127
if (!error) {
128+
// this.props.onCommentSubmit is actually CommentBox.handleCommentSubmit
123129
this.props.onCommentSubmit(this.state);
124130
this.setState({author: '', text: ''});
125131
}
@@ -136,70 +142,6 @@ var CommentForm = React.createClass({
136142
render: templates.commentForm
137143
});
138144

139-
var strategy = {
140-
createSchema: function (rules, messages, createValidatorCallback) {
141-
return {
142-
rules,
143-
messages,
144-
createValidatorCallback
145-
};
146-
},
147-
createInactiveSchema: function (rules, messages, createValidatorCallback) {
148-
var schema = this.createSchema(rules, messages, createValidatorCallback);
149-
schema.activeRules = [];
150-
151-
return schema;
152-
},
153-
activateRule: function(schema, rule) {
154-
if (schema.activeRules.indexOf(rule) === -1) {
155-
schema.activeRules.push(rule);
156-
}
157-
},
158-
/**
159-
* Validate using the validatorjs library
160-
*
161-
* @see https://www.npmjs.com/package/validatorjs
162-
*
163-
* @param {Object} data the data submitted
164-
* @param {Object} schema contains rules and custom error messages
165-
* @param {Object} options contains name of element being validated and previous errors
166-
* @param {Function} callback called and passed the errors
167-
*/
168-
validate: function (data, schema, options, callback) {
169-
var rules = {};
170-
171-
// Only add active rules to the validator if an initially inactive schema has been created.
172-
// Check all rules regardless if the form has been submitted (options.key is empty).
173-
if (typeof schema.activeRules !== 'undefined' && options.key) {
174-
for (let i in schema.activeRules) {
175-
let ruleName = schema.activeRules[i];
176-
177-
rules[ruleName] = schema.rules[ruleName];
178-
}
179-
} else {
180-
rules = schema.rules;
181-
}
182-
183-
var validator = new Validator(data, rules, schema.messages);
184-
185-
if (typeof schema.createValidatorCallback === 'function') {
186-
schema.createValidatorCallback(validator);
187-
}
188-
189-
var getErrors = () => {
190-
if (options.key) {
191-
options.prevErrors[options.key] = validator.errors.get(options.key);
192-
callback(options.prevErrors);
193-
} else {
194-
callback(validator.errors.all());
195-
}
196-
};
197-
198-
// Run the validator asynchronously in case any async rules have been added
199-
validator.checkAsync(getErrors, getErrors);
200-
}
201-
};
202-
203145
CommentForm = validation(strategy)(CommentForm);
204146

205147
module.exports = {

components/schemas.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
var strategy = require('./strategy');
2+
3+
module.exports = {
4+
commentForm: strategy.createInactiveSchema(
5+
{
6+
author: 'required',
7+
text: 'required|min:10|max:50'
8+
},
9+
{
10+
'min.text': 'Enter a message between 10 and 50 characters',
11+
'max.text': 'Enter a message between 10 and 50 characters'
12+
}
13+
)
14+
};

components/strategy.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* Validate using the validatorjs library as a strategy for react-validation-mixin
3+
*
4+
* @see https://www.npmjs.com/package/validatorjs
5+
* @see https://www.npmjs.com/package/react-validator-mixin
6+
*/
7+
8+
'use strict';
9+
10+
var Validator = require('validatorjs');
11+
12+
module.exports = {
13+
/**
14+
* Used to create this.validatorTypes in a React component and to be passed to validate or validateServer
15+
*
16+
* @param {Object} rules List of rules as specified by validatorjs
17+
* @param {Object} messages Optional list of custom messages as specified by validatorjs
18+
* @param {Function} callback if specified, called to allow customisation of validator
19+
* @returns {Object}
20+
*/
21+
createSchema: function (rules, messages, callback) {
22+
return {
23+
rules,
24+
messages,
25+
callback
26+
};
27+
},
28+
/**
29+
* Same as createSchema, but the rules are disabled until activateRule is called
30+
*
31+
* @param {Object} rules List of rules as specified by validatorjs
32+
* @param {Object} messages Optional list of custom messages as specified by validatorjs
33+
* @param {Function} callback if specified, called to allow customisation of validator
34+
* @returns {Object}
35+
*/
36+
createInactiveSchema: function (rules, messages, callback) {
37+
var schema = this.createSchema(rules, messages, callback);
38+
schema.activeRules = [];
39+
40+
return schema;
41+
},
42+
/**
43+
* Active a specific rule
44+
*
45+
* @param {Object} schema As created by createInactiveSchema
46+
* @param {Object} rule Name of the rule as a key in schema.rules
47+
*/
48+
activateRule: function(schema, rule) {
49+
if (typeof schema.activeRules !== 'undefined' && schema.activeRules.indexOf(rule) === -1) {
50+
schema.activeRules.push(rule);
51+
}
52+
},
53+
/**
54+
* Create a validator from submitted data and a schema
55+
*
56+
* @param {Object} data The data submitted
57+
* @param {Object} schema Contains rules and custom error messages
58+
* @param {Boolean} forceActive Whether to force all rules to be active even if not activated
59+
* @returns {Validator}
60+
*/
61+
createValidator: function (data, schema, forceActive) {
62+
var rules = {};
63+
64+
// Only add active rules to the validator if an initially inactive schema has been created.
65+
if (typeof schema.activeRules !== 'undefined') {
66+
// Force all rules to be active if specified
67+
if (forceActive) {
68+
schema.activeRules = Object.keys(schema.rules);
69+
}
70+
71+
for (let i in schema.activeRules) {
72+
let ruleName = schema.activeRules[i];
73+
74+
rules[ruleName] = schema.rules[ruleName];
75+
}
76+
} else {
77+
rules = schema.rules;
78+
}
79+
80+
var validator = new Validator(data, rules, schema.messages);
81+
82+
// If a callback has been specified on the schema, call it to allow customisation of the validator
83+
if (typeof schema.callback === 'function') {
84+
schema.callback(validator);
85+
}
86+
87+
return validator;
88+
},
89+
/**
90+
* Called by react-validation-mixin
91+
*
92+
* @param {Object} data The data submitted
93+
* @param {Object} schema Contains rules and custom error messages
94+
* @param {Object} options Contains name of element being validated and previous errors
95+
* @param {Function} callback Called and passed the errors after validation
96+
*/
97+
validate: function (data, schema, options, callback) {
98+
// If the whole form has been submitted, then activate all rules
99+
var forceActive = !options.key;
100+
var validator = this.createValidator(data, schema, forceActive);
101+
102+
var getErrors = () => {
103+
// If a single element is being validated, just get those errors.
104+
// Otherwise get all of them.
105+
if (options.key) {
106+
options.prevErrors[options.key] = validator.errors.get(options.key);
107+
callback(options.prevErrors);
108+
} else {
109+
callback(validator.errors.all());
110+
}
111+
};
112+
113+
// Run the validator asynchronously in case any async rules have been added
114+
validator.checkAsync(getErrors, getErrors);
115+
},
116+
/**
117+
* Validate server-side returning a Promise to easier handle results.
118+
* All inactive rules will be forced to activate.
119+
*
120+
* @param {Object} data The data submitted
121+
* @param {Object} schema Contains rules and custom error messages
122+
* @returns {Promise}
123+
*/
124+
validateServer: function (data, schema) {
125+
var validator = this.createValidator(data, schema, true);
126+
127+
return new Promise((resolve, reject) => {
128+
validator.checkAsync(
129+
() => {
130+
resolve();
131+
},
132+
() => {
133+
reject(validator.errors.all());
134+
}
135+
);
136+
});
137+
}
138+
};

components/templates.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ module.exports = {
5454
className={this.getClasses('author')}
5555
value={this.state.author}
5656
onChange={this.handleChange}
57-
onBlur={this.addValidation}
57+
onBlur={this.activateValidation}
5858
/>
5959
</p>
6060

@@ -68,7 +68,7 @@ module.exports = {
6868
className={this.getClasses('text')}
6969
value={this.state.text}
7070
onChange={this.handleChange}
71-
onBlur={this.addValidation}
71+
onBlur={this.activateValidation}
7272

7373
/>
7474
</p>

0 commit comments

Comments
 (0)