DEV Community

Cover image for Upleveling my form component design ⬆️
Andreas Riedmüller
Andreas Riedmüller

Posted on

Upleveling my form component design ⬆️

When I created a design system for my app, I first had components for many elemental input elements eg. <InputText />, <TextArea />, and <InputEmail />.

Later I figured that I often need those in combination with a label, error message, some description and so on, pretty basic stuff. I decided to call these groups FormField and started to implement components like FormFieldText and FormFieldEmail.

Here is an example:

<FormFieldPassword
  className={styles.formFieldPassword}
  autoComplete="new-password"
  label="Set a password"
  below={<FieldHelp>At least 8 characters</FieldHelp>}
  errorMessage={fieldFeedback.password}
  onChange={handleChangePassword}
  value={password}
  withShowPasswordToggle={true}
  autoFocus={emailWasInUrl}
  fullWidth
/>
Enter fullscreen mode Exit fullscreen mode

After some time I had quite a lot FormField… components and a fair amount of duplicate code inside of them. Eventually I created a generic FormField component and used it inside of the FormField… components to get rid of duplicate code. This is called composition and is commonly used for cases like that.

But with this approach I still had to create new FormField… components from time to time. I felt that skipping this step and just use the generic FormField component directly might be the better approach. That some Frameworks (like Chakra UI) go with a similar approach, supported my thinking.

The new approach:

<FormField
  className={styles.formFieldPassword}
  label="Set a password"
  below={<FieldHelp>At least 8 characters</FieldHelp>}
  errorMessage={fieldFeedback.password}
>
  <InputPassword
    autoComplete="new-password"
    onChange={handleChangePassword}
    value={password}
    withShowPasswordToggle={true}
    autoFocus={emailWasInUrl}
    error={fieldFeedback.password?.length > 0}
    fullWidth
  />
</FormField>
Enter fullscreen mode Exit fullscreen mode

What I like about this is that you can clearly see which props belong to the input. You can also directly access input props like className for example which is very handy. And I can add content above or below the input.

But it also creates two problems that I didn’t solve yet:

  1. The input may require a different design (e.g. a red border) if an error occurs. In the example above I do this manually by setting the error prop of my input.

  2. The label in FormField needs to know what input it labels. Since there can be multiple children now, it doesn't know exactly which of them is the input. Currently I'm just assuming it's the first child, but that prevents me from adding children before the input.

Ideas for the error issue

I already have two ideas how to solve this.

One is using the render props pattern e.g.

<FormField>
  {error => <Input error={error}  />}
</FormField>
Enter fullscreen mode Exit fullscreen mode

I do like the flexibility of this approach. What I don’t like is how complex the markup/code looks with this solution.

The other approach is to solve this by using a Context that can be consumed inside my input components.

The markup would be very clear as the implementation is inside of the input.

<FormField>
  <Input />
</FormField>
Enter fullscreen mode Exit fullscreen mode

Ideas for the label issue

This is a tricky one. Basically, I could use either of the two approaches above.

Using the render props pattern I can generate an ID inside FormField and then use it like this:

<FormField>
  {(error, labelFor) => <Input error={error} id={labelFor}  />}
</FormField>
Enter fullscreen mode Exit fullscreen mode

But I really want the markup to look simpler. And this is where it gets trickier. Providing the generated ID in a context will come with two minor problems:

  1. If there are two inputs, both will get the same ID. This doesn't seem like a big problem, but it's not allowed in the HTML specs and could cause problems.

  2. If for some reason the input is assigned an id, the FormField will not know this and the label will no longer be assigned to the input.

Chakra UI seems to take this approach and in fact hasn't solved both of these problems. Maybe I am overthinking 🤔

What do you think? Do you have an idea how to solve these problems? I am happy if you leave a comment.

I will update this post if I find better solutions.

Top comments (0)