Created
April 27, 2018 21:35
-
-
Save zgotsch/2a6a7ef5b4e529b63b9b5de387cb7a81 to your computer and use it in GitHub Desktop.
formula-one example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from "react"; | |
import Form, {FormErrors, FormError, FeedbackStrategy} from "formula-one"; | |
import {Draft, Person, Iso, Faction, Ship} from "../types"; | |
import Select from "./Select"; | |
function validateName(name: string | null): null | string { | |
if (name === null || name === "") { | |
return "Name is required"; | |
} | |
if (name.match(/^\s+$/)) { | |
return "Name cannot be all space characters"; | |
} | |
return null; | |
} | |
function validateAge(age: number | null): null | string { | |
if (age === null) { | |
return "Age is required"; | |
} | |
if (age < 0) { | |
return "Age can't be negative"; | |
} | |
return null; | |
} | |
function validateOnlyYodaAgeLimit( | |
draft: Draft<Person> | |
): Array<FormError<Draft<Person>>> { | |
const errors: Array<FormError<Draft<Person>>> = []; | |
if (draft.age && draft.age >= 120 && draft.name !== "Yoda") { | |
errors.push({ | |
message: "Age can't be more than 120", | |
fields: ["age"], | |
}); | |
} | |
return errors; | |
} | |
function emptyPerson(): Draft<Person> { | |
return { | |
id: null, | |
name: null, | |
age: null, | |
faction: null, | |
favoriteShip: null, | |
}; | |
} | |
const identity: <T>(x: T) => T = x => x; | |
const factionIso: Iso<Faction, string> = { | |
to: identity, | |
from: (s: string) => { | |
if (s === "REBEL") { | |
return "REBEL"; | |
} else if (s === "IMPERIAL") { | |
return "IMPERIAL"; | |
} else { | |
throw new Error( | |
"got an unexpected value when converting string to faction: " + s | |
); | |
} | |
}, | |
}; | |
const shipIso: Iso<Ship, string> = { | |
to: identity, | |
from: (s: string) => { | |
if (s === "X_WING") { | |
return "X_WING"; | |
} else if (s === "TIE_FIGHTER") { | |
return "TIE_FIGHTER"; | |
} else if (s === "STAR_DESTROYER") { | |
return "STAR_DESTROYER"; | |
} else if (s === "CORELLIAN_CORVETTE") { | |
return "CORELLIAN_CORVETTE"; | |
} else { | |
throw new Error( | |
"got an unexpected value when converting string to ship: " + s | |
); | |
} | |
}, | |
}; | |
interface StringInputProps { | |
error: boolean; | |
value: string; | |
onChange: (newValue: string) => void; | |
onBlur: () => void; | |
} | |
class StringInput extends React.Component<StringInputProps> { | |
handleChange = (e: React.FormEvent<HTMLInputElement>) => { | |
return this.props.onChange(e.currentTarget.value); | |
}; | |
render() { | |
return ( | |
<input | |
className={this.props.error ? "error" : ""} | |
type="text" | |
onChange={this.handleChange} | |
onBlur={this.props.onBlur} | |
value={this.props.value} | |
/> | |
); | |
} | |
} | |
interface NumberInputProps { | |
error: boolean; | |
value: number | null; | |
onChange: (newValue: number | null) => void; | |
onBlur: () => void; | |
} | |
class NumberInput extends React.Component<NumberInputProps> { | |
handleChange = (e: React.FormEvent<HTMLInputElement>) => { | |
return this.props.onChange(Number.parseInt(e.currentTarget.value, 10)); | |
}; | |
render() { | |
return ( | |
<input | |
className={this.props.error ? "error" : ""} | |
type="text" | |
onChange={this.handleChange} | |
onBlur={this.props.onBlur} | |
value={this.props.value ? this.props.value.toString() : ""} | |
/> | |
); | |
} | |
} | |
interface PersonFormProps { | |
onCreate: (x: Readonly<Pick<Person, Exclude<keyof Person, "id">>>) => void; | |
onUpdate: (x: Readonly<Person>) => void; | |
person: Readonly<Person> | null | undefined; | |
} | |
interface PersonFormState { | |
person: Draft<Person>; | |
} | |
export default class PersonForm extends React.Component< | |
PersonFormProps, | |
PersonFormState | |
> { | |
innerForm: React.RefObject<Form<Draft<Person>>> = React.createRef(); | |
handleSubmit = (draftPerson: Draft<Person>) => { | |
const {id, name, age, faction, favoriteShip} = draftPerson; | |
if ( | |
name != null && | |
age != null && | |
faction != null && | |
favoriteShip != null | |
) { | |
if (id != null) { | |
this.props.onUpdate({id, name, age, faction, favoriteShip}); | |
} else { | |
this.props.onCreate({name, age, faction, favoriteShip}); | |
} | |
} else { | |
throw new Error("validations failed"); | |
} | |
if (this.innerForm.current) { | |
this.innerForm.current.reset(); | |
} | |
}; | |
handleFieldChange = <P extends keyof Draft<Person>>( | |
fieldName: P, | |
newValue: Draft<Person>[P], | |
updateField: <P2 extends keyof Draft<Person>>( | |
fieldName: P2, | |
newValue: Draft<Person>[P2] | |
) => void | |
) => { | |
if (fieldName === "faction") { | |
updateField("faction", newValue as Draft<Person>["faction"]); | |
updateField("favoriteShip", null); | |
} else { | |
updateField(fieldName, newValue); | |
} | |
}; | |
render() { | |
class MonoForm extends Form<Draft<Person>> {} | |
return ( | |
<MonoForm | |
initialValue={emptyPerson()} | |
onSubmit={this.handleSubmit} | |
onFieldChange={this.handleFieldChange} | |
fieldValidations={{ | |
name: validateName, | |
age: validateAge, | |
}} | |
validations={[validateOnlyYodaAgeLimit]} | |
feedbackStrategy={FeedbackStrategy.OnFirstChange} | |
> | |
{({onSubmit, formState}, Fields) => { | |
return ( | |
<form onSubmit={onSubmit}> | |
<div> | |
<Fields.name label="Name"> | |
{({value, onChange, onBlur, showError}) => { | |
return ( | |
<StringInput | |
error={showError} | |
onChange={onChange} | |
onBlur={onBlur} | |
value={value || ""} | |
/> | |
); | |
}} | |
</Fields.name> | |
</div> | |
<div> | |
<Fields.age label="Age"> | |
{({value, onChange, onBlur, showError}) => { | |
return ( | |
<NumberInput | |
error={showError} | |
onChange={onChange} | |
onBlur={onBlur} | |
value={value} | |
/> | |
); | |
}} | |
</Fields.age> | |
</div> | |
<div> | |
<Fields.faction label="Faction"> | |
{({value, onChange, showError}) => { | |
return ( | |
<Select | |
error={showError} | |
options={[ | |
{label: "Rebel", value: "REBEL" as Faction}, | |
{label: "Imperial", value: "IMPERIAL" as Faction}, | |
]} | |
value={value} | |
onChange={onChange} | |
iso={factionIso} | |
/> | |
); | |
}} | |
</Fields.faction> | |
</div> | |
<div> | |
<Fields.favoriteShip label="Favorite ship"> | |
{({value, onChange, showError, formState: {faction}}) => { | |
let favoriteShipValues: {label: string; value: Ship}[] = []; | |
if (faction === "REBEL") { | |
favoriteShipValues = [ | |
{ | |
label: "X-Wing", | |
value: "X_WING", | |
}, | |
{ | |
label: "Corellian Corvette", | |
value: "CORELLIAN_CORVETTE", | |
}, | |
]; | |
} else if (faction === "IMPERIAL") { | |
favoriteShipValues = [ | |
{ | |
label: "TIE Fighter", | |
value: "TIE_FIGHTER", | |
}, | |
{ | |
label: "Star Destroyer", | |
value: "STAR_DESTROYER", | |
}, | |
]; | |
} | |
return ( | |
<Select | |
error={showError} | |
allowEmpty={true} | |
options={favoriteShipValues} | |
value={value} | |
onChange={onChange} | |
iso={shipIso} | |
/> | |
); | |
}} | |
</Fields.favoriteShip> | |
<FormErrors /> | |
</div> | |
<input | |
type="submit" | |
value={formState.id != null ? "Edit" : "Create"} | |
/> | |
</form> | |
); | |
}} | |
</MonoForm> | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment