Skip to content

Commit 7e81d04

Browse files
committed
feat: allow to configure transient props
Allow to configure transient props for a specific component using the `transientProps` constructor. Closes #29
1 parent 98c6d23 commit 7e81d04

File tree

3 files changed

+96
-11
lines changed

3 files changed

+96
-11
lines changed

src/index.test.tsx

+22-4
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,34 @@ describe("twc", () => {
131131

132132
test("accepts a function to define className", () => {
133133
type Props = {
134-
size: "sm" | "lg";
134+
$size: "sm" | "lg";
135135
children: React.ReactNode;
136136
};
137137
const Title = twc.h1<Props>((props) => ({
138-
"text-xl": props.size === "lg",
139-
"text-sm": props.size === "sm",
138+
"text-xl": props.$size === "lg",
139+
"text-sm": props.$size === "sm",
140140
}));
141-
render(<Title size="sm">Title</Title>);
141+
render(<Title $size="sm">Title</Title>);
142142
const title = screen.getByText("Title");
143143
expect(title).toBeDefined();
144+
expect(title.getAttribute("$size")).toBe(null);
145+
expect(title.tagName).toBe("H1");
146+
expect(title.classList.contains("text-sm")).toBe(true);
147+
});
148+
149+
test("allows to customize transient props", () => {
150+
type Props = {
151+
xsize: "sm" | "lg";
152+
children: React.ReactNode;
153+
};
154+
const Title = twc.h1.transientProps(["xsize"])<Props>((props) => ({
155+
"text-xl": props.xsize === "lg",
156+
"text-sm": props.xsize === "sm",
157+
}));
158+
render(<Title xsize="sm">Title</Title>);
159+
const title = screen.getByText("Title");
160+
expect(title).toBeDefined();
161+
expect(title.getAttribute("xsize")).toBe(null);
144162
expect(title.tagName).toBe("H1");
145163
expect(title.classList.contains("text-sm")).toBe(true);
146164
});

src/index.tsx

+27-6
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,23 @@ type FirstLevelTemplate<
5050
TCompose extends AbstractCompose,
5151
TExtraProps,
5252
> = Template<TComponent, TCompose, TExtraProps> & {
53+
/**
54+
* Add additional props to the component.
55+
*/
5356
attrs: <TProps = undefined>(
5457
attrs:
5558
| Record<string, any>
5659
| ((
5760
props: ResultProps<TComponent, TProps, TExtraProps, TCompose>,
5861
) => Record<string, any>),
5962
) => Template<TComponent, TCompose, TExtraProps, TProps>;
63+
} & {
64+
/**
65+
* Prevent props from being forwarded to the component.
66+
*/
67+
transientProps: (
68+
fn: string[] | ((prop: string) => boolean),
69+
) => FirstLevelTemplate<TComponent, TCompose, TExtraProps>;
6070
};
6171

6272
type Twc<TCompose extends AbstractCompose> = (<T extends React.ElementType>(
@@ -69,8 +79,6 @@ type Twc<TCompose extends AbstractCompose> = (<T extends React.ElementType>(
6979
>;
7080
};
7181

72-
type ShouldForwardProp = (prop: string) => boolean;
73-
7482
export type TwcComponentProps<
7583
TComponent extends React.ElementType,
7684
TCompose extends AbstractCompose = typeof clsx,
@@ -85,12 +93,12 @@ export type Config<TCompose extends AbstractCompose> = {
8593
* The function to use to determine if a prop should be forwarded to the
8694
* underlying component. Defaults to `prop => prop[0] !== "$"`.
8795
*/
88-
shouldForwardProp?: ShouldForwardProp;
96+
shouldForwardProp?: (prop: string) => boolean;
8997
};
9098

9199
function filterProps(
92100
props: Record<string, any>,
93-
shouldForwardProp: ShouldForwardProp,
101+
shouldForwardProp: (prop: string) => boolean,
94102
) {
95103
const filteredProps: Record<string, any> = {};
96104
const keys = Object.keys(props);
@@ -109,10 +117,13 @@ export const createTwc = <TCompose extends AbstractCompose = typeof clsx>(
109117
config: Config<TCompose> = {},
110118
) => {
111119
const compose = config.compose || clsx;
112-
const shouldForwardProp =
120+
const defaultShouldForwardProp =
113121
config.shouldForwardProp || ((prop) => prop[0] !== "$");
114122
const wrap = (Component: React.ElementType) => {
115-
const createTemplate = (attrs?: Attributes) => {
123+
const createTemplate = (
124+
attrs?: Attributes,
125+
shouldForwardProp = defaultShouldForwardProp,
126+
) => {
116127
const template = (
117128
stringsOrFn: TemplateStringsArray | Function,
118129
...values: any[]
@@ -147,6 +158,16 @@ export const createTwc = <TCompose extends AbstractCompose = typeof clsx>(
147158
});
148159
};
149160

161+
template.transientProps = (
162+
fnOrArray: string[] | ((prop: string) => boolean),
163+
) => {
164+
const shouldForwardProp =
165+
typeof fnOrArray === "function"
166+
? (prop: string) => !fnOrArray(prop)
167+
: (prop: string) => !fnOrArray.includes(prop);
168+
return createTemplate(attrs, shouldForwardProp);
169+
};
170+
150171
if (attrs === undefined) {
151172
template.attrs = (attrs: Attributes) => {
152173
return createTemplate(attrs);

website/pages/docs/guides/adapting-based-on-props.mdx

+47-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ return props accepted by a `twc` component. It's similar to `React.ComponentProp
3030
<details>
3131
<summary>Why is the prop prefixed by a dollar?</summary>
3232

33-
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 `<button>` will not get a `<button $primary="true">` attribute in the DOM.
33+
We call the prop `$primary` a "transient prop". A transient prop starts with a `$`, it can be consumed by the uppermost component layer but are not passed to the underlying components. It our case, it means the `<button>` will not get a `<button $primary="true">` attribute in the DOM.
3434

3535
</details>
3636

@@ -66,3 +66,49 @@ export default () => (
6666
</div>
6767
);
6868
```
69+
70+
## Customize transient props
71+
72+
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.
73+
74+
If you don't like the `$` prefix, you can customize transient props for a specific component using `transientProps` constructor.
75+
76+
```tsx {8} /props/ /$primary/
77+
import { twc, TwcComponentProps } from "react-twc";
78+
79+
type ButtonProps = TwcComponentProps<"button"> & { primary?: boolean };
80+
81+
// The "primary" prop is marked as transient
82+
const Button = twc.button.transientProps(["primary"])<ButtonProps>((props) => [
83+
"font-semibold border border-blue-500 rounded",
84+
props.primary ? "bg-blue-500 text-white" : "bg-white text-gray-800",
85+
]);
86+
87+
export default () => (
88+
<div>
89+
<Button>Normal</Button>
90+
<Button primary>Primary</Button>
91+
</div>,
92+
);
93+
```
94+
95+
`transientProps` also accepts a function:
96+
97+
```tsx
98+
const Button = twc.button.transientProps(
99+
(prop) => prop === "primary",
100+
)<ButtonProps>((props) => [
101+
"font-semibold border border-blue-500 rounded",
102+
props.primary ? "bg-blue-500 text-white" : "bg-white text-gray-800",
103+
]);
104+
```
105+
106+
It is also possible to configure this behaviour globally by creating a custom instance of `twc`:
107+
108+
```ts filename="utils.ts" {11}
109+
import { clsx } from "clsx";
110+
import { createTwc } from "react-twc";
111+
112+
// Forward all props not starting by "_"
113+
export const twx = createTwc({ shouldForwardProp: (prop) => prop[0] !== "_" });
114+
```

0 commit comments

Comments
 (0)