Skip to content
This repository was archived by the owner on Oct 23, 2023. It is now read-only.
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/fresh-ads-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@polymorphic-factory/preact': minor
---

Initial release
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ This turborepo uses [pnpm](https://pnpm.io) as a package manager. It includes th
### Packages

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

### Build
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"format:check": "prettier --check packages/**/src",
"format:write": "prettier --write packages/**/src",
"lint": "turbo run lint",
"preact": "pnpm --filter=@polymorphic-factory/preact",
"prepare": "husky install",
"react": "pnpm --filter=@polymorphic-factory/react",
"release": "changeset publish",
Expand Down
121 changes: 121 additions & 0 deletions packages/preact/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<h1 align="center">@polymorphic-factory/preact</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/preact"/>
<img alt="NPM Downloads" src="https://img.shields.io/npm/dm/@polymorphic-factory/preact?style=flat"/>
</p>

Create polymorphic Preact 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/preact
```

or

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

or

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

## Usage

Import the polymorphic factory and create your element factory.

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

### Custom `styled` function

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

```tsx
const poly = polymorphicFactory({
styled: (component, options) => (props) => {
const Component = props.as || component
return <Component data-custom-styled data-options={JSON.stringify(options)} {...props} />
},
})

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/preact'

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

## License

MIT © [Tim Kolberger](https://github.com/timkolberger)
14 changes: 14 additions & 0 deletions packages/preact/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"
}
}
}
51 changes: 51 additions & 0 deletions packages/preact/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@polymorphic-factory/preact",
"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/preact"
},
"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/jest-dom": "5.16.5",
"@testing-library/preact": "3.2.2",
"@types/testing-library__jest-dom": "5.14.5",
"@preact/preset-vite": "2.4.0",
"@vitest/coverage-c8": "0.25.2",
"clean-package": "2.1.2",
"jsdom": "20.0.2",
"preact": "10.11.3",
"typescript": "4.8.4",
"vite": "3.2.3",
"vitest": "0.25.2"
},
"peerDependencies": {
"preact": ">10"
}
}
66 changes: 66 additions & 0 deletions packages/preact/src/forwardRef.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { ComponentProps, Context, JSX } from 'preact'
import { ForwardFn, forwardRef as forwardRefPreact } from 'preact/compat'

export type ElementType = JSX.ElementType

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

/**
* 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

export type OmitCommonProps<
Target,
OmitAdditionalProps extends string | number | symbol = never,
> = Omit<Target, 'transition' | 'as' | 'color' | OmitAdditionalProps>

type AssignCommon<
SourceProps extends Record<string, unknown> = Record<never, never>,
OverrideProps extends Record<string, unknown> = Record<never, never>,
> = Assign<OmitCommonProps<SourceProps>, OverrideProps>

type MergeWithAs<
ComponentProps extends Record<string, unknown>,
AsProps extends Record<string, unknown>,
AdditionalProps extends Record<string, unknown> = Record<never, never>,
AsComponent extends ElementType = ElementType,
> = AssignCommon<ComponentProps, AdditionalProps> &
AssignCommon<AsProps, AdditionalProps> & {
as?: AsComponent
}

export type ComponentWithAs<
Component extends ElementType,
Props extends Record<string, unknown> = Record<never, never>,
> = {
<AsComponent extends ElementType = Component>(
props: MergeWithAs<ComponentProps<Component>, ComponentProps<AsComponent>, Props, AsComponent>,
): JSX.Element

displayName?: string
contextTypes?: Context<unknown>
defaultProps?: Partial<unknown>
id?: string
}

export function forwardRef<
Component extends ElementType,
Props extends Record<string, unknown> = Record<never, never>,
>(
component: ForwardFn<
AssignCommon<PropsOf<Component>, Props> & {
as?: ElementType
}
>,
) {
return forwardRefPreact(component) as unknown as ComponentWithAs<Component, Props>
}
6 changes: 6 additions & 0 deletions packages/preact/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { forwardRef, type ComponentWithAs, type PropsOf, type Assign } from './forwardRef'
export {
polymorphicFactory,
type HTMLPolymorphicComponents,
type HTMLPolymorphicProps,
} from './polymorphic-factory'
78 changes: 78 additions & 0 deletions packages/preact/src/polymorphic-factory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { type ComponentWithAs, ElementType, forwardRef, type PropsOf } from './forwardRef'
import type { JSX } from 'preact'

type DOMElements = keyof JSX.IntrinsicElements

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

export type HTMLPolymorphicProps<T extends ElementType> = Omit<PropsOf<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) {
return forwardRef((props, ref) => {
const { as, ...restProps } = props
const Component = as || originalComponent
return <Component {...restProps} ref={ref} />
})
}

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
}
30 changes: 30 additions & 0 deletions packages/preact/test/forward-ref.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createRef } from 'preact'
import type { HTMLPolymorphicProps } from '../src'
import { polymorphicFactory, forwardRef } from '../src'
import { render } from '@testing-library/preact'

describe('forwardRef', () => {
const poly = polymorphicFactory()

it('should forward the ref', () => {
const ComponentUnderTest = forwardRef<'div', HTMLPolymorphicProps<'div'>>((props, ref) => (
<poly.div {...props} ref={ref} />
))

const ref = createRef<HTMLDivElement>()
render(<ComponentUnderTest ref={ref} />)
expect(ref.current).toBeInstanceOf(HTMLDivElement)
})

it('should forward the ref with as prop', () => {
const ComponentUnderTest = forwardRef<'div', HTMLPolymorphicProps<'div'>>((props, ref) => (
<poly.div {...props} ref={ref} />
))

// known issue: with the `as` prop refs are not inherited correctly
// workaround:
const ref = createRef<HTMLDivElement & HTMLFormElement>()
render(<ComponentUnderTest as="form" ref={ref} />)
expect(ref.current).toBeInstanceOf(HTMLFormElement)
})
})
Loading