While this method may not be the best solution, it works well enough for my needs:
- AJAX request to API on entry
- Initializes form with data when request has been fulfilled or displays a server error
- Resetting form will still reset to initial seed data
- Allows the form to be reused for other purposes (for example, a simple if statement could bypass setting initial values): Add Post and Edit Post or Add Comment and Edit Comment...etc.
- Data is removed from Redux form on exit (no reason to store new data in Redux since it's being re-rendered by a Blog component)
Form.jsx:
import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import { connect } from 'react-redux';
import { browserHistory, Link } from 'react-router';
import { editPost, fetchPost } from '../../actions/BlogActions.jsx';
import NotFound from '../../components/presentational/notfound/NotFound.jsx';
import RenderAlert from '../../components/presentational/app/RenderAlert.jsx';
import Spinner from '../../components/presentational/loaders/Spinner.jsx';
// form validation checks
const validate = (values) => {
const errors = {}
if (!values.title) {
errors.title = 'Required';
}
if (!values.image) {
errors.image = 'Required';
}
if (!values.description) {
errors.description = 'Required';
} else if (values.description.length > 10000) {
errors.description = 'Error! Must be 10,000 characters or less!';
}
return errors;
}
// renders input fields
const renderInputField = ({ input, label, type, meta: { touched, error } }) => (
<div>
<label>{label}</label>
<div>
<input {...input} className="form-details complete-expand" placeholder={label} type={type}/>
{touched && error && <div className="error-handlers "><i className="fa fa-exclamation-triangle" aria-hidden="true"></i> {error}</div>}
</div>
</div>
)
// renders a text area field
const renderAreaField = ({ textarea, input, label, type, meta: { touched, error } }) => (
<div>
<label>{label}</label>
<div>
<textarea {...input} className="form-details complete-expand" placeholder={label} type={type}/>
{touched && error && <div className="error-handlers"><i className="fa fa-exclamation-triangle" aria-hidden="true"></i> {error}</div>}
</div>
</div>
)
class BlogPostForm extends Component {
constructor() {
super();
this.state = {
isLoaded: false,
requestTimeout: false,
};
}
componentDidMount() {
if (this.props.location.query.postId) {
// sets a 5 second server timeout
this.timeout = setInterval(this.timer.bind(this), 5000);
// AJAX request to API
fetchPost(this.props.location.query.postId).then((res) => {
// if data returned, seed Redux form
if (res.foundPost) this.initializeForm(res.foundPost);
// if data present, set isLoaded to true, otherwise set a server error
this.setState({
isLoaded: (res.foundPost) ? true : false,
serverError: (res.err) ? res.err : ''
});
});
}
}
componentWillUnmount() {
this.clearTimeout();
}
timer() {
this.setState({ requestTimeout: true });
this.clearTimeout();
}
clearTimeout() {
clearInterval(this.timeout);
}
// initialize Redux form from API supplied data
initializeForm(foundPost) {
const initData = {
id: foundPost._id,
title: foundPost.title,
image: foundPost.image,
imgtitle: foundPost.imgtitle,
description: foundPost.description
}
this.props.initialize(initData);
}
// onSubmit => take Redux form props and send back to server
handleFormSubmit(formProps) {
editPost(formProps).then((res) => {
if (res.err) {
this.setState({
serverError: res.err
});
} else {
browserHistory.push(/blog);
}
});
}
renderServerError() {
const { serverError } = this.state;
// if form submission returns a server error, display the error
if (serverError) return <RenderAlert errorMessage={serverError} />
}
render() {
const { handleSubmit, pristine, reset, submitting, fields: { title, image, imgtitle, description } } = this.props;
const { isLoaded, requestTimeout, serverError } = this.state;
// if data hasn't returned from AJAX request, then render a spinner
if (this.props.location.query.postId && !isLoaded) {
// if AJAX request returns an error or request has timed out, show NotFound component
if (serverError || requestTimeout) return <NotFound />
return <Spinner />
}
// if above conditions are met, clear the timeout, otherwise it'll cause the component to re-render on timer's setState function
this.clearTimeout();
return (
<div className="col-sm-12">
<div className="form-container">
<h1>Edit Form</h1>
<hr />
<form onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}>
<Field name="title" type="text" component={renderInputField} label="Post Title" />
<Field name="image" type="text" component={renderInputField} label="Image URL" />
<Field name="imgtitle" component={renderInputField} label="Image Description" />
<Field name="description" component={renderAreaField} label="Description" />
<div>
<button type="submit" className="btn btn-primary partial-expand rounded" disabled={submitting}>Submit</button>
<button type="button" className="btn btn-danger partial-expand rounded f-r" disabled={ pristine || submitting } onClick={ reset }>Clear Values</button>
</div>
</form>
{ this.renderServerError() }
</div>
</div>
)
}
}
BlogPostForm = reduxForm({
form: 'BlogPostForm',
validate,
fields: ['name', 'image', 'imgtitle', 'description']
})(BlogPostForm);
export default BlogPostForm = connect(BlogPostForm);
BlogActions.jsx:
import * as app from 'axios';
const ROOT_URL = 'http://localhost:3001';
// submits Redux form data to server
export const editPost = ({ id, title, image, imgtitle, description, navTitle }) => {
return app.put(`${ROOT_URL}/post/edit/${id}?userId=${config.user}`, { id, title, image, imgtitle, description, navTitle }, config)
.then(response => {
return { success: response.data.message }
})
.catch(({ response }) => {
if(response.data.deniedAccess) {
return { err: response.data.deniedAccess }
} else {
return { err: response.data.err }
}
});
}
// fetches a single post from the server for front-end editing
export const fetchPost = (id) => {
return app.get(`${ROOT_URL}/posts/${id}`)
.then(response => {
return { foundPost: response.data.post}
})
.catch(({ response }) => {
return { err: response.data.err };
});
}
RenderAlert.jsx:
import React, { Component } from 'react';
const RenderAlert = (props) => {
const displayMessage = () => {
const { errorMessage } = props;
if (errorMessage) {
return (
<div className="callout-alert">
<p>
<i className="fa fa-exclamation-triangle" aria-hidden="true"/>
<strong>Error! </strong> { errorMessage }
</p>
</div>
);
}
}
return (
<div>
{ displayMessage() }
</div>
);
}
export default RenderAlert;
Reducers.jsx
import { routerReducer as routing } from 'react-router-redux';
import { reducer as formReducer } from 'redux-form';
import { combineReducers } from 'redux';
const rootReducer = combineReducers({
form: formReducer,
routing
});
export default rootReducer;
values
object) is important. – Ellanellard