Skip to content

Commit

Permalink
Merge branch 'main' into fix-unhandled-rejection-handling
Browse files Browse the repository at this point in the history
  • Loading branch information
jivey authored Oct 16, 2023
2 parents cc7a383 + 42ab197 commit 1f09132
Show file tree
Hide file tree
Showing 15 changed files with 1,269 additions and 6 deletions.
4 changes: 3 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ module.exports = {
"import"
],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }],

"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error"],
"import/no-default-export": "error"
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
"typescript": "^4.7.4"
},
"dependencies": {
"crypto": "npm:crypto-browserify@^3.12.0",
"stream": "npm:stream-browserify@^3.0.0",
"@sentry/react": "^7.48.0",
"classnames": "^2.3.1"
}
Expand Down
121 changes: 121 additions & 0 deletions src/components/forms/Forms.stories.tsx
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>
};
138 changes: 138 additions & 0 deletions src/components/forms/controlled/hooks.ts
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,
};
};
Loading

0 comments on commit 1f09132

Please sign in to comment.