Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[V6RC4] FieldArray + Dynamic select options #1484

Closed
sir4ur0n opened this issue Aug 8, 2016 · 7 comments
Closed

[V6RC4] FieldArray + Dynamic select options #1484

sir4ur0n opened this issue Aug 8, 2016 · 7 comments

Comments

@sir4ur0n
Copy link

sir4ur0n commented Aug 8, 2016

Hi,

My scenario is as follows: I want a dynamic number of rows where each row contains 2 <select> fields (say, one with a country and one with a city).
I have the map [{"France": ["Paris", "Lyon"]},{"Belgium": ["Brussels", "Gent"]}] available as props of my form.
I can add or remove a row at will.

Example:

|France|Lyon|Delete row  
|Belgium|Gent|Delete row  
|France|Paris|Delete row  
Add row

Of course I don't want to be able to select |France|Brussels, so I must make the second <select> field of each row dynamic depending on the first.

I tried the following solutions:
#1) With form selector

As there are N dynamic rows, I wrapped the rows in a FieldArray. The problem is that formValueSelector does not appear to work well with FieldArrays. The only thing that works seems to select the whole FieldArray (e.g. selector(state, 'fieldArrayRows')). A weird consequence is that it doesn't seem to re-render the UI (as mentioned here), maybe because the changes happen on nested elements.
In particular, it is impossible to write selector(state, 'fieldArrayRows[].countryName'), which would be a decent solution for me.
#2) With Normalizing

Unfortunately Normalizing seems to exist only to fine-tune the field value right before it is written in the state. It doesn't allow to fine-tune the display based on values.
#3) With onChange

I can add an onChange to the country select, but I don't see how to rerender the corresponding city field based on the value. AFAIU the only way to rerender is to change the state/props.

Sorry for the long question/issue, I appreciate any help!

@clayne11
Copy link
Contributor

clayne11 commented Aug 8, 2016

I think you could use option 1. In your connect function you can first fetch the entire fieldArrayRows, check it's length, then dynamically fetch fieldArrayRows[0].countryName, fieldArrayRows[1].countryName ... fieldArrayRows[fieldArrayRows.length - 1].countryName. Then you'll only be re-rendering when one of the countryName changes.

import range from 'lodash/range'

const selector = formValueSelector('fooForm')
const mapStateToProps = (state) => {
  const fieldArrayRows = selector(state, 'fieldArrayRows')
  const countryNameFields = fieldArrayRows && range(fieldArrayRows).map((row) => 
    `fieldArrayRows[${row}].countryName`)
  return {
    countryNames: selector(state, countNameFields)
  }
}

Note: I haven't tested this code, it's just a gist. Should give you an idea though.

@sir4ur0n
Copy link
Author

sir4ur0n commented Aug 9, 2016

@clayne11 Thanks for the idea, I'll definitely try that to check if it works. It's still not perfect IMO because it means whenever there is a change (be it a change of value on the first fields, or an add/remove of rows), it will re render the whole form (as per formSelector documentation), whereas my need is to re-render only the second dropdown on change of the first.

@clayne11
Copy link
Contributor

clayne11 commented Aug 9, 2016

I just realized the gist I gave you was silly. You can just map over the fieldArrayRows:

const selector = formValueSelector('fooForm')
const mapStateToProps = (state) => {
  const fieldArrayRows = selector(state, 'fieldArrayRows')
  const countryNames = fieldArrayRows && fieldArrayRows.map(({countryName}) => 
    countryName)
  return {
    countryNames
  }
}

As for re-rendering, that's such a minute issue you on't even need to be concerned about it. The issue with extra renders is more for text fields. If you have the entire form re-rendering every time you enter a letter in a text field you'll end up with brutal lag on a big form. When you're looking at a dropdown like you're referring to you don't have the kind of rapid-fire input you get a from a text field and a single extra render won't cause any noticeable performance problems.

If you're still not happy with that performance you can add the connect directly to the form component thats dependent on the country field value rather than the top level form and that way only that single component will re-render when the dependent value changes.

@sir4ur0n
Copy link
Author

sir4ur0n commented Aug 9, 2016

@clayne11 don't worry about the range stuff, I had already changed this part :D
Anyway thank you, the solution seems to work just fine so far.
For information if anyone reads this, don't forget to add a check on fieldArrayRows.map as on initial rendering the value can be empty:
const countryNames = fieldArrayRows && fieldArrayRows.map((country) => country && country.name)

@sir4ur0n sir4ur0n closed this as completed Aug 9, 2016
@peletiah
Copy link

peletiah commented Nov 4, 2016

Please excuse my highjacking of this thread, but I have the same issue and tried to implement what you discussed. Apparently I'm missing something. Here's what I got so far (Shortened):

{
  "id": 1,
  "sequences": [
    {
      "sequence": 1,
      "command": "dothis",
      "data": "blablu"
    },
    {
      "sequence": 2,
      "command": "dothat",
      "data": "yaddayadda"
    },
    {
      "sequence": 3,
      "command": "dontlookback",
      "data": "1234567"
    }
  ]
}
class Routing extends Component {

  constructor(props) {
    super(props);
  }

  render() {
    const { sequences, sequenceCommands } = this.props

    console.log(`sequenceCommands: ${sequenceCommands}`)

    return (
      <div>
        {sequenceCommands && <div>
          <span>commandValue: {sequenceCommands[0].command}</span>
        </div>}

        <FieldArray name="sequences"
            component   =  { renderSequences }        
        />
    </div>
    );
  }
};

Routing = reduxForm({
    form: 'routingForm',
    enableReinitialize: true
  }
)(Routing)

Routing = connect(
  state => ({
    initialValues: {sequences:state.route.sequences}
  })
)(Routing)

const selector = formValueSelector("routingForm")

const mapStateToProps = (store, state) => {
  const sequenceFormArray = selector(state, 'sequences')
  const sequenceCommands = sequenceFormArray && sequenceFormArray.map(
    ({sequence}) => sequence && sequence.command
  )
  // maps store.route to this.props
  return {
    sequences: store.route,
    sequenceCommands: sequenceCommands
  }
  }


export default connect(mapStateToProps)(Routing);

Shouldn't sequenceCommands be filled with something useful after a change in sequences? At the moment it stays empty. I've been puzzling over this for a while now, but can't figure out what I'm missing...

@peletiah
Copy link

peletiah commented Nov 4, 2016

Hmm, I got it working, but not in mapStateToProps. Instead I just went back to the component-generator and declared the selector there:

Routing = reduxForm({
    form: 'routingForm',
    enableReinitialize: true
  }
)(Routing)

const selector = formValueSelector("routingForm")

Routing = connect(
  state => ({
    initialValues: {sequences:state.route.sequences},
    sequenceFormArray: selector(state, "sequences"),
  })
)(Routing)


const mapStateToProps = function(store) {
  // maps store.route to this.props
  return store.route
}

export default connect(mapStateToProps)(Routing);

and check and use its content on the render like this:

{sequenceFormArray && <div>
          <span>sequenceFormArray: {sequenceFormArray[0].command}</span>
        </div>}

Not the most elegant way I guess, but at least it's working now.

@lock
Copy link

lock bot commented Jun 2, 2018

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Jun 2, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants