From 15fc3b619b1e8356d66c1556887dd2db123b177c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Fri, 29 Dec 2023 09:21:05 +0400 Subject: [PATCH] feat: allow to configure transient props Allow to configure transient props for a specific component using the `transientProps` constructor. Closes #29 --- README.md | 2 +- src/index.test.tsx | 45 ++++++++++++++-- src/index.tsx | 33 +++++++++--- .../docs/guides/adapting-based-on-props.mdx | 53 ++++++++++++++++++- website/pages/index.mdx | 2 +- 5 files changed, 121 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 3f5100c..feb5ebe 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## Features -- ⚡️ Lightweight — only 0.46kb +- ⚡️ Lightweight — only 0.49kb - ✨ Autocompletion in all editors - 🎨 Adapt the style based on props - ♻️ Reuse classes with `asChild` prop diff --git a/src/index.test.tsx b/src/index.test.tsx index d971b4e..8d82a5c 100644 --- a/src/index.test.tsx +++ b/src/index.test.tsx @@ -131,16 +131,53 @@ describe("twc", () => { test("accepts a function to define className", () => { type Props = { - size: "sm" | "lg"; + $size: "sm" | "lg"; children: React.ReactNode; }; const Title = twc.h1((props) => ({ - "text-xl": props.size === "lg", - "text-sm": props.size === "sm", + "text-xl": props.$size === "lg", + "text-sm": props.$size === "sm", })); - render(Title); + render(Title); const title = screen.getByText("Title"); expect(title).toBeDefined(); + expect(title.getAttribute("$size")).toBe(null); + expect(title.tagName).toBe("H1"); + expect(title.classList.contains("text-sm")).toBe(true); + }); + + test("allows to customize transient props using array", () => { + type Props = { + xsize: "sm" | "lg"; + children: React.ReactNode; + }; + const Title = twc.h1.transientProps(["xsize"])((props) => ({ + "text-xl": props.xsize === "lg", + "text-sm": props.xsize === "sm", + })); + render(Title); + const title = screen.getByText("Title"); + expect(title).toBeDefined(); + expect(title.getAttribute("xsize")).toBe(null); + expect(title.tagName).toBe("H1"); + expect(title.classList.contains("text-sm")).toBe(true); + }); + + test("allows to customize transient props using function", () => { + type Props = { + xsize: "sm" | "lg"; + children: React.ReactNode; + }; + const Title = twc.h1.transientProps((prop) => prop === "xsize")( + (props) => ({ + "text-xl": props.xsize === "lg", + "text-sm": props.xsize === "sm", + }), + ); + render(Title); + const title = screen.getByText("Title"); + expect(title).toBeDefined(); + expect(title.getAttribute("xsize")).toBe(null); expect(title.tagName).toBe("H1"); expect(title.classList.contains("text-sm")).toBe(true); }); diff --git a/src/index.tsx b/src/index.tsx index 8ee8921..3f621d5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -50,6 +50,9 @@ type FirstLevelTemplate< TCompose extends AbstractCompose, TExtraProps, > = Template & { + /** + * Add additional props to the component. + */ attrs: ( attrs: | Record @@ -57,6 +60,13 @@ type FirstLevelTemplate< props: ResultProps, ) => Record), ) => Template; +} & { + /** + * Prevent props from being forwarded to the component. + */ + transientProps: ( + fn: string[] | ((prop: string) => boolean), + ) => FirstLevelTemplate; }; type Twc = (( @@ -69,8 +79,6 @@ type Twc = (( >; }; -type ShouldForwardProp = (prop: string) => boolean; - export type TwcComponentProps< TComponent extends React.ElementType, TCompose extends AbstractCompose = typeof clsx, @@ -85,12 +93,12 @@ export type Config = { * The function to use to determine if a prop should be forwarded to the * underlying component. Defaults to `prop => prop[0] !== "$"`. */ - shouldForwardProp?: ShouldForwardProp; + shouldForwardProp?: (prop: string) => boolean; }; function filterProps( props: Record, - shouldForwardProp: ShouldForwardProp, + shouldForwardProp: (prop: string) => boolean, ) { const filteredProps: Record = {}; const keys = Object.keys(props); @@ -109,10 +117,13 @@ export const createTwc = ( config: Config = {}, ) => { const compose = config.compose || clsx; - const shouldForwardProp = + const defaultShouldForwardProp = config.shouldForwardProp || ((prop) => prop[0] !== "$"); const wrap = (Component: React.ElementType) => { - const createTemplate = (attrs?: Attributes) => { + const createTemplate = ( + attrs?: Attributes, + shouldForwardProp = defaultShouldForwardProp, + ) => { const template = ( stringsOrFn: TemplateStringsArray | Function, ...values: any[] @@ -147,6 +158,16 @@ export const createTwc = ( }); }; + template.transientProps = ( + fnOrArray: string[] | ((prop: string) => boolean), + ) => { + const shouldForwardProp = + typeof fnOrArray === "function" + ? (prop: string) => !fnOrArray(prop) + : (prop: string) => !fnOrArray.includes(prop); + return createTemplate(attrs, shouldForwardProp); + }; + if (attrs === undefined) { template.attrs = (attrs: Attributes) => { return createTemplate(attrs); diff --git a/website/pages/docs/guides/adapting-based-on-props.mdx b/website/pages/docs/guides/adapting-based-on-props.mdx index a30c0f5..898214d 100644 --- a/website/pages/docs/guides/adapting-based-on-props.mdx +++ b/website/pages/docs/guides/adapting-based-on-props.mdx @@ -1,6 +1,6 @@ # Adapting based on props -You can pass a function to `twc` to adapt the components based on props. +You can pass a function to `twc` to adapt the classes based on props. ## Usage @@ -30,7 +30,7 @@ return props accepted by a `twc` component. It's similar to `React.ComponentProp
Why is the prop prefixed by a dollar? -We call the prop `$primary` a "transient prop", transient props can be consumed by the components but are not passed to the underlying components. It our case, it means the `
@@ -66,3 +66,52 @@ export default () => ( ); ``` + +## Customize transient props + +By default, all props starting with a `$` are considered _transient_. This is a is a hint that it is meant exclusively for the uppermost component layer and should not be passed further down. In other terms, it prevents your DOM element to have unexpected props. + +If you don't like the `$` prefix, you can customize transient props for a specific component using `transientProps` constructor. + +```tsx /transientProps(["primary"])/ +import { twc, TwcComponentProps } from "react-twc"; + +type ButtonProps = TwcComponentProps<"button"> & { primary?: boolean }; + +// The "primary" prop is marked as transient +const Button = twc.button.transientProps(["primary"])((props) => [ + "font-semibold border border-blue-500 rounded", + props.primary ? "bg-blue-500 text-white" : "bg-white text-gray-800", +]); + +export default () => ( +
+ + {/* The "primary" attribute will not be forwarded to the +
, +); +``` + +`transientProps` also accepts a function: + +```tsx /transientProps/ +const Button = twc.button.transientProps( + (prop) => prop === "primary", +)((props) => [ + "font-semibold border border-blue-500 rounded", + props.primary ? "bg-blue-500 text-white" : "bg-white text-gray-800", +]); +``` + +It is also possible to configure this behaviour globally by creating a custom instance of `twc`: + +```ts {5,6} +import { clsx } from "clsx"; +import { createTwc } from "react-twc"; + +export const twx = createTwc({ + // Forward all props not starting by "_" + shouldForwardProp: (prop) => prop[0] !== "_", +}); +``` diff --git a/website/pages/index.mdx b/website/pages/index.mdx index 9984b67..0967fc0 100644 --- a/website/pages/index.mdx +++ b/website/pages/index.mdx @@ -47,7 +47,7 @@ const Card = twc.div`rounded-lg border bg-slate-100 text-white shadow-sm`; With just one single line of code, you can create a reusable component with all these amazing features out-of-the-box: -- ⚡️ Lightweight — only 0.46kb +- ⚡️ Lightweight — only 0.49kb - ✨ Autocompletion in all editors - 🎨 Adapt the style based on props - ♻️ Reuse classes with `asChild` prop