Skip to content

Instantly share code, notes, and snippets.

@dbousamra
Last active June 13, 2023 15:46
Show Gist options
  • Save dbousamra/37d67e2f7b43261d424e7b0d75032d5b to your computer and use it in GitHub Desktop.
Save dbousamra/37d67e2f7b43261d424e7b0d75032d5b to your computer and use it in GitHub Desktop.
export interface OnSaveResponse<A> {
values: A;
existingChangesetId: string | null | undefined;
}
export interface OnSaveOptions<A> {
dirtyValues: A;
takeOwnership: boolean;
existingChangesetId: string | null | undefined;
changesetId: string;
}
interface AutoSaverProps<A> extends AutoSaveProps<A> {
control: Control;
}
function AutoSaver<A>(props: AutoSaverProps<A>) {
const {
changesetId,
initialExistingChangesetId,
control,
debounceMs,
onSave,
diff,
combine,
} = props;
const { watch, reset, getValues, handleSubmit } = useFormContext<A>();
const { isSubmitting } = useFormState({ control });
const [takeOwnership, setTakeOwnership] = React.useState(true);
const [existingChangesetId, setExistingChangesetId] = React.useState(initialExistingChangesetId);
const lastSubmittedData = React.useRef(getValues());
const debouncedSave = useAsyncDebounce(async () => {
if (!isSubmitting) {
await handleSubmit(save)();
}
}, debounceMs);
const save = React.useCallback(
async (data: UnpackNestedValue<A>) => {
const diffResponse = diff(lastSubmittedData.current as A, data as A);
// // If there are no dirty fields, don't bother sending.
// // This can occur when a user types, but then backspaces,
// // and the values remain the same.
if (!diffResponse.isDiff) {
return;
}
// TODO: Is this necessary
const values = _.cloneDeep(data as A);
const req = {
dirtyValues: diffResponse.diff,
takeOwnership,
existingChangesetId,
changesetId,
};
const response = await onSave(req);
setTakeOwnership(false);
setExistingChangesetId(response.existingChangesetId);
const serverValues = response.values;
const currentFormValues = getValues();
lastSubmittedData.current = serverValues as UnpackNestedValue<A>;
// Check to see if the form has changed since we submitted,
// by comparing the current form values, with the values
// we submitted.
// If the form has changed since we sent the request, i.e. a user
// has typed since the request was sent issue another save request.
// This prevents the form being overridden with values mid-typing.
const diffResponseSinceSent = diff(values, currentFormValues as A);
// Reset the form to the values we received from the server, merged with
// the current diff of the server and what we last sent.
reset(combine(serverValues, diffResponseSinceSent.diff) as UnpackNestedValue<DeepPartial<A>>);
},
[changesetId, existingChangesetId, takeOwnership, getValues, onSave, reset, diff, combine],
);
React.useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const subscription = watch(() => debouncedSave());
return () => subscription.unsubscribe();
}, [watch, debouncedSave]);
return null;
}
interface AutoSaveProps<A> {
debounceMs: number;
startingValues: A;
changesetId: string;
initialExistingChangesetId: string | null | undefined;
onSave: (options: OnSaveOptions<A>) => Promise<OnSaveResponse<A>>;
diff: (prev: A, curr: A) => DiffResponse<A>;
combine: (prev: A, curr: A) => A;
}
export function AutoSave<A>(props: React.PropsWithChildren<AutoSaveProps<A>>) {
const {
startingValues,
changesetId,
initialExistingChangesetId,
onSave,
diff,
combine,
debounceMs,
children,
} = props;
const methods = useForm({
defaultValues: startingValues as any,
});
return (
<FormProvider {...methods}>
<AutoSaver
control={methods.control}
startingValues={startingValues}
debounceMs={debounceMs}
changesetId={changesetId}
initialExistingChangesetId={initialExistingChangesetId}
onSave={onSave}
diff={diff}
combine={combine}
/>
{children}
</FormProvider>
);
}
export const AutoSaveMemo = typedMemo(AutoSave);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment