An open API service indexing awesome lists of open source software.

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

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;
```

---