feat(directives): introduce created
custom directive hook and ensure
`v-model` event listener fire before template/props listeners fix #1931
This commit is contained in:
parent
016ba116a8
commit
11804fe93f
@ -41,6 +41,7 @@ export type SSRDirectiveHook = (
|
|||||||
) => Data | undefined
|
) => Data | undefined
|
||||||
|
|
||||||
export interface ObjectDirective<T = any, V = any> {
|
export interface ObjectDirective<T = any, V = any> {
|
||||||
|
created?: DirectiveHook<T, null, V>
|
||||||
beforeMount?: DirectiveHook<T, null, V>
|
beforeMount?: DirectiveHook<T, null, V>
|
||||||
mounted?: DirectiveHook<T, null, V>
|
mounted?: DirectiveHook<T, null, V>
|
||||||
beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
|
beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
|
||||||
|
@ -720,6 +720,9 @@ function baseCreateRenderer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dirs) {
|
||||||
|
invokeDirectiveHook(vnode, null, parentComponent, 'created')
|
||||||
|
}
|
||||||
// props
|
// props
|
||||||
if (props) {
|
if (props) {
|
||||||
for (const key in props) {
|
for (const key in props) {
|
||||||
@ -741,10 +744,6 @@ function baseCreateRenderer(
|
|||||||
invokeVNodeHook(vnodeHook, parentComponent, vnode)
|
invokeVNodeHook(vnodeHook, parentComponent, vnode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dirs) {
|
|
||||||
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
|
|
||||||
}
|
|
||||||
|
|
||||||
// scopeId
|
// scopeId
|
||||||
if (scopeId) {
|
if (scopeId) {
|
||||||
hostSetScopeId(el, scopeId)
|
hostSetScopeId(el, scopeId)
|
||||||
@ -756,6 +755,9 @@ function baseCreateRenderer(
|
|||||||
hostSetScopeId(el, treeOwnerId + '-s')
|
hostSetScopeId(el, treeOwnerId + '-s')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (dirs) {
|
||||||
|
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
|
||||||
|
}
|
||||||
// #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
|
// #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
|
||||||
// #1689 For inside suspense + suspense resolved case, just call it
|
// #1689 For inside suspense + suspense resolved case, just call it
|
||||||
const needCallTransitionHooks =
|
const needCallTransitionHooks =
|
||||||
|
@ -29,6 +29,7 @@ beforeEach(() => {
|
|||||||
|
|
||||||
describe('vModel', () => {
|
describe('vModel', () => {
|
||||||
it('should work with text input', async () => {
|
it('should work with text input', async () => {
|
||||||
|
const manualListener = jest.fn()
|
||||||
const component = defineComponent({
|
const component = defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return { value: null }
|
return { value: null }
|
||||||
@ -37,7 +38,10 @@ describe('vModel', () => {
|
|||||||
return [
|
return [
|
||||||
withVModel(
|
withVModel(
|
||||||
h('input', {
|
h('input', {
|
||||||
'onUpdate:modelValue': setValue.bind(this)
|
'onUpdate:modelValue': setValue.bind(this),
|
||||||
|
onInput: () => {
|
||||||
|
manualListener(data.value)
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
this.value
|
this.value
|
||||||
)
|
)
|
||||||
@ -54,6 +58,8 @@ describe('vModel', () => {
|
|||||||
triggerEvent('input', input)
|
triggerEvent('input', input)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(data.value).toEqual('foo')
|
expect(data.value).toEqual('foo')
|
||||||
|
// #1931
|
||||||
|
expect(manualListener).toHaveBeenCalledWith('foo')
|
||||||
|
|
||||||
data.value = 'bar'
|
data.value = 'bar'
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
@ -46,7 +46,7 @@ type ModelDirective<T> = ObjectDirective<T & { _assign: AssignerFn }>
|
|||||||
export const vModelText: ModelDirective<
|
export const vModelText: ModelDirective<
|
||||||
HTMLInputElement | HTMLTextAreaElement
|
HTMLInputElement | HTMLTextAreaElement
|
||||||
> = {
|
> = {
|
||||||
beforeMount(el, { value, modifiers: { lazy, trim, number } }, vnode) {
|
created(el, { value, modifiers: { lazy, trim, number } }, vnode) {
|
||||||
el.value = value == null ? '' : value
|
el.value = value == null ? '' : value
|
||||||
el._assign = getModelAssigner(vnode)
|
el._assign = getModelAssigner(vnode)
|
||||||
const castToNumber = number || el.type === 'number'
|
const castToNumber = number || el.type === 'number'
|
||||||
@ -90,7 +90,7 @@ export const vModelText: ModelDirective<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
|
export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
|
||||||
beforeMount(el, binding, vnode) {
|
created(el, binding, vnode) {
|
||||||
setChecked(el, binding, vnode)
|
setChecked(el, binding, vnode)
|
||||||
el._assign = getModelAssigner(vnode)
|
el._assign = getModelAssigner(vnode)
|
||||||
addEventListener(el, 'change', () => {
|
addEventListener(el, 'change', () => {
|
||||||
@ -135,7 +135,7 @@ function setChecked(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const vModelRadio: ModelDirective<HTMLInputElement> = {
|
export const vModelRadio: ModelDirective<HTMLInputElement> = {
|
||||||
beforeMount(el, { value }, vnode) {
|
created(el, { value }, vnode) {
|
||||||
el.checked = looseEqual(value, vnode.props!.value)
|
el.checked = looseEqual(value, vnode.props!.value)
|
||||||
el._assign = getModelAssigner(vnode)
|
el._assign = getModelAssigner(vnode)
|
||||||
addEventListener(el, 'change', () => {
|
addEventListener(el, 'change', () => {
|
||||||
@ -151,16 +151,19 @@ export const vModelRadio: ModelDirective<HTMLInputElement> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const vModelSelect: ModelDirective<HTMLSelectElement> = {
|
export const vModelSelect: ModelDirective<HTMLSelectElement> = {
|
||||||
// use mounted & updated because <select> relies on its children <option>s.
|
created(el, binding, vnode) {
|
||||||
mounted(el, { value }, vnode) {
|
|
||||||
setSelected(el, value)
|
|
||||||
el._assign = getModelAssigner(vnode)
|
|
||||||
addEventListener(el, 'change', () => {
|
addEventListener(el, 'change', () => {
|
||||||
const selectedVal = Array.prototype.filter
|
const selectedVal = Array.prototype.filter
|
||||||
.call(el.options, (o: HTMLOptionElement) => o.selected)
|
.call(el.options, (o: HTMLOptionElement) => o.selected)
|
||||||
.map(getValue)
|
.map(getValue)
|
||||||
el._assign(el.multiple ? selectedVal : selectedVal[0])
|
el._assign(el.multiple ? selectedVal : selectedVal[0])
|
||||||
})
|
})
|
||||||
|
el._assign = getModelAssigner(vnode)
|
||||||
|
},
|
||||||
|
// set value in mounted & updated because <select> relies on its children
|
||||||
|
// <option>s.
|
||||||
|
mounted(el, { value }) {
|
||||||
|
setSelected(el, value)
|
||||||
},
|
},
|
||||||
beforeUpdate(el, _binding, vnode) {
|
beforeUpdate(el, _binding, vnode) {
|
||||||
el._assign = getModelAssigner(vnode)
|
el._assign = getModelAssigner(vnode)
|
||||||
@ -214,8 +217,8 @@ function getCheckboxValue(
|
|||||||
export const vModelDynamic: ObjectDirective<
|
export const vModelDynamic: ObjectDirective<
|
||||||
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||||
> = {
|
> = {
|
||||||
beforeMount(el, binding, vnode) {
|
created(el, binding, vnode) {
|
||||||
callModelHook(el, binding, vnode, null, 'beforeMount')
|
callModelHook(el, binding, vnode, null, 'created')
|
||||||
},
|
},
|
||||||
mounted(el, binding, vnode) {
|
mounted(el, binding, vnode) {
|
||||||
callModelHook(el, binding, vnode, null, 'mounted')
|
callModelHook(el, binding, vnode, null, 'mounted')
|
||||||
@ -233,7 +236,7 @@ function callModelHook(
|
|||||||
binding: DirectiveBinding,
|
binding: DirectiveBinding,
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
prevVNode: VNode | null,
|
prevVNode: VNode | null,
|
||||||
hook: 'beforeMount' | 'mounted' | 'beforeUpdate' | 'updated'
|
hook: keyof ObjectDirective
|
||||||
) {
|
) {
|
||||||
let modelToUse: ObjectDirective
|
let modelToUse: ObjectDirective
|
||||||
switch (el.tagName) {
|
switch (el.tagName) {
|
||||||
@ -244,7 +247,7 @@ function callModelHook(
|
|||||||
modelToUse = vModelText
|
modelToUse = vModelText
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
switch (el.type) {
|
switch (vnode.props && vnode.props.type) {
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
modelToUse = vModelCheckbox
|
modelToUse = vModelCheckbox
|
||||||
break
|
break
|
||||||
|
Loading…
Reference in New Issue
Block a user