Skip to content

Commit

Permalink
feat(directives): introduce created custom directive hook and ensure
Browse files Browse the repository at this point in the history
`v-model` event listener fire before template/props listeners

fix #1931
  • Loading branch information
yyx990803 committed Aug 24, 2020
1 parent 016ba11 commit 11804fe
Show file tree
Hide file tree
Showing 4 changed files with 28 additions and 16 deletions.
1 change: 1 addition & 0 deletions packages/runtime-core/src/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type SSRDirectiveHook = (
) => Data | undefined

export interface ObjectDirective<T = any, V = any> {
created?: DirectiveHook<T, null, V>
beforeMount?: DirectiveHook<T, null, V>
mounted?: DirectiveHook<T, null, V>
beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
Expand Down
10 changes: 6 additions & 4 deletions packages/runtime-core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,9 @@ function baseCreateRenderer(
)
}

if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// props
if (props) {
for (const key in props) {
Expand All @@ -741,10 +744,6 @@ function baseCreateRenderer(
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}

// scopeId
if (scopeId) {
hostSetScopeId(el, scopeId)
Expand All @@ -756,6 +755,9 @@ function baseCreateRenderer(
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
// #1689 For inside suspense + suspense resolved case, just call it
const needCallTransitionHooks =
Expand Down
8 changes: 7 additions & 1 deletion packages/runtime-dom/__tests__/directives/vModel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ beforeEach(() => {

describe('vModel', () => {
it('should work with text input', async () => {
const manualListener = jest.fn()
const component = defineComponent({
data() {
return { value: null }
Expand All @@ -37,7 +38,10 @@ describe('vModel', () => {
return [
withVModel(
h('input', {
'onUpdate:modelValue': setValue.bind(this)
'onUpdate:modelValue': setValue.bind(this),
onInput: () => {
manualListener(data.value)
}
}),
this.value
)
Expand All @@ -54,6 +58,8 @@ describe('vModel', () => {
triggerEvent('input', input)
await nextTick()
expect(data.value).toEqual('foo')
// #1931
expect(manualListener).toHaveBeenCalledWith('foo')

data.value = 'bar'
await nextTick()
Expand Down
25 changes: 14 additions & 11 deletions packages/runtime-dom/src/directives/vModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type ModelDirective<T> = ObjectDirective<T & { _assign: AssignerFn }>
export const vModelText: ModelDirective<
HTMLInputElement | HTMLTextAreaElement
> = {
beforeMount(el, { value, modifiers: { lazy, trim, number } }, vnode) {
created(el, { value, modifiers: { lazy, trim, number } }, vnode) {
el.value = value == null ? '' : value
el._assign = getModelAssigner(vnode)
const castToNumber = number || el.type === 'number'
Expand Down Expand Up @@ -90,7 +90,7 @@ export const vModelText: ModelDirective<
}

export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
beforeMount(el, binding, vnode) {
created(el, binding, vnode) {
setChecked(el, binding, vnode)
el._assign = getModelAssigner(vnode)
addEventListener(el, 'change', () => {
Expand Down Expand Up @@ -135,7 +135,7 @@ function setChecked(
}

export const vModelRadio: ModelDirective<HTMLInputElement> = {
beforeMount(el, { value }, vnode) {
created(el, { value }, vnode) {
el.checked = looseEqual(value, vnode.props!.value)
el._assign = getModelAssigner(vnode)
addEventListener(el, 'change', () => {
Expand All @@ -151,16 +151,19 @@ export const vModelRadio: ModelDirective<HTMLInputElement> = {
}

export const vModelSelect: ModelDirective<HTMLSelectElement> = {
// use mounted & updated because <select> relies on its children <option>s.
mounted(el, { value }, vnode) {
setSelected(el, value)
el._assign = getModelAssigner(vnode)
created(el, binding, vnode) {
addEventListener(el, 'change', () => {
const selectedVal = Array.prototype.filter
.call(el.options, (o: HTMLOptionElement) => o.selected)
.map(getValue)
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) {
el._assign = getModelAssigner(vnode)
Expand Down Expand Up @@ -214,8 +217,8 @@ function getCheckboxValue(
export const vModelDynamic: ObjectDirective<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
> = {
beforeMount(el, binding, vnode) {
callModelHook(el, binding, vnode, null, 'beforeMount')
created(el, binding, vnode) {
callModelHook(el, binding, vnode, null, 'created')
},
mounted(el, binding, vnode) {
callModelHook(el, binding, vnode, null, 'mounted')
Expand All @@ -233,7 +236,7 @@ function callModelHook(
binding: DirectiveBinding,
vnode: VNode,
prevVNode: VNode | null,
hook: 'beforeMount' | 'mounted' | 'beforeUpdate' | 'updated'
hook: keyof ObjectDirective
) {
let modelToUse: ObjectDirective
switch (el.tagName) {
Expand All @@ -244,7 +247,7 @@ function callModelHook(
modelToUse = vModelText
break
default:
switch (el.type) {
switch (vnode.props && vnode.props.type) {
case 'checkbox':
modelToUse = vModelCheckbox
break
Expand Down

0 comments on commit 11804fe

Please sign in to comment.