Skip to content

Commit 15fc3b6

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 15fc3b6

File tree

5 files changed

+121
-14
lines changed

5 files changed

+121
-14
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
## Features
1010

11-
- ⚡️ Lightweight — only 0.46kb
11+
- ⚡️ Lightweight — only 0.49kb
1212
- ✨ Autocompletion in all editors
1313
- 🎨 Adapt the style based on props
1414
- ♻️ Reuse classes with `asChild` prop

src/index.test.tsx

+41-4
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,53 @@ 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 using array", () => {
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);
162+
expect(title.tagName).toBe("H1");
163+
expect(title.classList.contains("text-sm")).toBe(true);
164+
});
165+
166+
test("allows to customize transient props using function", () => {
167+
type Props = {
168+
xsize: "sm" | "lg";
169+
children: React.ReactNode;
170+
};
171+
const Title = twc.h1.transientProps((prop) => prop === "xsize")<Props>(
172+
(props) => ({
173+
"text-xl": props.xsize === "lg",
174+
"text-sm": props.xsize === "sm",
175+
}),
176+
);
177+
render(<Title xsize="sm">Title</Title>);
178+
const title = screen.getByText("Title");
179+
expect(title).toBeDefined();
180+
expect(title.getAttribute("xsize")).toBe(null);
144181
expect(title.tagName).toBe("H1");
145182
expect(title.classList.contains("text-sm")).toBe(true);
146183
});

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

+51-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Adapting based on props
22

3-
You can pass a function to `twc` to adapt the components based on props.
3+
You can pass a function to `twc` to adapt the classes based on props.
44

55
## Usage
66

@@ -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,52 @@ 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 /transientProps(["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+
{/* The "primary" attribute will not be forwarded to the <button> element. */}
91+
<Button primary>Primary</Button>
92+
</div>,
93+
);
94+
```
95+
96+
`transientProps` also accepts a function:
97+
98+
```tsx /transientProps/
99+
const Button = twc.button.transientProps(
100+
(prop) => prop === "primary",
101+
)<ButtonProps>((props) => [
102+
"font-semibold border border-blue-500 rounded",
103+
props.primary ? "bg-blue-500 text-white" : "bg-white text-gray-800",
104+
]);
105+
```
106+
107+
It is also possible to configure this behaviour globally by creating a custom instance of `twc`:
108+
109+
```ts {5,6}
110+
import { clsx } from "clsx";
111+
import { createTwc } from "react-twc";
112+
113+
export const twx = createTwc({
114+
// Forward all props not starting by "_"
115+
shouldForwardProp: (prop) => prop[0] !== "_",
116+
});
117+
```

website/pages/index.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const Card = twc.div`rounded-lg border bg-slate-100 text-white shadow-sm`;
4747

4848
With just one single line of code, you can create a reusable component with all these amazing features out-of-the-box:
4949

50-
- ⚡️ Lightweight — only 0.46kb
50+
- ⚡️ Lightweight — only 0.49kb
5151
- ✨ Autocompletion in all editors
5252
- 🎨 Adapt the style based on props
5353
- ♻️ Reuse classes with `asChild` prop

0 commit comments

Comments
 (0)