Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support v-model for <details> and <dialog> #8048

Open
wants to merge 3 commits into
base: minor
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -127,6 +127,38 @@ return function render(_ctx, _cache) {
}"
`;

exports[`compiler: transform v-model > simple expression for details 1`] = `
"const _Vue = Vue

return function render(_ctx, _cache) {
with (_ctx) {
const { vModelDetails: _vModelDetails, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

return _withDirectives((_openBlock(), _createElementBlock(\\"details\\", {
\\"onUpdate:modelValue\\": $event => ((model) = $event)
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelDetails, model]
])
}
}"
`;

exports[`compiler: transform v-model > simple expression for dialog 1`] = `
"const _Vue = Vue

return function render(_ctx, _cache) {
with (_ctx) {
const { vModelDialog: _vModelDialog, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

return _withDirectives((_openBlock(), _createElementBlock(\\"dialog\\", {
\\"onUpdate:modelValue\\": $event => ((model) = $event)
}, null, 8 /* PROPS */, [\\"onUpdate:modelValue\\"])), [
[_vModelDialog, model]
])
}
}"
`;

exports[`compiler: transform v-model > simple expression for input (checkbox) 1`] = `
"const _Vue = Vue

Expand Down
16 changes: 16 additions & 0 deletions packages/compiler-dom/__tests__/transforms/vModel.spec.ts
Expand Up @@ -9,6 +9,8 @@ import { transformElement } from '../../../compiler-core/src/transforms/transfor
import { DOMErrorCodes } from '../../src/errors'
import {
V_MODEL_CHECKBOX,
V_MODEL_DETAILS,
V_MODEL_DIALOG,
V_MODEL_DYNAMIC,
V_MODEL_RADIO,
V_MODEL_SELECT,
Expand Down Expand Up @@ -83,6 +85,20 @@ describe('compiler: transform v-model', () => {
expect(generate(root).code).toMatchSnapshot()
})

test('simple expression for details', () => {
const root = transformWithModel('<details v-model="model" />')

expect(root.helpers).toContain(V_MODEL_DETAILS)
expect(generate(root).code).toMatchSnapshot()
})

test('simple expression for dialog', () => {
const root = transformWithModel('<dialog v-model="model" />')

expect(root.helpers).toContain(V_MODEL_DIALOG)
expect(generate(root).code).toMatchSnapshot()
})

test('simple expression for textarea', () => {
const root = transformWithModel('<textarea v-model="model" />')

Expand Down
4 changes: 4 additions & 0 deletions packages/compiler-dom/src/runtimeHelpers.ts
Expand Up @@ -4,6 +4,8 @@ export const V_MODEL_RADIO = Symbol(__DEV__ ? `vModelRadio` : ``)
export const V_MODEL_CHECKBOX = Symbol(__DEV__ ? `vModelCheckbox` : ``)
export const V_MODEL_TEXT = Symbol(__DEV__ ? `vModelText` : ``)
export const V_MODEL_SELECT = Symbol(__DEV__ ? `vModelSelect` : ``)
export const V_MODEL_DIALOG = Symbol(__DEV__ ? `vModelDialog` : ``)
export const V_MODEL_DETAILS = Symbol(__DEV__ ? `vModelDetails` : ``)
export const V_MODEL_DYNAMIC = Symbol(__DEV__ ? `vModelDynamic` : ``)

export const V_ON_WITH_MODIFIERS = Symbol(__DEV__ ? `vOnModifiersGuard` : ``)
Expand All @@ -19,6 +21,8 @@ registerRuntimeHelpers({
[V_MODEL_CHECKBOX]: `vModelCheckbox`,
[V_MODEL_TEXT]: `vModelText`,
[V_MODEL_SELECT]: `vModelSelect`,
[V_MODEL_DETAILS]: `vModelDetails`,
[V_MODEL_DIALOG]: `vModelDialog`,
[V_MODEL_DYNAMIC]: `vModelDynamic`,
[V_ON_WITH_MODIFIERS]: `withModifiers`,
[V_ON_WITH_KEYS]: `withKeys`,
Expand Down
10 changes: 9 additions & 1 deletion packages/compiler-dom/src/transforms/vModel.ts
Expand Up @@ -12,7 +12,9 @@ import {
V_MODEL_RADIO,
V_MODEL_SELECT,
V_MODEL_TEXT,
V_MODEL_DYNAMIC
V_MODEL_DYNAMIC,
V_MODEL_DETAILS,
V_MODEL_DIALOG
} from '../runtimeHelpers'

export const transformModel: DirectiveTransform = (dir, node, context) => {
Expand Down Expand Up @@ -49,6 +51,8 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
tag === 'input' ||
tag === 'textarea' ||
tag === 'select' ||
tag === 'details' ||
tag === 'dialog' ||
isCustomElement
) {
let directiveToUse = V_MODEL_TEXT
Expand Down Expand Up @@ -92,6 +96,10 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
}
} else if (tag === 'select') {
directiveToUse = V_MODEL_SELECT
} else if (tag === 'dialog') {
directiveToUse = V_MODEL_DIALOG
} else if (tag === 'details') {
directiveToUse = V_MODEL_DETAILS
} else {
// textarea
__DEV__ && checkDuplicatedValue()
Expand Down
20 changes: 20 additions & 0 deletions packages/compiler-ssr/__tests__/ssrElement.spec.ts
Expand Up @@ -340,4 +340,24 @@ describe('ssr: element', () => {
`)
})
})

describe('v-model', () => {
test('with details', () => {
expect(getCompiledString(`<details v-model="x">Foo</details>`))
.toMatchInlineSnapshot(`
"\`<details\${
(_ssrIncludeBooleanAttr(_ctx.x)) ? \\" open\\" : \\"\\"
}>Foo</details>\`"
`)
})

test('with dialog', () => {
expect(getCompiledString(`<dialog v-model="x">Foo</dialog>`))
.toMatchInlineSnapshot(`
"\`<dialog\${
(_ssrIncludeBooleanAttr(_ctx.x)) ? \\" open\\" : \\"\\"
}>Foo</dialog>\`"
`)
})
})
})
2 changes: 2 additions & 0 deletions packages/compiler-ssr/src/transforms/ssrVModel.ts
Expand Up @@ -129,6 +129,8 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => {
} else if (node.tag === 'textarea') {
checkDuplicatedValue()
node.children = [createInterpolation(model, model.loc)]
} else if (node.tag === 'dialog' || node.tag === 'details') {
res.props = [createObjectProperty('open', model)]
} else if (node.tag === 'select') {
node.children.forEach(option => {
if (option.type === NodeTypes.ELEMENT) {
Expand Down
82 changes: 80 additions & 2 deletions packages/runtime-dom/__tests__/directives/vModel.spec.ts
Expand Up @@ -9,8 +9,8 @@ import {
ref
} from '@vue/runtime-dom'

const triggerEvent = (type: string, el: Element) => {
const event = new Event(type)
const triggerEvent = (type: string, el: Element, arg?: any) => {
const event = new Event(type, arg)
el.dispatchEvent(event)
}

Expand Down Expand Up @@ -1172,4 +1172,82 @@ describe('vModel', () => {
await nextTick()
expect(data.value).toEqual('使用拼音输入')
})

it('should work with details', async () => {
const manualListener = vi.fn()
const component = defineComponent({
data() {
return { value: false }
},
render() {
return [
withVModel(
h('details', {
'onUpdate:modelValue': setValue.bind(this),
onToggle: () => {
manualListener(data.value)
}
}),
this.value
)
]
}
})
render(h(component), root)

const details = root.querySelector('details')! as HTMLDetailsElement
const data = root._vnode.component.data
expect(details.open).toEqual(false)

details.open = true
triggerEvent('toggle', details, { target: details })
await nextTick()
expect(data.value).toEqual(true)
expect(manualListener).toHaveBeenCalledWith(true)

data.value = false
await nextTick()
expect(details.open).toEqual(false)

data.value = true
await nextTick()
expect(details.open).toEqual(true)
})

it('should work with dialog', async () => {
const manualListener = vi.fn()
const component = defineComponent({
data() {
return { value: false }
},
render() {
return [
withVModel(
h('dialog', {
'onUpdate:modelValue': setValue.bind(this),
onClose: () => {
manualListener(data.value)
}
}),
this.value
)
]
}
})
render(h(component), root)

const dialog = root.querySelector('dialog')! as HTMLDialogElement
const data = root._vnode.component.data
expect(dialog.open).toEqual(false)

data.value = true
await nextTick()
expect(dialog.open).toEqual(true)

dialog.open = false
triggerEvent('close', dialog)
await nextTick()
expect(data.value).toEqual(false)
expect(manualListener).toHaveBeenCalledWith(false)
})
})
40 changes: 40 additions & 0 deletions packages/runtime-dom/src/directives/vModel.ts
Expand Up @@ -210,6 +210,42 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
}
}

export const vModelDetails: ModelDirective<HTMLDetailsElement> = {
antfu marked this conversation as resolved.
Show resolved Hide resolved
created(el, _, vnode) {
el._assign = getModelAssigner(vnode)
addEventListener(el, 'toggle', () => {
el._assign(el.open)
})
},
mounted(el, { value }) {
el.open = value
},
beforeUpdate(el, _, vnode) {
el._assign = getModelAssigner(vnode)
},
updated(el, { value }) {
el.open = value
}
}

export const vModelDialog: ModelDirective<HTMLDialogElement> = {
created(el, _, vnode) {
el._assign = getModelAssigner(vnode)
addEventListener(el, 'close', () => {
el._assign(false)
})
},
mounted(el, { value }) {
el.open = value
},
beforeUpdate(el, _, vnode) {
el._assign = getModelAssigner(vnode)
},
updated(el, { value }) {
el.open = value
}
}

function setSelected(el: HTMLSelectElement, value: any) {
const isMultiple = el.multiple
if (isMultiple && !isArray(value) && !isSet(value)) {
Expand Down Expand Up @@ -278,6 +314,10 @@ function resolveDynamicModel(tagName: string, type: string | undefined) {
return vModelSelect
case 'TEXTAREA':
return vModelText
case 'DETAILS':
return vModelDetails
case 'DIALOG':
return vModelDialog
default:
switch (type) {
case 'checkbox':
Expand Down
2 changes: 2 additions & 0 deletions packages/runtime-dom/src/index.ts
Expand Up @@ -226,6 +226,8 @@ export {
vModelCheckbox,
vModelRadio,
vModelSelect,
vModelDetails,
vModelDialog,
vModelDynamic
} from './directives/vModel'
export { withModifiers, withKeys } from './directives/vOn'
Expand Down