I've got a component that creates a field with list of Radio inputs. The created component seems to work, it is rendered correctly, dispatches the proper redux actions, and update it's state properly. However the selected radio input never gets checked in the UI.
import React from 'react';
import Immutable from 'immutable';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Field } from 'react-redux-form/immutable';
const RadioField = ({ fieldId, label, model, hasError, options }) => {
const error = hasError ? 'has-error' : '';
return (
<div className={`form-group ${error}`}>
<label className="control-label" htmlFor={fieldId}>{label}</label>
<Field model={model} id={fieldId} >
{options.map(option => (
<label
key={`${fieldId}-${option.get('id')}`}
htmlFor={`radio-${fieldId}-${option.get('value')}`}
className="radio-inline"
>
<input
id={`radio-${fieldId}-${option.get('value')}`}
value={`${option.get('value')}`}
type="radio"
/>
{option.get('display')}
</label>
))}
</Field>
</div>
);
};
RadioField.defaultProps = {
fieldId: 'radio-field-id',
label: 'Radio Field:',
model: '.radio',
hasError: false,
options: Immutable.List(),
};
RadioField.propTypes = {
fieldId: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
model: PropTypes.string.isRequired,
hasError: PropTypes.bool,
options: ImmutablePropTypes.listOf(
ImmutablePropTypes.shape({
id: PropTypes.number,
display: PropTypes.string,
value: PropTypes.any,
}),
).isRequired,
};
export default RadioField;
My gut tells me it has something to do with the Field not being able to properly locate the input that has the selected value or the redux state isn't getting passed properly to the Field's children Control components.
Any help would be greatly appreciated. Thanks!
After looking into the issue more and it turned out the created immutable Control.radio components weren't getting the modelValue from the app's redux state. Changing the component from a Field with inputs to multiple generic Control components and giving them the proper props for a radio input seemed to fix the issue.
Here's what the code looks like.
import React from 'react';
import Immutable from 'immutable';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Control } from 'react-redux-form/immutable';
const RadioField = ({ fieldId, label, model, hasError, options }) => {
const error = hasError ? 'has-error' : '';
return (
<div className={`form-group ${error}`}>
<label className="control-label" htmlFor={fieldId}>{label}</label>
<div id={fieldId}>
{options.map(option => (
<label key={`${fieldId}-${option.get('id')}`} htmlFor={`${fieldId}-${option.get('id')}`} className="radio-inline">
<Control
id={`${fieldId}-${option.get('id')}`}
value={option.get('value')}
model={model}
isToggle
type="radio"
/>
{option.get('display')}
</label>
))}
</div>
</div>
);
};
RadioField.defaultProps = {
fieldId: 'radio-field-id',
label: 'Radio Field:',
model: '.radio',
hasError: false,
options: Immutable.List(),
};
RadioField.propTypes = {
fieldId: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
model: PropTypes.string.isRequired,
hasError: PropTypes.bool,
options: ImmutablePropTypes.listOf(
ImmutablePropTypes.shape({
id: PropTypes.number,
display: PropTypes.string,
value: PropTypes.any,
}),
).isRequired,
};
export default RadioField;
I'll look into recreating the issue and submit a issue on react-redux-form's github.
Related
I am using a few different technologies in a self learning project (reddit clone):
react
react-redux
redux
react-router (v4)
Goal: When clicking on an edit link to a post, go to proper route with edit form fields populated.
I am using a container that dispatches an action and re-renders updating the component inside the containers props. It looks like this:
CONTAINER:
class PostContainer extends Component {
componentDidMount() {
debugger;
const {id} = this.props.match.params;
this.props.dispatch(fetchPost(id));
}
render() {
return (
<Post post={this.props.post} editing={this.props.location.state? this.props.location.state.editing : false}/>
);
}
}
const mapStateToProps = (state, ownProps) => {
debugger;
return {
...ownProps,
post: state.post
};
};
export default connect(mapStateToProps)(withRouter(PostContainer));
The nested Post component has additional nested components:
POST Component:
class Post extends Component {
constructor(props) {
super(props);
this.editToggle = this.editToggle.bind(this);
}
state = {
editing: this.props.location.state ? this.props.location.state.editing : false
};
editToggle() {
this.setState({editing: !this.state.editing});
}
render() {
const {editing} = this.state;
if (editing || this.props.post === undefined) {
return <PostEditForm editToggle={this.editToggle} editing={this.props.editing} post={this.props.post || {}}/>;
}
return (
<div>
<div>
<h2>Title: {this.props.post.title}</h2>
<span>Author: {this.props.post.author}</span>
<br/>
<span>Posted: {distanceInWordsToNow(this.props.post.timestamp)} ago f</span>
<p>Body: {this.props.post.body}</p>
<button type='button' className='btn btn-primary' onClick={() => this.editToggle()}>Make Edit</button>
</div>
<hr/>
<Comments post={this.props.post}></Comments>
</div>
);
}
}
export default withRouter(Post);
In the render function within the first if statement, I pass the updated props to <PostEditForm>. When PostEditForm receives the props, it re-renders the component, but the state of the component is not updated.
PostEditForm:
class PostEditForm extends Component {
state = {
timestamp: this.props.post.timestamp || Date.now(),
editing: this.props.editing || false,
body: this.props.post.body || '',
title: this.props.post.title || '',
category: this.props.post.category || '',
author: this.props.post.author || '',
id: this.props.post.id || uuid()
};
clearFormInfo = () => {
this.setState({
timestamp: Date.now(),
body: '',
title: '',
category: '',
author: '',
id: uuid(),
editing: false
});
};
handleOnChange = (e) => {
this.setState({[e.target.id]: e.target.value});
};
handleSubmit = event => {
event.preventDefault();
const post = {
...this.state
};
if(this.state.editing) {
this.props.dispatch(updatePostAPI(post));
this.setState({
editing: false
})
} else {
this.props.dispatch(createPostAPI(post));
this.clearFormInfo();
window.location.href = `/${post.category}/${post.id}`;
}
};
render() {
const {editing} = this.state;
return (
<form onSubmit={this.handleSubmit}>
<div>
<h2>Create New Post</h2>
<label htmlFor='text'>
Title:
<input
type="text"
onChange={this.handleOnChange}
value={this.state.title}
placeholder="Enter Title"
required
id="title"
name="title"
/>
</label>
<label>
Author:
<input
type="text"
onChange={this.handleOnChange}
value={this.state.author}
placeholder="Enter Author"
required
id="author"
name="author"
/>
</label>
<label>
</label>
<label>
Body:
<textarea
type="text"
onChange={this.handleOnChange}
value={this.state.body}
placeholder="Enter Body"
required
id="body"
name="body"
/>
</label>
</div>
<label>
Category:
<select id = 'category' onChange={this.handleOnChange} value={this.state.value} required>
<option value='Select Category'>Select Category</option>
<option value='react'>react</option>
<option value='redux'>redux</option>
<option value='udacity'>udacity</option>
</select>
</label>
<button type='submit'>Create Post</button>
</form>
);
}
}
export default connect()(PostEditForm);
I believe I need to call setState and assign the new props passed into the state, but I don't know which lifeCycle method to use.
When I use componentDidUpdate, something like:
class PostEditForm extends Component {
componentDidUpdate(prevProps) {
if(this.props.post.id !== prevProps.post.id) {
this.setState({
timestamp: this.props.post.timestamp,
editing: this.props.post.editing,
title: this.props.post.title,
category: this.props.post.category,
author: this.props.post.author,
id: this.props.post.id
})
}
}
.....
componentDidUpdate solves the initial problem, but whenever I update content in the form I am editing, the lifeclycle method is called, eliminating the need for the onChange handlers, is this a good practice or should I use a different approach?
just a note
componentDidUdpate is called after every re-render,
componentDidMount is called after the instantiation of the component.
if you modify your state inside the componentDidUdpate() hook you will end up in an infinite loop since a setState() will retrigger the render() and consequently the componentDidUpdate()
if you modify your state in the componentDidMount() this hook is called only once, after instantiation, not every re-render thus it won't work as well for subsequent updates.
However the problem lays here:
state = {
timestamp: this.props.post.timestamp || Date.now(),
editing: this.props.editing || false,
body: this.props.post.body || '',
title: this.props.post.title || '',
category: this.props.post.category || '',
author: this.props.post.author || '',
id: this.props.post.id || uuid()
};
render() {
const {editing} = this.state;
//change to const {editing} = this.props;
....
}
the way you're declaring the state won't work for future updates, state will point to the values of the props once the instantion is done, just primitive values that are immutable for definition.
you should just refer to this.props across the whole PostEditForm render() method instead of this.state and you should be fine.
to update the state you should pass a callback to PostEditForm from the parent (in this case Post component) and trigger it when needed.
And regarding the default values i'd suggest the use of Proptypes library.
something like:
import React, {Component} from 'react';
import PropTypes from 'prop-types';
class PostEditForm extends Component {
//your business logic
}
PostEditForm.propTypes = {
timestamp: Proptypes.Object,
editing: PropTypes.bool,
body: PropTypes.string,
title: PropTypes.string,
category: PropTypes.string,
author: PropTypes.string,
id: PropTypes.string
}
PostEditForm.defaultProps = {
timestamp: Date.now(),
editing: false,
body: '',
title: '',
category: '',
author: '',
id: uuid()
}
On an <EditView /> I am using <ReferenceArrayField /> to display an edit form of checkbox.
I need to fill the form with the data, but the checkbox list is so far implemented using the response entities from the ReferenceArrayField.
I have an the user entity fetched by the <EditView/>
In <EditView/> I am fetching related data using `
I write a custom grid using the <ReferenceArrayField /> data
I want to fill the checked props of my checkbox depending on values present in user.
This would be some trivial controller logic for the UI but I can't find how to do it.
So far I am only able to have one record by scope which means that every data displayed on the UI would be from a single entity and no controller logic can be written in between.
Use case
const CheckboxGrid = ({ ids, data, basePath }) => (
<div style={{ margin: '1em' }}>
{ids.map(id =>
<div key={id}>
<h2><TextField record={data[id]} source="description" /></h2>
<label>
<input type="checkbox" name="authority[]" disabled checked={true} />{' '}
<TextField record={data[id]} source="role.description" />
</label>
{data[id].siteFunctionList.map((siteFunction) => (
<div key={siteFunction.description} className="pl-3">
<h3><TextField record={siteFunction} source="description" disabled /></h3>
<label>
<input type="checkbox" name="authority[]" disabled />{' '}
<TextField record={siteFunction} source="role.description" />
</label>
</div>
))}
</div>
)}
</div>
);
CheckboxGrid.defaultProps = {
data: {},
ids: [],
};
export class AuthorityList extends React.Component {
render() {
const resourceName = 'users';
const resourceId = this.props.match.params.id;
return (
<Edit {...this.props} resource={resourceName} id={resourceId}>
<SimpleForm>
<ReferenceArrayField reference="siteServices" source="siteServiceIdList">
<CheckboxGrid />
</ReferenceArrayField>
</SimpleForm>
</Edit>
);
}
}
The user have a list of values that would match some of the values present in data.
Fact
I do not have access to the <Edit .../> record in the scope of checkbox grid.
I have seen that there is in redux a form['record-form'].values where the entity is store so I could access it with custom selectors:
EX:
const mapStateToProps = createStructuredSelector({
record: makeSelectFormRecordFormValues(),
});
const withConnect = connect(mapStateToProps);
const CheckboxGridConnect = compose(
withConnect,
)(CheckboxGrid);
There is also a store for each resources declared in admin on rest. It store the entities fetched with <ReferenceArrayField /> but I don't see these stores scalable in case we use more than one <ReferenceArrayField /> for the same resource.
Should I directly pick in the store and inject my self the entities ?
Please find below my hacky code for SelectInput. I have made this to basically edit an array of values. The API detects any changes in the array of values and updates the references accordingly. Below is a copy paste job from the AOR core and then some modifications for my use case.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import get from 'lodash.get';
import SelectField from 'material-ui/SelectField';
import MenuItem from 'material-ui/MenuItem';
export class CustomSelectInput extends Component {
/*
* Using state to bypass a redux-form comparison but which prevents re-rendering
* #see https://github.com/erikras/redux-form/issues/2456
*/
state = {
value: this.props.fields.get(this.props.selectionOrder) ? this.props.fields.get(this.props.selectionOrder) : null //selectionOrder is a prop I am passing to the component manually. It is very specific to my use case
}
handleChange = (event, index, value) => {
this.props.fields.remove(this.props.selectionOrder) //using redux-form fields methods to remove items and adding new ones. SelectionOrder now becomes an unfortunate hard coding of the particular value this selectInput is supposed to edit in the array of values
this.props.fields.push(value);
this.setState({ value });
}
renderMenuItem = (choice) => {
const {
optionText,
optionValue,
} = this.props;
const choiceName = React.isValidElement(optionText) ? // eslint-disable-line no-nested-ternary
React.cloneElement(optionText, { record: choice }) :
(typeof optionText === 'function' ?
optionText(choice) :
get(choice, optionText)
);
return (
<MenuItem
key={get(choice, optionValue)}
primaryText={choiceName}
value={choice.name}
/>
);
}
render() {
const {
allowEmpty,
choices,
elStyle,
input,
isRequired,
label,
meta: { touched, error },
options,
resource,
source,
} = this.props;
return (
<SelectField
value={this.state.value}
onChange={this.handleChange}
autoWidth
style={elStyle}
errorText={touched && error}
{...options}
>
{allowEmpty &&
<MenuItem value={null} primaryText="" />
}
{choices.map(this.renderMenuItem)}
</SelectField>
);
}
}
CustomSelectInput.propTypes = {
addField: PropTypes.bool.isRequired,
allowEmpty: PropTypes.bool.isRequired,
choices: PropTypes.arrayOf(PropTypes.object),
elStyle: PropTypes.object,
input: PropTypes.object,
isRequired: PropTypes.bool,
label: PropTypes.string,
meta: PropTypes.object,
options: PropTypes.object,
optionText: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.element,
]).isRequired,
optionValue: PropTypes.string.isRequired,
resource: PropTypes.string,
source: PropTypes.string
};
CustomSelectInput.defaultProps = {
addField: true,
allowEmpty: false,
choices: [],
options: {},
optionText: 'name',
optionValue: 'id'
};
export default CustomSelectInput
I am using react-tagsinput, react-input-autosize, and react-mailcheck to create input tags that also suggests the right domain when the user misspell it in an email address.
I have gotten the react-tagsinput to work with react-input-autosize but when added with react-mailcheck my input form does not work at all, the form is un-clickable and unable to type and text into the field. I'm not getting any errors in the console and i'm not sure what is wrong with my code. I followed what they did in the react-mailcheck documentation: https://github.com/eligolding/react-mailcheck. I was hoping someone could look at it with a fresh pair of eyes to see what I am missing that is making it not work.
Thanks!
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import TagsInput from 'react-tagsinput';
import AutosizeInput from 'react-input-autosize';
import MailCheck from 'react-mailcheck';
class EmailInputTags extends Component {
static propTypes = {
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
};
constructor() {
super();
this.state = { tags: [], inputText: '' };
this.handleChange = this.handleChange.bind(this);
this.handleInputText = this.handleInputText.bind(this);
this.renderInput = this.renderInput.bind(this);
}
handleChange(tags) {
this.setState({ tags });
}
handleInputText(e) {
this.setState({ inputText: e.target.value });
}
renderInput({ addTag, ...props }) {
const { ...other } = props;
return (
<MailCheck email={this.state.inputText}>
{suggestion => (
<div>
<AutosizeInput
type="text"
value={this.state.inputText}
onChange={this.handleInputText}
{...other}
/>
{suggestion &&
<div>
Did you mean {suggestion.full}?
</div>
}
</div>
)}
</MailCheck>
);
}
render() {
const { label, name } = this.props;
return (
<div className="input-tag-field">
<TagsInput inputProps={{ placeholder: '', className: 'input-tag' }} renderInput={this.renderInput} value={this.state.tags} onChange={this.handleChange} />
<label htmlFor={name}>{label}</label>
</div>
);
}
}
export default EmailInputTags;
I have not tried this out.
Try passing as a prop to TagsInput the onChange function.
ie
{... onChange={(e) => {this.setState(inputText: e.target.value}}
I'm struggling to pose this question in a concise manner. I am having some major performance issues with my app. I have installed the Perf add-on tools for react and can see where the issue is, however I am unsure of the best way to fix it.
I think it will probably have something to do with ReSelect... but need some guidance on where to begin.
I have a component that renders a number of other components. This uses size-me (to calculate the size of the browsing window), and react-grid-layout (to layout each component and permit their positioning to be changed). This is resource intensive, so I can't have this happening unnecessarily.
The user can click on a button to open a modal window (to add or edit the components that are being rendered in the grid).
The issue: When the modal window opens, the underlying component re-renders, causing size-me and react-grid-layout to re-render, which thus causes the modal to "jerkingly" open!
This is the entire state tree:
This is the only part of the state that changes when I open the modal:
The size-me and react-grid-layout stuff is rendering state from the formEngine.form part of the state tree, yet it is being re-rendered when state updates are made to the formEngine.addComponent part of the tree
Here are the performance logs:
As you can see, there are some wasted renders happening AND this will only grow incrementally based on the number of nested layout components the user decides to add to the form...
So to try and prevent this question from becoming too convoluted, let me ask first:
How do I prevent the underlying page from re-rendering when I open the modal?
Why are components that are watching formEngine.form triggered to re-render when fromEngine.addComponent gets modified?
Thank you.
EDIT 1:
I'm not sure if this is relevant, but to answer the comment, I added this code. The AddFormComponent is the Modal that jerks open.
Form.js:
const Form = (props) => (
<div className="form-engine">
<div className="card-block" style={{position: "relative"}}>
{
props.editMode &&
<div className="nula-form-controls">
<AddFormComponent parentId={"root"} />
</div>
}
{
props.form.components.root.childComponentIds.length > 0 ?
<LayoutComponent componentKey={"root"} />
:
<EmptyGridLayout />
}
</div>
</div>
)
LayoutComponent.js:
import React from 'react'
import _ from 'lodash'
import SizeMe from 'react-sizeme'
import { Responsive as ResponsiveReactGridLayout } from 'react-grid-layout'
import 'react-grid-layout/css/styles.css'
import 'react-resizable/css/styles.css'
import FormComponent from '../containers/FormComponent'
import NestedLayoutComponent from '../containers/LayoutComponent'
import AddFormComponent from '../containers/AddFormComponent'
import LayoutComponentEditor from '../containers/LayoutComponentEditor'
//Setup SizeMe Configuration
let sizeMeConfig = {
monitorWidth: true
}
let sizeMeHOC = SizeMe(sizeMeConfig)
//Wrap ResponsiveReactGridLayout in sizeMeHOC so that it is aware of it's width
var GridLayout = ResponsiveReactGridLayout
GridLayout = sizeMeHOC(GridLayout)
const LayoutComponent = (props) => (
<div>
<GridLayout
cols={props.cols}
className={props.className}
breakpoints={props.breakpoints}
rowHeight={props.rowHeight}
draggableCancel={props.draggableCancel}
layouts={props.layouts}
isDraggable={props.isDraggable}
isResizable={props.isResizable}
onLayoutChange={(currentLayout, allLayouts) => props.handleLayoutChange(props.componentKey, currentLayout, allLayouts)}
width={props.size.width}
>
{
//Map out any child layouts
props.childComponents.map((component) => {
if (component.type === "card") {
return (
<div className={"card card-outline-" + component.color} key={component.key}>
<div className={"card-header card-" + component.color}>
{component.header}
</div>
<div className="card-block" style={{overflowY: "auto", position: "relative"}}>
{
//Hide if editMode={false}
props.editMode &&
<div className="nula-card-controls">
<LayoutComponentEditor path={component.key} />
<span className="fa fa-trash" />
<AddFormComponent parentId={component.key} />
</div>
}
<NestedLayoutComponent componentKey={component.key} />
</div>
</div>
)
}
else if (component.type === "fieldGroup") {
return (
<div className="card" key={component.key}>
<div className="card-block pl-0 pr-0 pt-2 pb-0" style={{overflowY: "auto"}}>
{
//Hide if editMode={false}
props.editMode &&
<div className="nula-fieldgroup-controls">
<a className="text-warning" title="Edit"><span className="fa fa-pencil" /></a>
<a className="text-danger" title="Remove"><span className="fa fa-trash" /></a>
<AddFormComponent parentId={component.key} />
</div>
}
<NestedLayoutComponent componentKey={component.key} />
</div>
</div>
)
}
else if (component.type === "paragraph") {
return (
<div className="alert alert-success text-font-bold" key={component.key}>
{
<FormComponent component={component} editMode={props.editMode} />
}
</div>
)
}
else {
return (
<div key={component.key}>
{
<FormComponent component={component} editMode={props.editMode} />
}
</div>
)
}
})
}
</GridLayout>
</div>
)
export default SizeMe()(LayoutComponent)
EDIT 2:
AddFormComponent.js -- Component
import React from 'react'
import AddFormComponentDetails from './AddFormComponentDetails'
import Perf from 'react-addons-perf'; // ES6
class AddFormComponent extends React.Component {
constructor(props) {
super(props);
this.localOpenModal = this.localOpenModal.bind(this);
}
localOpenModal() {
console.log("----STARTING PERFORMANCE MONITOR-----")
Perf.start()
this.props.handleOpenModal();
}
componentDidUpdate() {
console.log("-----PERFORMANCE MONITOR STOPPING------")
Perf.stop()
console.log("-----PRINT INCLUSIVE------")
Perf.printInclusive()
console.log("-----PRINT WASTEED------")
Perf.printWasted()
}
render() {
return (
<span>
<a onTouchTap={this.localOpenModal} className="text-success" title="Add Component">
<span className="fa fa-plus" />
</a>
<Modal isOpen={this.props.modalOpen} size={"lgr"} toggle={this.props.handleCloseModal}>
<ModalHeader toggle={this.props.handleCloseModal}>Add Component</ModalHeader>
<ModalBody>
...Removed For Breviety
</ModalBody>
<ModalFooter>
...Removed For Breviety
</ModalFooter>
</Modal>
</span>
)
}
}
export default AddFormComponent
AddFormComponent.js -- Container
import { connect } from 'react-redux'
import {
handleOpenModal,
handleCloseModal,
handleGoBack,
handleComponentPropertyChange,
handleComponentNameChange,
handleComponentTypeChange,
handleSubmit
} from '../actions/addFormComponentActions'
import AddFormComponent from '../components/AddFormComponent'
const mapStateToProps = (state) => ({
steps: [
{ icon: 'superpowers', title: 'Select Component', description: 'Select the Component you wish to add', active: state.addComponent.currentStep == 1 },
{ icon: 'info circle', title: 'Enter Details', description: 'Enter details to customize component', active: state.addComponent.currentStep == 2 },
{ icon: 'check', title: 'Add Component', description: 'Add component to form' }
],
currentStep: state.addComponent.currentStep,
modalOpen: state.addComponent.modalOpen,
component: state.addComponent.component,
errors: state.addComponent.errors,
componentType: state.addComponent.componentType
})
export default connect(
mapStateToProps,
{
handleOpenModal,
handleCloseModal,
handleGoBack,
handleComponentPropertyChange,
handleComponentNameChange,
handleComponentTypeChange,
handleSubmit
}
)(AddFormComponent)
addFormComponentReducer.js
import _ from 'lodash'
import {
ADD_FORM_COMPONENT_TOGGLE_MODAL,
ADD_FORM_COMPONENT_CLOSE_MODAL,
ADD_FORM_COMPONENT_GO_BACK,
ADD_FORM_COMPONENT_SUBMIT,
ADD_FORM_COMPONENT_PROPERTY_CHANGE,
ADD_FORM_COMPONENT_PROPERTY_ERROR,
ADD_FORM_COMPONENT_KEY_ERROR,
ADD_FORM_COMPONENT_NAME_CHANGE,
ADD_FORM_COMPONENT_NAME_ERROR,
ADD_FORM_COMPONENT_TYPE_CHANGE,
ADD_FORM_COMPONENT_TYPE_ERROR
} from '../actions/addFormComponentActions'
let initialState = {
currentStep: 1,
modalOpen: false,
component: {
key: '',
label: '',
headingText: '',
text: ''
},
errors: {
key: {
hasError: false,
msg: ''
},
label: {
hasError: false,
msg: ''
},
text: {
hasError: false,
msg: ''
}
}
}
function addFormComponentReducer(state = initialState, action) {
switch (action.type) {
case ADD_FORM_COMPONENT_TOGGLE_MODAL:
return {
...state,
modalOpen: action.payload.isOpen,
currentStep: 1
}
case ADD_FORM_COMPONENT_CLOSE_MODAL:
return initialState;
case ADD_FORM_COMPONENT_GO_BACK:
return {
...state,
currentStep: 1
}
case ADD_FORM_COMPONENT_SUBMIT:
return initialState;
case ADD_FORM_COMPONENT_PROPERTY_CHANGE:
return {
...state,
component: {
...state.component,
[action.payload.key]: action.payload.value
}
}
case ADD_FORM_COMPONENT_PROPERTY_ERROR:
return {
...state,
errors: {
...state.errors,
[action.payload.key]: {
hasError: action.payload.hasError,
msg: action.payload.msg
}
}
}
case ADD_FORM_COMPONENT_TYPE_CHANGE:
return {
...state,
componentType: action.payload.componentType,
currentStep: 2
}
default:
return state
}
}
export default addFormComponentReducer
index.js -- Combine Reducers
import { combineReducers } from 'redux'
//import FormEngine reducers
import formReducer from './formReducer'
//import addFormComponentReducer from './addFormComponentReducer'
import componentEditorReducer from './componentEditorReducer'
const rootFormEngineReducer = combineReducers({
form: formReducer,
//addComponent: addFormComponentReducer,
componentEditor: componentEditorReducer
})
export default rootFormEngineReducer
rootReducer.js
import { combineReducers } from 'redux'
//import reducers
import rootCoreLayoutReducer from '../features/CoreLayout/reducers'
import rootFormEngineReducer from '../features/FormEngine/reducers'
import addComponentReducer from '../features/FormEngine/reducers/addFormComponentReducer'
const rootReducer = combineReducers({
coreLayout: rootCoreLayoutReducer,
formEngine: rootFormEngineReducer,
addComponent: addComponentReducer
})
export default rootReducer
If you are using a pure component any performance optimizations have to be handled manually(using shouldComponentUpdate). Since you are using redux it can handle that for you. But you have to "connect" it to the redux store.
If you choose to use redux connect ensure that the modal visibility is not related to your other properties specifically in your case:
modalOpen is nested in formEngine. When it changes anything else that listens to formEngine will rerender
I searched and tried a lot to use a select input type with my react form using redux-form library.
Everything works, all other input types are ok, but not the select one for the following actions : initialize, retrieve the value submitted etc.
I tried to use the model prop with "select" and with my own function to render it. When I use the select version for model, I manage to get the options of the combobox field but I don't manage to set a value and retrieve it when I submit. With my own function I don't even manage to set the options to the list...
Here is my code :
// FormComponent file
const { handleSubmit } = this.props;
...
<form onSubmit={handleSubmit(this.props.onSubmitProfileUpdate)}>
<Field name='ranking' className='input-row form-group form-control' component={renderSelectField}>
{tennisRankings.map(ranking =>
<option value={ranking} key={ranking}>{ranking}</option>
)}
</Field>
...
ProfileForm.propTypes = {
user: React.PropTypes.object,
fields: React.PropTypes.shape({
firstname: React.PropTypes.string,
lastname: React.PropTypes.string,
ranking: React.PropTypes.string,
email: React.PropTypes.string,
telephone: React.PropTypes.string,
city: React.PropTypes.string
}),
errorMessage: React.PropTypes.string,
confirmationMessage: React.PropTypes.string,
onSubmitProfileUpdate: React.PropTypes.func.isRequired,
handleSubmit: propTypes.handleSubmit,
initialize: propTypes.initialize
};
export default reduxForm({
form: 'profile',
validate: validateProfileForm
})(ProfileForm);
My function to render the field :
// Functions shared
export const renderSelectField = (field) => {
var styles = {};
var containerStyle = getInputStylesContainer();
if (field.input.value || field.meta.touched) {
if (!field.meta.error) {
styles = getInputStylesSuccess();
containerStyle = classNames(containerStyle, {'has-success': true});
} else {
styles = getInputStylesError();
containerStyle = classNames(containerStyle, {'has-error': true});
}
}
return (<div className={containerStyle}>
{displayInputLabel(styles.idInput, field.label)}
<select {...field.input} className='form-control' id={styles.idInput} value={field.input.value} type={field.type} placeholder={field.label} aria-describedby={styles.ariaDescribedBy} />
<span className={styles.glyphicon} aria-hidden='true' />
{field.meta.touched && field.meta.error &&
displayErrorMessage(field.meta.error)}
</div>);
};
Do you have any clue to perform that simple action ? Be indulgent I am a beginner :)
Thanks a lot for your help :)
EDIT :
Here is the way that I am initializing my form values :
// FormComponent file
handleInitialize () {
// TODO: Manage properly user not available case (redux form reason ?)
if (this.props.user.firstname === undefined) return;
const initData = {
'firstname': this.props.user.firstname.toString(),
'lastname': this.props.user.lastname.toString(),
'city': this.props.user.city === undefined ? '' : this.props.user.city.toString(),
'email': this.props.user.email.toString(),
'ranking': this.props.user.ranking.toString(),
'telephone': this.props.user.telephone === undefined ? '' : this.props.user.telephone.toString()
};
console.log('Ranking', this.props.user.ranking);
this.props.initialize(initData);
}
componentDidMount () {
this.handleInitialize();
}
....
export default reduxForm({
form: 'profile',
validate: validateProfileForm
})(ProfileForm);
Here's an example of a simple select field from the official docs:
<div>
<label>Favorite Color</label>
<div>
<Field name="favoriteColor" component="select">
<option></option>
<option value="ff0000">Red</option>
<option value="00ff00">Green</option>
<option value="0000ff">Blue</option>
</Field>
</div>
</div>
Your implementation does not have options mapped, so select does not work.
Sidenote
To pass initial values you should add initialValues property to your reduxForm configuration instead of adding value attribute to the field. Example below:
export default reduxForm({
form: 'profile',
validate: validateProfileForm,
initialValues: { ... }
})(ProfileForm);
select rating example : redux-form
example how to handle selection in redux-form
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
/*------- connect react with redux --------*/
import { connect } from 'react-redux';
/*------- action which all data to data base --------*/
import { addProduct } from '../actions'
/*------- redux form --------*/
import { Field, reduxForm } from 'redux-form';
class Form extends Component {
//PRISTINE / DIRTY // TOUCHED / ERROR : events in redux-form
/*------- renderSelectField --------*/
renderSelectField(field){
const className = `form-input ${field.meta.touched && field.meta.error ? 'has-error':''}`;
return(
<div className={className}>
<label>{field.myLabel}</label>
<select {...field.input} >
{field.children}
</select>
<div className="error">
{field.meta.touched ? field.meta.error:''}
</div>
</div>
)
}
/*------- onSubmit() : runs on submit --------*/
onSubmit(values){
// const error = this.validate(values);
// console.log(error)
console.log(values)
this.props.dispatch(addProduct({
values
}))
}
render(){
return(
<div className="Form">
<div className="top">
<h3>Add a Message</h3>
<Link to="/">Back</Link>
</div>
<form onSubmit={this.props.handleSubmit((event)=>this.onSubmit(event))}>
<Field
myLabel="Select Rating"
name="rating"
component={this.renderSelectField}>
<option></option>
<option value='1'>1</option>
<option value='2'>2</option>
<option value='3'>3</option>
<option value='4'>4</option>
<option value='5'>5</option>
</Field>
<button type="submit">Submit</button>
</form>
</div>
)
}
}
/*------- validate() : validates our form --------*/
function validate(values){
const errors = {};
if(!values.rating){
errors.rating = "The rating is empty"
}
return errors;
}
/*------- it returns messages when action is called and state going to change --------*/
function mapStateToProps(state){
return {
product: state.data
}
}
/*------- reduxForm : connects redux-form with react form --------*/
export default reduxForm({
validate,
form:'AddProduct',
})(
connect(mapStateToProps,{addProduct})(Form)
)