I have been trying to test the behavior of my method handleFormSubmit(). When submit is clicked, the call should be triggered. This is working absolutely fine until I add the two lines listed below. It seems enzyme is updating the context or not calling the constructor?
Any help, much appreciated. I've stripped out what I can to keep the post brief.
The lines that fail - state is undefined
data.username = this.state.username.trim();
data.password = this.state.password.trim();
Component:
import React from 'react';
// Renders login form.
export default class LoginView extends React.Component {
static propTypes = {
model: React.PropTypes.object.isRequired,
params: React.PropTypes.object.isRequired
}
constructor(props) {
super(props);
this.state = {
username: '',
password: ''
};
}
handleUsernameChange(event) {
this.setState({error: false, username: event.target.value});
}
handlePasswordChange(event) {
this.setState({error: false, password: event.target.value});
}
handleFormSubmit(event) {
event.preventDefault();
let failureMsg = {},
data = this.props.params,
options = {
success: (response) => {
if (response.attributes.successfulLogin) {
window.location = this.props.params.redirect +
'?authenticationToken=' + response.get('authenticationToken') +
this.createParams();
} else {
throw Error('Helpful error');
}
},
error: () => {
throw Error('Helpful error');
}
};
// PROBLEM LINES!
data.username = this.state.username.trim();
data.password = this.state.password.trim();
if (this.state.username && this.state.password && this.state.terms) {
this.props.model.save(data, options);
} else {
this.setState({
error: true,
errorMessage: !this.state.username || !this.state.password ? LoginConstants.LOGIN.BLANK_INPUT : LoginConstants.LOGIN.BLANK_TERMS
});
}
}
render() {
return (
<form>
<input type="text" ref="username" id="username" placeholder="Customer ID" onChange={this.handleUsernameChange.bind(this)} maxLength={75} value={this.state.username} />
<input type="password" ref="password" id="password" placeholder="Password" onChange={this.handlePasswordChange.bind(this)} maxLength={75} value={this.state.password} />
<button type="submit" ref="login" id="login" className="Button Button--primary u-marginT-md" onClick={this.handleFormSubmit.bind(this)}>LOGIN</button>
</form>
);
}
}
Unit test:
import React from 'react';
import { mount } from 'enzyme';
import LoginView from './LoginView';
describe('Login screen', () => {
let mountedComp;
beforeAll(() => {
mountedComp = mount(<LoginView />);
});
it('should show error if submitted blank', () => {
mountedComp.instance().state = {
username: '',
password: ''
};
expect(mountedComp.find('#js-errorMessage').length).toEqual(0);
mountedComp.ref('login').simulate('click', { preventDefault: () => undefined, state: {} });
expect(mountedComp.find('#js-errorMessage').length).toBeGreaterThan(0);
});
});
try binding your onClick function in
<form onClick={this.handleFormSubmit.bind(this)} >
Looks like your "this.state: is out of scope. consider adding
var _self = this;
and use
_self.state.username.trim();
_self.state.password.trim();
I think the source of the problem may lay in this very line:
let data = this.props.params
In JavaScript it will not copy this.props.params object to data object by value, but by reference. It will set the same reference for data and this.props.params – which means if you change contents of data it will be reflected in this.props.params and vice-versa.
So you're actually mutating props which are supposed to be immutable.
You should rather create a shallow copy, e.g. like that:
let data = Object.assign({}, this.props.params)
I managed to get around this by passing my params manually to my jsx object. I'm not sure this is the proper way to test, but it worked for now.
<LoginView model={model} params={params} />
Related
I have a simple word/definition app in React. There is an edit box that pops up to change definition when a user clicks on "edit". The new definition provided is updated in the state when I call getGlossary(), I see the new definition in inspector and a console.log statement in my App render() function triggers too. Unfortunately, I still have to refresh the page in order for the new definition to be seen on screen. I would think that calling set state for this.state.glossary in the App would trigger a re-render down to GlossaryList and then to GlossaryItem to update it's definition but I'm not seeing it :(.
App.js
class App extends React.Component {
constructor() {
super();
this.state = {
glossary: [],
searchTerm: '',
}
this.getGlossary = this.getGlossary.bind(this); //not really necessary?
this.handleSearchChange = this.handleSearchChange.bind(this);
this.handleAddGlossaryItem = this.handleAddGlossaryItem.bind(this);
this.handleDeleteGlossaryItem = this.handleDeleteGlossaryItem.bind(this);
//this.handleUpdateGlossaryDefinition = this.handleUpdateGlossaryDefinition.bind(this);
}
getGlossary = () => {
console.log('getGlossary fired');
axios.get('/words').then((response) => {
const glossary = response.data;
console.log('1: ' + JSON.stringify(this.state.glossary));
this.setState({ glossary }, () => {
console.log('2: ' + JSON.stringify(this.state.glossary));
});
})
}
componentDidMount = () => {
//console.log('mounted')
this.getGlossary();
}
handleSearchChange = (searchTerm) => {
this.setState({ searchTerm });
}
handleAddGlossaryItem = (glossaryItemToAdd) => {
//console.log(glossaryItemToAdd);
axios.post('/words', glossaryItemToAdd).then(() => {
this.getGlossary();
});
}
handleDeleteGlossaryItem = (glossaryItemId) => {
console.log('id to delete: ' + glossaryItemId);
axios.delete('/words', {
data: { glossaryItemId },
}).then(() => {
this.getGlossary();
});
}
render() {
console.log('render app fired');
const filteredGlossary = this.state.glossary.filter((glossaryItem) => {
return glossaryItem.word.toLowerCase().includes(this.state.searchTerm.toLowerCase());
});
return (
<div>
<div className="main-grid-layout">
<div className="form-left">
<SearchBox handleSearchChange={this.handleSearchChange} />
<AddWord handleAddGlossaryItem={this.handleAddGlossaryItem} />
</div>
<GlossaryList
glossary={filteredGlossary}
handleDeleteGlossaryItem={this.handleDeleteGlossaryItem}
getGlossary={this.getGlossary}
//handleUpdateGlossaryDefinition={this.handleUpdateGlossaryDefinition}
/>
</div>
</div>
);
}
}
export default App;
GlossaryItem.jsx
import React from 'react';
import EditWord from './EditWord.jsx';
const axios = require('axios');
class GlossaryItem extends React.Component {
constructor(props) {
super(props);
this.state = {
isInEditMode: false,
}
this.glossaryItem = this.props.glossaryItem;
this.handleDeleteGlossaryItem = this.props.handleDeleteGlossaryItem;
this.handleUpdateGlossaryDefinition = this.handleUpdateGlossaryDefinition.bind(this);
this.handleEditClick = this.handleEditClick.bind(this);
}
handleUpdateGlossaryDefinition = (updateObj) => {
console.log('update object: ' + JSON.stringify(updateObj));
axios.put('/words', {
data: updateObj,
}).then(() => {
this.props.getGlossary();
}).then(() => {
this.setState({ isInEditMode: !this.state.isInEditMode });
//window.location.reload();
});
}
handleEditClick = () => {
// display edit fields
this.setState({ isInEditMode: !this.state.isInEditMode });
// pass const name = new type(arguments); data up to App to handle with db
}
render() {
return (
<div className="glossary-wrapper">
<div className="glossary-item">
<p>{this.glossaryItem.word}</p>
<p>{this.glossaryItem.definition}</p>
<a onClick={this.handleEditClick}>{!this.state.isInEditMode ? 'edit' : 'cancel'}</a>
<a onClick={() => this.handleDeleteGlossaryItem(this.glossaryItem._id)}>delete</a>
</div>
{this.state.isInEditMode ?
<EditWord
id={this.glossaryItem._id}
handleUpdateGlossaryDefinition={this.handleUpdateGlossaryDefinition}
/> : null}
</div>
);
}
}
EditWord
import React from 'react';
class EditWord extends React.Component {
constructor(props) {
super(props);
this.state = {
definition: ''
};
this.handleUpdateGlossaryDefinition = this.props.handleUpdateGlossaryDefinition;
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
let definition = event.target.value;
this.setState({ definition });
}
handleSubmit(event) {
//console.log(event.target[0].value);
let definition = event.target[0].value;
let update = {
'id': this.props.id,
'definition': definition,
}
//console.log(update);
this.handleUpdateGlossaryDefinition(update);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit} className="glossary-item">
<div></div>
<input type="text" name="definition" placeholder='New definition' value={this.state.definition} onChange={this.handleChange} />
<input type="submit" name="update" value="Update" />
</form>
);
}
}
export default EditWord;
Thank you
One possible way I can see to fix this is to map the data to make the id uniquely identify each list item (even in case of update). We can to do this in getGlossary() by modifying the _id to _id + definition.
getGlossary = () => {
console.log('getGlossary fired');
axios.get('/words').then((response) => {
// Map glossary to uniquely identify each list item
const glossary = response.data.map(d => {
return {
...d,
_id: d._id + d.definition,
}
});
console.log('1: ' + JSON.stringify(this.state.glossary));
this.setState({ glossary }, () => {
console.log('2: ' + JSON.stringify(this.state.glossary));
});
})
}
In the constructor of GlossaryItem I set
this.glossaryItem = this.props.glossaryItem;
because I am lazy and didn't want to have to write the word 'props' in the component. Turns out this made react loose reference somehow.
If I just remove this line of code and change all references to this.glossaryItem.xxx to this.pros.glossaryItem.xxx then it works as I expect! On another note, the line of code can be moved into the render function (instead of the constructor) and that works too, but have to make sure I'm accessing variables properly in the other functions outside render.
state.firebase.profile always is undefined when I reload by browser.
Somehow, it goes well except for F5 as far as I can see.
I check by using console.log("TEST HERE"+ JSON.stringify(this.props.profile.name));.
Where should I modify it...
class ReagtTagSample extends Component {
constructor(props) {
super(props);
this.state = {
porco:""
tags: [{ id: 'Yugoslavia', text: 'Yugoslavia' }, { id: 'India', text: 'India' }],
suggestions: [
{ id: "England", text: "England" },
{ id: "Mexico", text: "Mexico" },
],
};
componentDidMount=()=>{
console.log("TEST HERE"+ JSON.stringify(this.props.profile.name));
}
handleAddition(tag) {
this.setState((state) => ({ tags: [...state.tags, tag] }));
}
handleDrag(tag, currPos, newPos) {
const tags = [...this.state.tags];
const newTags = tags.slice();
newTags.splice(currPos, 1);
newTags.splice(newPos, 0, tag);
this.setState({ tags: newTags });
}
//ommit
render() {
const { auth, authError, profile } = this.props;
return (
//ommit
const mapStateToProps = (state) => {
return {
auth: state.firebase.auth,
authError: state.auth.authError,
profile: state.firebase.profile,
};
};
const mapDispatchToProps = (dispatch) => {
return {
profileUpdate: (user) => dispatch(Update(user)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Update);
Update= (user) => {
return (dispatch, getState, { getFirebase, getFirestore }) => {
const firestore = getFirestore();
const firebase = getFirebase();
const profile = getState().firebase.profile;
const authorId = getState().firebase.auth.uid;
firestore.collection('users').doc(authorId).set({
name: user.userName,
tags:user.tags,
}).then(() => {
dispatch({ type: 'PROFILE_UPDATE_SUCCESS' })
}).catch(err => {
dispatch({ type: 'PROFILE_UPDATE_ERROR', err })
})
}
}
I would like to use profile.name as default input name...
<div className="input-field">
<label htmlFor="userName">DisplayName</label>
<input
type="text"
id="userName"
value={this.state.userName}
onChange={this.handleChange}
/>
React state and props will be reset to their initial values when we reload the web app in browser using F5 or refresh button (because the app restarts as fresh).
The console log in componentDidMount prints undefined:
componentDidMount = () => {
console.log("TEST HERE" + JSON.stringify(this.props.profile.name));
// side node: you do not really need an arrow function here as it is a special
// lifecycle method. It will the `this` automatically binded with component instance
}
because, probably you are getting this.props.profile data through an API call. Hence, this.props.profile will receive its values asynchronously. You can see it on console log in componentDidUpdate lifecycle method.
Solution:
But if you want to set the default value of below input from this.props.profile.name, you can use either of these options:
Option 1: Using key and defaultValue. It will work because React components or elements re-render when their key is changed. And due to re-render it will read new defaultValue.
<input
key={this.props.profile.name}
defaultValue={this.props.profile.name}
type="text"
id="userName"
value={this.state.userName}
onChange={this.handleChange}
/>
Option 2: Set the userName in state when data is available in props:
componentDidUpdate(prevProps, prevState) {
if (this.props.profile.name !== prevProps.profile.name) {
this.setState({
userName: this.props.profile.name,
})
}
}
...
<input
type="text"
id="userName"
value={this.state.userName}
onChange={this.handleChange}
/>
I'm building a simple note-taking app and I'm trying to add new note at the end of the list of notes, and then see the added note immediately. Unfortunately I'm only able to do it by refreshing the page. Is there an easier way?
I know that changing state would usually help, but I have two separate components and I don't know how to connect them in any way.
So in the NewNoteForm component I have this submit action:
doSubmit = async () => {
await saveNote(this.state.data);
};
And then in the main component I simply pass the NewNoteForm component.
Here's the whole NewNoteForm component:
import React from "react";
import Joi from "joi-browser";
import Form from "./common/form";
import { getNote, saveNote } from "../services/noteService";
import { getFolders } from "../services/folderService";
class NewNoteForm extends Form {
//extends Form to get validation and handling
state = {
data: {
title: "default title",
content: "jasjdhajhdjshdjahjahdjh",
folderId: "5d6131ad65ee332060bfd9ea"
},
folders: [],
errors: {}
};
schema = {
_id: Joi.string(),
title: Joi.string().label("Title"),
content: Joi.string()
.required()
.label("Note"),
folderId: Joi.string()
.required()
.label("Folder")
};
async populateFolders() {
const { data: folders } = await getFolders();
this.setState({ folders });
}
async populateNote() {
try {
const noteId = this.props.match.params.id;
if (noteId === "new") return;
const { data: note } = await getNote(noteId);
this.setState({ data: this.mapToViewModel(note) });
} catch (ex) {
if (ex.response && ex.response.status === 404)
this.props.history.replace("/not-found");
}
}
async componentDidMount() {
await this.populateFolders();
await this.populateNote();
}
mapToViewModel(note) {
return {
_id: note._id,
title: note.title,
content: note.content,
folderId: note.folder._id
};
}
scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth" });
}
doSubmit = async () => {
await saveNote(this.state.data);
};
render() {
return (
<div>
<h1>Add new note</h1>
<form onSubmit={this.handleSubmit}>
{this.renderSelect("folderId", "Folder", this.state.folders)}
{this.renderInput("title", "Title")}
{this.renderInput("content", "Content")}
{this.renderButton("Add")}
</form>
</div>
);
}
}
export default NewNoteForm;
And here's the whole main component:
import React, { Component } from "react";
import { getNotes, deleteNote } from "../services/noteService";
import ListGroup from "./common/listGroup";
import { getFolders } from "../services/folderService";
import { toast } from "react-toastify";
import SingleNote from "./singleNote";
import NewNoteForm from "./newNoteForm";
class Notes extends Component {
state = {
notes: [], //I initialize them here so they are not undefined while componentDidMount is rendering them, otherwise I'd get a runtime error
folders: [],
selectedFolder: null
};
async componentDidMount() {
const { data } = await getFolders();
const folders = [{ _id: "", name: "All notes" }, ...data];
const { data: notes } = await getNotes();
this.setState({ notes, folders });
}
handleDelete = async note => {
const originalNotes = this.state.notes;
const notes = originalNotes.filter(n => n._id !== note._id);
this.setState({ notes });
try {
await deleteNote(note._id);
} catch (ex) {
if (ex.response && ex.response.status === 404)
toast.error("This note has already been deleted.");
this.setState({ notes: originalNotes });
}
};
handleFolderSelect = folder => {
this.setState({ selectedFolder: folder }); //here I say that this is a selected folder
};
render() {
const { selectedFolder, notes } = this.state;
const filteredNotes =
selectedFolder && selectedFolder._id //if the selected folder is truthy I get all the notes with this folder id, otherwise I get all the notes
? notes.filter(n => n.folder._id === selectedFolder._id)
: notes;
return (
<div className="row m-0">
<div className="col-3">
<ListGroup
items={this.state.folders}
selectedItem={this.state.selectedFolder} //here I say that this is a selected folder
onItemSelect={this.handleFolderSelect}
/>
</div>
<div className="col">
<SingleNote
filteredNotes={filteredNotes}
onDelete={this.handleDelete}
/>
<NewNoteForm />
</div>
</div>
);
}
}
export default Notes;
How can I connect these two components so that the data shows smoothly after submitting?
You can use a callback-like pattern to communicate between a child component and its parent (which is the 3rd strategy in #FrankerZ's link)
src: https://medium.com/#thejasonfile/callback-functions-in-react-e822ebede766)
Essentially you pass in a function into the child component (in the main/parent component = "Notes": <NewNoteForm onNewNoteCreated={this.onNewNoteCreated} />
where onNewNoteCreated can accept something like the new note (raw data or the response from the service) as a parameter and saves it into the parent's local state which is in turn consumed by any interested child components, i.e. ListGroup).
Sample onNewNoteCreated implementation:
onNewNoteCreated = (newNote) => {
this.setState({
notes: [...this.state.notes, newNote],
});
}
Sample use in NewNoteForm component:
doSubmit/handleSubmit = async (event) => {
event.preventDefault();
event.stopPropagation();
const newNote = await saveNote(this.state.data);
this.props.onNewNoteCreated(newNote);
}
You probably want to stop the refresh of the page on form submit with event.preventDefault() and event.stopPropagation() inside your submit handler (What's the difference between event.stopPropagation and event.preventDefault?).
I have a problem with a static method in React using ESLint with airbnb config. I have a service like this that is both used for creating a user in my system, and getting all the field values for the create user form. The service looks like this:
import axios from 'axios';
import ServiceException from './ServiceException';
class CreateUserServiceException extends ServiceException {}
class CreateUserService {
constructor(config) {
this.apiUrl = config.API_URL;
this.userDomain = config.USER_DOMAIN;
}
static getFormFields() {
return [
{
id: 'username',
type: 'text',
title: 'E-postadress',
placeholder: 'Användarnamn',
mandatory: true,
extra: '',
},
{
id: 'password',
type: 'password',
title: 'Lösenord',
placeholder: 'Lösenord',
mandatory: true,
extra: '',
},
];
}
async createUser(data) {
try {
await axios.post(`${this.apiUrl}/users/${this.userDomain}`, data, { withCredentials: true });
} catch ({ response }) {
throw new CreateUserServiceException(
response.status, 'Failed to create user', response.data,
);
}
}
}
export default CreateUserService;
I also have a jsx controller to create my form. This controller gets the service via it's properties. The controller looks like this:
import React from 'react';
import './index.css';
class CreateUserController extends React.Component {
constructor(props) {
super(props);
this.state = {
formFields: [],
userData: {},
};
this.onCreate = this.onCreate.bind(this);
this.onLoad = this.onLoad.bind(this);
}
async componentDidMount() {
await this.onLoad();
}
async onLoad() {
const { createUserService } = await this.props;
const { getFormFields } = createUserService;
const formFields = getFormFields || []; // ALWAYS RETURNS UNDEFINED
const userData = {};
console.log(formFields); // ALWAYS DISPLAYS []
formFields.forEach((field) => {
userData[field.id] = '';
});
this.setState({ formFields, userData });
}
async onCreate(e) {
e.preventDefault();
const { userData } = this.state;
console.log(userData);
}
render() {
const { userData, formFields } = this.state;
return (
<section className="create-user-controller">
<h1>Skapa ny användare</h1>
<form
className="centered-container"
action=""
noValidate
onSubmit={this.onCreate}
>
<table>
<tbody>
{formFields.map(field => (
<tr key={field.id}>
<td>{field.title}</td>
<td>
<input
value={userData[field.id]}
onChange={e => this.setState({
userData: { ...userData, [field.id]: e.target.value },
})}
className={`create-${field.id}`}
name={field.id}
placeholder={field.placeholder}
type={field.type}
/>
</td>
<td>{field.extra}</td>
</tr>
))}
<tr>
<td colSpan={3}>* obligatoriskt</td>
</tr>
</tbody>
</table>
<input type="submit" className="btn btn-green" value="Skapa användare" />
</form>
</section>
);
}
}
export default CreateUserController;
My problem is that const formFields = getFormFields || []; always becomes [] which means that getFormFields always returns undefined.
If I remove static from getFormFields() in my service and call it using const formFields = createUserService.getFormFields(); it works fine, but then ESLint complains about ESLint: Expected 'this' to be used by class method 'getFormFields'. (class-methods-use-this).
Does anyone have an idea how to solve this?
import CreateUserService from './CreateUserService';
...
async onLoad() {
...
const formFields = CreateUserService.getFormFields() || [];
...
}
Should do the trick !
Notice that the static function is called using the Class name. Tou will also have to import it correctly (i don't know your path…)
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