Skip to content

2.3 useDynamicInputs

Avram Walden edited this page May 3, 2024 · 2 revisions

Provides methods for managing arrays in form data. Use it to make a reusable component with your own buttons and styles.

useDynamicInputs accepts an object:

{ 
  model: string, 
  emptyData: Record<string, unknown> 
}
Prop Description
model A dot-notation string used to access an array in the form data
emptyData An object with default (probably empty) values to be pushed to the end of the data array when a new input is added

It returns an object:

{
  addInput: (override?: Partial<T> | ((records: T[]) => Partial<T>)) => void,
  removeInput: (i: number) => T,
  paths: string[],
}

addInput has 3 call signatures.

  • When passed nothing, it will push a copy of emptyData to the form data array.

  • When passed an object, it will merge that object with emptyData, overriding any existing keys/values.

  • When passed a function, it will first call that function, and then merge it with emptyData

const PhoneInputs = () => {
  const { addInput, removeInput, paths } = useDynamicInputs({ 
    model: 'contact.phones', 
    emptyData: { number: '', type: '', order: 0 }
  })

  const handleAddInput = () => {
     // Pushes { number: '', type: '', order: '' } (from emptyData above) to the phones form data array
    addInput()
    
    // Override a value before pushing it to the form data array
    addInput({
      type: 'personal'
    })

    // Passing a function allows using the data to build dynamic values before pushing to the form data array
    addInput(records => {
      const highestOrder = records.reduce((acc, record) => record.order > acc ? record.order : acc, 0)

      return {
        order: highestOrder + 1
      }
    })
  }

  return (
    <>
      <div style={ { display: 'flex' } }>
        <label style={ { flex: 1 } }>{ label }</label>
        <button onClick={ handleAddInput }>+</button>
      </div>

      { paths.map((path, i) => (
        <NestedFields key={ i } model={ path }>
          <div style={ { display: 'flex' } }>
            <div>{ children }</div>
            <button onClick={ onClick: () => removeInput(i) }>-</button>
          </div>
        </NestedFields>
      )) }
    </>
  )
}

Reusable Dynamic Inputs Component

Using useDynamicInputs you can build a dynamic inputs interface using your own markup structure or FE component framework.

const DynamicInputs = ({ children, model, label, emptyData }) => {
  const { addInput, removeInput, paths } = useDynamicInputs({ model, emptyData })

  return (
    <>
      <div style={ { display: 'flex' } }>
        <label style={ { flex: 1 } }>{ label }</label>
        <button onClick={ addInput }>+</button>
      </div>

      { paths.map((path, i) => (
        <NestedFields key={ i } model={ path }>
          <div style={ { display: 'flex' } }>
            <div>{ children }</div>
            <button onClick={ onClick: () => removeInput(i) }>-</button>
          </div>
        </NestedFields>
      )) }
    </>
  )
}

This can then be used inside of a Form component:

const user = {
  user: {
    username: "bmo",
    emails: [
      { email: "bmo@treehouse.ooo", type: "personal" }
    ]
  }
}

const PageWithFormOnIt = () => {
  return (
    <Form model="user" data={ { user } } to={ `users/${user.id}` } method="patch">
      <TextInput name="firstName" label="First Name" />

      <DynamicInputs model="emails" emptyData={ { email: '', type: ''} } label="Emails">
        <TextInput name="email" label="Email" />
        <TextInput name="type" label="Type" />
      </DynamicInputs>
    </Form>
  )
}

A component called DynamicInputs is exported which implements this if you don't need to customize how the HTML is generated.

Clone this wiki locally