npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

morfi

v2.1.1

Published

Abstract form handling for any purpose (~2 kb gzipped)

Downloads

1,708

Readme

GitHub license npm package GitHub Push Coverage Status

morfi logo morfi

Abstract form handling for any purpose (2.3 kb gzipped)

Why morfi?

It offers simply one thing that I couldn't find in any other library. That is type safety. I've invested a lot of work to find the most convenient way to ensure type safety. In addition, I wanted to create a library with minimal footprint, i.e. no further dependencies, tiny bundle size, high runtime performance.

  • Type safe with TypeScript support
  • No dependencies
  • Small bundle size
  • High performance
  • Freely customizable form elements
  • Testing utility

Installation

npm i --save morfi
pnpm add morfi

Introduction

Let's get a first impression... Imagine you have created your own custom component to display and update some state like the following.

type User = { ID: string; name: string };
type UserPickerProps = { users: User[]; userID?: string; onPick: (user: User) => void };

const UserPicker: React.FC<UserPickerProps> = ({ users, userID, onPick }) => (
  <ul>
    {users.map((user) => (
      <li
        key={user.ID}
        className={userID === user.ID ? 'active' : undefined}
        role="button"
        onClick={() => onPick(user)}>
        {user.name}
      </li>
    ))}
  </ul>
);

To make this a picker that can be used directly in a form you need to create a wrapper form component for a specific field.

import { useCallback } from 'react';
import { Morfi, FormField } from 'morfi';

type FormUserPickerProps = { field: FormField<string | undefined>; users: User[] };

const FormUserPicker: React.FC<FormUserPickerProps> = ({ users, field }) => {
  const { value, onChange } = Morfi.useField(field);
  const onPick = useCallback((user: User) => onChange(user.ID), [onChange]);

  return <UserPicker users={users} userID={value} onPick={onPick} />;
};

And finally, you can use this custom form element in your form.

import { useState } from 'react';
import { Morfi, MorfiData } from 'morfi';

type FormValues = { userID?: string };
type MyFormProps = { users: User[]; onSubmit: (values: FormValues) => Promise<void> };

const MyForm: React.FC<MyFormProps> = ({ users, onSubmit }) => {
  const { Form, fields } = Morfi.useForm<FormValues>();
  const [data, setData] = useState<MorfiData<FormValues>>(Morfi.initialData({}));

  return (
      <Form onSubmit={onSubmit} data={data} onChange={setData}>
        <FormUserPicker users={users} field={fields.userID} />
      </Form>
  );
};

Asynchronous form field handling

To implement async form field validation, you simply have to return a promise, which will resolve the validation result. You can write validators which are both: async AND sync validator at the same time. One example:

import type { Validator } from 'morfi';

const specialValidator: Validator<string> = (val?: string) => {
    if (!val) { // if value is falsy we return sync error
        return {id: 'value.required'};
    }
    return myFetch('/my/api').then(result => {
        if (result.isInvalid) { // api answered that the value was invalid
            return {id: 'value.invalid'}; // return async error
        }
    });
}

HINT: You should always validate the value undefined synchronously, because morfi determines the requirements by these criteria. So if you use the comfortable async-await-Syntax, you should split your validator into two parts.

Configuration

You can bring your own comparison function in order to compare form values with each other using Morfi.configure({ comparator }). This check is used in order to determine the dirty state of the form. The default comparison is an identity check.

Control your submit

When the user submits the form, the following will happen:

  1. The submitting property will be propagated as true to your onChange
  2. All registered validators will be executed and their validation results will be awaited
  3. When 2 returned any errors your onChange will be called with the found errors and submitting as false (Early exit)
  4. When 2 returned no errors your onSubmit will be called
  5. When 4 took place, the submitting property will be propagated as false (if your onSubmit returned a Promise, this will happen after this Promise resolved)

morfi-Lifecycle

Types

morfi ships some important types that will be explained in this section.

First of the basic types explained:

| Name | TypeScript declaration | Information | |----------------------|-----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ErrorMessage | string \ { id: string; values?: { [key: string]: ReactNode } } | This structure allows to handle internationalization by transporting the required information like the intl key and placeholder values | | MaybeError | ErrorMessage \ undefined | This is the returned feedback of each validator | | Validator<F> | (value?: F) => MaybeError \ Promise<MaybeError> | The validator returns void if no error occurred or a Promise if the validation is asynchronous | | ValidationType | 'onChange' \ 'onBlur' \ 'onSubmit' | The validation types that can be specified | | FieldValidation<F> | { [t in ValidationType]?: Validator<F> } | An object containing all specified validators for one field | | FormField<F> | special key, which can be used to access field specific data | An object containing all specified validators for one field | | FormDirty | { [name: FormField<any>]: boolean \ undefined } | An object containing all dirty states | | FormFields<V> | on all (nested) keys of V: FormField<*> | An object containing all field keys | | FormValidation<V> | on all (nested) keys of V optional: FieldValidation<*> | An object containing all validations for the whole form | | FormErrors<V> | on all (nested) keys of V optional: ErrorMessage | An object containing all current errors | | MorfiData<V> | { values: V, errors: FormErrors<V>, isSubmitting: boolean; isDirty: boolean; hasErrors: boolean } | This is the main structure for the data represent the form state | | FormRef<V> | { submit: () => void; updateInitialData: (mapper: (data: V) => V) => void; } | Those are the accessible features when using the ref attribute on the Form. submit lets you trigger a form submit. updateInitialData can be super useful when working with hot updating forms. |

Next we can describe the FormProps<V>, which are the properties of the Form component:

| Props | Type | Description | Example | |--------------------|-------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------| | className | string (optional) | Will be applied to the form tag | 'my-form' | | validation | FormValidation<V> (optional) | Contains all validators | { name: { onChange: customValidator } } | | data | MorfiData<V> | Contains all values, errors and submitting state | Morfi.getIntialData({ name: 'Scotty' }) | | onChange | (next: MorfiData<V>) => void | Handles the next data after any changes have been made | (data) => setData(data) | | onSubmit | (values: V) => void \ Promise<any> (optional) | Will be called if submitted without any failing validators | (values) => updateUserData(values) | | onSubmitFailed | (err: Error, MorfiData<V>) => void (optional) | Will be called if submitting was aborted due to validation errors or if your submit returned a rejected promise | (err) => { if (err.code >= 500) showToaster('Sorry, please try again later'); } | | onSubmitFinished | (MorfiData<V>) => void (optional) | Will be called after submitting finished | () => navigate('/success') | | version | number (optional) | This can be used to reset the dirty states of the form. Updating the version will assume the data to be new. | 7 | | ref | React.RefObject<FormRef<V>> (optional) | This can be used to access the provided ref functionalities. See description of FormRef<V> | useRef<FormRef<MyFormValues>>(null) |

And finally, there are the FieldControls<F> returned by Morfi.useField(myField):

| Props | Type | Description | |------------|----------------------|-----------------------------------------------------------------------------------------------------------------| | onChange | (value: F) => void | Will update the field value and run the "onChange" validator | | onBlur | () => void | Will run the "onBlur" validator | | required | boolean | Will be true if any validator type returns something else than undefined after being invoked with undefined | | dirty | boolean | Will be true if the current value is not identical to the initial value. | | error | MaybeError | The current error | | name | string | Inferred name by the given names of the (nested) data structure. |

More examples

See here for some provided samples. If you're missing something, let me know.

Testing

Check out morfi-test-utils to simplify writing of your tests. The test util requires react version to be at least 18.0.0.

FAQ

  • Why are there no ready-to-use components exported like FormSelect from the above examples?

    morfi is primarily made to handle updates, validations, storing of data, assist the developer with strict types and serve as a guideline for form handling.

    In larger projects you want to have full control over all form components, and they are very individual. Creating Form* wrappers is most often very straight forward.

Alternatives

The following table includes results from bundlephobia. At the end you'll find my personal recommendation.

| Package | Version | Size (minified + gzipped) | |-------------------------|---------|---------------------------------------------------------------| | morfi | 2.0.0 | 2.3 kB | | react-hook-form | 7.33.1 | 8.6 kB | | formik | 2.2.9 | 13 kB | | react-final-form | 6.5.9 | 3.3 kB + 5.5 kB (hidden peer dependency: [email protected]) | | react-redux-form | 1.16.14 | 22.5 kB | | redux-form | 8.3.8 | 26.4 kB | | react-jsonschema-form | 1.8.1 | 69.3 kB |

The following statements represent only my personal opinion, although I did not work a lot with the following pretty good packages.

  • react-hook-form: Most trending, I guess. Great documentation. Embraces native HTML form validation. But hard to use with non-native elements such as the UserPicker from above. Pseudo type safety as it doesn't prevent data from being set differently. It doesn't support nested data structures as form data.

  • formik: Very flexible library that allows to do a lot with your forms. However, it is not type safe as it uses strings and dot-separated strings (for nested data structures) without any type reference to the used data structure. Also, the API is quite huge and in some aspects not very convenient, e.g. you have to take care about form state cleanup after submitting.

  • react-final-form: A lot of features, optimized for performance, small bundle size, totally customizable and needs no integrative work, but is not as well documented, has peerDependecies which you also need to install and has no flow assistance.

  • redux-form: A lot of features, nice documented and easy to use form handling tool, but it comes with the cost of a little integrative work, the cost of big package size, forces you to hold the form data in the redux store and to connect every form and has poor flow assistance. Almost the same for react-redux-form.

  • react-jsonschema-form: A lot of features and very nice docs, but a large package with less flexibility when it comes to individual form templating. No flow support.

  • react-form: Considered dead, as the last update was a long time ago.

Conclusion

Thank you for reading parts of my docs. ✌️

If you don't want to consider using morfi in your project for form handling, because it is currently maintained only by me, I would recommend to you to choose either react-hook-form or formik. Those have a great community behind them, are still well maintained and offer enough flexibility to reach your goals. I would say react-hook-form looks more opinionated and easy to use, but formik offers greater flexibility for the cost of complexity if you need to achieve very special things.