Connecting the Dots

So once we have a solution for validating the input independent from the form itself, we will want to connect the validation with the form.

Our Higher Order Component needs access to the validation rules . Expanding on the basics, let’s see how we can tackle all the different situations where a validation might occur, without locking the user from being able to define specific behaviours when needed.

We need to rethink our initial approach and enable user land to define and name specific functions as needed while keeping the state handling inside the higher order component.

What if we enable to override existing defaults when calling our form library? Enabling to define every single action upfront and then passing these functions to the wrapped component enables us to provide sane defaults that can be easily overridden.

const createForm = ({

// define functions like onChange, validate etc.

}) => {

// define and return class

}

Let’s implement a basic variant of that idea. All we want to do is update the state, when needed, at first.

const createForm = ({

mapSetStateToProps = (updateState, actions) => ({

onChange: e => {

const { name, value } = getValueName(e)

return updateState(actions.update(name, value))

},

}),

actions = {

update: (name, value, state) => {

return [assocPath(['values', name], value, state)]

},

}

}) => Component => {}

Ok, so if you glance over the example, we can see that we defined a mapSetStateToProps function and an actions object. Like the name implies we are defining functions available to the wrapped component. By receiving an update function (think of an extended setState ) and actions we are able to define specific functions and trigger actions corresponding to any events triggered inside the wrapped form. The actions handle common tasks like update or validate. What actions do is calculate the new state and return a next state and a callback tuple. The callback can be fired when the setState callback is fired, useful when we want to do actions after the user has submitted any actions.

So any function defined in mapSetStateToProps takes care of calling the correct action and returning the results back to the passed in updateState function. Our actions only calculate the state. Separating the actual calculation from the specific action opens up a number of interesting opportunities as we will see.

But to get this refactored higher order component to work, we will need to find away on how to connect the mapStateToProps to the actual component.

const createForm = ({

mapSetStateToProps,

actions,

}) => Component => {

return class HigherOrderFormComponent extends React.Component {

constructor(props) {

super(props)

this.state = { values:props.values }

this.actions = R.map(

f => (...args) => f(...args, this.state),

actions

)

}



updateState = (setState) => {

const [setStateFn, cb = () => {}] = setState

this.setState(setStateFn, () => cb(this.state))

}



render() {

const dispatchers =

mapSetStateToProps(this.updateState, this.actions)

return React.createElement(Component, {

...this.props,

...dispatchers,

state: this.state.values,

})

}

}

}

There is not really too much we need, to connect the actions with the actual component. Inside the constructor we map over the actions and wrap those inside another function which then passes in the actual arguments as well as the current state on to the action.

The updateState method destructs the passed in tuple to nextState and callback and then calls setState and passes in that defined nextState.

So we should have a running form again, what’s left is to pass in form values.

const enhanceForm = createForm({})

const EnhancedForm = enhanceForm(Form)



<EnhancedForm values={values} />

You can find a working example here.

Validation

Once we have validation rules defined, we can run these against the actual form state and keep track of any errors via local state, which we can pass down to the wrapped component again. But we know for a fact that the validation itself can be detached from the actual field value update, i.e. validating oBlur . Let’s see how this would work by writing some actual code.

First off we will add a new method validate to our existing mapSetStateToProps function as well as the corresponding action , which receives a name and a value and runs the validation against the corresponding spec for that given name.

If you recall we passed in the current state to the defined action functions. Let’s extend actions to also receive an object containing the component’s props as well as the defined validate function. But where is the validate function defined actually?

So, let’s extend the configuration object to also accept a validate property. Our validate is an object containing two functions, one for validating single fields, the other for validating all fields.

validateFns = {

all: (data) =>

spected(basicValidationRules, data),

input: (name, value) =>

spected(

pick([name], basicValidationRules), {[name]: value}

)

}

Now we can pass the specific validation via the config object.

createForm({ validate: validateFns })

And the our higher order component might look like this now.

const createForm = ({

validate,

mapSetStateToProps = (updateState, actions) => ({

// ...

validate: e => {

const { name, value } = getValueName(e)

return updateState(actions.validate(name, value))

},

// ...

}),

actions: {

validate: (name, value, state, {validate}) => {

return [

R.assoc('errors', validate.input(name, value), state)

]

},

}

}

The returned result for running the validate input function is an object consisting of the field name and an array of error messages in our case.

{ firstName: ['First Name is required'] } // in case of an error

{ firstName: [] } // in case of success

Then we merge the returned result with the current error state and update the actual state. So there is not too much involved in handling field validations on a form level. A second method validateAll as the name implies will validate all form values, i.e. when validating after submitting the form as opposed to dynamic onChange or onBlur validations.

validateAll: (cbFn, state, { validate }) => {

return [

assoc('errors', validate.all(state.values), state),

(state) => {

if (isValid(state.errors)) {

cbFn(state.values)

}

}

]

}

You might have noticed that we’re returning a tuple this time. The nextState as well as a callback that should run when React’s setState has finished. The callback should fire when our form is valid and we want to call a passed in function that passes up the form values up the tree again.

Finally let’s add an onSubmit prop, so we can run the validations and pass up the values.

mapSetStateToProps = (updateState, actions) => ({

// ...

onSubmit: (onsSubmitFn) => {

return updateState(actions.validateAll(onsSubmitFn))

}

}),

So what’s still missing? What about if we wanted to update the local field state and validate at the same time. The same principle applies we define the prop and a corresponding action. Here is the code for our implementation.

There are still a number of possible optimization regarding the current code, but it should give an overview of where this is heading. By doing the minimal work, of keeping state of values and errors, we open up the possibilities for user land to define the specific actions as needed.

You can also checkout the demo for our current example.

What’s Next?

In part 2 we will focus on asynchronous actions, how to debounce and how to react to the current form state, i.e. switching to dynamic inline validation as soon as the form has been submitted for the very first time etc.