GithubHelp home page GithubHelp logo

mati365 / under-control Goto Github PK

View Code? Open in Web Editor NEW
13.0 2.0 1.0 823 KB

๐Ÿ“ ๐Ÿ• Are you losing sanity every time you need to make a form? Are you have enough of all antipatterns and cursed frameworks in React? Screw that! Treat all forms and inputs as a recursive composable control!

License: MIT License

TypeScript 95.76% Shell 0.22% JavaScript 4.01%
controls design-system form framework inputs react two-way-databinding validation forms typescript

under-control's Introduction

Banner

under-control

Codacy Badge Codacy Badge NPM NPM Downloads NPM version

Are you losing sanity every time you need to make a form? Are you tired enough of all antipatterns and cursed React frameworks? Screw that! Treat all forms and inputs as a recursive composable controls! under-control is a lightweight alternative to libraries such as react-hook-form, formik, react-ts-form, which, unlike them, allows you to turn your components into controllable controls.

Object type check example

๐Ÿ“– Docs

๐Ÿš€ Quick start

๐Ÿ“ฆ Install

Edit React Typescript (forked)

npm bundle size (scoped)

npm install @under-control/forms

โœจ Features

  • Allows you to turn any component into a control with value and onChange properties. Treat your custom select-box the same as it would be plain <select /> tag! Other libs such as react-hook-form do not provide similar mechanism.
  • Better encapsulation of data. Due to low context usage it allows you to reuse built controllable controls in other forms.
  • Small size, it is around 4x smaller than react-hook-form and weights ~2.6kb (gzip).
  • Performance. Automatic caching of callbacks that binds controls. Modification of control A is not triggering rerender on control B.
  • Built in mind to be type-safe. Provides type-safe validation and controls binding.
  • Provides rerender-free control value side effects. Modify of control can reset value of form without doing additional useEffect.
  • Exports additional hooks such as use-promise-callback / use-update-effect that can be reused in your project.
  • Highly tested codebase with 100% coverage.

๐Ÿ—๏ธ Composition

๐Ÿ–Š๏ธ Basic Custom Control

Build and treat your forms as composable set of controlled controls. Do not mess with implementing value / onChange logic each time when you create standalone controls.

Example:

import { controlled } from '@under-control/forms';

type PrefixValue = {
  prefix: string;
  name: string;
};

const PrefixedInput = controlled<PrefixValue>(({ control: { bind } }) => (
  <>
    <input type="text" {...bind.path('prefix')} />
    <input type="text" {...bind.path('name')} />
  </>
));

Usage in bigger component:

import { controlled } from '@under-control/forms';
import { PrefixedInput } from './prefixed-input';

type PrefixPair = {
  a: PrefixValue;
  b: PrefixValue;
};

const PrefixedInputGroup = controlled<PrefixPair>(({ control: { bind } }) => (
  <>
    <PrefixedInput {...bind.path('a')} />
    <PrefixedInput {...bind.path('b')} />
  </>
));

onChange output from PrefixedInput component:

{
  a: { prefix, name },
  b: { prefix, name }
}

These newly created inputs can be later used in forms. Such like in this example:

import { useForm, error, flattenMessagesList } from '@under-control/forms';

const Form = () => {
  const { bind, handleSubmitEvent, isDirty, validator } = useForm({
    defaultValue: {
      a: { prefix: '', name: '' },
      b: { prefix: '', name: '' },
    },
    onSubmit: async data => {
      console.info('Submit!', data);
    },
  });

  return (
    <form onSubmit={handleSubmitEvent}>
      <PrefixedInputGroup {...bind.path('a')} />
      <PrefixedInputGroup {...bind.path('b')} />
      <input type="submit" value="Submit" disabled={!isDirty} />
    </form>
  );
};

You can use created in such way controls also in uncontrolled mode. In that mode defaultValue is required.

<PrefixedInputGroup defaultValue={{ prefix: 'abc', name: 'def' }} />

Check out example of custom controls with validation from other example:

Edit advanced-validation

๐Ÿ“ Forms

โš ๏ธ Forms without validation

The simplest possible form, without added validation:

import { useForm } from '@under-control/forms';

const Form = () => {
  const { bind, handleSubmitEvent, isDirty } = useForm({
    defaultValue: {
      a: '',
      b: '',
    },
    onSubmit: async data => {
      console.info('Submit!', data);
    },
  });

  return (
    <form onSubmit={handleSubmitEvent}>
      <input type="text" {...bind.path('a')} />
      <input type="text" {...bind.path('b')} />
      <input type="submit" value="Submit" disabled={!isDirty} />
    </form>
  );
};

Edit not-validated-form

โœ… Forms with validation

Validation by default can result sync or async result and can be run in these modes:

  1. blur - when user blurs any input. In this mode bind.path returns also onBlur handler. You have to assign it to input otherwise this mode will not work properly.
  2. change - when user changes any control (basically when getValue() changes)
  3. submit - when user submits form

Each validator can result also single error or array of errors with optional paths to inputs.

Single validator

Example of form that performs validation on blur or submit event.

import { useForm, error, flattenMessagesList } from '@under-control/forms';

const Form = () => {
  const { bind, handleSubmitEvent, isDirty, validator } = useForm({
    defaultValue: {
      a: '',
      b: '',
    },
    validation: {
      mode: ['blur', 'submit'],
      validators: ({ global }) =>
        global(({ value: { a, b } }) => {
          if (!a || !b) {
            return error('Fill all required fields!');
          }
        }),
    },
    onSubmit: async data => {
      console.info('Submit!', data);
    },
  });

  return (
    <form onSubmit={handleSubmitEvent}>
      <input type="text" {...bind.path('a')} />
      <input type="text" {...bind.path('b')} />
      <input type="submit" value="Submit" disabled={!isDirty} />
      <div>{flattenMessagesList(validator.errors.all).join(',')}</div>
    </form>
  );
};

Edit validated-form

Multiple validators

Multiple validators can be provided. In example above global validator validates all inputs at once. If you want to assign error to specific input you can:

  1. Return error("Your error", null "path.to.control") function call in all validator.
  2. User path validator and return plain error("Your error").

Example:

const Form = () => {
  const {
    bind,
    handleSubmitEvent,
    submitState,
    validator: { errors },
  } = useForm({
    validation: {
      mode: ['blur', 'submit'],
      validators: ({ path, global }) => [
        global(({ value: { a, b } }) => {
          if (!a || !b) {
            return error('Fill all required fields!');
          }

          if (b === 'World') {
            return error('It cannot be a world!', null, 'b');
          }
        }),
        path('a.c', ({ value }) => {
          if (value === 'Hello') {
            return error('It should not be hello!');
          }
        }),
      ],
    },
    defaultValue: {
      a: {
        c: '',
      },
      b: '',
    },
    onSubmit: () => {
      console.info('Submit!');
    },
  });

  return (
    <form onSubmit={handleSubmitEvent}>
      <FormInput {...bind.path('a.c')} {...errors.extract('a.c')} />
      <FormInput {...bind.path('b')} {...errors.extract('b')} />

      <input type="submit" value="Submit" />

      {submitState.loading && <div>Submitting...</div>}
      <div>{flattenMessagesList(errors.global().errors)}</div>
    </form>
  );
};

Edit advanced-validation

โœจ Binding controls

useControl is a core hook that is included into useForm and identical bind functions are exported there too. It allows you to bind values to input and it can be used alone without any form.

Bind whole state to input

In example below it's binding whole input text to string state with initial value Hello world.

import { useControl } from '@under-control/inputs';

const Component = () => {
  const { bind } = useControl({
    defaultValue: 'Hello world',
  });

  return <input type="text" {...bind.entire()} />;
};

Bind specific path to input

You can also bind specific nested path by providing path:

import { useControl } from '@under-control/inputs';

const Component = () => {
  const { bind } = useControl({
    defaultValue: {
      message: {
        nested: ['Hello world'],
      },
    },
  });

  return <input type="text" {...bind.path('message.nested[0]')} />;
};

Defining relations between inputs

When user modifies a input then b input is also modified with a value + ! character.

import { useForm } from '@under-control/forms';

const App = () => {
  const { bind } = useControl({
    defaultValue: {
      a: '',
      b: '',
    },
  });

  return (
    <div>
      <input
        type="text"
        {...bind.path('a', {
          relatedInputs: ({ newControlValue, newGlobalValue }) => ({
            ...newGlobalValue,
            b: `${newControlValue}!`,
          }),
        })}
      />
      <input type="text" {...bind.path('b')} />
    </div>
  );
};

Edit form-inputs-relations

Mapping bound value to input

It picks value from message.nested[0], appends ! character to it, and assigns as value to input:

import { useControl } from '@under-control/inputs';

const Component = () => {
  const { bind } = useControl({
    defaultValue: {
      message: {
        nested: ['Hello world'],
      },
    },
  });

  return (
    <input
      type="text"
      {...bind.path('message.nested[0]', {
        input: str => `${str}!`, // appends `!` value stored in message.nested[0]
      })}
    />
  );
};

License

MIT

under-control's People

Contributors

dependabot[bot] avatar kizuhane avatar mati365 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

kizuhane

under-control's Issues

Better docs

Current docs are not good enough.

Ideas:

  • Place code sandbox button for every example

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.