Drawing Redux Form FieldArrays with Pug

2019-01-09

Having been spoiled by the Rails Prime Stack for nearly a decade, in my React projects I prefer using Pug (née Jade) instead of JSX for its Haml-like syntax. A lot of people praise Pug and Haml for saving them typing, and while that’s nice, the real appeal to me is how easy they are to read. You spend a lot more time reading code than writing it, and Pug/Haml make the document structure immediately obvious. Making closing-tag errors obsolete is pretty nice, too.

With the babel-plugin-transform-react-pug package, you can replace your JSX with something like this:

class MyApp extends React.Component {
  render() {
    return pug`
      Provider(store=configureStore(this.props))
        table
          tbody
            tr
              td table
              td layout
              td anyone?
    `;
  }
}

But Pug is definitely not as widely adopted within React as Haml is within Rails, and it shows. I ran into a tricky issue using Pug to render FieldArrays with Redux Form and React Bootstrap. To combine those two packages I’m basically following John Bennett’s advice, except with Pug.

Here is how typical scalar fields work:

const renderInput = ({input, label, type, meta}) => pug`
  FormGroup(controlId=input.name validationState=${validState(meta)})
    Col(componentClass=${ControlLabel} sm=2)
      = label || humanizeString(input.name)
    Col(sm=5)
      FormControl(...input type=type)
`

class EditClientPage extends React.Component {
  render() {
    return pug`
      Form(horizontal onSubmit=${this.props.handleSubmit})
        fieldset
          Field(name='name' component=renderInput type='text')
          Field(name='paymentDue' component=renderInput type='number')
    `
  }
}

That’s from my time-tracking app where I wrote about using FieldArray with redux-form-validators. The Field component is from Redux Form, and everything else is from React-Bootstrap (Form, FormGroup, Col, ControlLabel, and FormControl). You can see that Field expects a custom component to draw its details, giving you total flexibility how you structure your form’s DOM. Most of the Boostrap components go inside the custom component used by Field.

So far that’s pretty nice, but if you have a FieldArray you need more nesting. A FieldArray is also part of Redux Form, and is used to draw a list of child records with their sub-fields. In my case I want the page to have one or more “work categories”, each with a name and hourly rate, e.g. “Design” and “Development”.

Like Field, a FieldArray delegates rendering to a custom component. Then that component will render the individual Fields (each with their own custom component in turn). If you adapted the Redux Form docs’ example, you might try something like this:

const renderSimpleInput = ({input, placeholder, type, meta}) => pug`
  span(class=${validClass(meta)})
    FormControl(...input placeholder=placeholder type=type)
`

const renderWorkCategories = ({fields, meta}) => `pug
  .noop
    = fields.map((wc, index) => `pug
      FormGroup(key=${index})
        Col(sm=2)
          Field(name=${`${wc}.name`} component=renderSimpleInput type='text' placeholder='name')
        Col(sm=5)
          Field(name=${`${wc}.rate`} component=renderSimpleInput type='number' placeholder='rate')
        Col(sm=2)
          a.btn.btn-default(onClick=${()=>fields.remove(index)}) remove
    `)
    FormGroup
      Col(smOffset=2 sm=5)
        a.btn.btn-default(onClick=${()=>fields.push({})}) add rate
`

class EditClientPage extends React.Component {
  render() {
    return pug`
      ...
      FieldArray(name='workCategories' component=${renderWorkCategories})
      ...
    `
  }
}

The problem is that you can’t nest pug strings like that. I’m not sure if the problem is with the Babel transformer or the pug parser itself, but you get an error. Of course that’s not idiomatic Pug anyway, but surprisingly, you can’t use Pug’s each command either:

const renderWorkCategories = ({fields, meta}) => `pug
  .noop
    each wc, index in fields
      FormGroup(key=${index})
        Col(sm=2)
          Field(name=${`${wc}.name`} component=renderSimpleInput type='text' placeholder='name')
        Col(sm=5)
          Field(name=${`${wc}.rate`} component=renderSimpleInput type='number' placeholder='rate')
        Col(sm=2)
          a.btn.btn-default(onClick=${()=>fields.remove(index)}) remove
    ...
`

This gives you the error Expected "fields" to be an array because it was passed to each. Apparently Redux Form is not using a normal array here, but its own special object.

The trick is to call getAll, like this:

each wc, index in fields.getAll()
  FormGroup(key=${index})
    Col(sm=2)
      Field(name=${`workCategories[${index}].name`} component=renderSimpleInput type='text' placeholder='name')
    Col(sm=5)
      Field(name=${`workCategories[${index}].rate`} component=renderSimpleInput type='number' placeholder='rate')
    Col(sm=2)
      a.btn.btn-default(onClick=${()=>fields.remove(index)}) remove
`

Note that we also had to stop using ${wc} and are building the field name “by hand”. Personally I think we can stop here and be done, but if that feels like breaking encapsulation to you, or if you want something more generic that doesn’t need to “know” its FieldArray name, there is another way to do it. Even if it’s a bit too much for a real project, it’s interesting enough that it’s maybe worth seeing.

To start, we need to call fields.map with another custom component. This almost works:

const renderWorkCategory = (wc, index) => `pug
  FormGroup(key=${index})
    Col(sm=2)
      Field(name=${`${wc}.name`} component=renderSimpleInput type='text' placeholder='name')
    Col(sm=5)
      Field(name=${`${wc}.rate`} component=renderSimpleInput type='number' placeholder='rate')
    Col(sm=2)
      a.btn.btn-default(onClick=${()=>fields.remove(index)}) remove
`

const renderWorkCategories = ({fields, meta}) => `pug
  .noop
    = fields.map(renderWorkCategory)
    ...

The only problem is the remove button: fields is no longer in scope!

The solution is to use currying. The component we hand to fields.map will be a partially-applied function, generated by passing in fields early. ES6 syntax makes it really easy. The full code looks like this:

const renderWorkCategory = (fields) => (wc, index) => `pug
  FormGroup(key=${index})
    Col(sm=2)
      Field(name=${`${wc}.name`} component=renderSimpleInput type='text' placeholder='name')
    Col(sm=5)
      Field(name=${`${wc}.rate`} component=renderSimpleInput type='number' placeholder='rate')
    Col(sm=2)
      a.btn.btn-default(onClick=${()=>fields.remove(index)}) remove
`

const renderWorkCategories = ({fields, meta}) => `pug
  .noop
    = fields.map(renderWorkCategory(fields))
    ...
`

You may recall we also used currying to combine redux-form-validators with FieldArray. It can really come in handy!

As I’ve said before, programming one thing is usually easy; it gets hard when we try to do many things at once. Here I show how to use Pug with a FieldArray from Redux Form, on a page styled with React Bootstrap. I hope you found it useful if like me you are trying to have your cake and eat it too. :-)

UPDATE: It turns out I was making things too complicated: fields.map takes an optional third parameter, fields. That means there is no need to curry renderWorkCategory and pass in fields early. Instead of this:

const renderWorkCategory = (fields) => (wc, index) => ...

you can just say this:

const renderWorkCategory = (wc, index, fields) => ...

I guess it pays to read the documentation! :-)

blog comments powered by Disqus Prev: Survey of SQL:2011 Temporal Features Next: Validating FieldArrays in Redux Form