Skip to content

Commit

Permalink
feat: allow to configure transient props
Browse files Browse the repository at this point in the history
Allow to configure transient props for a specific component using the
`transientProps` constructor.

Closes #29
  • Loading branch information
gregberge committed Dec 29, 2023
1 parent 98c6d23 commit 7e81d04
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 11 deletions.
26 changes: 22 additions & 4 deletions src/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,34 @@ 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>((props) => ({
"text-xl": props.size === "lg",
"text-sm": props.size === "sm",
"text-xl": props.$size === "lg",
"text-sm": props.$size === "sm",
}));
render(<Title size="sm">Title</Title>);
render(<Title $size="sm">Title</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", () => {
type Props = {
xsize: "sm" | "lg";
children: React.ReactNode;
};
const Title = twc.h1.transientProps(["xsize"])<Props>((props) => ({
"text-xl": props.xsize === "lg",
"text-sm": props.xsize === "sm",
}));
render(<Title xsize="sm">Title</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);
});
Expand Down
33 changes: 27 additions & 6 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,23 @@ type FirstLevelTemplate<
TCompose extends AbstractCompose,
TExtraProps,
> = Template<TComponent, TCompose, TExtraProps> & {
/**
* Add additional props to the component.
*/
attrs: <TProps = undefined>(
attrs:
| Record<string, any>
| ((
props: ResultProps<TComponent, TProps, TExtraProps, TCompose>,
) => Record<string, any>),
) => Template<TComponent, TCompose, TExtraProps, TProps>;
} & {
/**
* Prevent props from being forwarded to the component.
*/
transientProps: (
fn: string[] | ((prop: string) => boolean),
) => FirstLevelTemplate<TComponent, TCompose, TExtraProps>;
};

type Twc<TCompose extends AbstractCompose> = (<T extends React.ElementType>(
Expand All @@ -69,8 +79,6 @@ type Twc<TCompose extends AbstractCompose> = (<T extends React.ElementType>(
>;
};

type ShouldForwardProp = (prop: string) => boolean;

export type TwcComponentProps<
TComponent extends React.ElementType,
TCompose extends AbstractCompose = typeof clsx,
Expand All @@ -85,12 +93,12 @@ export type Config<TCompose extends AbstractCompose> = {
* 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<string, any>,
shouldForwardProp: ShouldForwardProp,
shouldForwardProp: (prop: string) => boolean,
) {
const filteredProps: Record<string, any> = {};
const keys = Object.keys(props);
Expand All @@ -109,10 +117,13 @@ export const createTwc = <TCompose extends AbstractCompose = typeof clsx>(
config: Config<TCompose> = {},
) => {
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[]
Expand Down Expand Up @@ -147,6 +158,16 @@ export const createTwc = <TCompose extends AbstractCompose = typeof clsx>(
});
};

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);
Expand Down
48 changes: 47 additions & 1 deletion website/pages/docs/guides/adapting-based-on-props.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ return props accepted by a `twc` component. It's similar to `React.ComponentProp
<details>
<summary>Why is the prop prefixed by a dollar?</summary>

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.
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.

</details>

Expand Down Expand Up @@ -66,3 +66,49 @@ export default () => (
</div>
);
```

## 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 {8} /props/ /$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"])<ButtonProps>((props) => [
"font-semibold border border-blue-500 rounded",
props.primary ? "bg-blue-500 text-white" : "bg-white text-gray-800",
]);

export default () => (
<div>
<Button>Normal</Button>
<Button primary>Primary</Button>
</div>,
);
```

`transientProps` also accepts a function:

```tsx
const Button = twc.button.transientProps(
(prop) => prop === "primary",
)<ButtonProps>((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 filename="utils.ts" {11}
import { clsx } from "clsx";
import { createTwc } from "react-twc";

// Forward all props not starting by "_"
export const twx = createTwc({ shouldForwardProp: (prop) => prop[0] !== "_" });
```

0 comments on commit 7e81d04

Please sign in to comment.