-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into fix-unhandled-rejection-handling
- Loading branch information
Showing
15 changed files
with
1,269 additions
and
6 deletions.
There are no files selected for viewing
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import React from 'react'; | ||
import styled from 'styled-components'; | ||
import {Controlled as Forms, Uncontrolled} from '.'; | ||
import { fetchSuccess } from "@openstax/ts-utils/fetch"; | ||
|
||
export const BasicControlledForm = () => { | ||
const data = { | ||
name: 'Test McTesterson', | ||
} | ||
|
||
type Data = typeof data; | ||
const state = fetchSuccess<Data>(data); | ||
const [submitted, setSubmitted] = React.useState<Partial<Data>>({}); | ||
|
||
return <Forms.Form state={state} onSubmit={(data: Partial<Data>) => setSubmitted(data)}> | ||
<Forms.TextInput name="name" label="Name" /> | ||
<Forms.Buttons /> | ||
<pre>{JSON.stringify(submitted, null, 2)}</pre> | ||
</Forms.Form> | ||
}; | ||
|
||
const FormRow = styled(Forms.FormSection)` | ||
display: flex; | ||
flex-direction: row; | ||
> *:not(:first-child) { | ||
margin-top: 0; | ||
margin-left: 5px; | ||
} | ||
`; | ||
export const FormRepeatableElements = () => { | ||
const data = { | ||
name: 'Test McTesterson', | ||
interests: [{interest: 'sleeping'}], | ||
} | ||
|
||
type Data = typeof data; | ||
const state = fetchSuccess<Data>(data); | ||
const [submitted, setSubmitted] = React.useState<Partial<Data>>({}); | ||
const [sortable, setSortable] = React.useState<boolean>(false); | ||
|
||
return <Forms.Form state={state} onSubmit={(data: Partial<Data>) => setSubmitted(data)}> | ||
<Forms.TextInput name="name" label="Name" /> | ||
<Uncontrolled.Checkbox name="sortable" label="Sortable" onChangeValue={checked => setSortable(!!checked)} /> | ||
<Forms.List name="interests"> | ||
<Forms.ListItems> | ||
<FormRow> | ||
{sortable ? <Forms.ListRecordSortableHandle style={{height: 'auto'}} /> : null} | ||
<Forms.TextInput name="interest" label="Interest" /> | ||
<Forms.ListRecordRemoveButton>remove interest</Forms.ListRecordRemoveButton> | ||
</FormRow> | ||
</Forms.ListItems> | ||
<Forms.ListRecordAddButton>add interest</Forms.ListRecordAddButton> | ||
</Forms.List> | ||
<Forms.Buttons /> | ||
<pre>{JSON.stringify(submitted, null, 2)}</pre> | ||
</Forms.Form> | ||
}; | ||
|
||
export const BasicDataReferences = () => { | ||
const data = { | ||
name: 'Test McTesterson', | ||
} | ||
|
||
type Data = typeof data; | ||
const state = fetchSuccess<Data>(data); | ||
|
||
return <Forms.Form state={state}> | ||
<Forms.TextInput name="name" label="Name" /> | ||
<Forms.Buttons /> | ||
<Forms.GetFormValue name="name"> | ||
{data => <pre>{JSON.stringify(data, null, 2)}</pre>} | ||
</Forms.GetFormValue> | ||
<Forms.GetFormData> | ||
{data => <pre>{JSON.stringify(data, null, 2)}</pre>} | ||
</Forms.GetFormData> | ||
</Forms.Form> | ||
}; | ||
|
||
export const DataReferencesInNamespaces = () => { | ||
const data = { | ||
name: 'Test McTesterson', | ||
interests: [{interest: 'sleeping'}], | ||
address: {line1: '1 main st'} | ||
} | ||
|
||
type Data = typeof data; | ||
const state = fetchSuccess<Data>(data); | ||
|
||
return <Forms.Form state={state}> | ||
<Forms.TextInput name="name" label="Name" /> | ||
<p>in a loop or namespace <code>Forms.GetFormData</code> and <code>Forms.GetFormValue</code> will return only values within the namespace</p> | ||
<Forms.GetFormData>{formData => <> | ||
<Forms.NameSpace name="address"> | ||
<Forms.TextInput name="line1" label="line1" /> | ||
<Forms.GetFormData> | ||
{data => <pre>{JSON.stringify(data, null, 2)}</pre>} | ||
</Forms.GetFormData> | ||
|
||
<p>if you need the parent context data in a subcomponent, you can wrap the entire section</p> | ||
<pre>{JSON.stringify(formData, null, 2)}</pre> | ||
</Forms.NameSpace> | ||
<Forms.List name="interests"> | ||
<Forms.ListItems> | ||
<FormRow> | ||
<Forms.TextInput name="interest" label="Interest" /> | ||
<Forms.ListRecordRemoveButton>remove interest</Forms.ListRecordRemoveButton> | ||
</FormRow> | ||
<Forms.GetFormData> | ||
{data => <pre>{JSON.stringify(data, null, 2)}</pre>} | ||
</Forms.GetFormData> | ||
</Forms.ListItems> | ||
<Forms.ListRecordAddButton>add interest</Forms.ListRecordAddButton> | ||
</Forms.List> | ||
<Forms.Buttons /> | ||
<Forms.GetFormValue name="name"> | ||
{data => <pre>{JSON.stringify(data, null, 2)}</pre>} | ||
</Forms.GetFormValue> | ||
</>}</Forms.GetFormData> | ||
</Forms.Form> | ||
}; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import React from 'react'; | ||
import { FetchState } from "@openstax/ts-utils/fetch.js"; | ||
import { merge } from "@openstax/ts-utils"; | ||
|
||
const randomId = () => window.crypto.getRandomValues(new Uint32Array(1))[0].toString(16) | ||
|
||
export type AbstractFormData = Record<string, any>; | ||
|
||
type FormStateHelpers<T extends AbstractFormData> = { | ||
data: Partial<T>; | ||
submit: () => void; | ||
namespace: string; | ||
state: FetchState<T, string>; | ||
setInput: { | ||
fields: React.Dispatch<React.SetStateAction<Partial<T>>>; | ||
field: <F extends keyof T>(fieldName: F) => (value: T[F]) => void; | ||
merge: (input: Partial<T>) => void; | ||
}; | ||
}; | ||
|
||
export const FormStateContext = React.createContext<() => FormStateHelpers<AbstractFormData>>( | ||
() => { throw new Error('form helpers not provided');} | ||
); | ||
|
||
export const useFormHelpers = () => React.useContext(FormStateContext)(); | ||
|
||
const makeSetInput = <T extends AbstractFormData>(setState: React.Dispatch<React.SetStateAction<T>>) => { | ||
const mergeFields = (input: Partial<T>) => setState(previous => merge(previous, input) as T); | ||
|
||
const setInputField = <F extends keyof T>(fieldName: F) => (value: T[F]) => { | ||
setState(previous => ({...previous, [fieldName]: value})); | ||
}; | ||
|
||
return {field: setInputField, fields: setState, merge: mergeFields}; | ||
}; | ||
|
||
export const useFormState = <T extends AbstractFormData>( | ||
state: FormStateHelpers<T>['state'], | ||
defaultValue?: Partial<T>, | ||
onSubmit?: (data: Partial<T>) => void | ||
): FormStateHelpers<T> => { | ||
const [inputFields, setInputFields] = React.useState<Partial<T>>( | ||
'data' in state && state.data ? state.data : (defaultValue || {}) | ||
); | ||
const inputFieldsRef = React.useRef<Partial<T>>(inputFields); | ||
inputFieldsRef.current = inputFields; | ||
const submitHandler = React.useCallback(() => { | ||
onSubmit?.(inputFieldsRef.current); | ||
}, [onSubmit]); | ||
return {namespace: 'form', submit: submitHandler, data: inputFields, state, setInput: makeSetInput(setInputFields)}; | ||
}; | ||
|
||
export const useFormNameSpace = (field: string): FormStateHelpers<AbstractFormData> => { | ||
const parentState = useFormHelpers(); | ||
|
||
const setInputFields: React.Dispatch<React.SetStateAction<any>> = (input) => | ||
parentState.setInput.fields(previous => ({ | ||
...previous, | ||
[field]: input instanceof Function ? input(previous[field] || {}) : input | ||
})) | ||
; | ||
|
||
return { | ||
namespace: parentState.namespace + '.' + field, | ||
submit: parentState.submit, | ||
data: parentState.data[field] || {}, | ||
state: parentState.state, | ||
setInput: makeSetInput(setInputFields) | ||
}; | ||
}; | ||
|
||
export const FormListContext = React.createContext<() => ListStateHelpers>( | ||
() => { throw new Error('form list helpers not provided');} | ||
); | ||
|
||
export const useFormListHelpers = () => React.useContext(FormListContext)(); | ||
|
||
type ListStateHelpers = { | ||
addRecord: (record?: AbstractFormData) => void; | ||
removeRecord: (id: string) => void; | ||
data: Array<AbstractFormData & {id: string}>; | ||
setData: React.Dispatch<React.SetStateAction<AbstractFormData[]>>; | ||
makeRecordHelpers: (record: AbstractFormData & {id: string}) => FormStateHelpers<AbstractFormData>; | ||
}; | ||
|
||
export type FormListConfig = { | ||
name: string; | ||
}; | ||
export const useFormList = ({name}: FormListConfig): ListStateHelpers => { | ||
const parentState = useFormHelpers(); | ||
|
||
const setData: React.Dispatch<React.SetStateAction<AbstractFormData[]>> = React.useCallback((input) => | ||
parentState.setInput.fields(previous => ({ | ||
...previous, | ||
[name]: input instanceof Function ? input(previous[name] || []) : input | ||
})) | ||
, [name, parentState.setInput]); | ||
|
||
const makeRecordHelpers = (data: ListStateHelpers['data'][number]) => ({ | ||
data, | ||
state: parentState.state, | ||
submit: parentState.submit, | ||
namespace: parentState.namespace + '.' + data.id, | ||
setInput: makeSetInput<AbstractFormData>((input) => | ||
setData(previous => previous.map( | ||
(record: AbstractFormData) => record.id === data.id | ||
? input instanceof Function ? input(record) : input | ||
: record | ||
)) | ||
) | ||
}); | ||
|
||
const value = React.useMemo(() => parentState.data[name] || [], [name, parentState.data]); | ||
const hasIds = React.useMemo(() => value.every((record: any) => !!record.id), [value]); | ||
|
||
React.useEffect(() => { | ||
if (!hasIds) { | ||
parentState.setInput.fields(previous => ({ | ||
...previous, | ||
[name]: value.map((record: any) => record.id === undefined ? {...record, id: randomId()} : record) | ||
})); | ||
} | ||
}, [value, hasIds, name, parentState.setInput]); | ||
|
||
return { | ||
addRecord: (record) => parentState.setInput.fields(previous => ({ | ||
...previous, | ||
[name]: [...(previous[name] || []), {id: randomId(), ...record}] | ||
})), | ||
removeRecord: (id) => parentState.setInput.fields(previous => ({ | ||
...previous, | ||
[name]: (previous[name] || []).filter((record: AbstractFormData) => record.id !== id) | ||
})), | ||
data: hasIds ? value : [], | ||
setData, | ||
makeRecordHelpers, | ||
}; | ||
}; |
Oops, something went wrong.