Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
sxzz committed Sep 21, 2024
1 parent ef8c4f6 commit 9a32ccc
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 145 deletions.
2 changes: 1 addition & 1 deletion packages/runtime-vapor/src/apiCreateFor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,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<any>)
return [source[idx], idx, undefined]
} else {
Expand Down
23 changes: 8 additions & 15 deletions packages/runtime-vapor/src/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,25 @@ import {
} from './component'
import { warn } from './warning'
import { normalizeBlock } from './dom/element'
import { getCurrentScope } from '@vue/reactivity'
import { type ShallowRef, getCurrentScope, shallowRef } from '@vue/reactivity'
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'

export type DirectiveModifiers<M extends string = string> = Record<M, boolean>

export interface DirectiveBinding<T = any, V = any, M extends string = string> {
instance: ComponentInternalInstance
source?: () => V
source: () => V
arg?: string
modifiers?: DirectiveModifiers<M>
dir: Directive<T, V, M>
}

export type DirectiveBindingsMap = Map<Node, DirectiveBinding[]>

export type Directive<
T = any,
V = any,
M extends string = string,
> = DirectiveHook<T, V, M>

export type DirectiveHook<
T = any | null,
V = any,
M extends string = string,
> = (node: T, binding: DirectiveBinding<T, V, M>) => void
export type Directive<T = any, V = any, M extends string = string> = (
node: ShallowRef<T>,
binding: DirectiveBinding<T, V, M>,
) => void

export function validateDirectiveName(name: string): void {
if (isBuiltInDirective(name)) {
Expand Down Expand Up @@ -77,7 +70,7 @@ export function withDirectives<T extends ComponentInternalInstance | Node>(
}

for (const directive of directives) {
let [dir, source, arg, modifiers] = directive
let [dir, source = () => undefined, arg, modifiers] = directive
if (!dir) continue

const binding: DirectiveBinding = {
Expand All @@ -89,7 +82,7 @@ export function withDirectives<T extends ComponentInternalInstance | Node>(
}

callWithAsyncErrorHandling(dir, instance, VaporErrorCodes.DIRECTIVE_HOOK, [
node,
shallowRef(node),
binding,
])
}
Expand Down
163 changes: 77 additions & 86 deletions packages/runtime-vapor/src/directives/vModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -41,12 +43,12 @@ const assigningMap = new WeakMap<HTMLElement, boolean>()

// 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 } = {} }) {
> = ({ value: el }, { source, modifiers: { lazy, trim, number } = {} }) => {
onBeforeMount(() => {
const assigner = getModelAssigner(el)
assignFnMap.set(el, assigner)

Expand Down Expand Up @@ -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
Expand All @@ -108,29 +113,34 @@ export const vModelText: ObjectDirective<
}

el.value = newValue
},
})
}

export const vModelRadio: ObjectDirective<HTMLInputElement> = {
beforeMount(el, { value }) {
el.checked = looseEqual(value, getValue(el))
export const vModelRadio: Directive<HTMLInputElement> = (
{ value: 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<HTMLSelectElement, any, 'number'> = {
// <select multiple> value need to be deep traversed
deep: true,
beforeMount(el, { value, modifiers: { number = false } = {} }) {
export const vModelSelect: Directive<HTMLSelectElement, any, 'number'> = (
{ value: el },
{ source, modifiers: { number = false } = {} },
) => {
onBeforeMount(() => {
const value = source()
const isSetModel = isSet(value)
addEventListener(el, 'change', () => {
const selectedVal = Array.prototype.filter
Expand All @@ -153,15 +163,17 @@ export const vModelSelect: ObjectDirective<HTMLSelectElement, any, 'number'> = {
})
assignFnMap.set(el, getModelAssigner(el))
setSelected(el, value, number)
},
beforeUpdate(el) {
})

onBeforeUnmount(() => {
assignFnMap.set(el, getModelAssigner(el))
},
updated(el, { value, modifiers: { number = false } = {} }) {
})

renderEffect(() => {
if (!assigningMap.get(el)) {
setSelected(el, value, number)
setSelected(el, source(), number)
}
},
})
}

function setSelected(el: HTMLSelectElement, value: any, number: boolean) {
Expand Down Expand Up @@ -223,27 +235,15 @@ function getCheckboxValue(el: HTMLInputElement, checked: boolean) {
return checked
}

const setChecked: DirectiveHook<HTMLInputElement> = (
el,
{ value, oldValue },
export const vModelCheckbox: Directive<HTMLInputElement> = (
{ value: el },
{ source },
) => {
if (isArray(value)) {
el.checked = looseIndexOf(value, getValue(el)) > -1
} else if (isSet(value)) {
el.checked = value.has(getValue(el))
} else if (value !== oldValue) {
el.checked = looseEqual(value, getCheckboxValue(el, true))
}
}

export const vModelCheckbox: ObjectDirective<HTMLInputElement> = {
// #4096 array checkboxes need to be deep traversed
deep: true,
beforeMount(el, binding) {
onBeforeMount(() => {
assignFnMap.set(el, getModelAssigner(el))

addEventListener(el, 'change', () => {
const modelValue = binding.value
const modelValue = source()
const elementValue = getValue(el)
const checked = el.checked
const assigner = assignFnMap.get(el)!
Expand All @@ -269,36 +269,38 @@ export const vModelCheckbox: ObjectDirective<HTMLInputElement> = {
assigner(getCheckboxValue(el, checked))
}
})
},
// set initial checked on mount to wait for true-value/false-value
mounted: setChecked,
beforeUpdate(el, binding) {
})

onMounted(() => {
setChecked()
})

onBeforeUpdate(() => {
assignFnMap.set(el, getModelAssigner(el))
setChecked(el, binding)
},
setChecked()
})

function setChecked() {
const value = source()
if (isArray(value)) {
el.checked = looseIndexOf(value, getValue(el)) > -1
} else if (isSet(value)) {
el.checked = value.has(getValue(el))
} else {
el.checked = looseEqual(value, getCheckboxValue(el, true))
}
}
}

export const vModelDynamic: ObjectDirective<
export const vModelDynamic: Directive<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
> = {
beforeMount(el, binding) {
callModelHook(el, binding, 'beforeMount')
},
mounted(el, binding) {
callModelHook(el, binding, 'mounted')
},
beforeUpdate(el, binding) {
callModelHook(el, binding, 'beforeUpdate')
},
updated(el, binding) {
callModelHook(el, binding, 'updated')
},
> = (elRef, binding) => {
const type = elRef.value.getAttribute('type')
const modelToUse = resolveDynamicModel(elRef.value.tagName, type)
modelToUse(elRef, binding)
}

function resolveDynamicModel(
tagName: string,
type: string | null,
): ObjectDirective {
function resolveDynamicModel(tagName: string, type: string | null): Directive {
switch (tagName) {
case 'SELECT':
return vModelSelect
Expand All @@ -315,14 +317,3 @@ function resolveDynamicModel(
}
}
}

function callModelHook(
el: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement,
binding: DirectiveBinding,
hook: DirectiveHookName,
) {
const type = el.getAttribute('type')
const modelToUse = resolveDynamicModel(el.tagName, type)
const fn = modelToUse[hook]
fn && fn(el, binding)
}
8 changes: 4 additions & 4 deletions packages/runtime-vapor/src/directives/vShow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ export interface VShowElement extends HTMLElement {
[vShowHidden]: boolean
}

export const vShow: Directive<VShowElement> = (node, { source }) => {
export const vShow: Directive<VShowElement> = ({ value: el }, { source }) => {
function getValue(): boolean {
return source ? source() : false
}

onBeforeMount(() => {
node[vShowOriginalDisplay] =
node.style.display === 'none' ? '' : node.style.display
el[vShowOriginalDisplay] =
el.style.display === 'none' ? '' : el.style.display
})

renderEffect(() => {
setDisplay(node, getValue())
setDisplay(el, getValue())
})
}

Expand Down
34 changes: 4 additions & 30 deletions playground/src/directive.vue
Original file line number Diff line number Diff line change
@@ -1,31 +1,10 @@
<script setup lang="ts">
import { ObjectDirective, FunctionDirective, ref } from '@vue/vapor'
import { Directive, ref } from '@vue/vapor'
const text = ref('created (overwrite by v-text), ')
const counter = ref(0)
const vDirective: ObjectDirective<HTMLDivElement, undefined> = {
created(node) {
if (!node.parentElement) {
node.textContent += 'created, '
node.style.color = 'red'
} else {
alert('!')
}
},
beforeMount(node) {
if (!node.parentElement) node.textContent += 'beforeMount, '
},
mounted(node) {
if (node.parentElement) node.textContent += 'mounted, '
},
beforeUpdate(node, binding) {
console.log('beforeUpdate', binding, node)
},
updated(node, binding) {
console.log('updated', binding, node)
},
}
const vDirectiveSimple: FunctionDirective<HTMLDivElement> = (node, binding) => {
const vDirective: Directive<HTMLDivElement> = (node, binding) => {
console.log('v-directive-simple:', node, binding)
}
const handleClick = () => {
Expand All @@ -34,12 +13,7 @@ const handleClick = () => {
</script>

<template>
<div
v-directive:foo.bar="text"
v-text="text"
v-directive-simple="text"
@click="handleClick"
/>
<div v-directive:foo.bar="text" v-text="text" @click="handleClick" />
<button @click="counter++">
{{ counter }} (Click to Update Other Element)
</button>
Expand Down
Loading

0 comments on commit 9a32ccc

Please sign in to comment.