Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Universal React forms #8

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions client-render.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { Router, browserHistory } from 'react-router';

import { routes } from './routes';

// import createBrowserHistory from 'history/lib/createBrowserHistory';

ReactDOM.render(
<Router routes={routes} history={browserHistory} />,
document.getElementById('app')
Expand Down
13 changes: 13 additions & 0 deletions components/about.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import React from 'react';
import EmailForm from './email-form';

export default class AboutComponent extends React.Component {
constructor(props) {
super(props);
const { success, errors, value } = this.props.location.query;
if (success) {
this.emailFormProps = {
success: success === "true",
errors: errors && errors.split(',') || [],
value
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when the form is submitted on the server, the URL query params contain info about the result: /about?succes=true&[email protected], and this code tidies them and passes them down to EmailForm

}
}
render() {
return (
<div>
<p>A little bit about me.</p>
<EmailForm {...this.emailFormProps} />
</div>
);
}
Expand Down
115 changes: 115 additions & 0 deletions components/email-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from 'react';
import { Link } from 'react-router';
import validateEmailForm from './validate-email-form';

export default class EmailForm extends React.Component {
constructor(props) {
super(props);
const { success, errors, value } = props;
this.state = {
success,
errors,
value: value || ''
};
this.formSubmit = this.formSubmit.bind(this);
this.inputChange = this.inputChange.bind(this);
this.resetForm = this.resetForm.bind(this);
}

formSubmittedSuccessfully() {
return this.state.success === true;
}

inputChange(e) {
this.setState({
value: e.target.value
});
}

resetForm(e) {
e.preventDefault();
this.setState({
success: undefined,
errors: [],
value: ''
});
}

formSubmit(e) {
e.preventDefault();
const emailValue = this.state.value;
// note how this is the same validator we ran on the server!
const result = validateEmailForm(emailValue);
// at this point you could POST to an endpoint to save
// this to the database, or do whatever you need to
this.setState({
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a real app you'd probably do something here to store this piece of data - most likely hitting an API endpoint. The API endpoint wouldn't do any validation though, just store into the database, because the validation has been done on the client.

Alternatively you could just do the validation all on the server and return all the validation input, but why do that when we can have it all on the client? :)

errors: result.errors,
success: result.valid
});
}

renderErrorsList() {
return this.state.errors.map((err) => {
return (
<li className="form-error" key={err}>
<p>{err}</p>
</li>
);
});
}

renderForm() {
return (
<form method="post" action="/submit-email-form" onSubmit={this.formSubmit}>
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a non-JS world the form will submit like a regular form would to /submit-email-form, but in a JS world React will capture it.


<div className="field">
<label>Your email address</label>
<input
onChange={this.inputChange}
type="text"
value={this.state.value}
name="email"
placeholder="[email protected]"
ref="emailInput" />
</div>
<button type="submit">Send</button>
</form>
);
}

renderSuccess() {
return (
<div>
<p>Thanks, {this.state.value} has been added to our totally cool list.</p>
<a href="/about" onClick={this.resetForm}>Reset page</a>
</div>
)
}

renderErrors() {
if (this.state.errors && this.state.errors.length > 0) {
return (
<div>
<h5>There were errors submitting this form.</h5>
<ul>{this.renderErrorsList()}</ul>
</div>
);
} else {
return null;
}
}

render() {
return (
<div>
{ this.renderErrors() }
{ this.formSubmittedSuccessfully() ? this.renderSuccess() : this.renderForm() }
</div>
)
}
}

EmailForm.propTypes = {
success: React.PropTypes.bool,
errors: React.PropTypes.arrayOf(React.PropTypes.string)
};
20 changes: 20 additions & 0 deletions components/validate-email-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// write any logic for your form in one file that can be used on both the client and the server

export default function(email) {
// IRL you probably want a better check!
if (email.indexOf('@') > -1) {
return {
valid: true,
email,
errors: []
}
} else {
return {
valid: false,
email,
errors: [
`Email address ${email} is not valid`
]
}
}
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the magic bit - all of your form's logic should be in a file that can be webpacked on the client or used on the server.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(A real app would have a much more complex version of this, I get that!).

Also doing it like this makes it really easy to test.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.15.0",
"ejs": "^2.3.4",
"express": "^4.13.3",
"history": "^1.13.1",
Expand All @@ -34,6 +35,7 @@
"babel-loader": "^6.2.0",
"babel-preset-es2015": "^6.1.18",
"babel-preset-react": "^6.1.18",
"nodemon": "^1.9.1",
"webpack": "^1.12.9"
}
}
22 changes: 21 additions & 1 deletion server.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { match, RouterContext } from 'react-router';
import bodyParser from 'body-parser';

import { routes } from './routes';

import validateEmailForm from './components/validate-email-form';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

omg the same validator we used on the client!


const app = express();

app.use(express.static('public'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.set('view engine', 'ejs');

Expand Down Expand Up @@ -36,7 +41,22 @@ app.get('*', (req, res) => {
}
});
});
app.listen(3003, 'localhost', function(err) {

app.post('/submit-email-form', (req, res) => {
const { email } = req.body;
const result = validateEmailForm(email);

if (result.valid === true) {
// here you would save this to a database, or whatever you want
// db.addEmail(email)

res.redirect(`/about?success=true&value=${email}`);
} else {
res.redirect(`/about?errors=${result.errors.join(',')}&success=false&value=${email}`);
}
});
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the route that the form will POST to if there's no JS. Notice how it's tiny - the logic is encapsulated in validateEmailForm.


app.listen(3003, 'localhost', (err) => {
if (err) {
console.log(err);
return;
Expand Down