I have a simple React app that I am going to post entirely (it is not that long). The app doesn't work but doesn't throw any error either. I tried to log the states and it turns out they never change. I am using big things like custom hook and useReducer, but I suspect I lack on understanding basic principles of how react works.
Here's a short summary of how the app should work:
There is a Form component which returns a series of custom Input elements (here only two).
The Input component outsources the validation logic to a custom hook which returns [isTouched, isValid, dispatcherOfTheCustomHookReducer]. When an event occurs, the Input component calls the dispatcher of the custom hook and then styles should be applied to the <input> element based on the state returned by the reducer in the custom hook.
Also since the Form component needs to know if the form as a whole is valid, each Input has an onChangeValidity property used to lift up the isValid state.
In theory the form should appear neutral at the beginning and then, after you focus and blur an input this should become either valid (blue background) or invalid (red background).
I should probably reset the inputs after submission and add something else, but for now I want to make the app work. At the moment the states never changes and the forms appears always neutral (white).
You may prefer look at the files in codesandbox.
App.js
import Form from './components/Form';
function App() {
return (
<div className="app">
<Form />
</div>
);
}
export default App;
Form.js
import { useReducer } from 'react';
import Input from './Input';
// put your inputs' ID here to generate the default state
const defaultState = (inputs = ['username', 'email']) => {
let inputsState = {};
for (const input of inputs) inputsState[input] = false;
return { ...inputsState, isValid: false };
};
const formReducer = (state, action) => {
let newInputsStateList = {...state, [action.id]: action.isValid};
delete newInputsStateList.isValid;
let isValid = true;
for(const key in newInputsStateList) {
if(!newInputsStateList[key]) isValid = false;
break;
}
return { ...newInputsStateList, isValid};
}
const Form = props => {
const [formState, dispatchFormState] = useReducer(formReducer, undefined, defaultState);
const submitHandler = event => {
event.preventDefault();
console.log('You are logged in.');
}
return <form onSubmit={submitHandler}>
<Input
id='username'
label='Username'
type='text'
test={username => username.trim().length > 6}
onChangeValidity={validity => dispatchFormState({id: 'username', isValid: validity})}
/>
<Input
id='email'
label='Email'
type='email'
test={email => email.includes('#')}
onChangeValidity={validity => dispatchFormState({id: 'email', isValid: validity})}
/>
<button type='submit' disabled={!formState.isValid} >Submit</button>
</form>
};
export default Form;
Input.js
import { useEffect } from 'react';
import classes from './Input.module.css';
import useValidation from '../hooks/use-validation';
const Input = props => {
const [isTouched, isValid, checkValidity] = useValidation();
// eslint-disable-next-line
useEffect(() => props.onChangeValidity(isValid), [isValid]);
return <div className={classes.generic_input}>
<label className={classes['generic_input-label']} htmlFor={props.id} >{props.label}</label>
<input
className={classes[`${isTouched ? 'generic_input-input--'+isValid ? 'valid' : 'invalid' : ''}`]}
type={props.type}
name={props.id}
id={props.id}
onChange={event => checkValidity({
type: 'CHANGE',
value: event.target.value,
test: props.test
})}
onBlur={event => checkValidity({
type: 'BLUR',
value: event.target.value,
test: props.test
})}
/>
</div>
};
export default Input;
use-validation.js
import { useReducer } from 'react';
const validationReducer = (state, action) => {
let isTouched = state.isTouched;
let isValid = state.isValid;
if(action.type === 'CHANGE') if (isTouched) isValid = action.test(action.value);
else if(action.type === 'BLUR') {
isValid = action.test(action.value);
if (!isTouched) isTouched = true;
}
else isTouched = isValid = false;
return {isTouched, isValid};
}
const useValidation = () => {
const [validationState, dispatchValidation] = useReducer(validationReducer, {isTouched: false, isValid: false});
return [validationState.isTouched, validationState.isValid, dispatchValidation];
};
export default useValidation;
Input.module.css
.generic_input {
display: flex;
flex-direction: column;
padding: 1rem;
}
.generic_input-label {
font-weight: bold;
}
.generic_input-input--valid {
background-color: lightblue;
}
.generic_input-input--invalid {
border-color: red;
background-color: rgb(250, 195, 187);
}
.submit:disabled {
background-color: #CCC;
color: #292929;
border-color: #CCC;
cursor: not-allowed;
}
I think you need to fix the isTouched logic in your validationReducer. isTouched never gets set to true:
Something like:
const validationReducer = (state, action) => {
let isTouched = state.isTouched;
let isValid = state.isValid;
if (action.type === "CHANGE") {
isTouched = true;
isValid = action.test(action.value)
} else if (action.type === "BLUR") {
isValid = action.test(action.value);
} else {
isTouched = isValid = false;
}
return { isTouched, isValid };
};
... though I'm not sure when you'd want isTouched to be set to false again, so that logic needs some work...
Also, the class on your input is not correct.
Its should look like:
<input
className={
classes[
isTouched
? `generic_input-input--${isValid ? "valid" : "invalid"}`
: ""
]
}
...
>
Take a look at this sandbox
Related
I'm trying to set up an onChange for a text box input but I can't work out why it isn't working... I've logged the output inside the handler function and the value seems to update. The problem is that when I'm passing this to the input component it shows an empty string still. Not sure why?
A second question I have is that I tried destructuring the input config initial value and the console yields an error saying the value is read-only. Could anyone explain why? This option is currently commented out.
See the component logic below:
import React, { useState } from 'react';
import classes from './BioSection.css';
import Input from '../../UI/Input/Input';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faCameraRetro, faUser } from '#fortawesome/free-solid-svg-icons';
const BioSection = () => {
const [addingBio, setAddingBio] = useState(false);
const [inputConfig, setInputConfig] = useState({
elementType: 'textarea',
elementConfig: {
placeholder: 'Bio',
},
value: '',
length: 0,
validation: {
required: true,
minLength: 10,
maxLength: 100,
},
valid: false,
});
const checkValidity = (value, rules) => {
let isValid = true;
if (rules.minLength) {
isValid = value.length >= rules.minLength && isValid;
}
if (rules.maxLength) {
isValid = value.length <= rules.maxLength && isValid;
}
return isValid;
};
const addBio = () => {
setAddingBio(true);
};
const saveBio = () => {
setAddingBio(!addingBio);
//request POST http
};
const cancelBioUpdate = () => {
setAddingBio(!addingBio);
};
const textAreaChangedHandler = (e) => {
console.log(e.target.value);
const copyOfInputConfig = inputConfig;
copyOfInputConfig.value = e.target.value;
copyOfInputConfig.length = e.target.value.length;
copyOfInputConfig.valid = checkValidity(
copyOfInputConfig.value.trim(),
copyOfInputConfig.validation
);
console.log(copyOfInputConfig);
// const { value, length, valid, validation } = copyOfInputConfig;
// value = e.target.value;
// value = e.target.value;
// length = e.targer.value.length;
// valid = checkValidity(copyOfInputConfig.value.trim(), validation);
let formIsValid = true;
for (let inputIdentifier in copyOfInputConfig) {
formIsValid = copyOfInputConfig.valid && formIsValid;
}
setInputConfig(copyOfInputConfig);
};
return (
<div className={classes.BioSection}>
{addingBio ? (
<div className={classes.UserBio}>
<Input
bioSection
key={inputConfig.elementType}
elementType={inputConfig.elementType}
elementConfig={inputConfig.elementConfig}
value={inputConfig.value}
valueLength={inputConfig.value.length}
invalid={!inputConfig.valid}
shouldValidate={inputConfig.validation}
maxCharacters={inputConfig.validation.maxLength}
changed={(e) => {
textAreaChangedHandler(e);
}}
/>
<div className={classes.BioButtonHolder}>
<button onClick={cancelBioUpdate}>cancel</button>
<button onClick={saveBio}>save</button>
</div>
</div>
) : (
<div>
<span>add travel bio</span>
<button onClick={addBio}>add bio</button>
</div>
)}
</div>
);
};
export default BioSection;
why don't you setState directly like this??
setInputConfig({
...inputConfig,
value: e.target.value,
length: e.target.value.length,
valid: checkValidity(
e.target.value.trim(),
inputConfig.validation
),
});
as for the second question you are trying to assign new value to a constant.
I'm using Formik for validating some data. It works fine when it should create new entity, but there are problems when I want to edit an entity.
The edit mode must be activated from the state (this.state.edit === true), also the data of the entity is stored on the state, for example this.state.name has a string value there.
I put a console log in render, the problem is that the log is printed several times, the first time with empty string on this.sate.name and the value of this.state.edit is false. The next prints it is correct, this edit on true and name containing a value.
Here is the code:
import React from 'react';
import { Redirect } from 'react-router-dom';
import { Formik, Form, Field } from 'formik';
import { Input, Button, Label, Grid } from 'semantic-ui-react';
import { connect } from 'react-redux';
import * as Yup from 'yup';
import { Creators } from '../../../actions';
class CreateCompanyForm extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
name: '',
redirectCreate: false,
redirectEdit: false,
edit: false,
};
}
componentDidMount() {
const {
getCompany,
getCompanies,
location: { pathname },
} = this.props;
getCompanies({
name: '',
});
if (pathname.substring(11) !== 'create') {
getCompany(pathname.substring(16));
this.setState({
edit: true,
});
this.setState({
name: this.props.company.name,
});
}
}
handleSubmitCreate = e => {
e.preventDefault();
const { createCompany, getCompanies } = this.props;
createCompany(this.state);
this.setState({ redirectCreate: true });
getCompanies(this.props.query);
};
handleSubmit = values => {
const { createCompany, getCompanies } = this.props;
createCompany(values);
this.setState({ redirectCreate: true });
getCompanies(this.props.query);
};
handleSubmitEdit = e => {
e.preventDefault();
const { name } = this.state;
const { updateCompany } = this.props;
updateCompany(this.props.company._id, {
name,
});
this.setState({ redirectEdit: true });
};
render() {
let title = 'Create company';
let buttonName = 'Create';
let submit = this.handleSubmitCreate;
const { redirectCreate, redirectEdit } = this.state;
if (redirectCreate) {
return <Redirect to="/companies" />;
}
if (redirectEdit) {
return <Redirect to={`/companies/${this.props.company._id}`} />;
}
if (this.state.edit) {
title = 'Edit company';
buttonName = 'Edit';
submit = this.handleSubmitEdit;
}
console.log('state: ', this.state); // first time it is empty, next times it has data
let initialValues = {};
if (this.state.edit) {
initialValues = {
name: this.state.name,
};
} else {
initialValues = {
name: '',
};
}
const validationSchema = Yup.object({
name: Yup.string().required('This field is required'),
});
return (
<>
<Button type="submit" form="amazing">
create company
</Button>
<Formik
htmlFor="amazing"
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={values => this.handleSubmit(values)}>
{({ values, errors, touched, setValues, setFieldValue }) => (
<Form id="amazing">
<Grid>
<Grid.Column>
<Label>Company name</Label>
<Field name="name" as={Input} placeholder="write a name" />
<div>{touched.name && errors.name ? errors.name : null}</div>
</Grid.Column>
</Grid>
<Button type="submit" floated="right" form="amazing">
{buttonName} company
</Button>
</Form>
)}
</Formik>
</>
);
}
}
const mapStateToProps = state => ({
companies: state.companies.companies,
company: state.companies.selectedCompany,
query: state.companies.query,
});
const mapDispatchToProps = {
getCompanies: Creators.getCompaniesRequest,
createCompany: Creators.createCompanyRequest,
getCompany: Creators.getCompanyRequest,
updateCompany: Creators.updateCompanyRequest,
};
export default connect(mapStateToProps, mapDispatchToProps)(CreateCompanyForm);
I put the whole file here to have more context. Is it a way to set the initialValue of name with the value from this.state.name and put it inside the input field?
By default Formik does not re-render if the initial values change. You can pass enableReinitialize prop to Formik component to allow it.
As you said in the comment, first time it renders, it has no data, hence it does initialise Formik with empty values. With that prop, it should re-render if the initial values change.
https://formik.org/docs/api/formik#enablereinitialize-boolean
I'm trying to fetch data from json server and validate it with the value entered into input fields.
If the fetched data and input data are the same it needs to add a div between fields and description text.
I've already created that component too and i think its ok.
I have already set the onChangeHandler but OnClickHandler i didin't accomplish the validation between inputs and related json fields.
Maybe i need to use a loop for validation ?
import React, { Component } from 'react';
import TextField from '#material-ui/core/TextField';
import styled from 'styled-components';
import ApplyButton from '../ApplyButton/ApplyButton';
import axios from 'axios';
import IsApplied from '../IsApplied/IsApplied';
const NumberContainer = styled.div`
margin-top: 10px;
`;
export default class NumberBox extends Component {
constructor(props) {
super(props);
this.state = {
giftcards: [],
first: '',
second: '',
isSeen: false
};
this.onClickHandler = this.onClickHandler.bind(this);
this.onHandleChange = this.onHandleChange.bind(this);
}
onHandleChange (property) {
return e => {
this.setState({
[property]: e.target.value
});
};
}
async componentDidMount() {
const response = await axios.get('http://localhost:3001/giftcards')
const giftcards = response.data
this.setState({giftcards: giftcards})
}
onClickHandler() {
if (this.state.first === this.state.giftcards.cardnumber &&
this.state.second === this.state.giftcards.control) {
return alert("correct") & this.setState({isSeen:true})
} else if (this.state.first.length === 0 &&
this.state.second.length === 0) {
return alert("error")
} else {
return alert("enter correct number") & console.log(this.state.giftcards)
}
}
render() {
let resultsbox;
if (this.state.isSeen) {
resultsbox = <IsApplied cardno={this.state.first}/>;
} else {
resultsbox = null;
}
return (
<NumberContainer>
{resultsbox}
<TextField
style={{ margin: 8, width: 430 }}
margin="normal"
variant="outlined"
type="search"
label="Gift Card Number"
value={this.state.first}
name="cardNomber"
onChange={this.onHandleChange('first')}
/>
<TextField
style={{ margin: 8, width: 200}}
margin="normal"
variant="outlined"
type="search"
label="Control Code"
value={this.state.second}
name="controlCoder"
onChange={this.onHandleChange('second')}
/>
<ApplyButton handle={this.onClickHandler}/>
</NumberContainer>
)
}
}
{
"giftcards": [
{
"cardnumber": "5078282848878291861",
"control": "175"
},
{
"cardnumber": "6435047555924007105",
"control": "201"
}
]
}
I'm getting undefined for this.state.giftcards.cardnumber & this.state.giftcards.control while checking with console.log
this.setState({giftcards: giftcards}).
You're setting giftcards state-value equal to an object. That JSON object has a key of giftcards. So at the minimum your condition in onClickHandler has to be something like :
if(this.state.first === this.state.giftcards.giftcards[index]){
....
}
Since the key giftcards, has an array for its value, you also have to decide which item in the array you want it to check against like this.state.giftcards.giftcards[0].cardnumber...
However, it sounds more like you just want to filter some data to determine whether the user entered the exact same information of a card.
We can use array.filter() to return any giftcards that match your user inputs. If any, then we will set isSeen to be true. Try doing something like this for onClickHandler():
onClickHandler(){
const { giftcards, first, second } = this.state
const matchingGiftCards = giftcards.filter((card) => {
return card.cardnumber == first && card.control == second
})
//if there are any matching giftcards we will set isSeen to tru
if(matchingGiftCards.length > 0){
this.setState({isSeen:true})
}
}
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.
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