GithubHelp home page GithubHelp logo

robinweser / react-controlled-form Goto Github PK

View Code? Open in Web Editor NEW
117.0 5.0 10.0 8.2 MB

Flexible, Modular & Controlled Forms for React

Home Page: https://react-controlled-form.js.org

License: MIT License

form form-validation redux redux-form controlled-form forms react-forms react

react-controlled-form's Introduction

Controlled Forms

react-controlled-form aims to simplify form management in React.
It ships functional APIs to create your very own form fields and is built with flexibility and customization in mind.

It allows you to bring your own components.
You do not have to struggle with predefined input components ever again!
It only uses React Hooks under-the-hood and is thus super fast.

npm downloads npm version

Installation

yarn add react-controlled-form

Controlled Forms requires react>=16.3.0 to be installed in your project.

Benefits

  • simple functional API
  • Controlled state using useState
  • full flexibility
  • custom form fields
  • reactive forms

The Gist

import { useField, useForm } from 'react-controlled-form'

function Input({ isValid, errorMessage, ...props }) {
  return (
    <div>
      <input style={{ borderColor: isValid ? 'black' : 'red' }} {...props} />
      {errorMessage && <div>{errorMessage}</div>}
    </div>
  )
}

const nameValidation = {
  'Please enter at least 2 characters.': (value) => value.length >= 2,
  'Only use alphabetic letters.': /^[a-z]*$/gi,
}

function Form() {
  const firstname = useField({
    name: 'firstname',
    validation: nameValidation,
  })

  const lastname = useField({
    name: 'firstname',
    validation: nameValidation,
  })

  const { submit, reset } = useForm(firstname, lastname)

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()

        submit((isValid, data) => {
          if (isValid) {
            console.log('Submitted: ', data)
            reset()
          }
        })
      }}>
      <Input {...firstname.props} />
      <Input {...lastname.props} />
      <input type="submit" />
    </form>
  )
}

API

useField(options)

This hook uses React.useState under-the-hood and controls the state changes and validation for each field. The internal representation of a field contains the following properties:

  • value
  • isValid
  • isTouched
  • isDisabled
  • isRequired
  • isLoading
  • errorMessage

Options

Option  Description
value The initial value.
touched  The initial isTouched value. 
loading  The initial isLoading value. 
disabled  The initial isDisabled value. 
required  The initial isRequired value. 
validation  An map of errorMessage-validator where validator can either be a function of value or a RegExp.
showValidationOn  When the field is "touched" and isValid / errorMessage are passed to the props.
Can be change or blur.

Returns

(Object) An object containing the following properties:

const shape = {
  // Those can be passed to your custom input implementation
  props,
  // A function used to update manually the field (use with caution)
  // It either takes properties from the internal field listed above
  // or a function of the current field that returns the new field
  update,
  initial,
  name,
  value,
  isValid,
  isTouched,
  isDisabled,
  isRequired,
  errorMessage,
}

useForm(...fields)

This hook takes a list of fields, where a field is the output of the useField hook.

Returns

(Object) An object containing the following properties:

const shape = {
  // Takes a function of (isValid, data) where isValid is the global validation state and data is a map of name-value
  // Calling submit will automatically touch all fields to reveal the error messages (if not already shown)
  submit,
  // Resets the form to its initial state
  reset,
  // Touches each field to reveal their validation state & error messages
  touchFields,
}

useFormDebugger(...fields)

This hook is only meant for debugging reasons. It takes the same fields as useForm, but returns all the field data on every render.

Returns

(Object) An object containing the following properties:

const shape = {
  // A map of name-value pairs
  data,
  // A map of name-field pairs, where field represents the full internal representation of each field
  fields,
  // The global validation state which is accumulated by checking each field's isValid
  isValid,
}

createUseField(resolveProps)

This factory function can be used to create your very own version of useField which can be useful if you want to implement different behaviour or return different props.

It takes a function that receives an object with the following shape:

const params = {
  // All values from the internal field representation
  field,
  // A function used to validate the value according to the passed validation object
  validate,
  // The update function which is also returned by useField and described above
  update,
  // Any additionally passed options that are not directly part of the field representation e.g. showValidationOn
  options,
}

Usage

import { createUseField } from 'react-controlled-form'

function resolveProps({ field, update, validate, options }) {
  const { name, value, isValid, isDisabled, isRequired } = field

  return {
    value,
    name,
    required: isRequired,
    disabled: isDisabled,
    onChange: (e) =>
      update({
        value: e.target.value,
        isValid: validate(e.target.value),
      }),
    style: {
      borderColor: isValid ? 'black' : options.validationColor,
    },
  }
}

const useField = createUseField(resolveProps)

// Usage
const firstname = useField({
  name: 'firstname',
  validationColor: 'pink',
  validation: {
    'Enter at least 2 characters.': (value) => value.length >= 2,
  },
})

// this will render an input with pink borders for invalid values
const Firstname = () => <input {...firstname.props} />

Examples

License

react-controlled-form is licensed under the MIT License.
Created with ♥ by @robinweser and all the great contributors.

react-controlled-form's People

Contributors

jwillem avatar khamper-nj avatar newyork-anthonyng avatar robinweser avatar voocoder avatar

Stargazers

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

Watchers

 avatar  avatar  avatar  avatar  avatar

react-controlled-form's Issues

Bug: buttons or components created by the asReset HoC always trigger the form onSubmit as a side effect

Hello rofrischmann!
First of all, allow me to compliment you on this wonderful library. Personally I think it's extremely clear and concise, and hits exactly a sweet spot in power, expressiveness, clarity of design and ease of use, without needing to be everything and more like the huge monolithic titans like redux-forms or react-redux-forms.
I stumbled upon it by chance some weeks ago, and immediately loved it.

That said, I found what seems to be a bug in the reset function.
Any component created by the asReset helper (for instance, a simple reset button) has what seems to be an unintended side-effect:
While it does reset the form to the initial state, it also triggers the onSubmit function of the Form, thus attempting to submit an empty (or reset) form!

I have easily reproduced the bug in the 'simple' example as well.
I created a new component Reset.js

import React from 'react'
import { asReset } from 'react-controlled-form'

const Reset = ({ resetForm }: { resetForm: Function }) =>
  <button onClick={resetForm}>Reset</button>

export default asReset(Reset)

Then, in the index.js, I imported it ad added it to the example page, adding an extra line of console.log to the onSubmit.

import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { Form, mapDataToValues } from 'react-controlled-form'

import Submit from './components/Submit'
import Reset from './components/Reset'
import Input from './components/Input'

import DataDisplay from './DataDisplay'
import store from './store'

function onSubmit({ data, resetForm }) {
  console.log('RCF onSubmit called!');
  alert(JSON.stringify(mapDataToValues(data), null, 2))
  resetForm()
}

const render = () =>
  ReactDOM.render(
    <Provider store={store}>
      <Form formId="simple" onSubmit={onSubmit}>
        <div>
          <h1>Simple Example</h1>
          <br />
          <Input fieldId="firstname" />
          <Input fieldId="lastname" />
          <Submit />
          <Reset />
          <br />
          <DataDisplay />
        </div>
      </Form>
    </Provider>,
    document.getElementById('root')
  )

render()
store.subscribe(render)

As you'll be able to see, the result is that after resetting the form, the onSubmit fires anyways.

I tried to a look at the code of your reducers for the initForm action, to see if this behaviour was caused by an error for which I could find a solution myself, but unfortunately I didn't see anything that could justify this bug at a first glance.

Examples

Nothing shows a tool better than some great examples. Although the docs already provide some solid examples, we should have some prepacked examples ready.

Would also be nice to visit them directly - we should push every example to Now!

onSubmit prop of Form component - absent in version 3.0? Discrepancy between documentation and implementation?

Greetings again!

I have found another issue related to version 3.0, hopefully you won't mind too much.
According to the docs, the Form component should have the quite fundamental 'onSubmit' prop, a function that would be called on form submit, whose first parameter matches the callback shape.
That would be coherent with versions 1.x and 2 of RCF.

Surprisingly, though, this prop is completely absent from the Component.
Examining the modules/component/Form.js file, there seems to be no trace of it. Not only that, a quick search of 'onSubmit' on the modules directory of the repo seems to find no matches.
In the examples it is also not used.

Aside from the mismatch between docs and code, is this absence by design or is it something that got accidentally cut? It seems like an heavy absence from the module.

initialFields causes react-unknown-prop error

Adding initialFields to Form causes the following error:

proxyConsole.js:56 Warning: Unknown prop initialFields on <form> tag. Remove this prop from the element. For details, see https://fb.me/react-unknown-prop
    in form (created by Form)
    in Form (created by Connect(Form))
    etc.

It looks like other values that should not be passed down to form as props are removed (L131-139 of Form.js) but initialFields is not.

Reference Implementations

In order to provide even more examples we should have some reference implementations of all common input fields.

  • Input
  • Checkbox
  • Radio
  • Select

Unknown prop warning in validation example.

Getting this warning in validation example:

Warning: Unknown prop `initialFields` on <form> tag. Remove this prop from the element. For details, see https://fb.me/react-unknown-prop
    in form (created by Form)
    in Form (created by Connect(Form))
    in Connect(Form) (at index.js:30)
    in Provider (at index.js:29)

Performance concerns: Excessive re-rendering of <Form> and <Field> components on unrelated redux store changes

Greetings,

While using the latest version of RCF, I noticed a weird behaviour of the <Form> component, which first I will summarize, then try to explain in more detail.

Summary of the problem:

<Form> performs an re-render from its own inner "render prop" on EVERY redux action dispatched that alters the store, regardless of where said alteration takes place. This causes a lot of unnecessary re-renders on unrelated user interaction, and I am concerned they might negatively affect performance.

Details:

During a checkup on performance in a page when there were two different forms being shown, in completely different parts of the tree, I saw that the Forms would call their own render props, thus triggering an internal re-render, even on input on the OTHER Form. Then I noticed that happened also when other Redux actions altering the store were dispatched in the same page.
At first I thought it was related to the connected Container component ancestor (grandfather of both forms) in the page, but I verified it was not the case, even using advanced react-redux connect options like arestateEqual: the re-renders originated inside the Form (which is a connected component of his own) render props.

Considering that I first noticed this behaviour in what looked a somewhat specific edge case (two different forms on the same page), I made a temporary experiment in a simpler setting.
The test page tree has two branches: on one there is a children component, which has inside its tree a simple Form (I even removed any passed down props to the Form like other action dispatchers).
On the other branch, there is a Button dispatching a very simple action to Redux (it simply toggles a boolean in an unrelated part of the store, simplest thing that came into my mind).

Some code snippets of the 'simpler' test case to explain myself better:

// Main Page
const MainArea = (props) => {
	const { className, manChanged, submitAC, children, ...others } = props;
	console.warn('------- MAIN AREA IS RE-RENDERING ----------');
	return (
		<div className={className}>
			<div><i> World Main Area </i></div>
			<Fragment forRoute={wk.NEW}>
				<NewForm />
			</Fragment>
			<GenericButton onClick={() => {
				console.log('CLICK CLICK');
                                // ManChanged is a dispatcher to redux;
				manChanged();
			}}>
				Test RE RENDERING BUTTON.
			</GenericButton>
		</div>
	);
};
// Written as a Component since I used it to check inside its ShouldComponentUpdate
class NewForm extends React.Component {	
// ....
	render () {
		console.warn('NF RENDERING');
		return (
			<div className={this.props.className}>
				<Form
					formId="NewForm"
					initialFields={defaultFormFields()}
					render={
						(formRP) => {
							const {
								state, formId, data, updateField, updateState, reset, validate,
							} = formRP;
							console.warn('NF RENDER PROPS RENDERING');
							return (
								<NewFormInner
									formState={state}
									data={data}
									reset={reset}
									validate={validate}
									formId={formId}
									updateField={updateField}
									updateState={updateState}
								/>
							);
						}
					}
				/>
			</div>
		);
	}
}

(NewFormInner is a generic form made with React-Controlled-Form)

Now, when I click on said Button, the action triggers a re-render for the Form render props (and no re-render for any other connected components, if any are added, as it should be).

I'll add a simple screen of the console to show what happens.
bugreport rcf 2018-09-05

At first you can see the form rendering normally after the app reaches the test location.
Then I perform some clicks on the Button, which should NOT trigger any kind of re-render by the Form, however, it does so every time.

[Likely bug] Sending an updateField with {value:null} has no effect, and the field is not updated

Greetings.

I noticed another issue with this excellent module, which seems to be more a bug or an unintended side-effect rather than something working as intended.

using the updateField action creator with a payload containing a { value: null } results in the UPDATE_FIELD action being completely ignored by the reducer, and the value stays the same in the form store slice.

As far as I have seen, the culprit is here:
https://github.com/rofrischmann/react-controlled-form/blob/master/modules/model/reducers/updateField.js, at line 26, which uses:
https://github.com/rofrischmann/react-controlled-form/blob/master/modules/model/composeData.js, at line 13.

A brief word on the use case of sending an UPDATE_FIELD to reset the value to null:
I am using it to reset specific fields in the form.
For instance, a field with a mutable datatype (which might be either a number or a string). If i want to reset it to a 'pristine' status, then null seems to be the most correct option for its value.

At the moment I would need to send an updateField({ value: '' }), which however seems more forced and less semantically correct than having a nullable value property in the field data. After all, 0, '', false, null, undefined and so on all have slightly different meanings.

Migration help & request for better documentation for version 3+

Greetings,

I have seen the update of RCF, and tried to migrate today on a couple of more elaborate forms, however I am completely at loss on several parts.

  1. I don't seem to find in the documentation what are the supposed input parameters of the Form render function. (Its render props, if I am correct in the terminology)
    Field is documented as passing the same props as before. But what about Form?
    In the examples there are some clues, for instance:
    render={({ reset, data }) =>
    But is it all? Is there anything else? Does it pass down state as well? Does it follow the callback shape?

  2. How can I pass additional properties to my custom components?
    With version up to v2 it was extremely easy to pass them, given that the HoC asField passed them down the hierarchy. Now I'm at loss on what I am supposed to do
    For instance, let's suppose I had a custom input component which used among the props some extra ones 'customOne' and 'customTwo'.
    I could simply do:

const myField = (props) => {
 const { updateField, value, isEnabled, isValid, customOne, customTwo, ...everythingElse } = props;
// All the various functions, e.g: myChangeHandler
return (
<div>
 <MyCustomInput customOne={customOne}, onChange={myChangeHandler}>
 <MyValidatorMessage customTwo={customTwo}, isValid={isValid}>
</div>
)
}

Then it was very simple to simply call

const InputName = asField(myField);

And to render it as:

<InputName
  fieldId="name"
  customOne='100px'
  customTwo='Invalid Input!'
/>

Ok, how am I supposed to obtain the same results in version 3?
As far as I can see from the docs, Field does not act as a proxy and does not pass on any extra props received down the hierarchy.
(Basically, I'm trying to do what I could do CreateComponentWithProxy in react-fela, to use an example of another awesome repo of yours).

The only solution that came into mind was to create a wrapper for the RCF Form, pass to that wrapper any additional props, and have the RCF Form use them in its render function, but it looks like it could grow very quickly in terms of length if you have non-trivial forms with a dozen of fields.

Suggestions?

  1. Almost every time I try to do anything I run into an error, for instance, even if I try to render an empty form like:
<Form
	formId="anotherId"
	render={({ reset, data }) => (
		<form
			onSubmit={() => {
				alert(JSON.stringify(mapDataToValues(data), null, 2));
				reset()
			}}>
			<div
				className={this.props.className}
				>
				<h1>Simple Example</h1>
				<span>Whether I put a Field or not here I get the same results</span>
				<br />
				<button>Submit</button>
				<br />
			</div>
		</form>
	)}
/>

Then I get....

connectAdvanced.js:241 Uncaught TypeError: Cannot read property 'anotherId' of undefined
at Function.mapStateToProps [as mapToProps] (form.js:20)
at mapToPropsProxy (wrapMapToProps.js:43)
at Function.detectFactoryAndVerify (wrapMapToProps.js:52)
at mapToPropsProxy (wrapMapToProps.js:43)
at handleFirstCall (selectorFactory.js:26)
at pureFinalPropsSelector (selectorFactory.js:74)
at Object.runComponentSelector [as run] (connectAdvanced.js:26)
at Connect.initSelector (connectAdvanced.js:178)
at new Connect (connectAdvanced.js:119)
at constructClassInstance (react-dom.development.js:6355)
mapStateToProps @ form.js:20
mapToPropsProxy @ wrapMapToProps.js:43
detectFactoryAndVerify @ wrapMapToProps.js:52
mapToPropsProxy @ wrapMapToProps.js:43
handleFirstCall @ selectorFactory.js:26
pureFinalPropsSelector @ selectorFactory.js:74
runComponentSelector @ connectAdvanced.js:26
initSelector @ connectAdvanced.js:178
Connect(Form) @ connectAdvanced.js:119
constructClassInstance @ react-dom.development.js:6355
updateClassComponent @ react-dom.development.js:7839
beginWork @ react-dom.development.js:8225
performUnitOfWork @ react-dom.development.js:10224
workLoop @ react-dom.development.js:10288
callCallback @ react-dom.development.js:542
invokeGuardedCallbackDev @ react-dom.development.js:581
invokeGuardedCallback @ react-dom.development.js:438
renderRoot @ react-dom.development.js:10366
performWorkOnRoot @ react-dom.development.js:11014
performWork @ react-dom.development.js:10967
batchedUpdates @ react-dom.development.js:11086
batchedUpdates @ react-dom.development.js:2330
dispatchEvent @ react-dom.development.js:3421

Any ideas?

previousData for onChange

In order to improve updating performance, we could have the previousData passed to every onChange callback as well. It could be used to check if values have been changed since the last update.

Global validation - marking specific field as invalid

Hi guys,
I'm a little bit lost in validation and I need you advice.
Let's have two fields: given name and family name. I want to make given name required only if the family name is entered. I have global validation function that basically goes like this:

const validation = (data) => !data.familyName.valueu || data.givenName.value

The issue here is that givenName field doesn't get invalid, only the whole form do. What is the best way to achieve this behavior?

Add global form isValid to Redux store

Right now the global isValid for forms is only saved locally and passed down via context. The only component receiving it right is the asSubmit HOC.

We could put it into the Redux store, to be accessible for everyone.

defaultData-parameter for asField

Right now the only option to set initial field data other than the built-in defaults is by passing an initialFields to the

component.
We could accept a second parameter on the asField HOC to set default field data for all input fields created from that component.

TypeScript type definitions

Currently, this library is not usable with TypeScript. Does it make sense to add a definition file to this project? I'd be happy to work on a .d.ts file and send in a pull request.

Render-props APIs

As I am going to use the package pretty soon again, I want to be able to use simple render props APIs instead of HoCs all the time.

data field is returned undefined in the Form renderProps despite being initialized with the initialFields form props

Greetings! It's me again, please don't hate me too much :).

I discovered a somewhat weird behaviour that I think merits to be reported.
I found out that in a Form whose default field values are initialized through the initialFields props, the { data } property of its render props is undefined at first, even if the INIT_FORM action of RCF is called correctly with its proper payload, including the default form values.
It's only after all the INIT_FIELD actions are dispatched that the form is re-rendered with the expected data values in its render props.

I'll attach an image of the chrome console to better illustrate what I meant. (I put a console.log for the renderProps at the beginning of the Form render function)

2018-02-14_11-07-07_1

I discovered this when migrating some components that where enhanced with the old 'withData' HoC of the previous version, (resulting in a crash due to properties being unexpectedly undefined). So, for what it's worth, it seems to be a behaviour that wasn't present in the previous version of RCF.

It's not really a very serious bug, since it's easy to pass the very same default values used for initialization in the initialFields of the form as a backup if data is found to be undefined, but it seems a quirk resulting in something that's not really supposed to work this way.

Form State

Sometimes we want to set some kind of state for each form, e.g. a validation state which can then be used to display the correct values/messages.
Theoretically, we can do that using updateField with a new unused fieldId, but that makes the form data kind of dirty. A better solution would be a separate form state which also lives in the Redux store itself.

Opt-out preventDefault

Right now, every form automatically calls event.preventDefault on form submit to prevent any reload or redirect while using Enter to submit a form.
We could provide a property to disable auto prevention.

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.