bottoms-up-react-form
v0.0.17
Published
A bottom-up approach to forms in React. Input fields do most of the work.
Downloads
4
Readme
Introduction
This package helps with building simple to complex forms in React.
Packages like Formik keep all form logic at a central place and prefer to keep input fields as stateless "dumb" presentational components.
Inspired by HTML inputs, Bottoms-Up-React-Forms shares the responsibility between the input fields and form.
The input components take care of validations and visualization, group components maintain the data structure and
logic that involves multiple input and group components. This allows for easy extendable and readable code as your forms
approach a plug and play structure.
The name Bottoms-Up-React-Forms emphasises on a bottom-up forms architecture that starts from complete standalone input components.
Each of these input components can be plugged into any group component, creating a new standalone system. If the need is there then each of these
input and group component can be grouped into another even bigger group and so on. All of this without any need to change or copy-paste the existing
components' logic. At the end of the day you'll get that feeling that you created something great and really deserve a nice drink :beer: :tropical_drink:
Installation
npm i -s bottoms-up-react-forms
Examples
Please clone the project and execute this in the command line: npm install npm run storybook
Just a form with first name/last name/array of emails/array of addresses (street, city, country)
class InputText extends PureComponent {
render() {
return <InputManager {...{...this.props}}>
{(input) => {
const {error, touched, focus, value, onChange, onFocus, onBlur, label, type} = input;
const showError = error && touched && !focus;
return <div {...{className: styles.input}}>
<label {...{className: styles.label}}>{label}
<input {...{
type,
value: value || '',
onChange: (event) => onChange(event.target.value),
onFocus,
onBlur
}}/>
{showError ? <label {...{className: styles.error}}>{error}</label> : null}
</label>
</div>
}}
</InputManager>;
}
}
class UserProfile extends Component {
state = {
value: initialValue(),
initialValue: initialValue()
};
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
onChange(newFormState) {
this.setState(newFormState);
}
render() {
const {onChange, state} = this;
const {value, initialValue} = state;
return <InputGroupManager {...{onChange, value, initialValue}}>
{(form) => {
return <Fragment>
<InputText {...{label: "First name", ...form.helpers.buildProps({name: "firstName"})}}/>
<InputText {...{label: "Last name", ...form.helpers.buildProps({name: "lastName"})}}/>
<InputGroupManager {...{valueType: "array", ...form.helpers.buildProps({name: "emailAddresses"})}}>
{(emails) => <Fragment>
{emails.value.map((email, index) => {
return <InputText {...{label: "Email", ...emails.helpers.buildProps({index})}}/>;
})}
<Button {...{label: "Add Email", onClick: emails.helpers.array.add}}/>
</Fragment>}
</InputGroupManager>
<InputGroupManager {...{valueType: "array", ...form.helpers.buildProps({name: "addresses"})}}>
{(addresses) => <Fragment>
<h5>My addresses</h5>
{addresses.value.map((address, index) => {
return <InputGroupManager {...{...addresses.helpers.buildProps({index})}}>
{(address) => <Fragment>
<InputText {...{label: "Street", ...address.helpers.buildProps({name: "street"})}}/>
<InputText {...{label: "City", ...address.helpers.buildProps({name: "city"})}}/>
<InputText {...{label: "Country", ...address.helpers.buildProps({name: "country"})}}/>
</Fragment>}
</InputGroupManager>;
})}
<Button {...{label: "Add address", onClick: addresses.helpers.array.add}}/>
</Fragment>}
</InputGroupManager>
</Fragment>;
}}
</InputGroupManager>;
}
}
Documentation
Bottoms-Up-React-Forms exposes 2 components: a InputGroupManager that takes care of the group responsibilities, and a InputManager assists with the input logic. The InputManager is designed to only act as a child of a group, whereas the InputGroupManager can act as a child and as a parent of other input/group components.
To get to this plug and play structure, both the group and input components support the following props:
- name: to identify the child component when it notifies a static group parent.
- index: to identify the child component when it notifies a dynamic group parent.
- key: to support React in mapping the Virtual DOM component to the Javascript DOM. Should never change during the lifetime of the component.
- value: the data source that can be mutated by input components.
- initialValue: the value used to track if the value changed (is dirty).
- onChange: the primary feedback channel from child to parent. Always exposes
the following information about the child whenever something changes:
- name: as provided by the parent
- index: as provided by the parent
- value: the most recent value of the input component or values of the group component
- valid: a boolean value that is true when the input or group is in a valid state
- error: false when the component is valid or information why it isn't valid.
- dirty: a boolean value that indicates whether the input or group has changes (value !== initialValue)
- touched: a boolean value that is true after the first onFocus event is triggered.
- focus: a boolean value that is true when the onFocus event is triggered and becomes false onBlur.
React components
InputGroupManager
Combines and manages inputs. Wraps around InputManager components or other InputGroupManager.
- Accepts props:
- Group: React Component that will be rendered. Higher priority than "children"
- Will receive the props that this InputGroupManager received
- Excluding: Group, reducer, name, index, valueType, debug, children, onChange
- Also receive these props:
- onChange(childState):
- Should be passed directly by a InputManager or InputGroupManager child.
- helpers: Some additional functions that assist with repetitive tasks
- array: null OR exposes functions. E.g:
- add(childState)
- delete(index)
- deleteAll()
- moveTo(fromIndex, toIndex)
- swap(index1, index2)
- key(index)
- buildProps({name: any, index: number, keyGenerator: function}): returns the most common props for a child. E.g.:
- value
- initialValue
- key
- index
- name
- onChange
- array: null OR exposes functions. E.g:
- onChange(childState):
- Will receive the props that this InputGroupManager received
- children: Function that returns a component.
- Lower priority than "Group"
- Executed with the the same values as Group
- name: any value
- Will be passed to the reducer to build the onChange notification
- index: number
- Will be passed to the reducer to build the onChange notification
- initialValue: Object or Array
- Optional, required to decide if the value of a child is dirty
- Should have the same structure as "value"
- value: Object or Array
- Describes the values of the children. Will be passed through.
- onChange: (:reducer-result) => void
- optional, required for change notifications
- reducer: ({name, childStates, initialValue}) => any
- optional, combines the states of the children and notifies the parent
- default: ({name, childStates, initialValue}) => {name, value, valid, focus, touched}
- valueType: 'array' or 'object'. Optional when the type can be clearly derived from the "value"
- debug: enables console logs on certain events.
- Group: React Component that will be rendered. Higher priority than "children"
InputManager
Keeps the state of a single input value. Listens to value changes of children and parents and notifies the other if a change happened. Handles validations. Handles dirty, focus, touched logic.
- newState/prevState object structure:
- dirty: boolean
- = !isEqual(this.props.initialValue, newValue)
- focus: boolean
- true after child triggers onFocus, false after child triggers onBlur
- touched: boolean
- true after first onFocus, never false after that
- value: any
- the value after the child triggers onChange(value)
- valid: boolean
- provided by this.props.validator
- error: any
- provided by this.props.validator
- name: any
- = this.props.name
- index: any
- = this.props.index
- dirty: boolean
- Accepts props:
- Input: React Component that will be rendered.
- name: any
- index: any
- initialValue: any
- validator: (newValue: any, this.props, prevState) => {valid: boolean, error: any}
- value: any
- onChange: (newState) => void
- Provides to this.props.Input/children:
- this.props
- ...newState
- onChange: (value) => void
- onFocus: () => void
- onBlur: () => void
Helper classes
FormResetter
Helps with resetting everything in the form by unmounting and remounting the whole form using the React property "key".
Add this to the constructor of your top form component:
this.formResetter = new FormResetter(this);
Spread the results of this.formResetter.toProps() to the props of the top <InputGroupManager>:
<InputGroupManager {...{...this.formResetter.toProps()}}>
Validators
Provides some functions that help with building the "validator" property for the InputManager. Note that the order matters sometimes. E.g. required() should be added before minimum(x) because minimum Common example: validator = validatorBuilder(minimum(3), maximum(50)). Note that the example chains multiple validators, keep in mind that the order is important when you do this (e.g. when combining required and minimum).
validatorBuilder(error: any) => {error: any, valid: boolean}
Uses the error variable to decide if the value is valid as follows: valid = !error.
required() => (value) => error: "required"|false
Given a value, the response will be the string "required" if !value else false.
minimum(minLength) => (value) => error: "minimum"|false
Given a value, the response will be the string "minimum" if value.length >= minLength else false.
maximum(maxLength) => (value) => error: "maximum"|false
Given a value, the response will be the string "maximum" if value.length <= maxLength else false.
password(pattern) => (value) => error: "password"|false
Given a value, the response will be the string "password" if pattern.test(value) else false.
passwordStrength(patternMedium, patternStrong) => (value) => error: "weakPassword"|"mediumPassword"|false
Given a value, the response will be false if patternStrong.test(value) else mediumPassword if patternMedium.test(value) else weakPassword.