Giter VIP home page Giter VIP logo

react-reformed's Introduction

React Reformed

Tiny form bindings for React so you can stop storing your form data in local component state. This higher-order component wraps your component and, through props, injects form data (a model) and simple bindings to update that model, giving you the opportunity to utilize composition in a middleware-like approach to accomplish more advanced functionality without bloating the form component itself.

There is no framework here, it's about 75 lines of code that you could write yourself in a few minutes. The code is not what is important. My goal is to encourage developers to stop using local state in their forms, and do so without locking themselves into a prescriptive, and potentially monolithic, form library. There are some really cool things you can do with simple composition, and this project is an attempt to shine some light on those alternative approaches.

This library does not concern itself with submission, validation, or anything of that sort (though there are demos to show how these can be done) -- it's just a simple read/write API, and isn't even all that specific to forms. Everybody's forms are different, and I'm not smart enough to create a universal abstraction. As such, over time, I've found it easy to encapsulate the core logic of a form (setting properties on a model) in a single component and leverage composition to perform more intricate functionality.

Check out a demo here.

Table of Contents

  1. Rationale
  2. Demo
  3. Usage
  4. Examples
  5. API Documentation

Rationale

Controlled components are great, they allow you to perform realtime validation, transform user inputs on the fly, track changes over time, and generally improve developer and user experience. However, often times controlled components lead to a proliferation of local component state. You begin tracking validation states (should we be validating yet? Are we performing asynchronous operations?), submission states, and more. Before you know it, a small form component is a sizeable tangle of local state. What happens when something else needs access to that state? How do you incorporate shared validation logic?

Some libraries solve this with refs, others offer bindings onto local state, and still more tout a Comprehensive Form Solution with their own components and validation systems, but none of them feel right. Either they do too much, are too prescriptive, or just get in the way. Chances are, at some point these libraries will no longer fit your use case, forcing you to fight against rather than work with them. That is why this project is more an appeal to a different way of thinking than a complete form library.

With the approach offered here, because everything important to your form now lives in props, you can easily:

  • Eliminate or reduce local component state
  • Compose higher-order components as "middleware" (e.g. form validation)
  • Serialize and rehydrate form state
  • Replay changes to your model over time
  • Change what the injected model setters do, without changing the form itself
  • Spy or stub all setModel/setProperty/etc. calls in testing
  • Avoid becoming locked into a specific framework

Most importantly, this approach allows for a pluggable/composeable ecosystem, rather than a One Solution To Rule Them All (but will soon change) approach.

Demo

If you want to play around with this in a sandbox, there's a working demo in the repo. It's the same one hosted here. Get your own copy by doing the following:

git clone [email protected]:davezuko/react-reformed    # Get that thing!
cd react-reformed                                   # Get in there!
npm i                                               # Install its things!
npm start                                           # Start the thing!

# You can now find the app at http://localhost:8080

Usage

npm i --save react-reformed

Then just import it and wrap your form component:

import reformed from 'react-reformed'

// Here's an example of a form that closely resembles basic form implementations
// that rely on `this.state`, but uses the form bindings instead.
class MyForm extends React.Component {
  _onSubmit = (e) => {
    e.preventDefault()
    this.props.onSubmit(this.props.model)
  }

  // This method is essentially just `this.props.bindToChangeEvent`,
  // which is provided by the reformed wrapper. We're just demoing
  // `setProperty` for clarity in the first example.
  _onChangeInput = (e) => {
    // `setProperty` is injected by reformed
    this.props.setProperty(e.target.name, e.target.value)
  }

  render () {
    // model is injected by reformed
    const { model } = this.props

    return (
      <form onSubmit={this._onSubmit}>
        <input name='firstName' value={model.firstName} onChange={this._onChangeInput} />
        <input name='lastName' value={model.lastName} onChange={this._onChangeInput} />
        <input name='dob' type='date' value={model.dob} onChange={this._onChangeInput} />
        <button type='submit'>Submit</button>
      </form>
    )
  }
}

// Wrap your form in the higher-order component
export default reformed()(MyForm)

You can also grab some of the example higher-order components via the npm package. These are just for demonstration, so they are not included in the main export. Use them just to give yourself some ideas!

import compose from 'react-reformed/lib/compose'
import syncWith from 'react-reformed/lib/syncWith'
import validate from 'react-reformed/lib/validate'
import validateSchema from 'react-reformed/lib/validateSchema'

Examples

Fast Prototypes

This library provides some simple bindings to speed up form creation. These are for convenience only; you can get by with just setProperty and setModel if you'd like. I encourage you to write your own abstractions over these core setters.

import reformed from 'react-reformed'

// "model" and "bindInput" both come from reformed
const MyForm = ({ bindInput }) => {
  <form onSubmit={/* ... */}>
    <input type='text' {...bindInput('name')} />
    <input type='date' {...bindInput('dob')} />
    <textarea {...bindInput('bio')} />
    <button type='submit'>Submit</button>
  </form>
)

const MyFormContainer = reformed()(MyForm)

// Then, use it just like you would in any other component
class Main extends React.Component {
  _onSubmit = (model) => {
    // do something...
  }

  render () {
    return (
      <MyFormContainer
        initialModel={{ firstName: 'Michael', lastName: 'Scott' }} // provide an initial model if you want
        submit={this._onSubmit}
      />
    )
  }
}

Advanced Uses

Here are just some ideas to get you thinking about what's possible when you stop isolating form data in local state and allow it to flow through props. These example are not necessarily production-ready, as they are simply meant as conceptual demonstrations.

Form Validation

This is an ultra-simple higher-order component for synchronous form validation. It is in no way specific to this library, all it does is expected a model prop and apply additional isValid and validationErrors props based on how the model conforms to the validation rules.

How It Might Look

compose(
  reformed(),
  validate([
    isRequired('firstName'),
    isRequired('lastName'),
    mustBeAtLeast('age', 18)
  ])
)(YourFormComponent)

You could totally go above and beyond and implement something like this:

compose(
  reformed(),
  validateSchema({
    firstName: {
      type: 'string',
      required: true
    },
    lastName: {
      type: 'string',
      required: true
    },
    age: {
      test: (value, fail) => {
        if (!value || value <= 18) {
          return fail('Age must be 18 or older');
        }
      }
    }
  })
)(YourFormComponent)

And no matter what you choose to do, your form component never changes.

Example Implementation

// treats `rules` as a tuple of [validator: Function, validationError: string]
// `validationError` could easily be a function, if you wanted, for more
// advanced error messages.
const validate = (rules) => (WrappedComponent) => {
  const getValidationErrors = (model) => rules.reduce((errors, [rule, err]) => {
    return !rule(model) ? errors.concat(err) : errors
  }, [])

  return (props) => {
    const validationErrors = getValidationErrors(props.model)

    return React.createElement(WrappedComponent, {
      ...props,
      isValid: !validationErrors.length,
      validationErrors,
    })
  }
}

const isRequired = (prop) => ([
  (model) => !!model[prop],
  `${prop} is a required field.`
])
const mustBeAtLeast = (prop, val) => ([
  (model) => model[prop] >= val,
  `${prop} must be at least ${val}`
])

Tracking Changes

The model is never mutated, so it's easy to check when it's been changed.

How It Might Look

compose(
  reformed(),
  tracker
)(YourFormComponent)

Example Implementation

// You could easily expand this to implement time travel
const tracker = (WrappedComponent) => {
  class Tracker extends React.Component {
    constructor (props, ctx) {
      super(props, ctx)
      this.state = {
        history: [],
      }
    }

    componentWillMount () {
      if (this.props.model) {
        this.addToHistory(this.props.model)
      }
    }

    componentWillReceiveProps (nextProps) {
      if (this.props.model !== nextProps.model) {
        this.addToHistory(nextProps.model)
      }
    }

    addToHistory = (model) => {
      this.setState({ history: this.state.history.concat(model) })
    }

    render () {
      return React.createElement(WrappedComponent, {
        ...this.props,
        history: this.state.history,
      })
    }
  }
  return Tracker
}

With Redux

How It Might Look

// Easily set initial form state from your redux store...
// and bind a submission handler while you're at it.
compose(
  connect(
    (state) => ({ initialModel: state.forms.myForm.cachedModel }),
    { onSubmit: mySubmitFunction }
  ),
  reformed()
)(YourFormComponent)

If you want to persist your form in Redux over time, you don't even need reformed. By following its pattern of simple model setters, you can just fulfill the same interface with connect and have a redux-ified form without needing a redux-specific form library (or any other framework/implementation). The best part is, if you switch to something else down the road you won't need to unreduxify the form, just its container.

connect(
  (state) => ({ model: state.form.myForm.model }),
  (dispatch) => ({
    setProperty: (prop, value) => dispatch(setFormProperty('myForm', prop, value)),
    // etc...
  })
)(YourFormComponent)

Local Storage

This, again, is a simplified example. You could very easily implement a debounce or throttle function to limit how often the data is written to local storage.

How It Might Look

compose(
  reformed(),
  syncAs('my-form-state')
)(YourFormComponent)

Example Implementation

const syncAs = (storageKey) => (WrappedComponent) => {
  class SyncedComponent extends React.Component {
    componentWillMount () {
      const fromStorage = localStorage.getItem(storageKey)
      if (fromStorage) {
        // probably want to try/catch this in a real app
        this.props.setModel(JSON.parse(fromStorage))
      }
    }

    componentWillReceiveProps (nextProps) {
      if (this.props.model !== nextProps.model) {
        localStorage.setItem(storageKey, JSON.stringify(nextProps.model))
      }
    }

    // or, maybe you only want to sync when the component is unmounted
    componentWillUnmount () {
      localStorage.setItem(storageKey, JSON.stringify(this.props.model))
    }

    render () {
      return React.createElement(WrappedComponent, this.props)
    }
  }
  // hoist statics, wrap name, etc.
  return SyncedComponent
}

Storage - Abstracted

Notice anything interesting about the local storage example? It's not at all specific to local storage...

How It Might Look

compose(
  reformed(),
  syncWith(
    'my-form',
    (key) => JSON.parse(localStorage.getItem(key)),
    (key, value) => localStorage.setItem(key, JSON.stringify(value))
  )
)(MyFormComponent)

Example Implementation

const syncWith = (key, get, set) => (WrappedComponent) => {
  class SyncedComponent extends React.Component {
    componentWillMount () {
      const fromStorage = get(key, this.props)
      if (fromStorage) {
        this.props.setModel(fromStorage)
      }
    }

    // When we call `set` we can provide the current props as a
    // third argument. This would be useful, for example, with other
    // higher-order components such as react-redux.
    componentWillReceiveProps (nextProps) {
      if (this.props.model !== nextProps.model) {
        set(key, nextProps.model, nextProps)
      }
    }

    render () {
      return React.createElement(WrappedComponent, this.props)
    }
  }
  // ...
  return SyncedComponent
}

API Documentation

reformed : (Props -> Props) -> ReactComponent -> ReactComponent

Wraps a React component and injects the form model and setters for that model. You can optionally pass in a function to the first reformed call that will transform the props that reformed applies to the wrapped component. This is really just for experimentation and to keep the API open for the future.

Example:

class YourForm extends React.Component {
  /* ... */
}

reformed()(YourForm)

setProperty : (String k, v) -> {k:v}

Injected by the reformed higher order component. Allows you to set a specific property on the model.

Example:

this.props.setProperty('firstName', 'Billy')

setModel : {k:v} -> {k:v}

Injected by the reformed higher order component. Allows you to completely override the model.

Example:

this.props.setModel({
  firstName: 'Bob',
  lastName: 'Loblaw'
})

bindInput : String k -> Props

Injected by the reformed higher order component. Applies name, value, and onChange properties to the input element. Because this does not have a ref to your component or know anything other than the name of the model property, it cannot handle every possible scenario. As such, this should mostly just be used for simple text inputs where the event target's name and value can be used to update the property.

When other use cases arise, it's recommended to just use setProperty or setModel directly or extend reformed to provide the bindings you need.

Example:

<input {...this.props.bindInput('firstName') />

react-reformed's People

Contributors

branweb1 avatar dylansmith avatar joekrill avatar lewie9021 avatar nytr0gen 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  avatar  avatar

react-reformed's Issues

UMD release

Hi there, any plan to publish a UMD release?

How to implement isSubmiting

Hello,
First of all I want to thank you for such a cool library.
I am still trying to wrap my head around it so I was wondering if you could give me advice how would you go about passing isSubmiting state into form from HOC with asumption you will always have onSubmit prop. I would like to keep my LoginForm stateless

Code example of what I am trying to achieve

export default compose(
  connect(
    state => ({
      initialModel: {...}
    }), {
      onSubmit: login
    }
  ),
  reformed(),
  isSubmiting()
)(LoginForm)

const LoginForm = ({
  model,
  bindInput,
  onSubmit,
  isSubmiting
}) => (
  <form onSubmit={onSubmit} >
    <input
      name='username'
      {...bindInput('username'))}
    />
    <Button
      value={isSubmiting
        ? 'Click me'
        : 'I am submiting now'}
    />
  </form>
)

Question regarding patterns with autofill and server rendering

As explained pretty well in this article, autofill creates a few problems with server-side rendering, because the change event happens before React finishes initialising. So we end up back on the world of having to use refs and reading the DOM when mounting the form. I'm wondering if you have any preferred patterns for this.

How to implement "middleware"?

I would sort of expect this to work, but it doesn't. Any idea what I might be doing wrong?

const middleware = (props) => {
  const { setModel, setProperty, ...xProps } = props
  const interceptedSetModel = (model)=> {
    console.log(model)
    return setModel(model)
  }
  const interceptedSetProperty = (name,value) => {
    console.log(value)
    return setProperty(name,value)
  }
  return { setModel: interceptedSetModel, setProperty: interceptedSetProperty, ...xProps }
}

const SimpleForm = props => {
  const { bindInput } = props
  return (
    <form>
      <Input {...bindInput('example')} />
    </form>
  )
}

const ReformedForm = reformed(middleware)(SimpleForm)

ReactDOM.render(<ReformedForm />, document.getElementById('app'))

There are no (expected) logs when the model inside the reformed form changes. I'm really stumped...

Forms without initial model cause React warning message

Hi,
Great work on this library! I noticed a minor issue that I wanted to get your input on (no pun intended):

If you don't use an initial model for a form, React with display this error message once the onChange event is triggered for the first time:

Warning: OtherForm is changing an uncontrolled input of type text to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://fb.me/react-controlled-components

Apparently this is caused by React treating any inputs with a value of null or undefined as uncontrolled. Typing in the input updates the model, which sets a new value on the input, converting it to controlled.

This warning is easily avoided by just using an initial model or by doing something like value={model.firstName || ''}. But perhaps bindInput could be updated to do this by default, something like this:

bindInput = (name) => {
      return {
        name,
        value: this.state.model[name] || '',
        onChange: this.bindToChangeEvent,
      }
    }

Happy to open a PR if you think this is good idea.

Validation should take keep track of dirtyness

Was implementing this myself when I decided to do a quick check if it wasn't already done, great work.

But it's kinda ugly to show all those error messages to a user when they haven't even typed a single character yet.
So either for schema validation put a isDirty flag on the field data right next to isValid or don't even validate if model[field] === initialModel[field]

Deprecation warning about PropTypes

I am getting this:

Warning: Accessing PropTypes via the main React package is deprecated. Use the prop-types package from npm instead.

React version: 15.5.4

Question regarding this vs. redux-form

I apologize for posting a question in the issues but I was evaluating using reformed vs redux-form today. I saw that redux-form is at v6 now and the migration guide Inversion of Control section specifically states that one of the issues with v5 and below was that every single keypress that changed a form value caused a complete re-render of the form which could lead to performance issues on larger forms.

I started to implement reformed and right away realized thats what was happening here as well. Having read the redux-form portion makes me cautious about moving forward with reformed but I want to make a somewhat informed decision on this and not just read what one module says as gospel.

Could you talk a bit about the effect reformed would have on more complex forms and if I'd be pigeon-holing myself into performance issues down the line? I don't foresee having huge forms in my app but you never know what comes in the future.

Question regarding Complex validation based on state of the model

Hi there,

The library is amazing and has taught me great deal of information. I am very new to react and I have come across a challenging validation problem. My validation is dependent on the state of the model. For example if a specific select option is selected a text field will appear and that's when the text field becomes required.

I'm still trying to get my head around HOCs so it's not very clear if they are indeed useful in my case (I have a strong feeling they are). Particularly I wanna be able to dynamically place different input elements on the form depending on the current state of the model (show/hide elements). This requires me to be able to remove and add properties to my model (which the reformed HOC does).

What happens in cases where validation must be applied on submit as oppose to synchronous validation. How can HOC solve such problems? I've been looking into the source code for a few hours and I mostly understand what's happening and the solution is great for simple forms. When complexities like dynamic field rendering and validation, I'm not sure how you can abstract these logics with the use of HOC. Any insight would be greatly appreciated.

How about just intercepting setModel?

OK, I was a bit too fast in stating that 2.0.0 fixed my issue. My goal was to intercept setModel, not setProperty and it's still impossible to override. Now that I understand what is happening, it's easy to see why: if you don't override setProperty, it will still be "locked in" with the original setModel function.

This for example will not work.

const middleware = function(props) {
  const { setModel, ...xProps } = props
  const interceptedSetModel = (model)=> {
    console.log("Intercepting model")
    console.log(model)
    return setModel(model)
  }
  return { setModel: interceptedSetModel, ...xProps }
}

const SimpleForm = props => {
  const { bindInput } = props
  return (
    <Form>
      <Input {...bindInput('example')} />
    </Form>
  )
}

const ReformedForm = reformed(middleware)(SimpleForm)

Now, you may begin to wonder why we would want to use that?

Let's say we're creating a dynamic form that has "repeatable" parts. The output we are aiming for is an array like this:

output = [
  {
     name: "foo",
     value: "hello"
  },
  {
     name: "bar",
     value: "world"
  },
]

If I can intercept setModel, I can easily store the state of the entire "form" into an array inside of a higher order state object. We could perhaps do this with setProperty as well, but I'm betting this will break bindInput, etc because the internal state of the "lower order" reformed component will not be updated anymore...

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.