diff --git a/.changeset/hip-cougars-reply.md b/.changeset/hip-cougars-reply.md
new file mode 100644
index 00000000..9dac8492
--- /dev/null
+++ b/.changeset/hip-cougars-reply.md
@@ -0,0 +1,5 @@
+---
+'@polymorphic-factory/vue': minor
+---
+
+Polymorphic Vue components now support the v-model directive.
diff --git a/packages/vue/package.json b/packages/vue/package.json
index bb66e508..738c4348 100644
--- a/packages/vue/package.json
+++ b/packages/vue/package.json
@@ -35,6 +35,7 @@
"@testing-library/vue": "6.6.1",
"@types/jsdom": "20.0.1",
"@types/testing-library__jest-dom": "5.14.5",
+ "@vitejs/plugin-vue": "4.0.0",
"@vitejs/plugin-vue-jsx": "3.0.0",
"@vitest/coverage-c8": "0.26.0",
"clean-package": "2.2.0",
diff --git a/packages/vue/src/@types/vue-shim.d.ts b/packages/vue/src/@types/vue-shim.d.ts
new file mode 100644
index 00000000..d9f24faa
--- /dev/null
+++ b/packages/vue/src/@types/vue-shim.d.ts
@@ -0,0 +1,4 @@
+declare module '*.vue' {
+ import Vue from 'vue'
+ export default Vue
+}
diff --git a/packages/vue/src/polymorphic-factory.tsx b/packages/vue/src/polymorphic-factory.tsx
index cce7b842..7a6921ad 100644
--- a/packages/vue/src/polymorphic-factory.tsx
+++ b/packages/vue/src/polymorphic-factory.tsx
@@ -2,11 +2,14 @@ import {
type AllowedComponentProps,
type ComponentCustomProps,
type DefineComponent,
+ h,
defineComponent,
type ExtractPropTypes,
type VNodeProps,
+ computed,
} from 'vue'
import type { IntrinsicElementAttributes } from './dom.types'
+import { useVModel } from './use-v-model'
export type DOMElements = keyof IntrinsicElementAttributes
@@ -51,10 +54,19 @@ type PolymorphFactory = {
function defaultStyled(originalComponent: ElementType) {
return defineComponent({
- props: ['as'],
- setup(props, { slots, attrs }) {
+ props: ['as', 'modelValue'],
+ emits: ['update:modelValue', 'input', 'change'],
+ setup(props, { slots, attrs, emit }) {
const Component = props.as || originalComponent
- return () => {slots}
+ const vmodelAttrs = computed(() =>
+ useVModel(Component as string, props.modelValue, emit, attrs),
+ )
+
+ return () => (
+
+ {slots?.default?.()}
+
+ )
},
}) as ComponentWithAs
}
diff --git a/packages/vue/src/use-v-model.ts b/packages/vue/src/use-v-model.ts
new file mode 100644
index 00000000..607d680c
--- /dev/null
+++ b/packages/vue/src/use-v-model.ts
@@ -0,0 +1,57 @@
+import { computed } from 'vue'
+
+const formElements = ['input', 'select', 'textarea', 'fieldset', 'datalist', 'option', 'optgroup']
+
+export const useVModel = (
+ elementTag: string,
+ modelValue: any,
+ emit: CallableFunction,
+ attrs: any,
+) => {
+ let vmodelAttrs = {}
+ const handleMultipleCheckbox = (value: any) => {
+ const currentModelValue = [...modelValue]
+ if (currentModelValue.includes(value)) {
+ currentModelValue.splice(currentModelValue.indexOf(value), 1)
+ return currentModelValue
+ } else {
+ return [...currentModelValue, value]
+ }
+ }
+ const handleInput = (e: Event) => {
+ emit(
+ 'update:modelValue',
+ attrs.type === 'checkbox' && Array.isArray(modelValue)
+ ? handleMultipleCheckbox((e?.currentTarget as HTMLInputElement)?.value)
+ : typeof modelValue === 'boolean'
+ ? (e?.currentTarget as HTMLInputElement)?.checked
+ : (e?.currentTarget as HTMLInputElement)?.value,
+ )
+ emit('input', e, (e?.currentTarget as HTMLInputElement)?.value)
+ emit(
+ 'change',
+ e,
+ typeof modelValue === 'boolean'
+ ? (e?.currentTarget as HTMLInputElement)?.checked
+ : (e?.currentTarget as HTMLInputElement)?.value,
+ )
+ }
+
+ if (formElements.includes(elementTag)) {
+ let val: Record = { value: modelValue }
+ if (elementTag === 'input' && (attrs.type === 'checkbox' || attrs.type === 'radio')) {
+ const isChecked = computed(() =>
+ typeof modelValue === 'boolean' ? modelValue : modelValue.includes(attrs.value),
+ )
+ val = {
+ checked: isChecked.value,
+ }
+ }
+ vmodelAttrs = {
+ ...val,
+ onInput: handleInput,
+ }
+ }
+
+ return vmodelAttrs
+}
diff --git a/packages/vue/test/poly-test.vue b/packages/vue/test/poly-test.vue
new file mode 100644
index 00000000..2d6a55c7
--- /dev/null
+++ b/packages/vue/test/poly-test.vue
@@ -0,0 +1,40 @@
+
+
+
+ {{ message }}
+
+ Value 1
+ Value 2
+ Value 3
+
+ selected : {{ selectValue }}
+
+
+
+
+
+
+ Radio: {{ radioValue }}
+
+
+
+ Jack
+
+ John
+
+ Mike
+
+ checked : {{checkedNames.join(',')}}
+
+ single checked : {{singleCheckbox}}
+
diff --git a/packages/vue/test/sfc.test.ts b/packages/vue/test/sfc.test.ts
new file mode 100644
index 00000000..d2570efb
--- /dev/null
+++ b/packages/vue/test/sfc.test.ts
@@ -0,0 +1,80 @@
+import { describe, expect, it } from 'vitest'
+import { screen, render, fireEvent } from '@testing-library/vue'
+import PolyTest from './poly-test.vue'
+
+describe('SFC related features', () => {
+ it('has default value with v-model', () => {
+ render(PolyTest)
+
+ const inputElement: HTMLInputElement = screen.getByTestId('poly-input')
+ const divElement: any = screen.getByTestId('poly-div')
+
+ expect(inputElement.value).toBe('test')
+ // Div element should not have "value" bound to them
+ expect(divElement.value).toBeUndefined()
+ expect(divElement.modelvalue).toBeUndefined()
+ })
+
+ it('updates input with vmodel correctly', async () => {
+ render(PolyTest)
+
+ const inputElement: HTMLInputElement = screen.getByTestId('poly-input')
+
+ await fireEvent.update(inputElement, 'Done')
+
+ expect(inputElement.value).toBe('Done')
+ })
+
+ it('updates select correctly', async () => {
+ render(PolyTest)
+
+ const select = screen.getByTestId('poly-select')
+
+ await fireEvent.change(select, { target: { value: 2 } })
+
+ const options: HTMLOptionElement[] = screen.getAllByRole("option")
+
+ expect(options[0].selected).toBeFalsy()
+ expect(options[1].selected).toBeTruthy()
+ expect(options[2].selected).toBeFalsy()
+ })
+
+ it('updates radio correctly', async () => {
+ render(PolyTest)
+
+ const radio = screen.getByTestId('react-radio')
+
+ await fireEvent.click(radio)
+
+ screen.getByText("Radio: react")
+ })
+
+ it("updates multiple checkbox selection", async () => {
+ render(PolyTest)
+
+ const john = screen.getByTestId("checkbox-john")
+ const jack = screen.getByTestId("checkbox-jack")
+
+ await fireEvent.click(john)
+
+ screen.getByText("checked : John")
+
+ await fireEvent.click(jack)
+
+ screen.getByText("checked : John,Jack")
+ })
+
+ it("updates single checkbox with boolean", async () => {
+ render(PolyTest)
+
+ const singleCheckbox = screen.getByTestId("single-checkbox")
+
+ await fireEvent.click(singleCheckbox)
+
+ screen.getByText("single checked : false")
+
+ await fireEvent.click(singleCheckbox)
+
+ screen.getByText("single checked : true")
+ })
+})
diff --git a/packages/vue/vite.config.ts b/packages/vue/vite.config.ts
index 399cbfd5..66d95d44 100644
--- a/packages/vue/vite.config.ts
+++ b/packages/vue/vite.config.ts
@@ -2,6 +2,7 @@
///
import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
@@ -14,5 +15,5 @@ export default defineConfig({
web: [/\.[jt]sx$/],
},
},
- plugins: [vueJsx()],
+ plugins: [vue(), vueJsx()],
})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b2e5c043..e9f96998 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -134,6 +134,7 @@ importers:
'@testing-library/vue': 6.6.1
'@types/jsdom': 20.0.1
'@types/testing-library__jest-dom': 5.14.5
+ '@vitejs/plugin-vue': 4.0.0
'@vitejs/plugin-vue-jsx': 3.0.0
'@vitest/coverage-c8': 0.26.0
clean-package: 2.2.0
@@ -148,6 +149,7 @@ importers:
'@testing-library/vue': 6.6.1_vue@3.2.45
'@types/jsdom': 20.0.1
'@types/testing-library__jest-dom': 5.14.5
+ '@vitejs/plugin-vue': 4.0.0_vite@4.0.2+vue@3.2.45
'@vitejs/plugin-vue-jsx': 3.0.0_vite@4.0.2+vue@3.2.45
'@vitest/coverage-c8': 0.26.0_jsdom@20.0.3
clean-package: 2.2.0
@@ -2053,6 +2055,17 @@ packages:
- supports-color
dev: true
+ /@vitejs/plugin-vue/4.0.0_vite@4.0.2+vue@3.2.45:
+ resolution: {integrity: sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ vite: ^4.0.0
+ vue: ^3.2.25
+ dependencies:
+ vite: 4.0.2
+ vue: 3.2.45
+ dev: true
+
/@vitest/coverage-c8/0.26.0_jsdom@20.0.3:
resolution: {integrity: sha512-1LSMHvX1Winy1dIV1XqQanIskYBvd3+TlQtxO6BeyFa57Lah2uNBm3gh5iDB+ZWCySN5o6bl7qOJdaZjEQZZeg==}
dependencies: