فهرست منبع

feat(directives): introduce `created` custom directive hook and ensure
`v-model` event listener fire before template/props listeners

fix #1931

Evan You 5 سال پیش
والد
کامیت
11804fe93f

+ 1 - 0
packages/runtime-core/src/directives.ts

@@ -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>

+ 6 - 4
packages/runtime-core/src/renderer.ts

@@ -720,6 +720,9 @@ function baseCreateRenderer(
         )
       }
 
+      if (dirs) {
+        invokeDirectiveHook(vnode, null, parentComponent, 'created')
+      }
       // props
       if (props) {
         for (const key in props) {
@@ -741,10 +744,6 @@ function baseCreateRenderer(
           invokeVNodeHook(vnodeHook, parentComponent, vnode)
         }
       }
-      if (dirs) {
-        invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
-      }
-
       // scopeId
       if (scopeId) {
         hostSetScopeId(el, scopeId)
@@ -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 =

+ 7 - 1
packages/runtime-dom/__tests__/directives/vModel.spec.ts

@@ -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 }
@@ -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
           )
@@ -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()

+ 14 - 11
packages/runtime-dom/src/directives/vModel.ts

@@ -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'
@@ -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', () => {
@@ -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', () => {
@@ -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)
@@ -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')
@@ -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) {
@@ -244,7 +247,7 @@ function callModelHook(
       modelToUse = vModelText
       break
     default:
-      switch (el.type) {
+      switch (vnode.props && vnode.props.type) {
         case 'checkbox':
           modelToUse = vModelCheckbox
           break