From 29b952d2f0346402763018be67ae98cb4bd7540e Mon Sep 17 00:00:00 2001 From: Shyrro Date: Sun, 4 Dec 2022 23:07:47 +0100 Subject: [PATCH 01/15] feat(vue): polymorphic factory for vue Signed-off-by: Shyrro --- packages/vue/README.md | 141 +++++++++++ packages/vue/clean-package.config.json | 14 ++ packages/vue/package.json | 46 ++++ packages/vue/src/dom.types.ts | 232 ++++++++++++++++++ packages/vue/src/index.ts | 1 + packages/vue/src/polymorphic-factory.tsx | 91 +++++++ .../vue/test/polymorphic-factory.test.tsx | 87 +++++++ packages/vue/test/setupTest.ts | 1 + packages/vue/tsconfig.json | 8 + packages/vue/tsup.config.ts | 10 + packages/vue/vite.config.ts | 18 ++ 11 files changed, 649 insertions(+) create mode 100644 packages/vue/README.md create mode 100644 packages/vue/clean-package.config.json create mode 100644 packages/vue/package.json create mode 100644 packages/vue/src/dom.types.ts create mode 100644 packages/vue/src/index.ts create mode 100644 packages/vue/src/polymorphic-factory.tsx create mode 100644 packages/vue/test/polymorphic-factory.test.tsx create mode 100644 packages/vue/test/setupTest.ts create mode 100644 packages/vue/tsconfig.json create mode 100644 packages/vue/tsup.config.ts create mode 100644 packages/vue/vite.config.ts diff --git a/packages/vue/README.md b/packages/vue/README.md new file mode 100644 index 00000000..1b0b3336 --- /dev/null +++ b/packages/vue/README.md @@ -0,0 +1,141 @@ +

@polymorphic-factory/vue

+ +

+ CodeCov + MIT License + Github Stars + Bundle Size + NPM Downloads +

+ +Create polymorphic VueJS 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/vue +``` + +or + +```bash +yarn add @polymorphic-factory/vue +``` + +or + +```bash +pnpm install @polymorphic-factory/vue +``` + +## Usage + +Import the polymorphic factory and create your element factory. + +```ts +import { polymorphicFactory } from '@polymorphic-factory/vue' +const poly = polymorphicFactory() +``` + +### Custom `styled` function + +You can override the default implementation by passing `styled` function in the options. + +```tsx +import { defineComponent } from 'vue' +const poly = polymorphicFactory({ + styled: (originalComponent, options) => + defineComponent({ + props: { + as: { + type: String as PropType, + default: '', + }, + }, + setup(props, { slots, attrs }) { + const component = props.as || originalComponent + + return () => + h( + component, + { 'data-custom-styled': true, 'data-options': JSON.stringify(options), ...attrs }, + slots, + ) + }, + }), +}) + +const WithOptions = poly('div', { hello: 'world' }) + +const App = () => { + return ( + <> + + {/* renders
*/} + + + {/* renders
*/} + + ) +} +``` + +### Inline + +Use the element factory to create elements inline. +Every JSX element is supported `div`, `main`, `aside`, etc. + +```tsx +<> + + + + This is rendered as a p element + + + +``` + +### Factory + +Use the factory to wrap custom components. + +```tsx +const OriginalComponent = defineComponent({ + setup(props) { + return () =>
+ }, +}) +const MyComponent = poly(OriginalComponent) + +const App = h(MyComponent) +// render
+``` + +It still supports the `as` prop, which would replace the `OriginalComponent`. + +```tsx + +// renders
+``` + +## Types + +```ts +import type { HTMLPolymorphicComponents, HTMLPolymorphicProps } from '@polymorphic-factory/vue' + +type PolymorphicDiv = HTMLPolymorphicComponents['div'] +type DivProps = HTMLPolymorphicProps<'div'> +``` + +## License + +MIT © [Tim Kolberger](https://github.com/timkolberger) diff --git a/packages/vue/clean-package.config.json b/packages/vue/clean-package.config.json new file mode 100644 index 00000000..67951381 --- /dev/null +++ b/packages/vue/clean-package.config.json @@ -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" + } + } +} diff --git a/packages/vue/package.json b/packages/vue/package.json new file mode 100644 index 00000000..b5a2722f --- /dev/null +++ b/packages/vue/package.json @@ -0,0 +1,46 @@ +{ + "name": "@polymorphic-factory/vue", + "version": "0.0.0", + "author": "Shyrro ", + "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/vue" + }, + "bugs": { + "url": "https://github.com/chakra-ui/polymorphic/issues" + }, + "scripts": { + "dev": "vite", + "build": "tsup src/index.ts", + "preview": "vite preview", + "build-only": "vite build", + "test": "vitest run --reporter verbose --coverage", + "test:watch": "vitest" + }, + "dependencies": { + "vue": "^3.2.45" + }, + "devDependencies": { + "@testing-library/jest-dom": "5.16.5", + "@testing-library/vue": "^6.6.1", + "@types/node": "^18.11.9", + "@vitejs/plugin-vue": "^3.2.0", + "@vitejs/plugin-vue-jsx": "^2.1.1", + "@vue/tsconfig": "^0.1.3", + "tsup": "6.5.0", + "typescript": "4.9.3", + "vite": "3.2.4", + "vite-plugin-solid": "2.4.0", + "vitest": "0.25.3" + } +} diff --git a/packages/vue/src/dom.types.ts b/packages/vue/src/dom.types.ts new file mode 100644 index 00000000..7d84ca3c --- /dev/null +++ b/packages/vue/src/dom.types.ts @@ -0,0 +1,232 @@ +import type { + DataHTMLAttributes, + DialogHTMLAttributes, + IframeHTMLAttributes, + ImgHTMLAttributes, + InputHTMLAttributes, + InsHTMLAttributes, + KeygenHTMLAttributes, + LabelHTMLAttributes, + LiHTMLAttributes, + LinkHTMLAttributes, + MapHTMLAttributes, + MenuHTMLAttributes, + MetaHTMLAttributes, + MeterHTMLAttributes, + ObjectHTMLAttributes, + OlHTMLAttributes, + OptgroupHTMLAttributes, + OptionHTMLAttributes, + OutputHTMLAttributes, + ParamHTMLAttributes, + ProgressHTMLAttributes, + QuoteHTMLAttributes, + ScriptHTMLAttributes, + SelectHTMLAttributes, + SourceHTMLAttributes, + StyleHTMLAttributes, + TableHTMLAttributes, + TdHTMLAttributes, + TextareaHTMLAttributes, + ThHTMLAttributes, + TimeHTMLAttributes, + TrackHTMLAttributes, + VideoHTMLAttributes, + WebViewHTMLAttributes, + SVGAttributes, + AnchorHTMLAttributes, + AreaHTMLAttributes, + AudioHTMLAttributes, + BaseHTMLAttributes, + BlockquoteHTMLAttributes, + ButtonHTMLAttributes, + CanvasHTMLAttributes, + ColgroupHTMLAttributes, + ColHTMLAttributes, + DelHTMLAttributes, + DetailsHTMLAttributes, + EmbedHTMLAttributes, + FieldsetHTMLAttributes, + FormHTMLAttributes, + HTMLAttributes, + HtmlHTMLAttributes, +} from 'vue' + +// NOt exported from Vue +export interface IntrinsicElementAttributes { + a: AnchorHTMLAttributes + abbr: HTMLAttributes + address: HTMLAttributes + area: AreaHTMLAttributes + article: HTMLAttributes + aside: HTMLAttributes + audio: AudioHTMLAttributes + b: HTMLAttributes + base: BaseHTMLAttributes + bdi: HTMLAttributes + bdo: HTMLAttributes + blockquote: BlockquoteHTMLAttributes + body: HTMLAttributes + br: HTMLAttributes + button: ButtonHTMLAttributes + canvas: CanvasHTMLAttributes + caption: HTMLAttributes + cite: HTMLAttributes + code: HTMLAttributes + col: ColHTMLAttributes + colgroup: ColgroupHTMLAttributes + data: DataHTMLAttributes + datalist: HTMLAttributes + dd: HTMLAttributes + del: DelHTMLAttributes + details: DetailsHTMLAttributes + dfn: HTMLAttributes + dialog: DialogHTMLAttributes + div: HTMLAttributes + dl: HTMLAttributes + dt: HTMLAttributes + em: HTMLAttributes + embed: EmbedHTMLAttributes + fieldset: FieldsetHTMLAttributes + figcaption: HTMLAttributes + figure: HTMLAttributes + footer: HTMLAttributes + form: FormHTMLAttributes + h1: HTMLAttributes + h2: HTMLAttributes + h3: HTMLAttributes + h4: HTMLAttributes + h5: HTMLAttributes + h6: HTMLAttributes + head: HTMLAttributes + header: HTMLAttributes + hgroup: HTMLAttributes + hr: HTMLAttributes + html: HtmlHTMLAttributes + i: HTMLAttributes + iframe: IframeHTMLAttributes + img: ImgHTMLAttributes + input: InputHTMLAttributes + ins: InsHTMLAttributes + kbd: HTMLAttributes + keygen: KeygenHTMLAttributes + label: LabelHTMLAttributes + legend: HTMLAttributes + li: LiHTMLAttributes + link: LinkHTMLAttributes + main: HTMLAttributes + map: MapHTMLAttributes + mark: HTMLAttributes + menu: MenuHTMLAttributes + meta: MetaHTMLAttributes + meter: MeterHTMLAttributes + nav: HTMLAttributes + noindex: HTMLAttributes + noscript: HTMLAttributes + object: ObjectHTMLAttributes + ol: OlHTMLAttributes + optgroup: OptgroupHTMLAttributes + option: OptionHTMLAttributes + output: OutputHTMLAttributes + p: HTMLAttributes + param: ParamHTMLAttributes + picture: HTMLAttributes + pre: HTMLAttributes + progress: ProgressHTMLAttributes + q: QuoteHTMLAttributes + rp: HTMLAttributes + rt: HTMLAttributes + ruby: HTMLAttributes + s: HTMLAttributes + samp: HTMLAttributes + script: ScriptHTMLAttributes + section: HTMLAttributes + select: SelectHTMLAttributes + small: HTMLAttributes + source: SourceHTMLAttributes + span: HTMLAttributes + strong: HTMLAttributes + style: StyleHTMLAttributes + sub: HTMLAttributes + summary: HTMLAttributes + sup: HTMLAttributes + table: TableHTMLAttributes + template: HTMLAttributes + tbody: HTMLAttributes + td: TdHTMLAttributes + textarea: TextareaHTMLAttributes + tfoot: HTMLAttributes + th: ThHTMLAttributes + thead: HTMLAttributes + time: TimeHTMLAttributes + title: HTMLAttributes + tr: HTMLAttributes + track: TrackHTMLAttributes + u: HTMLAttributes + ul: HTMLAttributes + var: HTMLAttributes + video: VideoHTMLAttributes + wbr: HTMLAttributes + webview: WebViewHTMLAttributes + + // SVG + svg: SVGAttributes + + animate: SVGAttributes + animateMotion: SVGAttributes + animateTransform: SVGAttributes + circle: SVGAttributes + clipPath: SVGAttributes + defs: SVGAttributes + desc: SVGAttributes + ellipse: SVGAttributes + feBlend: SVGAttributes + feColorMatrix: SVGAttributes + feComponentTransfer: SVGAttributes + feComposite: SVGAttributes + feConvolveMatrix: SVGAttributes + feDiffuseLighting: SVGAttributes + feDisplacementMap: SVGAttributes + feDistantLight: SVGAttributes + feDropShadow: SVGAttributes + feFlood: SVGAttributes + feFuncA: SVGAttributes + feFuncB: SVGAttributes + feFuncG: SVGAttributes + feFuncR: SVGAttributes + feGaussianBlur: SVGAttributes + feImage: SVGAttributes + feMerge: SVGAttributes + feMergeNode: SVGAttributes + feMorphology: SVGAttributes + feOffset: SVGAttributes + fePointLight: SVGAttributes + feSpecularLighting: SVGAttributes + feSpotLight: SVGAttributes + feTile: SVGAttributes + feTurbulence: SVGAttributes + filter: SVGAttributes + foreignObject: SVGAttributes + g: SVGAttributes + image: SVGAttributes + line: SVGAttributes + linearGradient: SVGAttributes + marker: SVGAttributes + mask: SVGAttributes + metadata: SVGAttributes + mpath: SVGAttributes + path: SVGAttributes + pattern: SVGAttributes + polygon: SVGAttributes + polyline: SVGAttributes + radialGradient: SVGAttributes + rect: SVGAttributes + stop: SVGAttributes + switch: SVGAttributes + symbol: SVGAttributes + text: SVGAttributes + textPath: SVGAttributes + tspan: SVGAttributes + use: SVGAttributes + view: SVGAttributes +} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts new file mode 100644 index 00000000..eeabb8c3 --- /dev/null +++ b/packages/vue/src/index.ts @@ -0,0 +1 @@ +export * from './polymorphic-factory' diff --git a/packages/vue/src/polymorphic-factory.tsx b/packages/vue/src/polymorphic-factory.tsx new file mode 100644 index 00000000..13fc56f8 --- /dev/null +++ b/packages/vue/src/polymorphic-factory.tsx @@ -0,0 +1,91 @@ +import { + defineComponent, + h, + type PropType, + type ExtractPropTypes, + type DefineComponent, + type AllowedComponentProps, + type ComponentCustomProps, + type VNodeProps, +} from 'vue' +import type { IntrinsicElementAttributes } from './dom.types' + +export type DOMElements = keyof IntrinsicElementAttributes + +export type ElementType = DOMElements | DefineComponent + +export type ComponentWithAs

> = { + new (): { + $props: AllowedComponentProps & + ComponentCustomProps & + VNodeProps & { props?: Record } & P & { + as?: ElementType + } + } +} + +export type HTMLPolymorphicComponents = { + [Tag in DOMElements]: ElementType +} + +export type HTMLPolymorphicProps = Omit, 'ref'> & { + as?: ElementType +} + +type PolymorphFactory = { + < + T extends ElementType, + P extends Record = Record, + Options = never, + >( + component: T, + option?: Options, + ): ComponentWithAs

+} + +function defaultStyled(originalComponent: ElementType) { + return defineComponent({ + props: { + as: { + type: String as PropType, + default: '', + }, + }, + setup(props, { slots, attrs }) { + const component = props.as || originalComponent + return () => h(component, attrs, slots) + }, + }) as ComponentWithAs> +} + +interface PolyFactoryParam { + styled?: (component: Component, options?: Options) => ComponentWithAs +} + +export function polymorphicFactory({ + styled = defaultStyled, +}: PolyFactoryParam = {}) { + const cache = new Map() + + return new Proxy(styled, { + /** + * @example + * const Div = poly("div") + * const WithPoly = poly(AnotherComponent) + */ + apply(target, thisArg, argArray: [Component, Options]) { + return styled(...argArray) + }, + /** + * @example + * + */ + get(_, element) { + const asElement = element as Component + if (!cache.has(asElement)) { + cache.set(asElement, styled(asElement)) + } + return cache.get(asElement) + }, + }) as PolymorphFactory & HTMLPolymorphicComponents +} diff --git a/packages/vue/test/polymorphic-factory.test.tsx b/packages/vue/test/polymorphic-factory.test.tsx new file mode 100644 index 00000000..7068e9a1 --- /dev/null +++ b/packages/vue/test/polymorphic-factory.test.tsx @@ -0,0 +1,87 @@ +import { describe, expect, it } from 'vitest' +import { screen, render } from '@testing-library/vue' +import { polymorphicFactory, type DOMElements } from '../src' +import { defineComponent, h, type PropType } from 'vue' + +describe('Polymorphic factory', () => { + describe('with default styled function', () => { + const poly = polymorphicFactory() + + it('should render an element', () => { + render(() => ) + const element = screen.getByTestId('poly') + + expect(element.nodeName).toBe('DIV') + }) + + it('should render an element with the as prop', () => { + render(() => ) + const element = screen.getByTestId('poly') + expect(element.nodeName).toBe('MAIN') + }) + + it('should render an element with the factory', () => { + const Aside = poly('aside') + render(() =>