react-vite-demo

react and vite demo

git clone https://9o.is/git/react-vite-demo.git

commit b7e7c23f0c78de7967cf0bbbf3e7f2f94aa9e025
parent 155a0bcab9ed961846d9c18f46974ad6d722b0db
Author: Jul <jul@9o.is>
Date:   Wed, 14 Aug 2024 20:19:38 +0800

create Form component and invariant util function

Diffstat:
Asrc/components/headless/Form.tsx | 34++++++++++++++++++++++++++++++++++
Msrc/components/headless/index.ts | 1+
Msrc/components/page/EventsPage/EventsPage.tsx | 29++++++++++++++---------------
Msrc/components/page/EventsPage/useFilteredSHEvents.ts | 18++++++++++++------
Msrc/lib/index.ts | 4++--
Asrc/lib/invariant.ts | 5+++++
Msrc/lib/objects.ts | 8+++++---
7 files changed, 73 insertions(+), 26 deletions(-)

diff --git a/src/components/headless/Form.tsx b/src/components/headless/Form.tsx @@ -0,0 +1,34 @@ +import { ReactNode, useRef } from "react" + +type FormProps = { + onSubmit: (data: FormDataMap) => void + children: ReactNode + ['aria-label']?: string +} + +export type FormDataMap = { + [key: string]: FormDataEntryValue | undefined +} + +export function Form({ onSubmit, children, ...props }: FormProps) { + const formRef = useRef<HTMLFormElement>(null) + + const internalOnSubmit = (event: React.FormEvent<HTMLFormElement>) => { + event.preventDefault() + + const formData = new FormData(formRef.current || undefined) + const result: FormDataMap = {} + + for (const [k, v] of formData.entries()) { + result[k] = v + } + + onSubmit(result) + } + + return ( + <form onSubmit={internalOnSubmit} ref={formRef} {...props}> + {children} + </form> + ) +} diff --git a/src/components/headless/index.ts b/src/components/headless/index.ts @@ -1,5 +1,6 @@ export * from './Alert' export * from './ComboBox' export * from './Checkbox' +export * from './Form' export * from './Progress' export * from './Select' \ No newline at end of file diff --git a/src/components/page/EventsPage/EventsPage.tsx b/src/components/page/EventsPage/EventsPage.tsx @@ -1,7 +1,8 @@ -import { Checkbox, ComboBox, Select, Progress, Alert } from '../../headless' +import { Checkbox, ComboBox, Select, Progress, Alert, Form, FormDataMap } from '../../headless' import { useNumberFormatter } from '../../hooks' import { useSHEvents } from './useSHEvents' import { useFilteredSHEvents } from './useFilteredSHEvents' +import { invariant, isSortType } from '../../../lib' export function EventsPage() { const { events: loadedEvents, error, loading } = useSHEvents('node-a') @@ -14,29 +15,27 @@ export function EventsPage() { const cities = [...new Set(loadedEvents.map(({ city }) => city))] - const onSubmit = (event: React.FormEvent<HTMLFormElement>) => { - event.preventDefault() - - const formData = new FormData(event.currentTarget) - const city = formData.get("city") - const priceSort = formData.get("price-sort") - const cheapest = formData.get("cheapest") === "on" - - if (typeof city !== 'string' || typeof priceSort !== 'string') return - if (priceSort !== 'ascending' && priceSort !== 'descending' && priceSort !== '') return + const onSubmit = ({ city, priceSort, cheapest }: FormDataMap) => { + invariant(typeof city === 'string') + invariant(priceSort === '' || (typeof priceSort === 'string' && isSortType(priceSort))) + invariant(cheapest === undefined || cheapest === 'on') - filterSHEvents({ city, cheapest, priceSort: priceSort || undefined }) + filterSHEvents({ + city, + priceSort: priceSort || undefined, + cheapest: cheapest === 'on', + }) } return ( <> <Alert error={error} /> - <form onSubmit={onSubmit} aria-label='Filter Events'> + <Form onSubmit={onSubmit} aria-label='Filter Events'> <ComboBox label="City" name="city" options={cities} /> - <Select label="Sort by price" name="price-sort" options={['ascending', 'descending']} /> + <Select label="Sort by price" name="priceSort" options={['ascending', 'descending']} /> <Checkbox label="Find Cheapest" name="cheapest" /> <button type="submit" disabled={loading}>Search</button> - </form> + </Form> <Progress loading={loading}> <Progress.Bar> <Progress.Label>Loading events...</Progress.Label> diff --git a/src/components/page/EventsPage/useFilteredSHEvents.ts b/src/components/page/EventsPage/useFilteredSHEvents.ts @@ -3,15 +3,21 @@ import { asArray, filterByProp, minByProp, pipe, sortByProp, SortType } from ".. import { SHEvent } from "./useSHEvents" type FilteredSHEventsInput = { - city: string - priceSort: SortType - cheapest: boolean + city?: string + priceSort?: SortType + cheapest?: boolean } -const sortByPrice = (priceSort: SortType) => (events: SHEvent[]) => sortByProp('price', priceSort, events) -const filterByCity = (city: string) => (events: SHEvent[]) => city ? filterByProp("city", city, events) : events -const filterCheapest = (cheapest: boolean) => (events: SHEvent[]) => cheapest ? minByProp('price', events) : events +const sortByPrice = (priceSort: FilteredSHEventsInput['priceSort']) => + (events: SHEvent[]) => priceSort ? sortByProp('price', priceSort, events) : events + +const filterByCity = (city: FilteredSHEventsInput['city']) => + (events: SHEvent[]) => city ? filterByProp("city", city, events) : events + +const filterCheapest = (cheapest: FilteredSHEventsInput['cheapest']) => + (events: SHEvent[]) => cheapest ? minByProp('price', events) : events + export function useFilteredSHEvents(initialEvents: SHEvent[]) { const [events, setEvents] = useState<SHEvent[]>(initialEvents) diff --git a/src/lib/index.ts b/src/lib/index.ts @@ -1,2 +1,3 @@ +export * from './invariant' +export * from './objects' export * from './pipe' -export * from './objects' -\ No newline at end of file diff --git a/src/lib/invariant.ts b/src/lib/invariant.ts @@ -0,0 +1,5 @@ + +export function invariant(condition: any, message?: string): asserts condition { + if (condition) return + throw new Error(message || "Invariant failed") +} diff --git a/src/lib/objects.ts b/src/lib/objects.ts @@ -4,7 +4,11 @@ type KeysOfType<T, U> = { type NumberKeysOf<T> = KeysOfType<T, number> -export type SortType = "ascending" | "descending" | undefined +export type SortType = "ascending" | "descending" + +export function isSortType(value: string | undefined | null): value is SortType { + return value === "ascending" || value === "descending" +} export function asArray<T>(value: T | T[] | undefined | null): T[] { return Array.isArray(value) ? value : @@ -16,8 +20,6 @@ export function filterByProp<T extends object>(prop: keyof T, value: T[keyof T], } export function sortByProp<T,>(prop: NumberKeysOf<T>, order: SortType, objects: T[]): T[] { - if (!order) return objects - return [...objects].sort((a: T, b: T) => { if (!order || typeof a[prop] !== "number" || typeof b[prop] !== "number") return 0 if (order === "ascending") return a[prop] - b[prop]