MathiasGilson / Tailwind-Styled-Component

Create Tailwind CSS React components like styled components with class names on multiple lines and conditional class rendering
MIT License
818 stars 43 forks source link

Support composition with any component, not just the ones created with `tw` #94

Open jahirfiquitiva opened 1 year ago

jahirfiquitiva commented 1 year ago

I would like this library to support styling any component and not just the ones created with tw

I have tried styling 2 pre-built components from Next without success.

1. Image

Shot 2023-04-10 at 16 58 38@2x

Shot 2023-04-10 at 16 58 55@2x

As you can see, it does not recognize the Image properties as valid

2. Link with the styles of a Button

Having created a StyledButton component with tw (const StyledButton = tw.button), I want a Link component to look like that StyledButton

Shot 2023-04-10 at 17 21 05@2x Shot 2023-04-10 at 17 55 12@2x

The whole error reads:

const otherProps: {
    title: string;
    openInNewTab?: boolean | undefined;
    download?: any;
    hrefLang?: string | undefined;
    media?: string | undefined;
    ping?: string | undefined;
    target?: HTMLAttributeAnchorTarget | undefined;
    ... 273 more ...;
    key?: Key | ... 1 more ... | undefined;
}
No overload matches this call.
  Overload 1 of 2, '(props: { onMouseEnter?: MouseEventHandler<HTMLButtonElement> | undefined; onTouchStart?: TouchEventHandler<HTMLButtonElement> | undefined; ... 272 more ...; $outlined?: boolean | undefined; } & { ...; }): ReactElement<...>', gave the following error.
    Type '(props: LinkProps) => JSX.Element' is not assignable to type 'undefined'.
  Overload 2 of 2, '(props: TailwindComponentPropsWith$As<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, { ...; }, (props: LinkProps) => Element, LinkProps>): ReactElement<...>', gave the following error.
    Type '{ title: string; openInNewTab?: boolean | undefined; download?: any; hrefLang?: string | undefined; media?: string | undefined; ping?: string | undefined; target?: HTMLAttributeAnchorTarget | undefined; ... 275 more ...; $outlined: boolean | undefined; }' is not assignable to type 'ClassAttributes<HTMLButtonElement>'.
      Types of property 'ref' are incompatible.
        Type 'Ref<HTMLAnchorElement> | undefined' is not assignable to type 'LegacyRef<HTMLButtonElement> | undefined'.
          Type '(instance: HTMLAnchorElement | null) => void' is not assignable to type 'LegacyRef<HTMLButtonElement> | undefined'.
            Type '(instance: HTMLAnchorElement | null) => void' is not assignable to type '(instance: HTMLButtonElement | null) => void'.
              Types of parameters 'instance' and 'instance' are incompatible.
                Type 'HTMLButtonElement | null' is not assignable to type 'HTMLAnchorElement | null'.
                  Type 'HTMLButtonElement' is missing the following properties from type 'HTMLAnchorElement': charset, coords, download, hreflang, and 19 more.ts(2769)
tailwind.d.ts(51, 9): The expected type comes from property '$as' which is declared here on type 'IntrinsicAttributes & { onMouseEnter?: MouseEventHandler<HTMLButtonElement> | undefined; onTouchStart?: TouchEventHandler<HTMLButtonElement> | undefined; ... 272 more ...; $outlined?: boolean | undefined; } & { ...; }'

Basically it is expecting otherProps to be of the type of props for StyledButton, even when I have set it to render $as={Link} and therefore should work with Link props

jahirfiquitiva commented 1 year ago

Before finding this library I was using a custom implementation which was working good enough although without many strict types.

anyway, my version of your templateFunctionFactory accepted an element of type WebTarget which I got from the styled-components repo, as is as follows:

import type { ExoticComponent, ComponentType } from 'react';

type AnyComponent<P = unknown> = ExoticComponent<P> | ComponentType<P>;

type KnownTarget = keyof JSX.IntrinsicElements | AnyComponent;

export type WebTarget = string | KnownTarget;

...

I hope this can help you set an initial step for better support of any component

jahirfiquitiva commented 1 year ago

Here's the code for the function that I created in case it helps too

const twx = (classes: TemplateStringsArray): string => {
  const cleanClasses = classes
    .join(' ')
    .split(/\r?\n/)
    .map((it) => it.trim());
  return twMerge(cleanClasses.join(' ').trim());
};

function baseStyled<T>(tag: WebTarget) {
  return (classes: TemplateStringsArray | string): FC => {
    const Component = tag;
    // eslint-disable-next-line react/display-name
    return (
      props?: ComponentProps<typeof Component> & { as?: ElementType } & T,
    ) => {
      const { as: asTag, ...otherProps } = props || {};
      const FinalComponentTag = asTag || Component;
      return (
        <FinalComponentTag
          {...otherProps}
          className={cx(
            Array.isArray(classes)
              ? twx(classes as TemplateStringsArray)
              : (classes as string),
            otherProps.className,
          )}
        />
      );
    };
  };
}
jahirfiquitiva commented 1 year ago

@MathiasGilson would you mind looking into this?