Component not able to re-render when state is changed - javascript

I am new to react. I want to confirm the input JSON is valid or not and show that on scree. The action ValidConfiguration is being fired and reducer is returning the new state but the smart component add-config-container is not being re-rendered
Here are my files:
Action
import {
VALID_CONFIGURATION,
INVALID_CONFIGURATION,
SAVE_CONFIGURATION,
START_FETCHING_CONFIGS,
FINISH_FETCHING_CONFIGS,
EDIT_CONFIGURAION
} from '../constants';
function validateConfiguration(jsonString) {
try {
JSON.parse(jsonString);
} catch (e) {
return false;
}
return true;
}
export function isConfigurationValid(state) {
if (validateConfiguration(state.jsonText)) {
return({type: VALID_CONFIGURATION, state : state});
} else {
return({type: INVALID_CONFIGURATION, state : state});
}
}
export function fetchConfiguration() {
return ({type : START_FETCHING_CONFIGS});
}
export function finishConfiguration(configs) {
return ({type : FINISH_FETCHING_CONFIGS, configs: configs});
}
export function editConfiguration(index) {
return ({type : EDIT_CONFIGURATION, index : index});
}
export function saveConfiguration(config) {
return ({type: SAVE_CONFIGURATION, config : config});
}
Container component
import React, {Component} from 'react';
import {Button, Input, Snackbar} from 'react-toolbox';
import {isConfigurationValid, saveConfiguration} from '../../actions/config';
import { connect } from 'react-redux';
import style from '../../theme/layout.scss';
class AddConfigContainer extends Component {
constructor(props) {
super(props);
this.state = {jsonText: '', key: '', valid: false, showBar : true};
}
handleChange(text, value) {
this.setState({[text]: value});
}
handleSnackbarClick() {
this.setState({ showBar: false});
};
handleSnackbarTimeout() {
this.setState({ showBar: false});
};
render() {
let {onValid} = this.props;
return (
<div>
<h4>Add Configs</h4>
<span>Add configs in text box and save</span>
<Input type='text' label='Enter Key'
value={this.state.key} onChange={this.handleChange.bind(this, 'key')} required/>
<Input type='text' multiline label='Enter JSON configuration'
value={this.state.jsonText} onChange={this.handleChange.bind(this, 'jsonText')} required/>
<div>IsJSONValid = {this.state.valid ? 'true': 'false'}</div>
<Snackbar action='Dismiss'
label='JSON is invalid'
icon='flag'
timeout={2000}
active={ this.state.showBar }
onClick={this.handleSnackbarClick.bind(this)}
onTimeout={this.handleSnackbarTimeout.bind(this)}
type='accept'
class = {style.loader}
/>
<Button type="button" label = "Save Configuration" icon="add" onClick={() => {onValid(this.state)}}
accent
raised/>
</div>
);
}
}
const mapStateToProps = (state) => {
let {
jsonText,
key,
valid
} = state;
return {
jsonText,
key,
valid
};
};
const mapDispatchToProps = (dispatch) => {
return {
onValid : (value) => dispatch(isConfigurationValid(value)),
saveConfiguration: (config) => dispatch(saveConfiguration(config))
}
};
export default connect(mapStateToProps, mapDispatchToProps)(AddConfigContainer);
Reducer
import assign from 'object.assign';
import {VALID_CONFIGURATION, INVALID_CONFIGURATION} from '../constants';
const initialState = {
jsonText : '',
key : '',
valid : false,
showBar: false,
configs: [json],
activeConfig : {},
isFetching: false
};
export default function reducer(state = initialState, action) {
if (action.type === VALID_CONFIGURATION) {
return (assign({}, state, action.state, {valid: true}));
} else if (action.type === INVALID_CONFIGURATION) {
return assign({}, state, action.state, {valid: false});
} else {
return state;
}
}

I think your component does re-render, but you never actually use the valid value from props (i.e. this.props.valid). You only use this.state.valid, but that is not changed anywhere in the code. Note that Redux won't (and can't) change the component's internal state, it only passes new props to the component, so you need to use this.props.valid to see the change happen. Essentially, you should consider whether you need valid to exist in the component's state at all. I don't think you do, in this case all the data you have in state (except perhaps showBar) doesn't need to be there and you can just take it from props.
If you do need to have them in state for some reason, you can override e.g. componentWillReceiveProps to update the component's state to reflect the new props.

Related

How can I use the context in the constructor in my React Form?

I have an issue in my React form. I must use the context to know what the name of the form is to set/get the value from the Redux store.
However, I have an issue. My form is in two parts. I set the values in the Redux store and if I need to go back to the previous part of the form, I still have the value saved. However, I have a little problem. I can't set the default state of the form input using the context since I don't know how to access the context in the constructor.
Could you help me achieve this?
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import { handleChange } from 'redux/actions';
import { connect } from 'react-redux';
import FormContext from 'context/FormContext';
export class TextInput extends Component {
constructor (props, context) {
super(props, context);
this.state = { value: this.getValue(context) || '' };
this.handleChange = this.handleChange.bind(this);
this.handleBlur = this.handleBlur.bind(this);
}
getRequired () {
if (this.props.required === true) {
return <span className="tw-font-semibold tw-text-red-500 tw-text-sm tw-ml-2">{this.props.t('required')}</span>;
}
}
handleChange (e) {
var value = e.target.value;
this.setState({ value: value });
}
handleBlur (context) {
this.props.handleChange(this.props.name, this.state.value, context.name);
}
getValue (context) {
if (this.props.input && this.props.input[context.name] && this.props.input[context.name][this.props.name]) {
return this.props.input[context.name][this.props.name];
} else {
return undefined;
}
}
render () {
return (
<FormContext.Consumer>
{context =>
<div className={`tw-flex tw-flex-col ${this.props.size} tw-px-2 tw-mb-3`}>
<label htmlFor={this.props.name} className="tw-text-sm tw-font-bold">{this.props.title || this.props.t('common:' + this.props.name)}{this.getRequired()}</label>
<input
value={this.state.value}
onChange={this.handleChange}
onBlur={() => {
this.handleBlur(context);
}}
type={this.props.type} id={this.props.name} placeholder={this.props.title} className="focus:tw-outline-none focus:tw-shadow-outline tw-bg-gray-300 tw-rounded-lg tw-py-2 tw-px-3" />
{this.props.errors && this.props.errors[context.name] && this.props.errors[context.name][this.props.name] && (
<div className="tw-bg-red-100 tw-mt-2 tw-border-l-4 tw-border-red-500 tw-text-red-700 tw-p-2 tw-text-sm">
<p>{this.props.errors[context.name][this.props.name]}</p>
</div>
)}
</div>
}
</FormContext.Consumer>
);
}
}
TextInput.defaultProps = {
size: 'w-full',
required: true,
type: 'text'
};
TextInput.propTypes = {
name: PropTypes.string.isRequired,
title: PropTypes.string,
size: PropTypes.string.isRequired,
required: PropTypes.bool,
type: PropTypes.string,
t: PropTypes.func.isRequired
};
const mapStateToProps = ({ errors, input }, ownProps) => {
return {
errors: errors,
input: input
};
};
export default connect(mapStateToProps, { handleChange })(withTranslation(['input'])(TextInput));
How about you wrap your FormContext wherever you call your TextInput. In that way, you could access your FormContext in your constructor.
function FormThatUsesTextInput() {
return (
<FormContext.Consumer>
{context => <TextInput context={context} {...otherProps} />}
</FormContext.Consumer>
)
}

How do I display only the text user has written from component to another?

After I'm done creating an article via CreateArticle component (which works fine), I want to display only what the user has written (its value) in <textarea value={value}> in the SearchArticle component via displayName() function.
In other words, in CreateArticle component when the user's done typing something in <textarea/> followed by clicking Submit (which's a button that saves what the user wrote), I want to only display what the user has written in Search Article inside displayName() function.
What am I doing wrong and how can I fix it?
Here's CreateArticle:
import React, { Component } from 'react';
import {connect} from "react-redux";
import * as actionType from "../../store/actions/actions";
class CreateArticle extends Component {
constructor(props) {
super(props);
}
handleSubmit = event => {
this.setState({storyTextValue: event.target.value});
this.props.storyTextValueRedux(event.target.storyTextValue);
event.preventDefault();
}
handleStoryText = event => {
event.preventDefault();
this.setState({value: event.target.value});
}
onSubmit = () => {
if(this.state.value === "") {
alert("Please enter the value and then click submit");
} else {
alert("Article saved " + '\n' + this.state.value + this.props.articleIdValue);
}
}
render() {
return(
<div>
<form onSubmit={this.handleSubmit}>
<input onChange={this.handleChange} value={this.props.cityCodeValue} type="text" placeholder="city code"/>
<input type="text" placeholder="author name"/>
<textarea value={this.props.storyTextValue} onChange={this.handleStoryText} rows="2" cols="25" />
<button type="submit" value="Submit" onClick={() => this.onSubmit()}>Submit</button>
</form>
</div>
);
}
}
const mapStateToProps = state => {
return {
articleIdValue: state.articleIdValue.articleIdValue,
storyTextValue: state.storyTextValue.storyTextValue
};
};
const mapDispatchToProps = dispatch => {
return {
articleIdValueRedux: (value) => dispatch({type: actionType.ARTICLE_ID_VALUE, value}),
storyTextValueRedux: (value) => dispatch({type: actionType.STORY_VALUE, value})
};
};
export default connect(mapStateToProps, mapDispatchToProps)(CreateArticle);
Here's SearchArticle:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actionType from '../../store/actions/actions';
import ArticleText from '../../containers/ArticleText/ArticleText';
class SearchArticle extends Component {
constructor(props) {
super(props);
this.state = {
flag: false
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
this.props.CityCodeReducerRedux(event.target.value);
}
handleSubmit(event) {
this.setState({flag: true});
event.preventDefault();
}
displayName = () => {
if(this.props.cityCodeValue === "nyc" || this.props.articleIdValue === 1) {
return(
<div>
<p>author name: {this.props.authorNameValue}</p>
<p>article text: {<ArticleText/>}</p>
</div>
);
}
}
render() {
return(
<div>
<form onSubmit={this.handleSubmit}>
<input onChange={this.handleChange} value={this.props.cityCodeValue} type="text" placeholder="city code"/>
<input onChange={this.handleChange} value={this.props.articleIdValue} placeholder="article id"/>
<button onClick={() => this.displayName} value="Search">Submit</button>
{this.state.flag ? this.displayName() : null}
</form>
</div>
);
}
}
const mapStateToProps = state => {
return {
cityCodeValue: state.cityCodeValue.cityCodeValue,
authorNameValue: state.authorNameValue.authorNameValue,
articleIdValue: state.articleIdValue.articleIdValue
};
};
const mapDispatchToProps = dispatch => {
return {
CityCodeReducerRedux: (value) => dispatch({type: actionType.CITY_CODE_VALUE, value}),
articleIdValueRedux: (value) => dispatch({type: actionType.ARTICLE_ID_VALUE, value})
};
};
export default connect(mapStateToProps, mapDispatchToProps)(SearchArticle);
Here's StoryTextReducer:
import * as actionType from '../store/actions/actions';
const initialState = {
storyTextValue: ''
};
const StoryTextReducer = (state = initialState, action) => {
switch (action.type) {
case actionType.STORY_VALUE:
return {
...state,
storyTextValue: action.value
};
default:
return state;
}
};
export default StoryTextReducer;
You're looking at 2 different instances of <ArticleText>. Changing properties of the instance in CreateArticle will not change the instance in SearchArticle
Instead of trying to share a value by using the same class, you should be sharing a value by whats in the redux store.
You should define an action that dispatches and event like {type: updateArticleText, articleText: foo}, then in your reducer you can set the redux state to have a property ArticleText equal to event.articleText.
Now that your value is stored in redux store rather than component state, your components simply get props from the redux store in the mapStateToProps function.

Async data loading in Redux-form

I am working with React and Redux, and I'm using Redux-form for my forms.
I am trying to load some initial data from database. In the docs it is recommended to use
{
load: loadAccount
}
in connect component, but I'm struggling to set it properly.
I can load initial data in a different way: adding a dispatch action to connect component and call it from componentDidMount, but I would like to understand how to set { load: loadAccount }.
This is my actions file —I show only the action that matters—:
const actions = {
requestProject: () => {
return {
type: C.LOAD_PROJECT_STARTED,
};
},
receiveProject: (id, data) => {
return {
type: C.LOAD_PROJECT_SUCCESS,
id,
data,
};
},
loadProject: (id = '') => {
const url = '/api/projects/';
const encodedURI = isBrowser
? encodeURI(window.location.origin + url + id)
: encodeURI('http://localhost:' + config.SERVER + url + id);
return isBrowser
? function(dispatch) {
dispatch(actions.requestProject());
return fetch(encodedURI)
.then(
(response) => {
return response.json();
},
(error) => {
return console.log('An error occurred.', error);
}
)
.then((data) => {
return dispatch(actions.receiveProject(id, data));
});
}
: fetch(encodedURI)
.then((response) => {
return response.json();
})
.then((data) => {
return data;
})
.catch((error) => {
return Promise.reject(Error(error.message));
});
},
};
export default actions;
Then my reducers —again, only one—:
export const Project = (state = {}, action) => {
switch (action.type) {
case C.LOAD_PROJECT_STARTED:
console.log('LOAD_PROJECT_STARTED');
return Object.assign({}, state, {
isFetching: true,
});
case C.LOAD_PROJECT_SUCCESS:
return Object.assign({}, state, {
...action.data.Project,
isFetching: false,
});
case C.SUBMIT_PROJECT_FORM_STARTED:
return Object.assign({}, state, {
isFetching: true,
});
case C.SUBMIT_PROJECT_FORM_SUCCESS:
return Object.assign({}, state, {
...action.data.Project,
isFetching: false,
});
default:
return state;
}
};
export const form = formReducer;
This is the connect ProjectForm component for the form:
import { connect } from 'react-redux';
import ProjectFormUi from './ProjectFormUi';
import actions from '../../redux/actions';
import { load as loadAccount } from './account'
export const ProjectForm = connect(
(state) => {
return {
initialValues: state.Project,
};
},
(dispatch) => {
return {
loadProject(id) {
dispatch(actions.loadProject(id));
},
onSubmit(data) {
dispatch(actions.sendingProject(data));
},
};
},
{
load: loadAccount,
}
)(ProjectFormUi);
export default ProjectForm;
And finally, the actual form component, ProjectFormUi.
import React from 'react';
import { Field, reduxForm } from 'redux-form';
class ProjectFormUi extends React.Component {
constructor(props) {
super(props);
this.state = {
};
}
componentDidMount() {
this.props.loadProject(1);
}
static getDerivedStateFromProps(newProps, prevState) {
return newProps != prevState ? newProps : null;
}
componentDidUpdate(prevProps) {
if (this.props.Project !== prevProps.Project) {
this.setState({
isFetching: this.props.Project.isFetching,
});
}
}
render() {
const { handleSubmit, load, pristine, reset, submitting } = this.props;
return (
<form onSubmit={handleSubmit}>
<div>
<label>First Name</label>
<div>
<Field name="title" component="input" type="text" placeholder="First Name" />
</div>
</div>
<div>
<button type="submit" disabled={submitting}>
Submit
</button>
<button type="button" disabled={pristine || submitting} onClick={reset}>
Undo Changes
</button>
</div>
</form>
);
}
}
export default reduxForm({
form: 'ProjectForm',
enableReinitialize: true,
})(ProjectFormUi);
As I said, currently I'm loading data in connect ProjectForm component with:
loadProject(id) {
dispatch(actions.loadProject(id));
},
which is called from componentDidMount in ProjectFormUi component.
But I would like to understand how to load data as in the docs, setting
{
load: loadAccount
}
in connect ProjectForm component.

Dispatching an in-progress action in Redux

I have a signup form, which posts data asynchronously to an API and then does some stuff based on the response. I am dispatching a "signup_in_progress" action as soon as the function is called, with a payload of "true", and update my state based on that, then dispatching this action with a payload of "false" when the promise is resolved.
I can see that the dispatch happens as intended by putting a console.log() statement in the reducer.
What I would like to happen is for a spinner to appear instead of the signup form when the signup_in_progress piece of state is "true." But that's not happening. Any idea what I'm missing?
My action:
export function signUpUser(props) {
return function(dispatch) {
dispatch({ type: SIGNUP_IN_PROGRESS, payload: true });
axios
.post(`${ROOT_URL}/signup`, props)
.then(() => {
dispatch({ type: SIGNUP_IN_PROGRESS, payload: false });
dispatch({ type: SIGNUP_SUCCESS });
browserHistory.push(`/signup/verify-email?email=${props.email}`);
})
.catch(response => {
dispatch({ type: SIGNUP_IN_PROGRESS, payload: false });
dispatch(authError(SIGNUP_FAILURE, response.response.data.error));
});
};
}
The relevant part of my reducer:
import {
...
SIGNUP_IN_PROGRESS,
SIGNUP_SUCCESS,
SIGNUP_FAILURE...
} from '../actions/types';
export default function(state = {}, action) {
switch (action.type) {
...
case SIGNUP_IN_PROGRESS:
return { ...state, signing_up: action.payload };
...
My connected component:
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import * as actions from '../../actions';
import SignupFirstPage from './signupComponents/signupFirstPage';
import SignupSecondPage from './signupComponents/signupSecondPage';
import SignupThirdPage from './signupComponents/SignupThirdPage';
import Spinner from 'react-spinkit';
class SignUp extends Component {
constructor(props) {
super(props);
this.nextPage = this.nextPage.bind(this);
this.previousPage = this.previousPage.bind(this);
this.state = {
page: 1
};
this.handleFormSubmit = this.handleFormSubmit.bind(this);
}
nextPage() {
this.setState({ page: this.state.page + 1 });
}
previousPage() {
this.setState({ page: this.state.page - 1 });
}
handleFormSubmit(props) {
this.props.signUpUser(props);
}
render() {
const { handleSubmit } = this.props;
const { page } = this.state;
if (this.props.signingUp) {
return (
<div className="dashboard loading">
<Spinner name="chasing-dots" />
</div>
);
}
return (
<div>
{page === 1 && <SignupFirstPage onSubmit={this.nextPage} />}
{page === 2 && (
<SignupSecondPage
previousPage={this.previousPage}
onSubmit={this.nextPage}
/>
)}
{page === 3 && (
<SignupThirdPage
previousPage={this.previousPage}
onSubmit={values => this.props.signUpUser(values)}
/>
)}
<div>
{this.props.errorMessage &&
this.props.errorMessage.signup && (
<div className="error-container">
Oops! {this.props.errorMessage.signup}
</div>
)}
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
singingUp: state.auth.signing_up,
errorMessage: state.auth.error
};
}
SignUp = reduxForm({ form: 'wizard' })(SignUp);
export default connect(mapStateToProps, actions)(SignUp);
You just have a typo in your code:
function mapStateToProps(state) {
return {
singingUp: state.auth.signing_up, // TYPO!!
errorMessage: state.auth.error
};
}
should be changed to
function mapStateToProps(state) {
return {
signingUp: state.auth.signing_up,
errorMessage: state.auth.error
};
}
I recommend using redux-pack which allows you to handle three phases of the request in the reducer: start, success and error.
Then you can use the start handler to set the boolean to true, and on success set it to false.

React + Redux - What's the best way to handle CRUD in a form component?

I got one form who is used to Create, Read, Update and Delete. I created 3 components with the same form but I pass them different props. I got CreateForm.js, ViewForm.js (readonly with the delete button) and UpdateForm.js.
I used to work with PHP, so I always did these in one form.
I use React and Redux to manage the store.
When I'm in the CreateForm component, I pass to my sub-components this props createForm={true} to not fill the inputs with a value and don't disable them. In my ViewForm component, I pass this props readonly="readonly".
And I got another problem with a textarea who is filled with a value and is not updatable. React textarea with value is readonly but need to be updated
What's the best structure to have only one component which handles these different states of the form?
Do you have any advice, tutorials, videos, demos to share?
I found the Redux Form package. It does a really good job!
So, you can use Redux with React-Redux.
First you have to create a form component (obviously):
import React from 'react';
import { reduxForm } from 'redux-form';
import validateContact from '../utils/validateContact';
class ContactForm extends React.Component {
render() {
const { fields: {name, address, phone}, handleSubmit } = this.props;
return (
<form onSubmit={handleSubmit}>
<label>Name</label>
<input type="text" {...name}/>
{name.error && name.touched && <div>{name.error}</div>}
<label>Address</label>
<input type="text" {...address} />
{address.error && address.touched && <div>{address.error}</div>}
<label>Phone</label>
<input type="text" {...phone}/>
{phone.error && phone.touched && <div>{phone.error}</div>}
<button onClick={handleSubmit}>Submit</button>
</form>
);
}
}
ContactForm = reduxForm({
form: 'contact', // the name of your form and the key to
// where your form's state will be mounted
fields: ['name', 'address', 'phone'], // a list of all your fields in your form
validate: validateContact // a synchronous validation function
})(ContactForm);
export default ContactForm;
After this, you connect the component which handles the form:
import React from 'react';
import { connect } from 'react-redux';
import { initialize } from 'redux-form';
import ContactForm from './ContactForm.react';
class App extends React.Component {
handleSubmit(data) {
console.log('Submission received!', data);
this.props.dispatch(initialize('contact', {})); // clear form
}
render() {
return (
<div id="app">
<h1>App</h1>
<ContactForm onSubmit={this.handleSubmit.bind(this)}/>
</div>
);
}
}
export default connect()(App);
And add the redux-form reducer in your combined reducers:
import { combineReducers } from 'redux';
import { appReducer } from './app-reducers';
import { reducer as formReducer } from 'redux-form';
let reducers = combineReducers({
appReducer, form: formReducer // this is the form reducer
});
export default reducers;
And the validator module looks like this:
export default function validateContact(data, props) {
const errors = {};
if(!data.name) {
errors.name = 'Required';
}
if(data.address && data.address.length > 50) {
errors.address = 'Must be fewer than 50 characters';
}
if(!data.phone) {
errors.phone = 'Required';
} else if(!/\d{3}-\d{3}-\d{4}/.test(data.phone)) {
errors.phone = 'Phone must match the form "999-999-9999"'
}
return errors;
}
After the form is completed, when you want to fill all the fields with some values, you can use the initialize function:
componentWillMount() {
this.props.dispatch(initialize('contact', {
name: 'test'
}, ['name', 'address', 'phone']));
}
Another way to populate the forms is to set the initialValues.
ContactForm = reduxForm({
form: 'contact', // the name of your form and the key to
fields: ['name', 'address', 'phone'], // a list of all your fields in your form
validate: validateContact // a synchronous validation function
}, state => ({
initialValues: {
name: state.user.name,
address: state.user.address,
phone: state.user.phone,
},
}))(ContactForm);
If you got any other way to handle this, just leave a message! Thank you.
UPDATE: its 2018 and I'll only ever use Formik (or Formik-like libraries)
There is also react-redux-form (step-by-step), which seems exchange some of redux-form's javascript (& boilerplate) with markup declaration. It looks good, but I've not used it yet.
A cut and paste from the readme:
import React from 'react';
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import { modelReducer, formReducer } from 'react-redux-form';
import MyForm from './components/my-form-component';
const store = createStore(combineReducers({
user: modelReducer('user', { name: '' }),
userForm: formReducer('user')
}));
class App extends React.Component {
render() {
return (
<Provider store={ store }>
<MyForm />
</Provider>
);
}
}
./components/my-form-component.js
import React from 'react';
import { connect } from 'react-redux';
import { Field, Form } from 'react-redux-form';
class MyForm extends React.Component {
handleSubmit(val) {
// Do anything you want with the form value
console.log(val);
}
render() {
let { user } = this.props;
return (
<Form model="user" onSubmit={(val) => this.handleSubmit(val)}>
<h1>Hello, { user.name }!</h1>
<Field model="user.name">
<input type="text" />
</Field>
<button>Submit!</button>
</Form>
);
}
}
export default connect(state => ({ user: state.user }))(MyForm);
Edit: Comparison
The react-redux-form docs provide a comparison vs redux-form:
https://davidkpiano.github.io/react-redux-form/docs/guides/compare-redux-form.html
For those who doesn't care about an enormous library for handling form related issues, I would recommend redux-form-utils.
It can generate value and change handlers for your form controls, generate reducers of the form, handy action creators to clear certain(or all) fields, etc.
All you need to do is assemble them in your code.
By using redux-form-utils, you end up with form manipulation like following:
import { createForm } from 'redux-form-utils';
#createForm({
form: 'my-form',
fields: ['name', 'address', 'gender']
})
class Form extends React.Component {
render() {
const { name, address, gender } = this.props.fields;
return (
<form className="form">
<input name="name" {...name} />
<input name="address" {...address} />
<select {...gender}>
<option value="male" />
<option value="female" />
</select>
</form>
);
}
}
However, this library only solves problem C and U, for R and D, maybe a more integrated Table component is to antipate.
Just another thing for those who want to create fully controlled form component without using oversized library.
ReduxFormHelper - a small ES6 class, less than 100 lines:
class ReduxFormHelper {
constructor(props = {}) {
let {formModel, onUpdateForm} = props
this.props = typeof formModel === 'object' &&
typeof onUpdateForm === 'function' && {formModel, onUpdateForm}
}
resetForm (defaults = {}) {
if (!this.props) return false
let {formModel, onUpdateForm} = this.props
let data = {}, errors = {_flag: false}
for (let name in formModel) {
data[name] = name in defaults? defaults[name] :
('default' in formModel[name]? formModel[name].default : '')
errors[name] = false
}
onUpdateForm(data, errors)
}
processField (event) {
if (!this.props || !event.target) return false
let {formModel, onUpdateForm} = this.props
let {name, value, error, within} = this._processField(event.target, formModel)
let data = {}, errors = {_flag: false}
if (name) {
value !== false && within && (data[name] = value)
errors[name] = error
}
onUpdateForm(data, errors)
return !error && data
}
processForm (event) {
if (!this.props || !event.target) return false
let form = event.target
if (!form || !form.elements) return false
let fields = form.elements
let {formModel, onUpdateForm} = this.props
let data = {}, errors = {}, ret = {}, flag = false
for (let n = fields.length, i = 0; i < n; i++) {
let {name, value, error, within} = this._processField(fields[i], formModel)
if (name) {
value !== false && within && (data[name] = value)
value !== false && !error && (ret[name] = value)
errors[name] = error
error && (flag = true)
}
}
errors._flag = flag
onUpdateForm(data, errors)
return !flag && ret
}
_processField (field, formModel) {
if (!field || !field.name || !('value' in field))
return {name: false, value: false, error: false, within: false}
let name = field.name
let value = field.value
if (!formModel || !formModel[name])
return {name, value, error: false, within: false}
let model = formModel[name]
if (model.required && value === '')
return {name, value, error: 'missing', within: true}
if (model.validate && value !== '') {
let fn = model.validate
if (typeof fn === 'function' && !fn(value))
return {name, value, error: 'invalid', within: true}
}
if (model.numeric && isNaN(value = Number(value)))
return {name, value: 0, error: 'invalid', within: true}
return {name, value, error: false, within: true}
}
}
It doesn't do all the work for you. However it facilitates creation, validation and handling of a controlled form component.
You may just copy & paste the above code into your project or instead, include the respective library - redux-form-helper (plug!).
How to use
The first step is add specific data to Redux state which will represent the state of our form.
These data will include current field values as well as set of error flags for each field in the form.
The form state may be added to an existing reducer or defined in a separate reducer.
Furthermore it's necessary to define specific action initiating update of the form state as well as respective action creator.
Action example:
export const FORM_UPDATE = 'FORM_UPDATE'
export const doFormUpdate = (data, errors) => {
return { type: FORM_UPDATE, data, errors }
}
...
Reducer example:
...
const initialState = {
formData: {
field1: '',
...
},
formErrors: {
},
...
}
export default function reducer (state = initialState, action) {
switch (action.type) {
case FORM_UPDATE:
return {
...ret,
formData: Object.assign({}, formData, action.data || {}),
formErrors: Object.assign({}, formErrors, action.errors || {})
}
...
}
}
The second and final step is create a container component for our form and connect it with respective part of Redux state and actions.
Also we need to define a form model specifying validation of form fields.
Now we instantiate ReduxFormHelper object as a member of the component and pass there our form model and a callback dispatching update of the form state.
Then in the component's render() method we have to bind each field's onChange and the form's onSubmit events with processField() and processForm() methods respectively as well as display error blocks for each field depending on the form error flags in the state.
The example below uses CSS from Twitter Bootstrap framework.
Container Component example:
import React, {Component} from 'react';
import {connect} from 'react-redux'
import ReduxFormHelper from 'redux-form-helper'
class MyForm extends Component {
constructor(props) {
super(props);
this.helper = new ReduxFormHelper(props)
this.helper.resetForm();
}
onChange(e) {
this.helper.processField(e)
}
onSubmit(e) {
e.preventDefault()
let {onSubmitForm} = this.props
let ret = this.helper.processForm(e)
ret && onSubmitForm(ret)
}
render() {
let {formData, formErrors} = this.props
return (
<div>
{!!formErrors._flag &&
<div className="alert" role="alert">
Form has one or more errors.
</div>
}
<form onSubmit={this.onSubmit.bind(this)} >
<div className={'form-group' + (formErrors['field1']? ' has-error': '')}>
<label>Field 1 *</label>
<input type="text" name="field1" value={formData.field1} onChange={this.onChange.bind(this)} className="form-control" />
{!!formErrors['field1'] &&
<span className="help-block">
{formErrors['field1'] === 'invalid'? 'Must be a string of 2-50 characters' : 'Required field'}
</span>
}
</div>
...
<button type="submit" className="btn btn-default">Submit</button>
</form>
</div>
)
}
}
const formModel = {
field1: {
required: true,
validate: (value) => value.length >= 2 && value.length <= 50
},
...
}
function mapStateToProps (state) {
return {
formData: state.formData, formErrors: state.formErrors,
formModel
}
}
function mapDispatchToProps (dispatch) {
return {
onUpdateForm: (data, errors) => {
dispatch(doFormUpdate(data, errors))
},
onSubmitForm: (data) => {
// dispatch some action which somehow updates state with form data
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(MyForm)
Demo

Categories

Resources