Skip to content

Instantly share code, notes, and snippets.

@zgotsch
Created April 27, 2018 21:35
Show Gist options
  • Save zgotsch/2a6a7ef5b4e529b63b9b5de387cb7a81 to your computer and use it in GitHub Desktop.
Save zgotsch/2a6a7ef5b4e529b63b9b5de387cb7a81 to your computer and use it in GitHub Desktop.
formula-one example
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