diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index dabeb27e7..8fcddf317 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -405,7 +405,7 @@ function getItem( } else if (typeof source === 'number') { return [idx + 1, idx, undefined] } else if (isObject(source)) { - if (source && source[Symbol.iterator as any]) { + if (source[Symbol.iterator as any]) { source = Array.from(source as Iterable) return [source[idx], idx, undefined] } else { diff --git a/packages/runtime-vapor/src/apiRender.ts b/packages/runtime-vapor/src/apiRender.ts index db4eed533..d691a48dc 100644 --- a/packages/runtime-vapor/src/apiRender.ts +++ b/packages/runtime-vapor/src/apiRender.ts @@ -128,7 +128,7 @@ function mountComponent( } // hook: beforeMount - invokeLifecycle(instance, VaporLifecycleHooks.BEFORE_MOUNT, 'beforeMount') + invokeLifecycle(instance, VaporLifecycleHooks.BEFORE_MOUNT) insert(instance.block!, instance.container) @@ -136,7 +136,6 @@ function mountComponent( invokeLifecycle( instance, VaporLifecycleHooks.MOUNTED, - 'mounted', instance => (instance.isMounted = true), true, ) @@ -156,7 +155,7 @@ export function unmountComponent(instance: ComponentInternalInstance): void { const { container, scope } = instance // hook: beforeUnmount - invokeLifecycle(instance, VaporLifecycleHooks.BEFORE_UNMOUNT, 'beforeUnmount') + invokeLifecycle(instance, VaporLifecycleHooks.BEFORE_UNMOUNT) scope.stop() container.textContent = '' @@ -165,7 +164,6 @@ export function unmountComponent(instance: ComponentInternalInstance): void { invokeLifecycle( instance, VaporLifecycleHooks.UNMOUNTED, - 'unmounted', instance => queuePostFlushCb(() => (instance.isUnmounted = true)), true, ) diff --git a/packages/runtime-vapor/src/componentLifecycle.ts b/packages/runtime-vapor/src/componentLifecycle.ts index 41e9edb2e..4c2918c3e 100644 --- a/packages/runtime-vapor/src/componentLifecycle.ts +++ b/packages/runtime-vapor/src/componentLifecycle.ts @@ -2,12 +2,10 @@ import { invokeArrayFns } from '@vue/shared' import type { VaporLifecycleHooks } from './enums' import { type ComponentInternalInstance, setCurrentInstance } from './component' import { queuePostFlushCb } from './scheduler' -import type { DirectiveHookName } from './directives' export function invokeLifecycle( instance: ComponentInternalInstance, lifecycle: VaporLifecycleHooks, - directive: DirectiveHookName, cb?: (instance: ComponentInternalInstance) => void, post?: boolean, ): void { @@ -27,8 +25,6 @@ export function invokeLifecycle( } function invokeSub() { - instance.comps.forEach(comp => - invokeLifecycle(comp, lifecycle, directive, cb, post), - ) + instance.comps.forEach(comp => invokeLifecycle(comp, lifecycle, cb, post)) } } diff --git a/packages/runtime-vapor/src/directives.ts b/packages/runtime-vapor/src/directives.ts index b6a09c322..58eeb7a04 100644 --- a/packages/runtime-vapor/src/directives.ts +++ b/packages/runtime-vapor/src/directives.ts @@ -1,54 +1,30 @@ import { isBuiltInDirective } from '@vue/shared' -import { type ComponentInternalInstance, currentInstance } from './component' +import { + type ComponentInternalInstance, + currentInstance, + isVaporComponent, +} from './component' import { warn } from './warning' +import { normalizeBlock } from './dom/element' +import { getCurrentScope } from '@vue/reactivity' +import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling' export type DirectiveModifiers = Record export interface DirectiveBinding { instance: ComponentInternalInstance - source?: () => V - value: V - oldValue: V | null + source: () => V arg?: string modifiers?: DirectiveModifiers - dir: ObjectDirective + dir: Directive } export type DirectiveBindingsMap = Map -export type DirectiveHook< - T = any | null, - V = any, - M extends string = string, -> = (node: T, binding: DirectiveBinding) => void - -// create node -> `created` -> node operation -> `beforeMount` -> node mounted -> `mounted` -// effect update -> `beforeUpdate` -> node updated -> `updated` -// `beforeUnmount`-> node unmount -> `unmounted` -export type DirectiveHookName = - | 'created' - | 'beforeMount' - | 'mounted' - | 'beforeUpdate' - | 'updated' - | 'beforeUnmount' - | 'unmounted' -export type ObjectDirective = { - [K in DirectiveHookName]?: DirectiveHook | undefined -} & { - /** Watch value deeply */ - deep?: boolean | number -} - -export type FunctionDirective< - T = any, - V = any, - M extends string = string, -> = DirectiveHook - -export type Directive = - | ObjectDirective - | FunctionDirective +export type Directive = ( + node: T, + binding: DirectiveBinding, +) => void export function validateDirectiveName(name: string): void { if (isBuiltInDirective(name)) { @@ -77,7 +53,54 @@ export function withDirectives( return nodeOrComponent } - // NOOP + let node: Node + if (isVaporComponent(nodeOrComponent)) { + const root = getComponentNode(nodeOrComponent) + if (!root) return nodeOrComponent + node = root + } else { + node = nodeOrComponent + } + + const instance = currentInstance! + const parentScope = getCurrentScope() + + if (__DEV__ && !parentScope) { + warn(`Directives should be used inside of RenderEffectScope.`) + } + + for (const directive of directives) { + let [dir, source = () => undefined, arg, modifiers] = directive + if (!dir) continue + + const binding: DirectiveBinding = { + dir, + source, + instance, + arg, + modifiers, + } + + callWithAsyncErrorHandling(dir, instance, VaporErrorCodes.DIRECTIVE_HOOK, [ + node, + binding, + ]) + } return nodeOrComponent } + +function getComponentNode(component: ComponentInternalInstance) { + if (!component.block) return + + const nodes = normalizeBlock(component.block) + if (nodes.length !== 1) { + warn( + `Runtime directive used on component with non-element root node. ` + + `The directives will not function as intended.`, + ) + return + } + + return nodes[0] +} diff --git a/packages/runtime-vapor/src/directives/vModel.ts b/packages/runtime-vapor/src/directives/vModel.ts index b212437fd..1c8ad5adf 100644 --- a/packages/runtime-vapor/src/directives/vModel.ts +++ b/packages/runtime-vapor/src/directives/vModel.ts @@ -6,16 +6,18 @@ import { looseIndexOf, looseToNumber, } from '@vue/shared' -import type { - DirectiveBinding, - DirectiveHook, - DirectiveHookName, - ObjectDirective, -} from '../directives' +import type { Directive } from '../directives' import { addEventListener } from '../dom/event' import { nextTick } from '../scheduler' import { warn } from '../warning' import { MetadataKind, getMetadata } from '../componentMetadata' +import { + onBeforeMount, + onBeforeUnmount, + onBeforeUpdate, + onMounted, +} from '../apiLifecycle' +import { renderEffect } from '../renderEffect' type AssignerFn = (value: any) => void function getModelAssigner(el: Element): AssignerFn { @@ -41,12 +43,12 @@ const assigningMap = new WeakMap() // We are exporting the v-model runtime directly as vnode hooks so that it can // be tree-shaken in case v-model is never used. -export const vModelText: ObjectDirective< +export const vModelText: Directive< HTMLInputElement | HTMLTextAreaElement, any, 'lazy' | 'trim' | 'number' -> = { - beforeMount(el, { modifiers: { lazy, trim, number } = {} }) { +> = (el, { source, modifiers: { lazy, trim, number } = {} }) => { + onBeforeMount(() => { const assigner = getModelAssigner(el) assignFnMap.set(el, assigner) @@ -78,12 +80,15 @@ export const vModelText: ObjectDirective< // fires "change" instead of "input" on autocomplete. addEventListener(el, 'change', onCompositionEnd) } - }, - // set value on mounted so it's after min/max for type="range" - mounted(el, { value }) { + }) + + onMounted(() => { + const value = source() el.value = value == null ? '' : value - }, - beforeUpdate(el, { value, modifiers: { lazy, trim, number } = {} }) { + }) + + renderEffect(() => { + const value = source() assignFnMap.set(el, getModelAssigner(el)) // avoid clearing unresolved text. #2302 @@ -108,29 +113,31 @@ export const vModelText: ObjectDirective< } el.value = newValue - }, + }) } -export const vModelRadio: ObjectDirective = { - beforeMount(el, { value }) { - el.checked = looseEqual(value, getValue(el)) +export const vModelRadio: Directive = (el, { source }) => { + onBeforeMount(() => { + el.checked = looseEqual(source(), getValue(el)) assignFnMap.set(el, getModelAssigner(el)) addEventListener(el, 'change', () => { assignFnMap.get(el)!(getValue(el)) }) - }, - beforeUpdate(el, { value, oldValue }) { + }) + + renderEffect(() => { + const value = source() assignFnMap.set(el, getModelAssigner(el)) - if (value !== oldValue) { - el.checked = looseEqual(value, getValue(el)) - } - }, + el.checked = looseEqual(value, getValue(el)) + }) } -export const vModelSelect: ObjectDirective = { - //