Skip to content

Instantly share code, notes, and snippets.

@frontsideair
Created March 12, 2018 11:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save frontsideair/67b3b93df9d8796b94388837c0143b6e to your computer and use it in GitHub Desktop.
Save frontsideair/67b3b93df9d8796b94388837c0143b6e to your computer and use it in GitHub Desktop.
Type-safe form validation
// @flow
import React, { Component } from "react"
import { isIP, isURL, isEmail, isPort } from "validator"
import { allValid, getErrors } from "./validation"
import { makeAPICall } from "./api"
// a flat data structure for viewing in the UI
type Inputs = {
hasAuth: boolean,
username: string,
password: string
}
// a nested data structure received from and sent to the API
type Auth = {
username: string,
password: string,
} | null
function fromInputs({ hasAuth, username, password }: Inputs): Auth {
return hasAuth ? { username, password } : null
}
function toInputs(auth: Auth): Inputs {
return auth === null ? { username: "", password: "" } : auth
}
type Props = {
auth: Auth
}
type State = {
inputs: Inputs
}
class Form extends Component<Props, State> {
state = {
inputs: toInputs(this.props.auth)
}
handleAuthChange = e => {}
handleUsernameChange = e => {}
handlePasswordChange = e => {}
render() {
const { hasAuth, username, password } = this.state.inputs
const validators = {
username: username => (hasAuth && username === "") ? "Username can't be empty" : null,
password: password => (hasAuth && password.length < 6) ? "Password must be at least 6 characters" : null
}
const errors = getErrors(this.state.inputs, validators)
return (
<form onSubmit={e => makeAPICall(fromInputs(this.state.inputs))}>
<input type="checkbox" checked={hasAuth} onChange={this.handleAuthChange} />
<input type="text" value={username} onChange={this.handleUsernameChange} disabled={!hasAuth} />
<input type="password" value={password} onChange={this.handlePasswordChange} disabled={!hasAuth} />
<button type="submit" disabled={!allValid(errors)}>Submit</button>
</form>
)
}
}
// @flow
import { evolve, pick, keys } from "ramda"
type Err = null | string
type Errors<T> = { [$Keys<T>]: Err }
type Validator = string => Err
type Validators<T> = { [$Keys<T>]: Validator }
export function allValid<T>(errors: Errors<T>): boolean {
return Object.values(errors).every(err => err === null)
}
export function getErrors<T>(state: T, validators: Validators<T>): Errors<T> {
const interestedState = pick(keys(validators), state)
return evolve(validators, interestedState)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment