Search code examples
javascriptreactjsreduxreact-reduxreact-component

React losing focus on input field (component added dynamically)


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!


Solution

  • 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 !