Skip to content

Commit 11804fe

Browse files
committed
feat(directives): introduce created custom directive hook and ensure
`v-model` event listener fire before template/props listeners fix #1931
1 parent 016ba11 commit 11804fe

File tree

4 files changed

+28
-16
lines changed

4 files changed

+28
-16
lines changed

packages/runtime-core/src/directives.ts

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export type SSRDirectiveHook = (
4141
) => Data | undefined
4242

4343
export interface ObjectDirective<T = any, V = any> {
44+
created?: DirectiveHook<T, null, V>
4445
beforeMount?: DirectiveHook<T, null, V>
4546
mounted?: DirectiveHook<T, null, V>
4647
beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>

packages/runtime-core/src/renderer.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,9 @@ function baseCreateRenderer(
720720
)
721721
}
722722

723+
if (dirs) {
724+
invokeDirectiveHook(vnode, null, parentComponent, 'created')
725+
}
723726
// props
724727
if (props) {
725728
for (const key in props) {
@@ -741,10 +744,6 @@ function baseCreateRenderer(
741744
invokeVNodeHook(vnodeHook, parentComponent, vnode)
742745
}
743746
}
744-
if (dirs) {
745-
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
746-
}
747-
748747
// scopeId
749748
if (scopeId) {
750749
hostSetScopeId(el, scopeId)
@@ -756,6 +755,9 @@ function baseCreateRenderer(
756755
hostSetScopeId(el, treeOwnerId + '-s')
757756
}
758757
}
758+
if (dirs) {
759+
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
760+
}
759761
// #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
760762
// #1689 For inside suspense + suspense resolved case, just call it
761763
const needCallTransitionHooks =

packages/runtime-dom/__tests__/directives/vModel.spec.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ beforeEach(() => {
2929

3030
describe('vModel', () => {
3131
it('should work with text input', async () => {
32+
const manualListener = jest.fn()
3233
const component = defineComponent({
3334
data() {
3435
return { value: null }
@@ -37,7 +38,10 @@ describe('vModel', () => {
3738
return [
3839
withVModel(
3940
h('input', {
40-
'onUpdate:modelValue': setValue.bind(this)
41+
'onUpdate:modelValue': setValue.bind(this),
42+
onInput: () => {
43+
manualListener(data.value)
44+
}
4145
}),
4246
this.value
4347
)
@@ -54,6 +58,8 @@ describe('vModel', () => {
5458
triggerEvent('input', input)
5559
await nextTick()
5660
expect(data.value).toEqual('foo')
61+
// #1931
62+
expect(manualListener).toHaveBeenCalledWith('foo')
5763

5864
data.value = 'bar'
5965
await nextTick()

packages/runtime-dom/src/directives/vModel.ts

+14-11
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type ModelDirective<T> = ObjectDirective<T & { _assign: AssignerFn }>
4646
export const vModelText: ModelDirective<
4747
HTMLInputElement | HTMLTextAreaElement
4848
> = {
49-
beforeMount(el, { value, modifiers: { lazy, trim, number } }, vnode) {
49+
created(el, { value, modifiers: { lazy, trim, number } }, vnode) {
5050
el.value = value == null ? '' : value
5151
el._assign = getModelAssigner(vnode)
5252
const castToNumber = number || el.type === 'number'
@@ -90,7 +90,7 @@ export const vModelText: ModelDirective<
9090
}
9191

9292
export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
93-
beforeMount(el, binding, vnode) {
93+
created(el, binding, vnode) {
9494
setChecked(el, binding, vnode)
9595
el._assign = getModelAssigner(vnode)
9696
addEventListener(el, 'change', () => {
@@ -135,7 +135,7 @@ function setChecked(
135135
}
136136

137137
export const vModelRadio: ModelDirective<HTMLInputElement> = {
138-
beforeMount(el, { value }, vnode) {
138+
created(el, { value }, vnode) {
139139
el.checked = looseEqual(value, vnode.props!.value)
140140
el._assign = getModelAssigner(vnode)
141141
addEventListener(el, 'change', () => {
@@ -151,16 +151,19 @@ export const vModelRadio: ModelDirective<HTMLInputElement> = {
151151
}
152152

153153
export const vModelSelect: ModelDirective<HTMLSelectElement> = {
154-
// use mounted & updated because <select> relies on its children <option>s.
155-
mounted(el, { value }, vnode) {
156-
setSelected(el, value)
157-
el._assign = getModelAssigner(vnode)
154+
created(el, binding, vnode) {
158155
addEventListener(el, 'change', () => {
159156
const selectedVal = Array.prototype.filter
160157
.call(el.options, (o: HTMLOptionElement) => o.selected)
161158
.map(getValue)
162159
el._assign(el.multiple ? selectedVal : selectedVal[0])
163160
})
161+
el._assign = getModelAssigner(vnode)
162+
},
163+
// set value in mounted & updated because <select> relies on its children
164+
// <option>s.
165+
mounted(el, { value }) {
166+
setSelected(el, value)
164167
},
165168
beforeUpdate(el, _binding, vnode) {
166169
el._assign = getModelAssigner(vnode)
@@ -214,8 +217,8 @@ function getCheckboxValue(
214217
export const vModelDynamic: ObjectDirective<
215218
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
216219
> = {
217-
beforeMount(el, binding, vnode) {
218-
callModelHook(el, binding, vnode, null, 'beforeMount')
220+
created(el, binding, vnode) {
221+
callModelHook(el, binding, vnode, null, 'created')
219222
},
220223
mounted(el, binding, vnode) {
221224
callModelHook(el, binding, vnode, null, 'mounted')
@@ -233,7 +236,7 @@ function callModelHook(
233236
binding: DirectiveBinding,
234237
vnode: VNode,
235238
prevVNode: VNode | null,
236-
hook: 'beforeMount' | 'mounted' | 'beforeUpdate' | 'updated'
239+
hook: keyof ObjectDirective
237240
) {
238241
let modelToUse: ObjectDirective
239242
switch (el.tagName) {
@@ -244,7 +247,7 @@ function callModelHook(
244247
modelToUse = vModelText
245248
break
246249
default:
247-
switch (el.type) {
250+
switch (vnode.props && vnode.props.type) {
248251
case 'checkbox':
249252
modelToUse = vModelCheckbox
250253
break

0 commit comments

Comments
 (0)