diff --git a/.eslintrc.js b/.eslintrc.js index fd9ba02..2a6f399 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -21,6 +21,7 @@ module.exports = { "error", { allowList: { + prop: true, props: true, Props: true, ref: true, diff --git a/__tests__/styled/__snapshots__/props.tsx.snap b/__tests__/styled/__snapshots__/props.tsx.snap new file mode 100644 index 0000000..c0c3066 --- /dev/null +++ b/__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 [ + , + , +] +`; diff --git a/__tests__/styled/props.tsx b/__tests__/styled/props.tsx new file mode 100644 index 0000000..9b53ee2 --- /dev/null +++ b/__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 ( + <> + + + + ); + }, + { + props: ["style2"], + } +) as any; + +describe("Styled - Custom Props", () => { + test("can style custom props", () => { + const tree = render( + + + + ).toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/styled.tsx b/src/styled.tsx index b9688a2..e0d6bec 100644 --- a/src/styled.tsx +++ b/src/styled.tsx @@ -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

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

= PropsWithChildren< type Component

= string | FunctionComponent

| ComponentClass

; +export interface StyledOptions

{ + props?: boolean | Array; +} + +// Transform no props export function styled

( - Component: Component

-): FunctionComponent> { + Component: Component

, + options?: { props: false } +): FunctionComponent>; +// Transform extra props +export function styled

( + Component: Component

, + options: { props: Array } +): FunctionComponent>>; +// Transform all props +export function styled

( + Component: Component

, + options: { props: true } +): FunctionComponent>>; +// Implementation +export function styled

( + Component: Component

, + { props: propsToTransform }: StyledOptions

= {} +) { function Styled({ className, tw, - style: styleProperty, + style: componentStyles, children: componentChildren, - ...props + ...componentProps }: StyledProps

) { - 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; + const { childStyles, ...styledProps } = useStyledProps({ + tw: twCallback, + classes, + componentStyles, + propsToTransform, + componentProps, + }); - 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); - } - } - - 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); diff --git a/src/use-styled-children.ts b/src/use-styled-children.ts new file mode 100644 index 0000000..d8883c7 --- /dev/null +++ b/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; +} diff --git a/src/use-styled-props.ts b/src/use-styled-props.ts new file mode 100644 index 0000000..3ba5c71 --- /dev/null +++ b/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; + classes: string | undefined; + componentStyles: StyleProp; + propsToTransform?: boolean | string[]; + componentProps: Record; +} + +export function useStyledProps({ + tw, + classes, + componentStyles, + propsToTransform, + componentProps, +}: UseStyledPropsOptions) { + const mainStyles = tw(classes); + + const style = componentStyles ? [mainStyles, componentStyles] : mainStyles; + + const styledProps: Record = {}; + + 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, + }; +}