Skip to content

Commit d1d4fdd

Browse files
authored
Data attribute hook [copy of #921] [Updated] (#934)
1 parent 4f09610 commit d1d4fdd

File tree

3 files changed

+100
-14
lines changed

3 files changed

+100
-14
lines changed

src/components/ui/Button/Button.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, { ButtonHTMLAttributes, DetailedHTMLProps, PropsWithChildren } fro
33
import { customClassSwitcher } from '~/core';
44
import { clsx } from 'clsx';
55
import ButtonPrimitive from '~/core/primitives/Button';
6+
import { useCreateDataAttribute, useComposeAttributes } from "~/core/hooks/createDataAttribute"
67

78
// make the color prop default accent color
89
const COMPONENT_NAME = 'Button';
@@ -18,25 +19,15 @@ const Button = ({ children, type = 'button', customRootClass = '', className = '
1819
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
1920
// apply data attribute for accent color
2021
// apply attribute only if color is present
21-
const data_attributes: Record<string, string> = {};
22-
23-
if (variant) {
24-
data_attributes['data-button-variant'] = variant;
25-
}
26-
27-
if (size) {
28-
data_attributes['data-button-size'] = size;
29-
}
30-
31-
if (color) {
32-
data_attributes['data-accent-color'] = color;
33-
}
22+
const dataAttributes = useCreateDataAttribute("button", { variant, size });
23+
const accentAttributes = useCreateDataAttribute("accent", { color });
24+
const composedAttributes = useComposeAttributes(dataAttributes(), accentAttributes());
3425

3526
return (
3627
<ButtonPrimitive
3728
type={type}
3829
className={clsx(rootClass, className)}
39-
{...data_attributes}
30+
{...composedAttributes()}
4031
{...props}
4132
>
4233
{children}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { renderHook } from "@testing-library/react";
2+
import { useCreateDataAttribute, useComposeAttributes } from ".";
3+
4+
/**
5+
* Test case: Verify that attributes are correctly created and applied.
6+
*/
7+
test("attributes are created and applied", () => {
8+
// Render the hook for creating data attributes with the prefix "button"
9+
const { result: dataAttributes } = renderHook(() =>
10+
useCreateDataAttribute("button", { variant: "primary", size: "large" })
11+
);
12+
13+
// Render the hook for creating data attributes with the prefix "accent"
14+
const { result: accentAttributes } = renderHook(() =>
15+
useCreateDataAttribute("accent", { color: "red" })
16+
);
17+
18+
// Render the hook that merges the two attribute objects into a single object
19+
const { result: composedAttributes } = renderHook(() =>
20+
useComposeAttributes(dataAttributes.current(), accentAttributes.current())
21+
);
22+
23+
// Assert that the composed attributes object contains the expected `data-*` attributes
24+
expect(composedAttributes.current()).toEqual({
25+
"data-button-variant": "primary",
26+
"data-button-size": "large",
27+
"data-accent-color": "red",
28+
});
29+
});
30+
31+
/**
32+
* Test case: Verify that attributes are correctly created, ignoring undefined or empty values.
33+
*/
34+
test("attributes are created and applied with undefined and empty values", () => {
35+
// Render the hook with an undefined variant and a defined size
36+
const { result: dataAttributes } = renderHook(() =>
37+
useCreateDataAttribute("button", { variant: undefined, size: "large" })
38+
);
39+
40+
// Render the hook with an empty string for color (should be ignored)
41+
const { result: accentAttributes } = renderHook(() =>
42+
useCreateDataAttribute("accent", { color: "" })
43+
);
44+
45+
// Merge the attributes
46+
const { result: composedAttributes } = renderHook(() =>
47+
useComposeAttributes(dataAttributes.current(), accentAttributes.current())
48+
);
49+
50+
// Assert that only the valid `data-*` attributes are present
51+
expect(composedAttributes.current()).toEqual({
52+
"data-button-size": "large", // "variant" is ignored since it's undefined, and "color" is ignored since it's an empty string
53+
});
54+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { useCallback } from "react";
2+
3+
/**
4+
* Custom hook to generate `data-*` attributes dynamically.
5+
*
6+
* @param {string} prefix - The prefix to be used for the data attributes (e.g., "button" for "data-button-*").
7+
* @param {Record<string, any> | null} attributes - An object containing key-value pairs of attributes.
8+
* @returns {Function} - A memoized function that returns an object containing formatted `data-*` attributes.
9+
*/
10+
export const useCreateDataAttribute = (
11+
prefix: string,
12+
attributes: Record<string, any> | null
13+
) => {
14+
return useCallback(() => {
15+
// If attributes is null, return an empty object
16+
if (!attributes) return {};
17+
18+
// Transform the attributes object into `data-*` attributes
19+
return Object.fromEntries(
20+
Object.entries(attributes)
21+
.filter(([_, value]) => value !== undefined && value !== "") // Remove undefined or empty values
22+
.map(([key, value]) => [`data-${prefix}-${key}`, value]) // Convert keys to `data-prefix-key`
23+
);
24+
}, [prefix, attributes]); // Dependencies: recompute only if prefix or attributes change
25+
};
26+
27+
/**
28+
* Custom hook to merge multiple attribute objects into a single object.
29+
*
30+
* @param {...(Record<string, any> | null)[]} attributeObjects - Multiple attribute objects to be combined.
31+
* @returns {Function} - A memoized function that returns a merged attributes object.
32+
*/
33+
export const useComposeAttributes = (
34+
...attributeObjects: (Record<string, any> | null)[]
35+
) => {
36+
return useCallback(() => {
37+
// Merge all attribute objects, ignoring null values
38+
return Object.assign({}, ...attributeObjects.filter((obj) => obj !== null));
39+
}, [attributeObjects]); // Dependencies: recompute only if attributeObjects change
40+
};
41+

0 commit comments

Comments
 (0)