DEV Community

Chris Opperwall
Chris Opperwall

Posted on

Reading through a project: Formy

Sometimes it helps to take a software project and just read through the source code. If the documentation is good enough or the interface is simple enough you can probably getting away with not knowing how most of the project works, but sometimes it’s kind of nice to look a little deeper.
I’ve used a React form library called Formy for a few projects at iFixit. Formy lets you configure a form using JavaScript objects and apply that configuration to a view using React components. The documentation has a lot of examples, which makes it really easy to get up and running with it, but to be honest I don’t really understand how most of it works. Here’s my attempt at learning a little bit more about it.

Where to start

It’s probably not a bad idea to start looking in the entrypoint of the module. In the package.json file that’s specified by the main field of the JSON document. For Formy, it’s dist/Formy/Form.js, but that file doesn’t show up in GitHub. The dist directory is the result of a build step which converts each file in the src directory to an ES5 target, so it’s safe to say that we can treat src/Formy/Form.js as the entrypoint. The src/example and src/index.js directories and files are only used for documentation and development, so those can be ignored.

Exports

Form.js is responsible for exporting functions and data that users of the library can access. The file specifies a default export named Form, which is an object which contains named functions. It doesn’t look like Form has any state or prototype (apart from the Object prototype), so the functions it holds can be viewed as static functions and can be looked at individually.

Form.Component

Form.Component = ({ id, name, onSubmit, children }) => (
   <form
      id={id}
      name={name}
      onSubmit={onSubmit}
   >
      {children}
   </form>
);

Form.Component.propTypes = {
   id: PropTypes.string,
   name: PropTypes.string,
   onSubmit: PropTypes.func,
};
Enter fullscreen mode Exit fullscreen mode

Component is a functional React component that takes id, name, onSubmit, and children as props. The return value of that functional component is a form with those props applied to it. Any child components that are included within Form.Component are passed through to the form component. That’s probably used for including form inputs or submit buttons as children to a form.

Component seems like a kind of general name for a React component. Maybe it would be better to name it Form, because it wraps an actual form JSX tag.

Form.Field

Form.Field is defined in a separate file, so I’m not totally sure what that means yet. Why is FormField in a different file, but not Form.Component? That might make things seems a little more consistent. We can revisit this later after going through Form.js.

Form.customValidityFactory

Form.customValidityFactory = (constraint, validationMessage = 'Invalid') => (...args) => (
   constraint(...args) ?  '' : validationMessage
);
Enter fullscreen mode Exit fullscreen mode

A function that takes a constraint and validation message and returns a function that takes a variadic number of arguments and applies its arguments to the constraint function provided in the first function and returns either empty string if truthy or validationMessage if not. Maybe it’d be cool if the custom validity factory let the validity constraint function return its own error message, and not just empty string vs validationMessage. Would that allow multiple validation messages?

The end result of the customValidityFactory is to call setCustomValidity on the form input with the string result from calling the constraint function on the arguments passed to the resulting function. However, this happens in the component library and not in Formy itself. Formy assumes that passing a customValidity property to an input component will handle that properly, so that’s important to know if you want to include your own component library to use with Formy.

Form.fields

Form.fields = (globalProps = {}, fields) => Object.assign({},
   ...Object.entries(fields).map(([fieldKey, field]) => ({
      [fieldKey]: {
         ...Form.Field.defaultProps,
         ...{ name: fieldKey },
         ...globalProps,
         ...field,
      },
   })),
);
Enter fullscreen mode Exit fullscreen mode

Function that takes globalProps and an object of field definitions. Global props are useful for when you want to use the same onChange handler. The global field props will be applied to any field component, unless overridden by the individual field itself. Setting a global onChange prop to update state whenever any form field is changed is a good example of a global prop. The return value of this function is an object with form input name keys that map to an object with properties for that form input.

Form.getData

Form.getData = form => Object.assign({},
   ...Object.entries(Form.getProps(form).fields)
   .filter(([fieldKey, field]) => !field.disabled)
   .filter(([fieldKey, field]) =>
      !['checkbox', 'radio'].includes(field.type) || field.checked
   )
   .map(([fieldKey, field]) => ({ [fieldKey]: field.value })),
);
Enter fullscreen mode Exit fullscreen mode

Wow, this function is kind of dense. The gist looks like it returns an object with data from the value of each form field, but does not include unchecked radio or checkbox fields or disabled fields. The shape of the returned object is field name keys that map to the value of that field. This is particularly helpful if you want to get input data out of the form for submitting.

Form.getProps

Form.getProps = form => Object.assign({},
   ...Object.entries(form)
   .filter(([formPropKey, formProp]) => formPropKey !== 'fields')
   .map(([formPropKey, formProp]) => ({
      [formPropKey]: formProp instanceof Function ? formProp(form) : formProp,
   })),
   {
      fields: Object.assign({}, ...Object.entries(form.fields).map(([fieldKey, field]) => ({
         [fieldKey]: Object.assign({}, ...Object.entries(field).map(([fieldPropKey, fieldProp]) => ({
            [fieldPropKey]: fieldProp instanceof Function ? fieldProp(form, fieldKey) : fieldProp,
         }))),
      }))),
   },
);
Enter fullscreen mode Exit fullscreen mode

form.getProps goes over all non “fields” fields and if the value is a function, calls it with the form. An example of a prop like this is the return value from Form.onSubmitFactory, which expects a form and returns an event handler that goes on the actual form. The “fields” field maps each form field name, and for each form field prop, if it’s a function it passes the form and the fieldName to the function value. A good example of this is Form.customValidityFactory, which takes a constraint function and returns a function that takes a form and fieldKey, which is called by Form.getProps.

For all the ES6+ magic going on here, we’re basically mapping an object full of form level props and transforming properties that are functions by applying them with the form object and a fieldKey (if it’s a form field property).

Wow there’s a lot going on here. From examples it looks like this returns a list of props that can be passed to Form.Component and Form.Field in user component’s render method.

This function (and Form.getData) makes pretty heavy use of Object.assign. What does Object.assign actually do?

Object.assign is like an object spread operator. The first argument is the target object and all other arguments are sources to copy fields from into the target object. Later source properties override earlier ones. It looks like most of its uses use an empty target object and a list of sources from global to more specific properties. Object.assign can also take a source that is an array of objects and it will merge those together and then copy those into the target object.

The project’s babelrc specifies using the transform-object-rest-spread plugin, so maybe those *Object.assign*s can be converted to use the object spread operator.

Form.onChangeFactory

Form.onChangeFactory = fn => (form, fieldKey) => updatedProps => fn({
   ...form,
   fields: {
      ...form.fields,
      [fieldKey]: {
         ...form.fields[fieldKey],
         ...updatedProps,
      },
   },
});
Enter fullscreen mode Exit fullscreen mode

A function that takes a handler function fn, which returns a function that takes a form and fieldKey, which returns a function that takes an updatedProps object, which applies the handler function to a merged object with form as a base, an overridden fields key with the keys from form.fields with the fieldKey key overridden by the updatedProps object.

The example handler function receives a new form object with the updated fields and calls setState with that new form state. Kind of interesting that you have to specify that in order for the form to work. Maybe it could be a nice default.

Form.onSubmitFactory

Form.onSubmitFactory = fn => form => ev => {
   ev.preventDefault();
   fn(Form.getData(form));
};
Enter fullscreen mode Exit fullscreen mode

A function that takes a handler function fn, which returns a function that takes the form object, which returns a function that takes an event, which I would assume is the submit event. That function prevents the default behavior of the submit event, calls the handler function of the result of calling getData on the form object. This is useful for specifying what to do when the form is submitted, such as sending off an AJAX request or creating some action with the form data.

The resulting function from calling Form.onSubmitFactory is used as the value for the onSubmit key in the form state. The Form.Component component needs a onSubmit function that takes an event. In order to convert the onSubmit function in the form state into the onSubmit function prop, call From.getProps on the form state. This will supply the form state to the onSubmit function in the state, which takes a form and returns a function that takes an event. The result from calling that function will.

FormField.js

import React from 'react';
import FormFieldPropTypes from './FormFieldPropTypes';
import FormDefaultComponentLibrary from './FormDefaultComponentLibrary';

const FormField = ({componentLibrary, ...props}) => {
   const Component = componentLibrary[props.type];
   return <Component {...props} />;
}

FormField.defaultProps = {
   checked: false,
   componentLibrary: FormDefaultComponentLibrary,
   type: 'text',
   value: '',
};

FormField.propTypes = FormFieldPropTypes;

export default FormField;
Enter fullscreen mode Exit fullscreen mode

So FormField isn’t actually that complicated. FormField is functional React component that accepts componentLibrary and type props along with additional props. The type prop given is used as the key in the componentLibrary object to grab the component from, the return value is the JSX of that component with the props given to FormField.

FormField specifies some defaultProps such as checked, componentLibrary, type, and value. Checked is false by default, componentLibrary is Toolbox by default, type is text by default, and value is empty string by default. Not too weird for defaults.

FormField’s propTypes are imported from the FormFieldPropTypes.js file. Maybe that’s something that would be better specified by the component library? I’m not sure.

Top comments (0)