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/hip-cougars-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@polymorphic-factory/vue': minor
---

Polymorphic Vue components now support the v-model directive.
1 change: 1 addition & 0 deletions packages/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/vue/src/@types/vue-shim.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
18 changes: 15 additions & 3 deletions packages/vue/src/polymorphic-factory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 () => <Component {...attrs}>{slots}</Component>
const vmodelAttrs = computed(() =>
useVModel(Component as string, props.modelValue, emit, attrs),
)

return () => (
<Component {...vmodelAttrs.value} {...attrs}>
{slots?.default?.()}
</Component>
)
},
}) as ComponentWithAs<never>
}
Expand Down
57 changes: 57 additions & 0 deletions packages/vue/src/use-v-model.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> = { 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
}
40 changes: 40 additions & 0 deletions packages/vue/test/poly-test.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script setup lang="ts">
import { polymorphicFactory } from '../src'
import { ref } from 'vue'

const poly = polymorphicFactory()
const message = ref('test')
const selectValue = ref('3')
const radioValue = ref('vue')
const checkedNames = ref([])
const singleCheckbox = ref(true)
</script>
<template>
<poly.input v-model="message" data-testid="poly-input" />
<poly.div data-testid="poly-div">{{ message }}</poly.div>
<poly.select v-model="selectValue" data-testid="poly-select">
<poly.option value="1">Value 1</poly.option>
<poly.option value="2">Value 2</poly.option>
<poly.option value="3">Value 3</poly.option>
</poly.select>
<poly.p>selected : {{ selectValue }}</poly.p>

<poly.div>
<poly.input type="radio" v-model="radioValue" value="vue"></poly.input>
<poly.input type="radio" value="react" v-model="radioValue" data-testid="react-radio"></poly.input>
<poly.input type="radio" value="angular" v-model="radioValue"></poly.input>
</poly.div>
<poly.p>Radio: {{ radioValue }}</poly.p>

<poly.div>
<poly.input type="checkbox" id="jack" value="Jack" v-model="checkedNames" data-testid="checkbox-jack"/>
<poly.label for="jack">Jack</poly.label>
<poly.input type="checkbox" id="john" value="John" v-model="checkedNames" data-testid="checkbox-john" />
<poly.label for="john">John</poly.label>
<poly.input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
<poly.label for="mike">Mike</poly.label>
</poly.div>
<poly.p>checked : {{checkedNames.join(',')}}</poly.p>
<poly.input type="checkbox" v-model="singleCheckbox" data-testid="single-checkbox"/>
<poly.p>single checked : {{singleCheckbox}}</poly.p>
</template>
80 changes: 80 additions & 0 deletions packages/vue/test/sfc.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise (non-blocking): love the test suite!

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")
})
})
3 changes: 2 additions & 1 deletion packages/vue/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/// <reference types="vite/client" />

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'

export default defineConfig({
Expand All @@ -14,5 +15,5 @@ export default defineConfig({
web: [/\.[jt]sx$/],
},
},
plugins: [vueJsx()],
plugins: [vue(), vueJsx()],
})
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.