Skip to content

Commit

Permalink
feat: allow for styled to parse additional props
Browse files Browse the repository at this point in the history
  • Loading branch information
marklawlor committed May 18, 2022
1 parent 4054966 commit 1037027
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 49 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Expand Up @@ -21,6 +21,7 @@ module.exports = {
"error",
{
allowList: {
prop: true,
props: true,
Props: true,
ref: true,
Expand Down
30 changes: 30 additions & 0 deletions __tests__/styled/__snapshots__/props.tsx.snap
@@ -0,0 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Styled - Custom Props can style custom props 1`] = `
Array [
<View
style={
Array [
Object {
"marginBottom": 4,
"marginLeft": 4,
"marginRight": 4,
"marginTop": 4,
},
]
}
/>,
<View
style={
Array [
Object {
"paddingBottom": 8,
"paddingLeft": 8,
"paddingRight": 8,
"paddingTop": 8,
},
]
}
/>,
]
`;
30 changes: 30 additions & 0 deletions __tests__/styled/props.tsx
@@ -0,0 +1,30 @@
import { render } from "@testing-library/react-native";
import { View, ViewProps, ViewStyle } from "react-native";
import { styled } from "../../src";
import { TestProvider } from "../tailwindcss/runner";

const StyledTestComponent = styled(
({ style, style2 }: ViewProps & { style2: ViewStyle }) => {
return (
<>
<View style={style} />
<View style={style2} />
</>
);
},
{
props: ["style2"],
}
) as any;

describe("Styled - Custom Props", () => {
test("can style custom props", () => {
const tree = render(
<TestProvider css="m-1 p-2">
<StyledTestComponent className="m-1" style2="p-2" />
</TestProvider>
).toJSON();

expect(tree).toMatchSnapshot();
});
});
93 changes: 44 additions & 49 deletions src/styled.tsx
Expand Up @@ -3,17 +3,14 @@ import {
FunctionComponent,
ComponentClass,
PropsWithChildren,
Children,
cloneElement,
ComponentProps,
} from "react";
import { ImageStyle, StyleProp, TextStyle, ViewStyle } from "react-native";
import { useTailwind } from "./use-tailwind";
import { ChildClassNameSymbol } from "./utils/child-styles";
import { isFragment } from "react-is";
import { useInteraction } from "./use-interaction";
import { ComponentContext } from "./context";
import { matchChildAtRule } from "./match-at-rule";
import { useStyledProps } from "./use-styled-props";
import { useStyledChildren } from "./use-styled-children";

type StyledProps<P> = PropsWithChildren<
P & {
Expand All @@ -25,68 +22,66 @@ type StyledProps<P> = PropsWithChildren<

type Component<P> = string | FunctionComponent<P> | ComponentClass<P>;

export interface StyledOptions<P> {
props?: boolean | Array<keyof P & string>;
}

// Transform no props
export function styled<P>(
Component: Component<P>,
options?: { props: false }
): FunctionComponent<StyledProps<P>>;
// Transform extra props
export function styled<P>(
Component: Component<P>,
options: { props: Array<keyof P & string> }
): FunctionComponent<StyledProps<P & Record<keyof P, string>>>;
// Transform all props
export function styled<P>(
Component: Component<P>
): FunctionComponent<StyledProps<P>> {
Component: Component<P>,
options: { props: true }
): FunctionComponent<StyledProps<P & Record<keyof P, string>>>;
// Implementation
export function styled<P>(
Component: Component<P>,
{ props: propsToTransform }: StyledOptions<P> = {}
) {
function Styled({
className,
tw,
style: styleProperty,
style: componentStyles,
children: componentChildren,
...props
...componentProps
}: StyledProps<P>) {
const { hover, focus, active, ...handlers } = useInteraction(props);
const { hover, focus, active, ...handlers } =
useInteraction(componentProps);

const classes = tw ?? className ?? "";

const tailwindStyles = useTailwind({
const twCallback = useTailwind({
hover,
focus,
active,
flatten: false,
})(classes);

const style = styleProperty
? [tailwindStyles, styleProperty]
: tailwindStyles;
});

let children = isFragment(componentChildren)
? // This probably needs to be recursive
componentChildren.props.children
: componentChildren;

if (tailwindStyles[ChildClassNameSymbol]) {
children = Children.map(children, (child, index) => {
const childStyles: P[] = [];
for (const { atRules, ...styles } of tailwindStyles[
ChildClassNameSymbol
] ?? []) {
const matches = atRules.every(([rule, params]) => {
return matchChildAtRule({
nthChild: index + 1,
rule,
params,
});
});
if (matches) {
childStyles.push(styles as P);
}
}
const { childStyles, ...styledProps } = useStyledProps({
tw: twCallback,
classes,
componentStyles,
propsToTransform,
componentProps,
});

return cloneElement(child, {
style: child.props.style
? [child.props.style, childStyles]
: childStyles.length > 0
? childStyles
: undefined,
});
});
}
const children = useStyledChildren({
componentChildren,
childStyles,
});

const element = createElement(Component, {
...props,
...componentProps,
...handlers,
style,
...styledProps,
children,
} as unknown as P);

Expand Down
47 changes: 47 additions & 0 deletions src/use-styled-children.ts
@@ -0,0 +1,47 @@
import { ReactNode, Children, cloneElement } from "react";
import { isFragment } from "react-is";
import { matchChildAtRule } from "./match-at-rule";
import { AtRuleRecord } from "./types/common";

export interface UseStyledChildrenOptions {
componentChildren: ReactNode;
childStyles?: AtRuleRecord[];
}

export function useStyledChildren({
componentChildren,
childStyles,
}: UseStyledChildrenOptions): ReactNode {
let children = isFragment(componentChildren)
? // This probably needs to be recursive
componentChildren.props.children
: componentChildren;

if (childStyles) {
children = Children.map(children, (child, index) => {
const matchingStyles = [];
for (const { atRules, ...styles } of childStyles) {
const matches = atRules.every(([rule, params]) => {
return matchChildAtRule({
nthChild: index + 1,
rule,
params,
});
});
if (matches) {
matchingStyles.push(styles);
}
}

return cloneElement(child, {
style: child.props.style
? [child.props.style, matchingStyles]
: matchingStyles.length > 0
? matchingStyles
: undefined,
});
});
}

return children;
}
47 changes: 47 additions & 0 deletions src/use-styled-props.ts
@@ -0,0 +1,47 @@
import { StyleProp } from "react-native";
import { UseTailwindCallback } from "./use-tailwind";
import { ChildClassNameSymbol } from "./utils/child-styles";

export interface UseStyledPropsOptions {
tw: UseTailwindCallback<any>;
classes: string | undefined;
componentStyles: StyleProp<any>;
propsToTransform?: boolean | string[];
componentProps: Record<string, unknown>;
}

export function useStyledProps({
tw,
classes,
componentStyles,
propsToTransform,
componentProps,
}: UseStyledPropsOptions) {
const mainStyles = tw(classes);

const style = componentStyles ? [mainStyles, componentStyles] : mainStyles;

const styledProps: Record<string, unknown> = {};

if (propsToTransform) {
if (propsToTransform === true) {
propsToTransform = Object.keys(componentProps);
}

for (const prop of propsToTransform) {
const value = componentProps[prop];

if (typeof value === "string") {
styledProps[prop] = tw(value);
}
}
}

console.log(styledProps);

return {
childStyles: mainStyles[ChildClassNameSymbol],
style,
...styledProps,
};
}

0 comments on commit 1037027

Please sign in to comment.