Stack : React16, ES6, Redux
I'm currently unable to figure what's wrong here. The goal here is to add dynamically an infinite number of components (one by one) when clicking on an add button. I need to make them appear, if possible by pair or more, e.g. if I click on the ADD button, there should be 2 fields appearing each time (one select field and one textfield at the same time, for ex)
I'm able to make the components appear, with the help of Redux, and I'm also able to manage the datas correctly (everything's wired on the back of the app)
THE PROBLEM HERE :
When trying to type text in an input field, it's ALWAYS losing the focus. I've seen that each time I update my props, the whole component named MultipleInputChoiceList is mounted again, and that each fields are re-created anew. That's what I need to fix here :
EDIT : The MultipleInputChoiceList component is mounted via a Conditional Rendering HOC (It takes some values and check if they are true, if they are, it's rendering the component without touching the whole form)
ConditionalRenderingHOC.js
import React from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
const mapStateToProps = state => {
return {
form: state.form.form
}
}
const mapDispatchToProps = dispatch => {
return {
}
}
/**
* HOC Component to check conditional rendering on form component, using requireField property
* To be enhanced at will
*/
export default (WrappedComponent, formItem = {}) => {
class ConditionalRenderingHOC extends React.Component {
componentWillMount() {
//Check if all informations are available
if (formItem.requireField !== undefined) {
const requireField = formItem.requireField
if (requireField.value !== undefined &&
requireField.name !== undefined &&
requireField.field !== undefined &&
requireField.property !== undefined) {
//If everything's here let's call canBeRendered
this.canBeRendered()
}
}
}
//Check if the count of fetched values is directly linked to the number of fetched config asked, if true, return the same number
canBeRendered() {
formItem.requireField.isRendered = false
let required = formItem.requireField
let isEqual = false
if (this.props.form[required.field] !== undefined) {
let field = this.props.form[required.field]
_.forEach(field.value, (properties, index) => {
if (properties[required.name] !== undefined) {
if (properties[required.name] === required.value) {
if (properties[required.property] === required.isEqualTo) {
formItem.requireField.isRendered = true
isEqual = true
}
}
}
})
}
return isEqual
}
render() {
let isConditionMet = this.canBeRendered()
let render = null
if (isConditionMet === true) {
render = <WrappedComponent items={formItem}/>
}
return (<React.Fragment>
{render}
</React.Fragment>)
}
}
return connect(mapStateToProps, mapDispatchToProps)(ConditionalRenderingHOC)
}
The code
//Essentials
import React, { Component } from 'react'
import _ from 'lodash'
//Material UI
import TextField from 'material-ui/TextField'
import IconButton from 'material-ui/IconButton'
import AddBox from 'material-ui/svg-icons/content/add-box'
//Components
import SelectItemChoiceList from '../form/SelectItemChoiceList'
import TextFieldGeneric from './TextFieldGeneric'
//Redux
import { connect } from 'react-redux'
import { createNewField } from '../../../actions/formActions'
const mapStateToProps = (state) => {
return {
form: state.form.form
}
}
const mapDispatchToProps = (dispatch) => {
return {
createNewField: (field, state) => dispatch(createNewField(field, state))
}
}
class MultipleInputChoiceList extends Component {
constructor(props) {
super(props)
this.state = {
inputList: [],
}
}
onAddBtnClick() {
const name = this.props.items.name
/**Create a new field in Redux store, giving it some datas to display */
this.props.createNewField(this.props.form[name], this.props.form)
}
render() {
const name = this.props.items.name
/**I think the error is around this place, as it always re-render the same thing again and again */
const inputs = this.props.form[name].inputList.map((input, index) => {
switch(input) {
case 'selectfield': {
return React.createElement(SelectItemChoiceList, {
items: this.props.form[name].multipleField[index],
key:this.props.form[name].multipleField[index].name
})
}
case 'textfield': {
return React.createElement(TextFieldGeneric, {
items: this.props.form[name].multipleField[index],
index:index,
key:this.props.form[name].multipleField[index].name
})
}
default: {
break
}
}
})
return (
<div>
<IconButton onClick={this.onAddBtnClick.bind(this)}>
<AddBox />
</IconButton>
{inputs}
</div>
)
}
}
const MultipleInputChoiceListRedux = connect(mapStateToProps, mapDispatchToProps)(MultipleInputChoiceList)
export default MultipleInputChoiceListRedux
And the TextField used here :
TextFieldGeneric.js
//Essentials
import React, { Component } from 'react';
//Components
import TextField from 'material-ui/TextField'
//Redux
import { connect } from 'react-redux'
import { validateField, isValid } from '../../../actions/formActions'
const mapStateToProps = (state) => {
return {
form: state.form.form
}
}
const mapDispatchToProps = (dispatch) => {
return {
validateField: (field) => dispatch(validateField(field)),
isValid: () => dispatch(isValid())
}
}
class TextFieldGeneric extends Component {
constructor(props) {
super(props)
this.state = {
form: {},
field: {},
index: 0
}
}
componentWillMount() {
console.log(this.props)
//first, let's load those dynamic datas before rendering
let form = this.props.form
let index = this.props.index
/** Check if there's a correctly defined parent in form (taken from the name) */
let matchName = /[a-zA-Z]+/g
let origin = this.props.items.name.match(matchName)
//form.company.value = this.getCompaniesFormChoice()
this.setState({form: form, field: form[origin], index: index})
}
//setState and check validationFields if errors
handleFieldChange(event){
const name = event.target.name
const value = event.target.value
//Change value of state form field
const item = this.props.items
item.value = value
//validate each fields
this.props.validateField(item)
//validate form
this.props.isValid()
event.preventDefault()
}
render() {
const index = this.state.index
console.log(index)
return (
<React.Fragment>
<TextField
key={index}
floatingLabelText={this.state.field.multipleField[index].namefield}
name={this.state.field.multipleField[index].namefield}
floatingLabelFixed={true}
value = {this.state.field.multipleField[index].value}
onChange = {this.handleFieldChange.bind(this)}
errorText={this.state.field.multipleField[index].error === 0 ? '' : this.state.field.multipleField[index].error}
/>
</React.Fragment>
)
}
}
const TextFieldGenericRedux = connect(mapStateToProps, mapDispatchToProps)(TextFieldGeneric)
export default TextFieldGenericRedux
I also do understand that a part of the problem lies in the render method of the parent class (MultipleInputChoiceList.js) ...
Any help or comments REALLY appreciated!
As of now, I've not come to a real answer to that question, as it's a structural problem in my data (When updating a field via a Redux action, it's re-rendering the whole component). Maybe stock those data elsewhere would be a better option.
I've only used a onBlur method on the field, that dismiss the validation I want to do on each user input, but for the moment I could not think to another viable solution.
So far, the TextFieldGeneric.js looks like that :
//setState and check validationFields if errors
handleFieldChange(event){
this.setState({value: event.target.value})
event.preventDefault()
}
handleValidation(event){
const value = this.state.value
//Change value of state form field
const item = this.props.items
item.value = value
//validate each fields
this.props.validateField(item)
//validate form
this.props.isValid()
}
render() {
return (
<React.Fragment>
<TextField
name='name'
floatingLabelFixed={true}
value = {this.state.value}
onChange = {this.handleFieldChange.bind(this)}
onBlur = {this.handleValidation.bind(this)}
/>
</React.Fragment>
)
}
If anyone have another solution, I'll gladly hear it !