Code Monkey home page Code Monkey logo

react-polymorphic-box's Introduction

react-polymorphic-box

Building blocks for strongly typed polymorphic components in React.

npm Language grade: JavaScript Travis (.com) Commitizen friendly

Animated demonstration of package capabilities

๐Ÿ’ก Motivation

Popularized by Styled Components v4, the as prop allows changing the HTML tag rendered by a component, e.g.:

import { Box } from 'react-polymorphic-box';
import { Link } from 'react-router-dom';

<Box as="a" href="https://github.com/kripod">GitHub</Box>
<Box as={Link} to="/about">About</Box>

While this pattern has been encouraged by several libraries, typings had lacked support for polymorphism, missing benefits like:

  • Automatic code completion, based on the value of the as prop
  • Static type checking against the associated component's inferred props
  • HTML element name validation

๐Ÿ“š Usage

A Heading component can demonstrate the effectiveness of polymorphism:

<Heading color="rebeccapurple">Heading</Heading>
<Heading as="h3">Subheading</Heading>

Custom components like the previous one may utilize the package as shown below.

import { Box, PolymorphicComponentProps } from "react-polymorphic-box";

// Component-specific props should be specified separately
export type HeadingOwnProps = {
  color?: string;
};

// Merge own props with others inherited from the underlying element type
export type HeadingProps<
  E extends React.ElementType
> = PolymorphicComponentProps<E, HeadingOwnProps>;

// An HTML tag or a different React component can be rendered by default
const defaultElement = "h2";

export function Heading<E extends React.ElementType = typeof defaultElement>({
  color,
  style,
  ...restProps
}: HeadingProps<E>): JSX.Element {
  // The `as` prop may be overridden by the passed props
  return <Box as={defaultElement} style={{ color, ...style }} {...restProps} />;
}

Typing external components

Alternatively, you can also type your custom components by using the PolymorphicComponent type. This is especially handy when working with external libraries that already expose polymorphic components. Here's an example implementing the Heading component from above using styled-components:

import { PolymorphicComponent } from "react-polymorphic-box";
import styled from "styled-components";

// Component-specific props
export type HeadingProps = {
  color?: string;
};

// An HTML tag or a different React component can be rendered by default
const defaultElement = "h2";

export const Heading: PolymorphicComponent<
  HeadingProps, // Merged with props from the underlying element type
  typeof defaultElement // Default element type (optional, defaults to 'div')
> = styled(defaultElement)<HeadingProps>`
  color: ${(props) => props.color};
`;

Forwarding Refs

Library authors should consider encapsulating reusable components, passing a ref through each of them:

import { Box } from "react-polymorphic-box";

export const Heading: <E extends React.ElementType = typeof defaultElement>(
  props: HeadingProps<E>
) => React.ReactElement | null = React.forwardRef(
  <E extends React.ElementType = typeof defaultElement>(
    { color, style, ...restProps }: HeadingProps<E>,
    ref: typeof restProps.ref
  ) => {
    return (
      <Box
        as={defaultElement}
        ref={ref}
        style={{ color, ...style }}
        {...restProps}
      />
    );
  }
);

The component can then receive a ref prop (live demo), just like a regular HTML element:

import { useRef } from "react";

function App() {
  const ref = useRef<HTMLHeadingElement>(null);
  return <Heading ref={ref}>It works!</Heading>;
}

react-polymorphic-box's People

Contributors

biowaffeln avatar christianalfoni avatar dependabot[bot] avatar kripod avatar mskelton avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-polymorphic-box's Issues

jest failed when testing a component that use

Description

I've got an error during jest test process when I test a component that import a component who use Box.
The problem is that this component is located in our private component library and the issue only happened in this particular case. If I directly test the component directly in the library there is no problem at all.

Here is my architecture :

  • MyCard (Component where test failed) --> import Card component from @library/Card --> Card component contains the polymorphic box logic

Here is the error :

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

The react render works, the problem came only when using jest

Workaround

  • Move the Box component directly from the react-polymorphic-box into my library (Works)

Reproduction

To reproduce it you have to import a component from a library that use polymorphic.

Possible issue location

Maybe the issue is the bundle generated

Environment

System:

  • OS: macOS 10.15.6
  • CPU: (8) x64 Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
  • Memory: 86.46 MB / 16.00 GB
  • Shell: 3.1.2 - /usr/local/bin/fish

Binaries:

  • Node: 12.18.2 - ~/.nvm/versions/node/v12.18.2/bin/node
  • Yarn: 1.22.4 - /usr/local/bin/yarn
  • npm: 6.14.5 - ~/.nvm/versions/node/v12.18.2/bin/npm

Browsers:

  • Chrome: 84.0.4147.89
  • Firefox Developer Edition: 79.0
  • Safari: 13.1.2

Libs:

react-scripts: 3.4.1
jest: 24.9.0
@testing-library/react: 10.4.4

I can't create a codesandbox to reproduce this issue but if you have any question, please ask :)

Target ES5 instead of ES2020

Motivation

The package is being compiled to ES2020 (see tsconfig.json) and modern JS features as arrow functions are not being transpiled.

Basic example

As you can see in version 1.1.1 (https://unpkg.com/browse/[email protected]/dist-web/index.js) arrow functions are not transpiled.

Details

For people giving support for older browsers (IE11, older versions of Edge, etc.) it woud be great to target ES5 instead of ES2020. The package is so small that it won't affect the size of it.

I can open a PR with this change.

PS: thanks for this package, I love it!

Type error with TypeScript 5.2.2

First off, thanks for a fantastic package! Long time happy user, first time bug reporter here :)

Description

Updating TypeScript to 5.2.2 (possibly lower versions as well) causes/reveals a type error in the Box component. My guess would be that TS has changes how it resolves the generics, but to be honest, I don't fully understand how this package works.

Reproduction

Take the example Code Sandbox and update the TypeScript version to 5.2.2 in package.json (it might require reloading the tab)

Here's a link to an updated Sandbox showing the error

Expected behavior

Types should work

Actual behavior

Box component shows the following type error

Type '{ as: "h2"; style: { color: string | undefined; } & LibraryManagedAttributes<E, ComponentPropsWithRef<E>>[string]; } & Omit<...> & { ...; }' is not assignable to type 'IntrinsicAttributes & BoxOwnProps<"h2" & HeadingProps<E>["as"]> & Omit<LibraryManagedAttributes<"h2" & HeadingProps<E>["as"], ComponentPropsWithRef<...>>, "as">'.
  Type '{ as: "h2"; style: { color: string | undefined; } & LibraryManagedAttributes<E, ComponentPropsWithRef<E>>[string]; } & Omit<...> & { ...; }' is not assignable to type 'Omit<LibraryManagedAttributes<"h2" & HeadingProps<E>["as"], ComponentPropsWithRef<"h2" & HeadingProps<E>["as"]>>, "as">'.ts(2322)

Environment

All / Code Sandbox

"string" is not assignable to type '"as"'

I like this solution to the "as" prop issue, and attempted to use it, but am getting a type error that is pretty convoluted. I'm not sure how to diagnose it. Any ideas?

Here is a codesandbox:
https://codesandbox.io/s/naughty-jackson-f68fs

The expected behavior is that I should be able to define a polymorphic Button without type errors.

This is the error we are seeing:

Type 'Pick<PolymorphicComponentProps<E, ButtonOwnProps>, "as" | Exclude<Exclude<keyof LibraryManagedAttributes<E, ComponentPropsWithRef<E>>, "as">, "variant">>' is not assignable to type 'IntrinsicAttributes & BoxOwnProps<E> & Pick<LibraryManagedAttributes<E, ComponentPropsWithRef<E>>, Exclude<...>>'.
  Type 'Pick<PolymorphicComponentProps<E, ButtonOwnProps>, "as" | Exclude<Exclude<keyof LibraryManagedAttributes<E, ComponentPropsWithRef<E>>, "as">, "variant">>' is not assignable to type 'Pick<LibraryManagedAttributes<E, ComponentPropsWithRef<E>>, Exclude<keyof LibraryManagedAttributes<E, ComponentPropsWithRef<E>>, "as">>'.
    Type 'Exclude<keyof LibraryManagedAttributes<E, ComponentPropsWithRef<E>>, "as">' is not assignable to type '"as" | Exclude<Exclude<keyof LibraryManagedAttributes<E, ComponentPropsWithRef<E>>, "as">, "variant">'.
      Type 'keyof LibraryManagedAttributes<E, ComponentPropsWithRef<E>>' is not assignable to type '"as" | Exclude<Exclude<keyof LibraryManagedAttributes<E, ComponentPropsWithRef<E>>, "as">, "variant">'.
        Type 'string | number | symbol' is not assignable to type '"as" | Exclude<Exclude<keyof LibraryManagedAttributes<E, ComponentPropsWithRef<E>>, "as">, "variant">'.
          Type 'string' is not assignable to type '"as" | Exclude<Exclude<keyof LibraryManagedAttributes<E, ComponentPropsWithRef<E>>, "as">, "variant">'.
            Type 'keyof LibraryManagedAttributes<E, ComponentPropsWithRef<E>>' is not assignable to type '"as"'.
              Type 'string | number | symbol' is not assignable to type '"as" | Exclude<Exclude<keyof LibraryManagedAttributes<E, ComponentPropsWithRef<E>>, "as">, "variant">'.
                Type 'string' is not assignable to type '"as" | Exclude<Exclude<keyof LibraryManagedAttributes<E, ComponentPropsWithRef<E>>, "as">, "variant">'.
                  Type 'Exclude<keyof LibraryManagedAttributes<E, ComponentPropsWithRef<E>>, "as">' is not assignable to type '"as"'.
                    Type 'keyof LibraryManagedAttributes<E, ComponentPropsWithRef<E>>' is not assignable to type '"as"'.
                      Type 'string | number | symbol' is not assignable to type '"as"'.
                        Type 'string' is not assignable to type '"as"'.
                          Type 'string | number | symbol' is not assignable to type '"as"'.
                            Type 'string' is not assignable to type '"as"'.

Best approach to creating a hierarchy of components

Hi I'm just wondering what the best way to approach building up a hierarchy of components would be. I.e. start off with the polymorphic box as the top level item and then compose this into a very basic "view" component and use this "view" component as the basis for all other components (in an approach similar to "styled system"). Would I be better creating my own polymorphic box for this root component?

Here's an approach I started to make:

interface ViewOwnProps {
  px?: "0" | "1";
  py?: "0" | "1";
}

export type ViewProps<E extends React.ElementType> = PolymorphicComponentProps<
  E,
  ViewOwnProps
>;

const viewDefaultProp = "div";

function View<E extends React.ElementType = typeof defaultElement>({
  px,
  py,
  ...restProps
}: ViewProps<E>): JSX.Element {
  return (
    <Box
      as={viewDefaultProp}
      {...restProps}
      style={{ padding: `${px} ${py}` }}
    />
  );
}

// Component-specific props should be specified separately
interface HeadingOwnProps extends ViewOwnProps {
  color?: string;
}

// Merge own props with others inherited from the underlying element type
export type HeadingProps<
  E extends React.ElementType
> = PolymorphicComponentProps<E, HeadingOwnProps>;

// An HTML tag or a different React component can be rendered by default
const defaultElement = "div";

function Heading<E extends React.ElementType = typeof defaultElement>({
  color,
  style,
  ...restProps
}: HeadingProps<E>): JSX.Element {
  // The `as` prop may be overridden by the passed props
  return (
    <View as={defaultElement} style={{ color, ...style }} {...restProps} />
  );
}

Here I create a heading that uses the view component and uses the various polymorphic box type utilities but also extends the heading own props from the view own props.

Function types are typed as any

Description

Function types (e.g. event handlers) are typed as any. I'm not convinced react-polymorphic-box is at fault here but thought I'd raise it to discuss. In my example typescript seems to infer the correct type for the event handler (pictured below), however when implementing the handler it is not typed.

image

Reproduction

import React from 'react';
import { Box } from 'react-polymorphic-box'; 

const Test = () => (
  <Box
    as="li"
    onMouseDown={(event) => {
      // event is implicitly typed as any
    }}
  />
);

Expected behaviour

The event handler should be typed as expected.

Actual behaviour

Event handlers seem to be typed as any.

image

Environment

System:

  • OS: macOS Mojave 10.14.6
  • CPU: (8) x64 Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
  • Memory: 42.69 MB / 16.00 GB
  • Shell: 5.3 - /bin/zsh

Binaries:

  • Node: 12.18.2
  • Yarn: 1.22.4
  • npm: 6.14.5

Browsers:

  • Chrome: 84.0.4147.89
  • Firefox: 78.0.1
  • Safari: 12.1.2

npmPackages:

  • react-polymorphic-box: ^2.0.4 => 2.0.4
  • typescript: 3.9.6

Unable to built Next.js since upgrading to v2

Hey @kripod

As you probably have seen I've been contributing to Stitches recently to help test it and make sure things are working as users would expect.

@christianalfoni has been working on improving the types and as part of the latest Stitches release, he upgraded to react-polymorphic-box v2.0.3

Since this upgrade, I'm unable to get my Next.js project to build correctly. Things were working fine before that. We suspect it's something to do with the new build strategy used in v2.

I've abstracted a reproducible branch in this repo which you're free to clone and test it out.

Do you think this could be related to the latest release?

Let me know if there's any way I can help.

Type mismatch

Description

Creating polymorphic components generates TypeScript errors. It appears the TypeScript compiler doesn't like it when you pass in ...restProps if it contains the as prop. If you remove that it no longer complains so it appears there is some kind of mismatch for the as type.

Reproduction

Create the Heading component example from the README.

Expected behavior

No TypeScript errors

Actual behavior

image
Type '{ as: "h2"; style: { color: string | undefined; } & ComponentProps<E>[string]; } & Omit<HeadingProps<E>, "style" | "color">' is not assignable to type 'IntrinsicAttributes & BoxOwnProps<"h2" & HeadingProps<E>["as"]> & Omit<ComponentProps<"h2" & HeadingProps<E>["as"]>, "as">'.
Type '{ as: "h2"; style: { color: string | undefined; } & ComponentProps<E>[string]; } & Omit<HeadingProps<E>, "style" | "color">' is not assignable to type 'Omit<ComponentProps<"h2" & HeadingProps<E>["as"]>, "as">'.

Environment

System:

  • OS: macOS 13.4
  • CPU: (16) x64 Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
  • Memory: 1.07 GB / 32.00 GB
  • Shell: 5.9 - /bin/zsh

Binaries:

  • Node: 18.15.0 - ~/.nvm/versions/node/v18.15.0/bin/node
  • Yarn: 3.5.1 - ~/.nvm/versions/node/v18.15.0/bin/yarn
  • npm: 8.19.4 - ~/Repos/motech-development/platform/node_modules/.bin/npm

Browsers:

  • Chrome: 113.0.5672.126
  • Edge: 113.0.1774.57
  • Firefox: 109.0.1
  • Safari: 16.5

Suggestion: PolymorphicComponent type

Motivation

Hi! First of all, thanks for the library, I spent quite a few hours thinking about how to solve this problem myself, and it's nice to see a community solution.

I tried out the library with styled-components and figured it would make sense to export a PolymorphicComponent type as well as PolymorphicComponentProps. A simple implementation would look like this:

export type PolymorphicComponent<P, D extends React.ElementType = "div"> = <
  E extends React.ElementType = D
>(
  props: PolymorphicComponentProps<E, P>
) => JSX.Element;

I found this to be especially handy when working with external libraries that already expose polymorphic components.

Basic Example

The Heading example from the README with styled-components would look like this:

import styled from "styled-components";

type HeadingProps = { color?: string };
const defaultElement = "h2";

const Heading: PolymorphicComponent<
  HeadingProps,
  typeof defaultElement // optional, defaults to "div"
> = styled(defaultElement)<HeadingProps>`
  color: ${(props) => props.color};
`;

which makes the whole API a bit more concise since you don't have to manually merge props.
I have some time on my hands rn, so if you think it's a sensible addition I could make a pull request and add some docs.

picking `ref` from component's props is not allowed

Description

Picking ref from component's props produces an error. Picking it is necessary to assign proper type to innerRef.

Error:

Warning: [object Object]: `ref` is not a prop. Trying to access it will result in `undefined` being returned. 
If you need to access the same value within the child component, you should pass it as a different prop.
(https://fb.me/react-special-props)

Reproduction

My code is similar to example code from Readme.md section on how to pass ref:

export const Box = React.forwardRef(
  <E extends React.ElementType = typeof defaultElement>(
    { ref, ...restProps }: BoxProps<E>,
    innerRef: typeof ref
  ) => {
    return <StyledBox ref={innerRef} as={defaultElement} {...restProps} />;
  }
) as <E extends React.ElementType = typeof defaultElement>(
  props: BoxProps<E>
) => JSX.Element;

Expected behavior

innerRef should be somehow typed without picking ref from component's props

Environment

I am using Next.js 9.4.4 and React 16.13.1

Missing Types on PolymorphicComponent

I just realized that PolymorphicComponent is missing React's component attributes (defaultProps, displayName, etc.). One should probably add React.ComponentType as an intersection type to the definition.

Incorrect work with Unions Types

Description

Polymorphic box has partialized props if "as" component has unioned props.

Reproduction

import React from 'react';
import { Box } from 'react-polymorphic-box';

interface ITestA {
  value: 'a';
}

interface ITestB {
  value: 'b';
  extra: string;
}

type Test = ITestA | ITestB;

const Test: React.FC<Test> = (props) => {
  return <></>;
};

function App() {
  return (
    <>
      <Test value="b" extra="extra" />
      <Box as={Test} value="b" extra="extra" />
    </>
  );
}

Expected behavior

Polymorphic box has correct props types.

Actual behavior

Type '{ as: FC<Test>; value: "b"; extra: string; }' is not assignable to type 'IntrinsicAttributes & BoxOwnProps<FC<Test>> & Pick<PropsWithChildren<Test>, "value" | "children">'. Property 'extra' does not exist on type 'IntrinsicAttributes & BoxOwnProps<FC<Test>> & Pick<PropsWithChildren<Test>, "value" | "children">'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.