Skip to content
This repository was archived by the owner on Oct 23, 2023. It is now read-only.

feat: add polymorphic factory for solid #27

Merged
merged 7 commits into from
Nov 15, 2022
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
5 changes: 5 additions & 0 deletions .changeset/wicked-parrots-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@polymorphic-factory/solid': minor
---

Initial release for solid-js.
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,39 @@
<img alt="Github Stars" src="https://badgen.net/github/stars/chakra-ui/polymorphic" />
</p>

## What's inside?
Create polymorphic React/SolidJS components with a customizable `styled` function.

A polymorphic component is a component that can be rendered with a different element.

```tsx
import { polymorphicFactory } from '@polymorphic-factory/{react,solid}'

const poly = polymorphicFactory()

const App = () => (
<>
<poly.div />
<poly.main>
<poly.section>
<poly.div as="p">This is rendered as a p element</poly.div>
</poly.section>
</poly.main>
</>
)
```

> **Known drawbacks for the type definitions:**
>
> Event handlers are not typed correctly when using the `as` prop.
>
> This is a deliberate decision to keep the usage as simple as possible.

This turborepo uses [pnpm](https://pnpm.io) as a package manager. It includes the following packages:

### Packages

- [react](./packages/react/README.md)
- [solid](./packages/solid/README.md)

### Build

Expand Down
132 changes: 132 additions & 0 deletions packages/solid/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<h1 align="center">@polymorphic-factory/solid</h1>

<p align="center">
<img alt="CodeCov" src="https://codecov.io/gh/chakra-ui/polymorphic/branch/main/graph/badge.svg?token=GISB4HXIK7"/>
<img alt="MIT License" src="https://img.shields.io/github/license/chakra-ui/polymorphic"/>
<img alt="Github Stars" src="https://badgen.net/github/stars/chakra-ui/polymorphic" />
<img alt="Bundle Size" src="https://badgen.net/bundlephobia/minzip/@polymorphic-factory/solid"/>
<img alt="NPM Downloads" src="https://img.shields.io/npm/dm/@polymorphic-factory/solid?style=flat"/>
</p>

Create polymorphic SolidJS components with a customizable `styled` function.

A polymorphic component is a component that can be rendered with a different element.

> **Known drawbacks for the type definitions:**
>
> Event handlers are not typed correctly when using the `as` prop.
>
> This is a deliberate decision to keep the usage as simple as possible.

## Installation

```bash
npm install @polymorphic-factory/solid
```

or

```bash
yarn add @polymorphic-factory/solid
```

or

```bash
pnpm install @polymorphic-factory/solid
```

## Usage

Import the polymorphic factory and create your element factory.

```ts
import { polymorphicFactory } from '@polymorphic-factory/solid'
const poly = polymorphicFactory()
```

### Custom `styled` function

You can override the default implementation by passing `styled` function in the options.

```tsx
import { Dynamic } from 'solid-js/web'

const poly = polymorphicFactory({
styled: (component, options) => (props) => {
const [local, others] = splitProps(props, ['as'])
const component = local.as || originalComponent

return (
<Dynamic
component={component}
data-custom-styled
data-options={JSON.stringify(options)}
{...others}
/>
)
},
})

const WithOptions = poly('div', { hello: 'world' })

const App = () => {
return (
<>
<poly.div hello="world" />
{/* renders <div data-custom-styled hello="world" /> */}

<WithOptions />
{/* renders <div data-custom-styled data-options="{ \"hello\": \"world\" }" /> */}
</>
)
}
```

### Inline

Use the element factory to create elements inline.
Every JSX element is supported `div`, `main`, `aside`, etc.

```tsx
<>
<poly.div />
<poly.main>
<poly.section>
<poly.div as="p">This is rendered as a p element</poly.div>
</poly.section>
</poly.main>
</>
```

### Factory

Use the factory to wrap custom components.

```tsx
const OriginalComponent = (props) => <div data-original="true" {...props}></div>
const MyComponent = poly(OriginalComponent)

const App = () => <MyComponent />
// render <div data-original="true" />
```

It still supports the `as` prop, which would replace the `OriginalComponent`.

```tsx
<MyComponent as="div" />
// renders <div />
```

## Types

```ts
import type { HTMLPolymorphicComponents, HTMLPolymorphicProps } from '@polymorphic-factory/solid'

type PolymorphicDiv = HTMLPolymorphicComponents['div']
type DivProps = HTMLPolymorphicProps<'div'>
```

## License

MIT © [Tim Kolberger](https://github.com/timkolberger)
14 changes: 14 additions & 0 deletions packages/solid/clean-package.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"replace": {
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js"
},
"./package.json": "./package.json"
}
}
}
53 changes: 53 additions & 0 deletions packages/solid/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@polymorphic-factory/solid",
"version": "0.0.0",
"description": "",
"keywords": [],
"homepage": "https://github.com/chakra-ui/polymorphic",
"author": "Tim Kolberger <tim@kolberger.eu>",
"license": "MIT",
"main": "src/index.ts",
"sideEffects": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/chakra-ui/polymorphic.git",
"directory": "packages/solid"
},
"bugs": {
"url": "https://github.com/chakra-ui/polymorphic/issues"
},
"scripts": {
"dev": "pnpm run build --watch",
"build": "tsup src/index.ts",
"test": "vitest run --reporter verbose --coverage",
"test:watch": "vitest",
"lint": "eslint --ext .ts,.tsx src",
"typecheck": "tsc --noEmit",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"devDependencies": {
"@testing-library/dom": "8.19.0",
"@testing-library/jest-dom": "5.16.5",
"@types/jsdom": "20.0.1",
"@types/testing-library__jest-dom": "5.14.5",
"@vitest/coverage-c8": "0.25.1",
"jsdom": "20.0.2",
"solid-js": "1.6.1",
"solid-testing-library": "0.5.0",
"tsup": "6.4.0",
"typescript": "4.8.4",
"vite": "3.2.3",
"vitest": "0.25.1",
"vite-plugin-solid": "2.4.0"
},
"peerDependencies": {
"solid-js": ">=1.6.0"
}
}
8 changes: 8 additions & 0 deletions packages/solid/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export {
polymorphicFactory,
type HTMLPolymorphicComponents,
type HTMLPolymorphicProps,
type ComponentWithAs,
type PropsOf,
type Assign,
} from './polymorphic-factory'
104 changes: 104 additions & 0 deletions packages/solid/src/polymorphic-factory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { ValidComponent, Component, JSX, ComponentProps, splitProps } from 'solid-js'
import { Dynamic } from 'solid-js/web'

type DOMElements = keyof JSX.IntrinsicElements

// any is required for the import('solid/web').ValidComponent typings:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ElementType = DOMElements | Component<any>

/**
* Assign property types from right to left.
* Think `Object.assign` for types.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
*/
export type Assign<Target, Source> = Omit<Target, keyof Source> & Source

/**
* Extract the props of a solid element or component
*/
export type PropsOf<T extends ElementType> = ComponentProps<T> & {
as?: ElementType
}

export type ComponentWithAs<T extends ValidComponent, Props = Record<never, never>> = Component<
Assign<Assign<ComponentProps<T>, Props>, { as?: ElementType }>
>

export type HTMLPolymorphicComponents = {
[Tag in DOMElements]: ComponentWithAs<Tag>
}

export type HTMLPolymorphicProps<T extends ElementType> = Omit<ComponentProps<T>, 'ref'> & {
as?: ElementType
}

type PolymorphFactory = {
<
T extends ElementType,
P extends Record<string, unknown> = Record<never, never>,
Options = never,
>(
component: T,
option?: Options,
): ComponentWithAs<T, P>
}

function defaultStyled(originalComponent: ElementType) {
// any is required for the import('solid/web').ValidComponent typings:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (props: ComponentProps<ComponentWithAs<any>>) => {
const [local, others] = splitProps(props, ['as'])
const component = local.as || originalComponent

return <Dynamic component={component} {...others} />
}
}

interface PolyFactoryParam<
Component extends ElementType,
Props extends Record<string, unknown>,
Options,
> {
styled?: (component: Component, options?: Options) => ComponentWithAs<Component, Props>
}

/**
* Create a polymorphic factory, which is an object of JSX elements to render React Components accepting the `as` prop.
*
* @example
* const poly = polymorphicFactory()
* <poly.div /> // => renders div
* <poly.main /> // => renders main
* <poly.section as="main" /> => // renders main
*/
export function polymorphicFactory<
Component extends ElementType,
Props extends Record<string, unknown>,
Options = never,
>({ styled = defaultStyled }: PolyFactoryParam<Component, Props, Options> = {}) {
const cache = new Map<Component, ComponentWithAs<Component, Props>>()

return new Proxy(styled, {
/**
* @example
* const Div = poly("div")
* const WithPoly = poly(AnotherComponent)
*/
apply(target, thisArg, argArray: [Component, Options]) {
return styled(...argArray)
},
/**
* @example
* <poly.div />
*/
get(_, element) {
const asElement = element as Component
if (!cache.has(asElement)) {
cache.set(asElement, styled(asElement))
}
return cache.get(asElement)
},
}) as PolymorphFactory & HTMLPolymorphicComponents
}
Loading