Skip to content

Commit

Permalink
Merge pull request #10 from murageh/refactor#strict-typing
Browse files Browse the repository at this point in the history
refactor#data saving and retrieval logic
  • Loading branch information
murageh authored Dec 3, 2024
2 parents 7f29ba6 + d7d1448 commit f8c89c1
Show file tree
Hide file tree
Showing 9 changed files with 714 additions and 180 deletions.
219 changes: 160 additions & 59 deletions README.md

Large diffs are not rendered by default.

442 changes: 409 additions & 33 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@crispice/save-progress",
"version": "1.0.11",
"description": "A React hook to save progress in a form, or any other scenario, and restore it when the user returns to the form. It uses localStorage to save the progress.",
"version": "2.0.0",
"description": "A React hook and Formik component to save and manage form progress, with support for custom save and clear functions, and TypeScript types.",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"files": [
Expand All @@ -19,11 +19,14 @@
},
"keywords": [
"react-hooks",
"use-save-progress",
"formik",
"save-progress",
"react",
"typescript",
"component",
"package"
"form",
"localStorage",
"sessionStorage"
],
"license": "MIT",
"scripts": {
Expand All @@ -38,7 +41,7 @@
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
"formik": "^2.2.9",
"rollup": "^3.3.0",
"rollup": "4.22.4",
"rollup-plugin-dts": "^5.0.0",
"tslib": "^2.4.1"
},
Expand Down
3 changes: 2 additions & 1 deletion src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
// Export each component individually, as a named export.
export { default as AutoSaveForm } from "./utilities/AutoSaveForm";
export {AutoSaveForm} from "./utilities/AutoSaveFormikForm";
export {default as AutoSaveFormikForm} from "./utilities/AutoSaveFormikForm";
32 changes: 0 additions & 32 deletions src/components/utilities/AutoSaveForm.tsx

This file was deleted.

77 changes: 77 additions & 0 deletions src/components/utilities/AutoSaveFormikForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {useFormikContext} from 'formik';
import React from 'react';
import {useProgressProps} from "../../hooks/saveProgress/useProgress";
import {useFormProgress} from "../../hooks";

export interface AutoSaveFormProps<T extends {}> extends useProgressProps<T> {
children?: React.ReactNode | Element;
}

/**
* This component is a wrapper for the `useFormProgress` custom hook.
* It is used to save the form data when the user changes the form data.
* This is useful for forms that are long, and the user may not want to click the save button.
*
* Note: This requires a valid Formik context.
* Thus, this component will only work if there is a parent Formik React Context from which it can pull from.
* If called without a parent context (i.e., a descendant of a `<Formik>` component or `withFormik` higher-order component),
* you will get a warning in your console.
* For more details regarding this, visit the [Formik documentation](https://formik.org/docs/api/useFormikContext).
*
* Note: The `values` from Formik will be prioritized over `initialValues`.
*
* @template T - The type of the form values.
* @param {AutoSaveFormProps<T>} props - The properties for the component.
* @param {string} props.dataKey - The dataKey to identify the stored values.
* @param {T} [props.initialValues] - The initial values for the form.
* @param {Storage} [props.storage] - The storage to use (localStorage or sessionStorage). Defaults to localStorage.
* @param {(values: T) => void} [props.saveFunction] - Custom function to save values. If provided, this will be used instead of the storage.
* @param {(values?: T) => void} [props.clearFunction] - Custom function to clear values. Crucial for custom save functions.
* @param {boolean} [props.forceLocalActions] - Whether to force local storage actions even if custom functions are provided. Could be useful for debugging.
* @param {React.ReactNode} [props.children] - The children of the form.
* @returns {React.ReactNode} - Returns the children or null.
*/
const AutoSaveFormikForm = React.memo(
function AutoSaveFormikForm<T extends {}>(props: AutoSaveFormProps<T>) {
const [initialized, setInitialized] = React.useState(false);
const { values, setValues } = useFormikContext<T>();
const { dataKey, storage = localStorage, saveFunction, clearFunction, forceLocalActions, children } = props;
const [_, updateValues] = useFormProgress<T>({
dataKey: dataKey,
storage,
saveFunction,
clearFunction,
forceLocalActions,
});

React.useEffect(() => {
const savedValues = storage.getItem(dataKey);
if (savedValues) {
const parsedValues = JSON.parse(savedValues);
setValues((prevValues) => ({ ...prevValues, ...parsedValues }));
}
setInitialized(true);
}, [dataKey, setValues, storage]);

React.useEffect(() => {
if (!initialized) return;
try {
updateValues(values);
} catch (error) {
console.warn("Error saving form data. Please check the save function. If the error persists, please contact the developer.");
console.log(error);
}
}, [initialized, values, updateValues]);

return <>{children || null}</>;
});

/**
* @deprecated Use {@link AutoSaveFormikForm} instead.
* The naming was unclear regarding the Formik context,
* so this function was renamed to {@link AutoSaveFormikForm}.
* This function may be removed in the next major release.
*/
export const AutoSaveForm = AutoSaveFormikForm;

export default AutoSaveFormikForm;
4 changes: 2 additions & 2 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* export hooks */

export {useSaveProgress} from './saveProgress/useProgress';
export {default as useProgress} from './saveProgress/useProgress';
export {useProgress, useSaveProgress} from './saveProgress/useProgress';
export {default as useFormProgress} from './saveProgress/useProgress';
102 changes: 55 additions & 47 deletions src/hooks/saveProgress/useProgress.ts
Original file line number Diff line number Diff line change
@@ -1,99 +1,107 @@
import React from 'react';

export interface useProgressProps<T extends {}> {
key: string;
dataKey: string;
initialValues?: T;
storage?: Storage;
saveFunction?: (values: T) => void;
clearFunction?: (values?: T) => void;
forceLocalActions?: boolean;
fetchInitialValues?: () => Promise<T>;
}

/**
* If you are using `useSaveProgress`, please migrate to `useProgress` instead.
* Custom hook to save and manage the progress of user input, such as form data.
* This hook ensures that user input is preserved even if the user navigates away from the page.
*
* This is a custom hook that is used to save the progress of the user's input,
* for instance, if the user is filling out a form, and they leave the page,
* the next time they come back, the form will be filled out with the data
* they had previously entered.
*
*
* Typical usage:
* Use this hook to save the progress of the user's input, for instance, if the user is filling out a form, and they leave the page,
* the next time they come back, the form will be filled out with the data they had previously entered.
*
* The {@link AutoSaveForm} component is a helper component that will automatically save the data when the form is submitted.
* The {@link AutoSaveForm} requires a saveFunction prop, which is the setValues function returned by this hook, or any other function.
*
* @param key The key to use to save the data in local storage
* @param initialValue The initial value to use if there is no data in local storage
* @param storage The storage to use, defaults to local storage
* @param saveFunction A function to save the data to a server. If this is provided, the data will not be saved locally. You can use this function to customize the save logic.
* @param clearFunction A function to clear the data from a server. If this is provided, the data will not be cleared locally. You can use this function to customize the clear logic.
* @param forceLocalActions If true, the data will be saved locally even if a saveFunction is provided. This is useful if you want to save the data locally, in addition to saving it to a server. This also applies to the clearFunction
*
* @returns [values, setValues, clearValues] The data, a function to update the data, and a function to clear the data
*/
function useProgress<T extends {}>({
key,
initialValues = {} as T,
storage,
saveFunction,
clearFunction,
forceLocalActions = false
}: useProgressProps<T>) {
* @template T - The type of the form values.
* @param {Object} props - The properties for the hook.
* @param {string} props.dataKey - The dataKey to identify the stored values.
* @param {T} [props.initialValues] - The initial values for the form.
* @param {Storage} [props.storage] - The storage to use (localStorage or sessionStorage). Defaults to localStorage.
* @param {(values: T) => void} [props.saveFunction] - Custom function to save values. If provided, this will be used instead of the storage.
* @param {(values?: T) => void} [props.clearFunction] - Custom function to clear values. Crucial for custom save functions.
* @param {boolean} [props.forceLocalActions] - Whether to force local storage actions even if custom functions are provided. Could be useful for debugging.
* @param {() => Promise<T>} [props.fetchInitialValues] - Function to fetch initial values asynchronously. Values from this function will override the storage values.
* @returns {[T, React.Dispatch<React.SetStateAction<T>>, () => void]} - Returns the current values, a function to update the values, and a function to clear the values.
* */
function useFormProgress<T extends {}>({
dataKey,
initialValues = {} as T,
storage,
saveFunction,
clearFunction,
forceLocalActions = false,
fetchInitialValues,
}: useProgressProps<T>) {
const [initialized, setInitialized] = React.useState(false);
const [values, setValues] = React.useState(initialValues);

React.useEffect(() => {
if (typeof window !== 'undefined') {
const saved = (storage ?? window.localStorage).getItem(key);
let initialValue: T;
try {
initialValue = JSON.parse(saved!);
} catch (e) {
initialValue = {} as T;
const initializeValues = async () => {
let initialValue: T = initialValues;

if (fetchInitialValues) {
try {
initialValue = await fetchInitialValues();
} catch (e) {
console.error('Failed to fetch initial values:', e);
}
} else if (typeof window !== 'undefined') {
const saved = (storage ?? window.localStorage).getItem(dataKey);
try {
initialValue = JSON.parse(saved!);
} catch (e) {
initialValue = {} as T;
}
}

setValues(initialValue || initialValues || {} as T);
saveValues(values);
saveValues(initialValue);
setInitialized(true);
}
};

void initializeValues();
}, []);

React.useEffect(() => {
if (!initialized) return;
saveValues(values);
}, [values]);

// Helper function to save the data to local storage
const saveValues = (values: T) => {
if (saveFunction) {
saveFunction(values);
if (!forceLocalActions) return;
}
if (typeof window !== 'undefined') {
(storage ?? window.localStorage).setItem(key, JSON.stringify(values));
(storage ?? window.localStorage).setItem(dataKey, JSON.stringify(values));
}
};

// Provide helper function to clear the data from local storage
const clearValues = () => {
setValues(initialValues);
if (clearFunction) {
clearFunction(values);
if (!forceLocalActions) return;
}
if (typeof window !== 'undefined') {
(storage ?? window.localStorage).removeItem(key);
(storage ?? window.localStorage).removeItem(dataKey);
}
};

return [values, setValues, clearValues] as const;
}

export default useProgress;
export default useFormProgress;

/**
* @deprecated Use the {@link useProgress} hook instead.
*/
export const useProgress = useFormProgress;

/**
* @deprecated Use the {@link useProgress} hook instead.
* This will be removed in the next major version.
*/
export const useSaveProgress = useProgress;
export const useSaveProgress = useFormProgress;
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"strict": true,
"target": "es5",
"target": "es2015",
"module": "ESNext",
"esModuleInterop": true,
"moduleResolution": "node",
Expand Down

0 comments on commit f8c89c1

Please sign in to comment.