Created: July 2, 2024

Creating forms using JSON schemas is highly opinionated practice and for sure doesn’t fit all kind of projects.

There are some for which it’s an optimal choice, for example backends with a lot of forms and fields which, quite often, are the results of layers of business requests happened over the time (where it will not be strange to co-exist different versions of the same form).

That’s a lot of knowledge stored in a unstructured way, which can be difficult to track and manipulate / transform if some new business rule requires it.

One of the example where it turned out to be useful. At some point I wanted to enable LestForm for defining form with tag-like DSL and not just a pure JSON, for example

<LetsForm>
  <LfField 
	  component="input-text" 
	  label="My field" 
	  name="my_field"
	/>
  <LfColumns>
    <LfColumn>
      <LfField 
	      component="input-text" 
	      label="My field left" 
	      name="my_field_left"
	    />
    </LfColumn>
    <LfColumn>
      <LfField 
	      component="input-text" 
	      label="My field right" 
	      name="my_field_right"
	    />
    </LfColumn>
  </LfColumns>
</LetsForm>

LetsForm is already rendering the form using a JSON schema, just wanted to enable both modes.

First idea was “map” each native component to the generic component <LfField>, basically a big switch based on the component property and then try to deal with nested components like <LfColumn>. Too complex.

Turned out that making these components returning nothing and treating like a Domain Specific Language was the quicker approach, 200 line of codes including syntax checking.

It basically traverse all children components, grab the properties and build the JSON schema and use it to render the form.

Something like in vanilla React

const traverseChildren = children => {
  return elements
    .map(element => {
      if (elementOf(element, LfField)) {
        return {
          ...element.props
        };
      } else if (elementOf(element, LfGroup)) {
        return {
          ..._.omit(element.props, 'children'),
          component: 'group',
          fields: traverseChildren(element.props.children)
        };
      } else if (elementOf(element, LfColumns)) {
        return {
          component: 'two-columns',
          ..._.omit(element.props, 'children'),
          leftFields: traverseChildren(element.props.children[0].props.children),
          rightFields: traverseChildren(element.props.children[1].props.children)
        }
      }
      // ..omitted implemenentation for stesp, tabs, arrays, etc
    })
    .filter(Boolean);
};

http://localhost:3000/w/ybmkanrx89r/rsuite5