https://github.com/patternhelloworld/headless-formik-helper
A Clean and Useful Helper Hook for useFormik
https://github.com/patternhelloworld/headless-formik-helper
formik formik-yup headless react
Last synced: about 2 months ago
JSON representation
A Clean and Useful Helper Hook for useFormik
- Host: GitHub
- URL: https://github.com/patternhelloworld/headless-formik-helper
- Owner: patternhelloworld
- Created: 2025-01-03T05:14:58.000Z (11 months ago)
- Default Branch: main
- Last Pushed: 2025-01-03T06:24:20.000Z (11 months ago)
- Last Synced: 2025-06-06T23:04:28.311Z (6 months ago)
- Topics: formik, formik-yup, headless, react
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/headless-formik-helper
- Size: 121 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Headless-Formik-Helper
> A Clean and Useful Helper Hook for useFormik
## Table of Contents
- [Features](#features)
- [Usage](#Usage)
- [Sample Codes](#sample-codes)
- [APIs](#apis)
## Features
- Show headless Formik examples using useFormik combined with Yup
- Eager validation for all inputs
- Functions for updating key-value pairs
- Normalization of Formik values before sending them to the server
- ...More features will be added over time
## Usage
- Registered in NPM
```shell
npm install headless-formik-helper
```
## Sample Codes
- Check comments in the sample code below.
- ```javascript
"use client";
import React, {useContext, useEffect, useState} from "react";
import {FormikErrors, FormikHelpers, FormikProps, useFormik} from "formik";
import styles from "./productManagement.module.scss";
import {Button, Text} from "@mantine/core";
import {usePathname, useRouter, useSearchParams} from "next/navigation";
import {
ProductType,
CreateProductInput,
UpdateProductInput,
useCreateProductMutation,
useFindProductLazyQuery,
useRemoveProductMutation,
useUpdateProductMutation,
} from "@/generated/graphql";
import MemoizedTextInput from "@/components/common/form-input/MemoizedTextInput";
import MemoizedSelect from "@/components/common/form-input/MemoizedSelect";
import {GlobalToastContext} from "@/providers/GlobalToastProvider";
import {UI_COMMON_VALUES} from "@/util/value-util";
import {
ExtendedCreateProductInput,
ExtendedUpdateProductInput,
PRODUCT_CREATE_INITIAL_VALUES,
PRODUCT_UPDATE_INITIAL_VALUES,
PRODUCT_VALIDATION_SCHEMA,
} from "@/components/product/schema/crud-schema";
import {useRecoilState} from "recoil";
import {globalLoadingState} from "@/recoil/common";
import {useIsFirstRender} from "@/hooks/useIsFirstRender";
// 0. Import : headless-formik-helper
import {useHeadlessFormikHelper} from "headless-formik-helper";
import {CreateOrUpdateMode} from "headless-formik-helper/dist/types";
const ProductManagement = ({ PK_NAME }: { PK_NAME: string }) => {
const searchParams = useSearchParams();
const idForUpdate: number | null = Number(searchParams.get(PK_NAME));
const isFirstRender = useIsFirstRender();
const router = useRouter();
const { sendErrorMsgToGlobalToast, sendSuccessMsgToGlobalToast } = useContext(GlobalToastContext);
const [globalLoading, setGlobalLoading] = useRecoilState(globalLoadingState);
const [
fetchProduct,
{
data: fetchProductData,
loading: fetchProductLoading,
error: fetchProductError,
},
] = useFindProductLazyQuery();
// 1. useFormik
const formik: FormikProps<
ExtendedCreateProductInput | ExtendedUpdateProductInput
> = useFormik({
initialValues: {
...(idForUpdate
? PRODUCT_UPDATE_INITIAL_VALUES
: PRODUCT_CREATE_INITIAL_VALUES),
...fetchProductData?.product
},
validationSchema: PRODUCT_VALIDATION_SCHEMA,
validateOnMount: false,
validateOnChange: true,
validateOnBlur: true,
enableReinitialize: true
});
// 2. **useHeadlessFormikHelper**
const {
formikValuesChanged,
onKeyValueChangeByEventMemoized,
onKeyValueChangeByNameValueMemoized,
normalizeFormikValues,
} = useHeadlessFormikHelper({
formik: formik,
eagerValidationInitialOptions: {
CREATE_OR_UPDATE: idForUpdate ? CreateOrUpdateMode.UPDATE : CreateOrUpdateMode.CREATE,
afterMileSeconds: 0,
keyNameToCheckFetchedForUpdate : "id"
}
});
const isSubmitDisabled = formik === undefined ? false : !(formik.isValid && formik.dirty);
const [
createProduct,
{
data: createProductData,
loading: createProductLoading,
error: createProductError,
},
] = useCreateProductMutation();
const [
updateProduct,
{
data: updateProductData,
loading: updateProductLoading,
error: updateProductError,
},
] = useUpdateProductMutation();
const [
removeProduct,
{
data: removeProductData,
loading: removeProductLoading,
error: removeProductError,
},
] = useRemoveProductMutation();
const createOrUpdateProduct = () => {
if (formik.values.productType === UI_COMMON_VALUES.SELECT_OPTION_EMPTY) {
sendErrorMsgToGlobalToast("Product type is required.", true);
return;
}
// 3. normalizeFormikValues
if (!idForUpdate) {
createProduct({
variables: {
createProductInput: normalizeFormikValues(formik.values),
},
onCompleted(data) {
sendSuccessMsgToGlobalToast("Product created successfully.");
redirectToList();
},
});
} else {
updateProduct({
variables: {
pickProductInput: { id: idForUpdate },
updateProductInput: normalizeFormikValues(formik.values),
},
onCompleted(data) {
sendSuccessMsgToGlobalToast("Product updated successfully.");
router.refresh();
},
});
}
};
const fetchProductWrapper = () => {
if (idForUpdate) {
fetchProduct({
variables: {
pickProductInput: {
id: idForUpdate,
},
},
});
}
};
const redirectToList = () => {
router.push("/products");
};
useEffect(() => {
fetchProductWrapper();
}, [idForUpdate]);
useEffect(() => {
if (idForUpdate && fetchProductData && !fetchProductLoading) {
const product = fetchProductData.product;
formik.setValues(product);
}
}, [fetchProductData]);
return (
Product {!idForUpdate ? "Creation" : "Update"}
Product Name *
Product Type
{
onKeyValueChangeByNameValueMemoized({
name: "productType",
value,
});
}}
error={formik.errors.productType}
touched={formik.touched.productType}
/>
{formik.values.productType === ProductType.Physical && (
)}
{formik.values.productType === ProductType.Digital && (
)}
redirectToList()}
>
Cancel
{
createOrUpdateProduct();
}}
disabled={isSubmitDisabled}
>
{!idForUpdate ? "Create" : "Update"}
);
};
export default React.memo(ProductManagement);
```
- ```javascript
import * as Yup from "yup";
import {
ProductType,
CreateProductInput,
ProductOutput,
UpdateProductInput,
} from "@/generated/graphql";
import { PRODUCT_CATEGORY_TYPE } from "@/components/product/meta/schema";
import { UI_COMMON_VALUES } from "@/util/value-util";
import { YUP_EMPTY_VALUE_ERROR_MESSAGE } from "@/util/yup-utils";
/*
* The purpose of defining these extended types is to accommodate UI-specific requirements that differ from server constraints.
* For instance, "productType" is restricted to "Physical" and "Digital" on the server, but the UI may need to allow an empty value for better user experience.
*/
export type ExtendedCreateProductInput = Omit<
CreateProductInput,
"productType"> & {
productType: ProductType | "-";
};
export type ExtendedUpdateProductInput = Omit<
UpdateProductInput,
"productType"
> & {
productType: ProductType | "-";
};
export const PRODUCT_VALIDATION_SCHEMA = Yup.object().shape({
name: Yup.string().required(YUP_EMPTY_VALUE_ERROR_MESSAGE),
sku: Yup.string()
.required(YUP_EMPTY_VALUE_ERROR_MESSAGE)
.matches(/^[a-zA-Z0-9_-]+$/, "SKU must consist of alphanumeric characters, dashes, or underscores."),
category: Yup.mixed().oneOf(
Object.values(PRODUCT_CATEGORY_TYPE).map((value) => value.value?.toString()),
"Invalid product category."
)
.required("Product category is required."),
price: Yup.number()
.required(YUP_EMPTY_VALUE_ERROR_MESSAGE)
.min(0, "Price must be a positive number."),
stock: Yup.number()
.required(YUP_EMPTY_VALUE_ERROR_MESSAGE)
.min(0, "Stock must be a non-negative number."),
productType: Yup.mixed()
.oneOf(Object.values(ProductType) as ProductType[], "Invalid product type.")
.required(YUP_EMPTY_VALUE_ERROR_MESSAGE),
description: Yup.string().nullable(),
createdAt: Yup.date().nullable(),
updatedAt: Yup.date().nullable(),
});
export const PRODUCT_CREATE_INITIAL_VALUES = {
name: "",
sku: "",
category: UI_COMMON_VALUES.SELECT_OPTION_EMPTY, // Example: "Electronics"
price: 0, // Example: 100
stock: 0, // Example: 50
productType: UI_COMMON_VALUES.SELECT_OPTION_EMPTY, // "Physical" or "Digital"
description: "", // Example: "This is a sample product description."
createdAt: undefined,
updatedAt: undefined,
};
export const PRODUCT_UPDATE_INITIAL_VALUES = {
name: "",
sku: "",
category: UI_COMMON_VALUES.SELECT_OPTION_EMPTY,
price: 0,
stock: 0,
productType: UI_COMMON_VALUES.SELECT_OPTION_EMPTY,
description: "",
createdAt: undefined,
updatedAt: undefined,
};
```
## APIs
The `headless-formik-helper` library provides the following utilities to enhance the functionality of Formik forms:
### 1. **onKeyValueChangeByNameValue**
Updates a specific field in Formik using its name and a new value.
**Signature:**
```typescript
({
name,
value,
}: {
name: keyof T & string;
value: any;
}) => void;
```
### 2. **onKeyValueChangeByNameIndexFieldValueMemoized**
Handles updates for complex structures like arrays or objects with nested fields. Supports actions like adding or removing items.
**Signature:**
```typescript
({
name,
index,
field,
value,
action,
newItem,
}: {
name: keyof T & string;
index?: number;
field?: string;
value?: any;
action?: 'add' | 'remove';
newItem?: any;
}) => void;
```
### 3. **normalizeFormikValues**
Normalizes Formik values before sending them to the server, ensuring compliance with backend requirements.
**Signature:**
```typescript
>(obj: T) => T;
```
### 4. **onKeyValueChangeByNameIndexFieldTouchedMemoized**
Marks a specific field in a nested structure as "touched" based on its name, index, and field name.
**Signature:**
```typescript
({
name,
index,
field,
}: {
name: keyof T & string;
index: number;
field: string;
}) => void;
```
### 5. **onKeyValueChangeByEvent**
Handles changes in Formik fields triggered by standard React change events (e.g., ``).
**Signature:**
```typescript
(e: React.ChangeEvent) => void;
```
### 6. **onKeyValueChangeByNameValueMemoized**
A memoized version of `onKeyValueChangeByNameValue` for optimizing updates to Formik fields.
**Signature:**
```typescript
({
name,
value,
}: KeyValueChangeByNameValueMemoized) => void;
```
### 7. **formikValuesChanged**
Indicates whether Formik values have changed from their initial state.
**Signature:**
```typescript
T | boolean;
```
### 8. **onKeyValueChangeByEventMemoized**
A memoized version of `onKeyValueChangeByEvent` for efficiently handling React change events.
**Signature:**
```typescript
(e: React.ChangeEvent) => void;
```
---