Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transient props #30

Merged
merged 1 commit into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 41 additions & 4 deletions src/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>((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 using array", () => {
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);
});

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>(
(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
53 changes: 51 additions & 2 deletions website/pages/docs/guides/adapting-based-on-props.mdx
Original file line number Diff line number Diff line change
@@ -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

Expand Down 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,52 @@ 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 /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"])<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>
{/* The "primary" attribute will not be forwarded to the <button> element. */}
<Button primary>Primary</Button>
</div>,
);
```

`transientProps` also accepts a function:

```tsx /transientProps/
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 {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] !== "_",
});
```
2 changes: 1 addition & 1 deletion website/pages/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down