Merge remote-tracking branch 'github/master' into changing_unwrap_ref

# Conflicts:
#	packages/reactivity/src/ref.ts
#	packages/runtime-core/__tests__/apiTemplateRef.spec.ts
#	packages/runtime-core/src/apiWatch.ts
This commit is contained in:
pikax
2020-04-08 21:21:04 +01:00
339 changed files with 26645 additions and 8965 deletions

View File

@@ -9,5 +9,5 @@ const RootComponent = {
}
}
createApp().mount(RootComponent, '#app')
createApp(RootComponent).mount('#app')
```

View File

@@ -0,0 +1,20 @@
import { render, h } from '@vue/runtime-dom'
describe('customimized built-in elements support', () => {
let createElement: jest.SpyInstance
afterEach(() => {
createElement.mockRestore()
})
test('should created element with is option', () => {
const root = document.createElement('div')
createElement = jest.spyOn(document, 'createElement')
render(h('button', { is: 'plastic-button' }), root)
expect(createElement.mock.calls[0]).toMatchObject([
'button',
{ is: 'plastic-button' }
])
// should also render the attribute
expect(root.innerHTML).toBe(`<button is="plastic-button"></button>`)
})
})

View File

@@ -0,0 +1,12 @@
import { createApp } from '@vue/runtime-dom'
describe('vCloak', () => {
test('should be removed after compile', () => {
const root = document.createElement('div')
root.setAttribute('v-cloak', '')
createApp({
render() {}
}).mount(root)
expect(root.hasAttribute('v-cloak')).toBe(false)
})
})

View File

@@ -1,11 +1,12 @@
import {
createApp,
h,
render,
nextTick,
createComponent,
defineComponent,
vModelDynamic,
withDirectives,
VNode
VNode,
ref
} from '@vue/runtime-dom'
const triggerEvent = (type: string, el: Element) => {
@@ -20,16 +21,15 @@ const setValue = function(this: any, value: any) {
this.value = value
}
let app: any, root: any
let root: any
beforeEach(() => {
app = createApp()
root = document.createElement('div') as any
})
describe('vModel', () => {
it('should work with text input', async () => {
const component = createComponent({
const component = defineComponent({
data() {
return { value: null }
},
@@ -44,9 +44,9 @@ describe('vModel', () => {
]
}
})
app.mount(component, root)
render(h(component), root)
const input = root.querySelector('input')
const input = root.querySelector('input')!
const data = root._vnode.component.data
input.value = 'foo'
@@ -59,8 +59,74 @@ describe('vModel', () => {
expect(input.value).toEqual('bar')
})
it('should work with multiple listeners', async () => {
const spy = jest.fn()
const component = defineComponent({
data() {
return { value: null }
},
render() {
return [
withVModel(
h('input', {
'onUpdate:modelValue': [setValue.bind(this), spy]
}),
this.value
)
]
}
})
render(h(component), root)
const input = root.querySelector('input')!
const data = root._vnode.component.data
input.value = 'foo'
triggerEvent('input', input)
await nextTick()
expect(data.value).toEqual('foo')
expect(spy).toHaveBeenCalledWith('foo')
})
it('should work with updated listeners', async () => {
const spy1 = jest.fn()
const spy2 = jest.fn()
const toggle = ref(true)
const component = defineComponent({
render() {
return [
withVModel(
h('input', {
'onUpdate:modelValue': toggle.value ? spy1 : spy2
}),
'foo'
)
]
}
})
render(h(component), root)
const input = root.querySelector('input')!
input.value = 'foo'
triggerEvent('input', input)
await nextTick()
expect(spy1).toHaveBeenCalledWith('foo')
// udpate listener
toggle.value = false
await nextTick()
input.value = 'bar'
triggerEvent('input', input)
await nextTick()
expect(spy1).not.toHaveBeenCalledWith('bar')
expect(spy2).toHaveBeenCalledWith('bar')
})
it('should work with textarea', async () => {
const component = createComponent({
const component = defineComponent({
data() {
return { value: null }
},
@@ -75,7 +141,7 @@ describe('vModel', () => {
]
}
})
app.mount(component, root)
render(h(component), root)
const input = root.querySelector('textarea')
const data = root._vnode.component.data
@@ -91,7 +157,7 @@ describe('vModel', () => {
})
it('should support modifiers', async () => {
const component = createComponent({
const component = defineComponent({
data() {
return { number: null, trim: null, lazy: null }
},
@@ -136,7 +202,7 @@ describe('vModel', () => {
]
}
})
app.mount(component, root)
render(h(component), root)
const number = root.querySelector('.number')
const trim = root.querySelector('.trim')
@@ -160,7 +226,7 @@ describe('vModel', () => {
})
it('should work with checkbox', async () => {
const component = createComponent({
const component = defineComponent({
data() {
return { value: null }
},
@@ -176,7 +242,7 @@ describe('vModel', () => {
]
}
})
app.mount(component, root)
render(h(component), root)
const input = root.querySelector('input')
const data = root._vnode.component.data
@@ -201,7 +267,7 @@ describe('vModel', () => {
})
it('should work with checkbox and true-value/false-value', async () => {
const component = createComponent({
const component = defineComponent({
data() {
return { value: null }
},
@@ -219,7 +285,7 @@ describe('vModel', () => {
]
}
})
app.mount(component, root)
render(h(component), root)
const input = root.querySelector('input')
const data = root._vnode.component.data
@@ -244,7 +310,7 @@ describe('vModel', () => {
})
it('should work with checkbox and true-value/false-value with object values', async () => {
const component = createComponent({
const component = defineComponent({
data() {
return { value: null }
},
@@ -262,7 +328,7 @@ describe('vModel', () => {
]
}
})
app.mount(component, root)
render(h(component), root)
const input = root.querySelector('input')
const data = root._vnode.component.data
@@ -287,7 +353,7 @@ describe('vModel', () => {
})
it(`should support array as a checkbox model`, async () => {
const component = createComponent({
const component = defineComponent({
data() {
return { value: [] }
},
@@ -314,7 +380,7 @@ describe('vModel', () => {
]
}
})
app.mount(component, root)
render(h(component), root)
const foo = root.querySelector('.foo')
const bar = root.querySelector('.bar')
@@ -357,7 +423,7 @@ describe('vModel', () => {
})
it('should work with radio', async () => {
const component = createComponent({
const component = defineComponent({
data() {
return { value: null }
},
@@ -384,7 +450,7 @@ describe('vModel', () => {
]
}
})
app.mount(component, root)
render(h(component), root)
const foo = root.querySelector('.foo')
const bar = root.querySelector('.bar')
@@ -417,7 +483,7 @@ describe('vModel', () => {
})
it('should work with single select', async () => {
const component = createComponent({
const component = defineComponent({
data() {
return { value: null }
},
@@ -437,7 +503,7 @@ describe('vModel', () => {
]
}
})
app.mount(component, root)
render(h(component), root)
const input = root.querySelector('select')
const foo = root.querySelector('option[value=foo]')
@@ -473,7 +539,7 @@ describe('vModel', () => {
})
it('should work with multiple select', async () => {
const component = createComponent({
const component = defineComponent({
data() {
return { value: [] }
},
@@ -494,7 +560,7 @@ describe('vModel', () => {
]
}
})
app.mount(component, root)
render(h(component), root)
const input = root.querySelector('select')
const foo = root.querySelector('option[value=foo]')

View File

@@ -22,9 +22,9 @@ describe('runtime-dom: v-on directive', () => {
const child = document.createElement('input')
parent.appendChild(child)
const childNextValue = withModifiers(jest.fn(), ['prevent', 'stop'])
patchEvent(child, 'click', null, childNextValue, null)
patchEvent(child, 'onClick', null, childNextValue, null)
const parentNextValue = jest.fn()
patchEvent(parent, 'click', null, parentNextValue, null)
patchEvent(parent, 'onClick', null, parentNextValue, null)
expect(triggerEvent(child, 'click').defaultPrevented).toBe(true)
expect(parentNextValue).not.toBeCalled()
})
@@ -35,7 +35,7 @@ describe('runtime-dom: v-on directive', () => {
parent.appendChild(child)
const fn = jest.fn()
const handler = withModifiers(fn, ['self'])
patchEvent(parent, 'click', null, handler, null)
patchEvent(parent, 'onClick', null, handler, null)
triggerEvent(child, 'click')
expect(fn).not.toBeCalled()
})
@@ -48,7 +48,7 @@ describe('runtime-dom: v-on directive', () => {
'esc',
'arrow-left'
])
patchEvent(el, 'keyup', null, nextValue, null)
patchEvent(el, 'onKeyup', null, nextValue, null)
triggerEvent(el, 'keyup', e => (e.key = 'a'))
expect(fn).not.toBeCalled()
@@ -77,7 +77,7 @@ describe('runtime-dom: v-on directive', () => {
// Case 1: <div @keyup.exact="test"/>
const fn1 = jest.fn()
const next1 = withModifiers(fn1, ['exact'])
patchEvent(el, 'keyup', null, next1, null)
patchEvent(el, 'onKeyup', null, next1, null)
triggerEvent(el, 'keyup')
expect(fn1.mock.calls.length).toBe(1)
triggerEvent(el, 'keyup', e => (e.ctrlKey = true))
@@ -85,7 +85,7 @@ describe('runtime-dom: v-on directive', () => {
// Case 2: <div @keyup.ctrl.a.exact="test"/>
const fn2 = jest.fn()
const next2 = withKeys(withModifiers(fn2, ['ctrl', 'exact']), ['a'])
patchEvent(el, 'keyup', null, next2, null)
patchEvent(el, 'onKeyup', null, next2, null)
triggerEvent(el, 'keyup', e => (e.key = 'a'))
expect(fn2).not.toBeCalled()
triggerEvent(el, 'keyup', e => {
@@ -109,7 +109,7 @@ describe('runtime-dom: v-on directive', () => {
const el = document.createElement('div')
const fn = jest.fn()
const handler = withModifiers(fn, [button])
patchEvent(el, 'mousedown', null, handler, null)
patchEvent(el, 'onMousedown', null, handler, null)
buttons.filter(b => b !== button).forEach(button => {
triggerEvent(el, 'mousedown', e => (e.button = buttonCodes[button]))
})

View File

@@ -1,25 +1,24 @@
import {
withDirectives,
createComponent,
defineComponent,
h,
nextTick,
VNode
} from '@vue/runtime-core'
import { createApp, vShow } from '@vue/runtime-dom'
import { render, vShow } from '@vue/runtime-dom'
const withVShow = (node: VNode, exp: any) =>
withDirectives(node, [[vShow, exp]])
let app: any, root: any
let root: any
beforeEach(() => {
app = createApp()
root = document.createElement('div') as any
root = document.createElement('div')
})
describe('runtime-dom: v-show directive', () => {
test('should check show value is truthy', async () => {
const component = createComponent({
const component = defineComponent({
data() {
return { value: true }
},
@@ -27,7 +26,7 @@ describe('runtime-dom: v-show directive', () => {
return [withVShow(h('div'), this.value)]
}
})
app.mount(component, root)
render(h(component), root)
const $div = root.querySelector('div')
@@ -35,7 +34,7 @@ describe('runtime-dom: v-show directive', () => {
})
test('should check show value is falsy', async () => {
const component = createComponent({
const component = defineComponent({
data() {
return { value: false }
},
@@ -43,7 +42,7 @@ describe('runtime-dom: v-show directive', () => {
return [withVShow(h('div'), this.value)]
}
})
app.mount(component, root)
render(h(component), root)
const $div = root.querySelector('div')
@@ -51,7 +50,7 @@ describe('runtime-dom: v-show directive', () => {
})
it('should update show value changed', async () => {
const component = createComponent({
const component = defineComponent({
data() {
return { value: true }
},
@@ -59,7 +58,7 @@ describe('runtime-dom: v-show directive', () => {
return [withVShow(h('div'), this.value)]
}
})
app.mount(component, root)
render(h(component), root)
const $div = root.querySelector('div')
const data = root._vnode.component.data
@@ -100,7 +99,7 @@ describe('runtime-dom: v-show directive', () => {
})
test('should respect display value in style attribute', async () => {
const component = createComponent({
const component = defineComponent({
data() {
return { value: true }
},
@@ -110,7 +109,7 @@ describe('runtime-dom: v-show directive', () => {
]
}
})
app.mount(component, root)
render(h(component), root)
const $div = root.querySelector('div')
const data = root._vnode.component.data

View File

@@ -0,0 +1,27 @@
import { patchAttr, xlinkNS } from '../../src/modules/attrs'
describe('attrs', () => {
test('xlink attributes', () => {
const el = document.createElementNS('http://www.w3.org/2000/svg', 'use')
patchAttr(el, 'xlink:href', 'a', true)
expect(el.getAttributeNS(xlinkNS, 'href')).toBe('a')
patchAttr(el, 'xlink:href', null, true)
expect(el.getAttributeNS(xlinkNS, 'href')).toBe(null)
})
test('boolean attributes', () => {
const el = document.createElement('input')
patchAttr(el, 'readonly', true, false)
expect(el.getAttribute('readonly')).toBe('')
patchAttr(el, 'readonly', false, false)
expect(el.getAttribute('readonly')).toBe(null)
})
test('attributes', () => {
const el = document.createElement('div')
patchAttr(el, 'id', 'a', false)
expect(el.getAttribute('id')).toBe('a')
patchAttr(el, 'id', null, false)
expect(el.getAttribute('id')).toBe(null)
})
})

View File

@@ -1,6 +1,6 @@
// https://github.com/vuejs/vue/blob/dev/test/unit/features/directives/class.spec.js
import { h, render, createComponent } from '../../src'
import { h, render, defineComponent } from '../../src'
type ClassItem = {
value: string | object | string[]
@@ -70,13 +70,11 @@ describe('class', () => {
const childClass: ClassItem = { value: 'd' }
const child = {
props: {},
render: () => h('div', { class: ['c', childClass.value] })
}
const parentClass: ClassItem = { value: 'b' }
const parent = {
props: {},
render: () => h(child, { class: ['a', parentClass.value] })
}
@@ -100,29 +98,26 @@ describe('class', () => {
})
test('class merge between multiple nested components sharing same element', () => {
const component1 = createComponent({
props: {},
const component1 = defineComponent({
render() {
return this.$slots.default()[0]
return this.$slots.default!()[0]
}
})
const component2 = createComponent({
props: {},
const component2 = defineComponent({
render() {
return this.$slots.default()[0]
return this.$slots.default!()[0]
}
})
const component3 = createComponent({
props: {},
const component3 = defineComponent({
render() {
return h(
'div',
{
class: 'staticClass'
},
[this.$slots.default()]
[this.$slots.default!()]
)
}
})

View File

@@ -1,18 +1,19 @@
import { patchEvent } from '../../src/modules/events'
import { nextTick } from '@vue/runtime-dom'
const timeout = () => new Promise(r => setTimeout(r))
describe(`events`, () => {
it('should assign event handler', async () => {
const el = document.createElement('div')
const event = new Event('click')
const fn = jest.fn()
patchEvent(el, 'click', null, fn, null)
patchEvent(el, 'onClick', null, fn, null)
el.dispatchEvent(event)
await nextTick()
await timeout()
el.dispatchEvent(event)
await nextTick()
await timeout()
el.dispatchEvent(event)
await nextTick()
await timeout()
expect(fn).toHaveBeenCalledTimes(3)
})
@@ -21,14 +22,14 @@ describe(`events`, () => {
const event = new Event('click')
const prevFn = jest.fn()
const nextFn = jest.fn()
patchEvent(el, 'click', null, prevFn, null)
patchEvent(el, 'onClick', null, prevFn, null)
el.dispatchEvent(event)
patchEvent(el, 'click', prevFn, nextFn, null)
await nextTick()
patchEvent(el, 'onClick', prevFn, nextFn, null)
await timeout()
el.dispatchEvent(event)
await nextTick()
await timeout()
el.dispatchEvent(event)
await nextTick()
await timeout()
expect(prevFn).toHaveBeenCalledTimes(1)
expect(nextFn).toHaveBeenCalledTimes(2)
})
@@ -38,9 +39,9 @@ describe(`events`, () => {
const event = new Event('click')
const fn1 = jest.fn()
const fn2 = jest.fn()
patchEvent(el, 'click', null, [fn1, fn2], null)
patchEvent(el, 'onClick', null, [fn1, fn2], null)
el.dispatchEvent(event)
await nextTick()
await timeout()
expect(fn1).toHaveBeenCalledTimes(1)
expect(fn2).toHaveBeenCalledTimes(1)
})
@@ -49,10 +50,10 @@ describe(`events`, () => {
const el = document.createElement('div')
const event = new Event('click')
const fn = jest.fn()
patchEvent(el, 'click', null, fn, null)
patchEvent(el, 'click', fn, null, null)
patchEvent(el, 'onClick', null, fn, null)
patchEvent(el, 'onClick', fn, null, null)
el.dispatchEvent(event)
await nextTick()
await timeout()
expect(fn).not.toHaveBeenCalled()
})
@@ -66,11 +67,11 @@ describe(`events`, () => {
once: true
}
}
patchEvent(el, 'click', null, nextValue, null)
patchEvent(el, 'onClick', null, nextValue, null)
el.dispatchEvent(event)
await nextTick()
await timeout()
el.dispatchEvent(event)
await nextTick()
await timeout()
expect(fn).toHaveBeenCalledTimes(1)
})
@@ -85,12 +86,12 @@ describe(`events`, () => {
once: true
}
}
patchEvent(el, 'click', null, prevFn, null)
patchEvent(el, 'click', prevFn, nextValue, null)
patchEvent(el, 'onClick', null, prevFn, null)
patchEvent(el, 'onClick', prevFn, nextValue, null)
el.dispatchEvent(event)
await nextTick()
await timeout()
el.dispatchEvent(event)
await nextTick()
await timeout()
expect(prevFn).not.toHaveBeenCalled()
expect(nextFn).toHaveBeenCalledTimes(1)
})
@@ -105,12 +106,30 @@ describe(`events`, () => {
once: true
}
}
patchEvent(el, 'click', null, nextValue, null)
patchEvent(el, 'click', nextValue, null, null)
patchEvent(el, 'onClick', null, nextValue, null)
patchEvent(el, 'onClick', nextValue, null, null)
el.dispatchEvent(event)
await nextTick()
await timeout()
el.dispatchEvent(event)
await nextTick()
await timeout()
expect(fn).not.toHaveBeenCalled()
})
it('should assign native onclick attribute', async () => {
const el = document.createElement('div')
const event = new Event('click')
const fn = ((window as any)._nativeClickSpy = jest.fn())
patchEvent(el, 'onclick', null, '_nativeClickSpy()' as any)
el.dispatchEvent(event)
await timeout()
expect(fn).toHaveBeenCalledTimes(1)
const fn2 = jest.fn()
patchEvent(el, 'onclick', null, fn2)
el.dispatchEvent(event)
await timeout()
expect(fn).toHaveBeenCalledTimes(1)
expect(fn2).toHaveBeenCalledTimes(1)
})
})

View File

@@ -21,7 +21,7 @@ describe(`module style`, () => {
it('remove if falsy value', () => {
const el = document.createElement('div')
patchStyle(el, { color: 'red' }, { color: null })
patchStyle(el, { color: 'red' }, { color: undefined })
expect(el.style.cssText.replace(/\s/g, '')).toBe('')
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,18 @@
{
"name": "@vue/runtime-dom",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.11",
"description": "@vue/runtime-dom",
"main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js",
"types": "dist/runtime-dom.d.ts",
"unpkg": "dist/runtime-dom.global.js",
"files": [
"index.js",
"dist"
],
"types": "dist/runtime-dom.d.ts",
"unpkg": "dist/runtime-dom.global.js",
"sideEffects": false,
"buildOptions": {
"name": "VueDOMRuntime",
"name": "VueRuntimeDOM",
"formats": [
"esm-bundler",
"cjs",
@@ -25,7 +25,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/vue.git"
"url": "git+https://github.com/vuejs/vue-next.git"
},
"keywords": [
"vue"
@@ -33,10 +33,12 @@
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/vue/issues"
"url": "https://github.com/vuejs/vue-next/issues"
},
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/runtime-dom#readme",
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/runtime-dom#readme",
"dependencies": {
"@vue/runtime-core": "3.0.0-alpha.0"
"@vue/shared": "3.0.0-alpha.11",
"@vue/runtime-core": "3.0.0-alpha.11",
"csstype": "^2.6.8"
}
}

View File

@@ -13,7 +13,7 @@ import { ErrorCodes } from 'packages/runtime-core/src/errorHandling'
const TRANSITION = 'transition'
const ANIMATION = 'animation'
export interface TransitionProps extends BaseTransitionProps {
export interface TransitionProps extends BaseTransitionProps<Element> {
name?: string
type?: typeof TRANSITION | typeof ANIMATION
css?: boolean
@@ -37,7 +37,7 @@ export const Transition: FunctionalComponent<TransitionProps> = (
{ slots }
) => h(BaseTransition, resolveTransitionProps(props), slots)
export const TransitionPropsValidators = {
export const TransitionPropsValidators = (Transition.props = {
...(BaseTransition as any).props,
name: String,
type: String,
@@ -45,7 +45,7 @@ export const TransitionPropsValidators = {
type: Boolean,
default: true
},
duration: Object,
duration: [String, Number, Object],
enterFromClass: String,
enterActiveClass: String,
enterToClass: String,
@@ -55,11 +55,7 @@ export const TransitionPropsValidators = {
leaveFromClass: String,
leaveActiveClass: String,
leaveToClass: String
}
if (__DEV__) {
Transition.props = TransitionPropsValidators
}
})
export function resolveTransitionProps({
name = 'v',
@@ -76,7 +72,7 @@ export function resolveTransitionProps({
leaveActiveClass = `${name}-leave-active`,
leaveToClass = `${name}-leave-to`,
...baseProps
}: TransitionProps): BaseTransitionProps {
}: TransitionProps): BaseTransitionProps<Element> {
if (!css) {
return baseProps
}
@@ -94,7 +90,7 @@ export function resolveTransitionProps({
enterToClass = appearToClass
}
type Hook = (el: HTMLElement, done?: () => void) => void
type Hook = (el: Element, done?: () => void) => void
const finishEnter: Hook = (el, done) => {
removeTransitionClass(el, enterToClass)
@@ -124,7 +120,7 @@ export function resolveTransitionProps({
onEnter(el, done) {
nextFrame(() => {
const resolve = () => finishEnter(el, done)
onEnter && callHookWithErrorHandling(onEnter, [el, resolve])
onEnter && callHookWithErrorHandling(onEnter as Hook, [el, resolve])
removeTransitionClass(el, enterFromClass)
addTransitionClass(el, enterToClass)
if (!(onEnter && onEnter.length > 1)) {
@@ -141,7 +137,7 @@ export function resolveTransitionProps({
addTransitionClass(el, leaveFromClass)
nextFrame(() => {
const resolve = () => finishLeave(el, done)
onLeave && callHookWithErrorHandling(onLeave, [el, resolve])
onLeave && callHookWithErrorHandling(onLeave as Hook, [el, resolve])
removeTransitionClass(el, leaveFromClass)
addTransitionClass(el, leaveToClass)
if (!(onLeave && onLeave.length > 1)) {
@@ -199,17 +195,21 @@ export interface ElementWithTransition extends HTMLElement {
_vtc?: Set<string>
}
export function addTransitionClass(el: ElementWithTransition, cls: string) {
el.classList.add(cls)
;(el._vtc || (el._vtc = new Set())).add(cls)
export function addTransitionClass(el: Element, cls: string) {
cls.split(/\s+/).forEach(c => c && el.classList.add(c))
;(
(el as ElementWithTransition)._vtc ||
((el as ElementWithTransition)._vtc = new Set())
).add(cls)
}
export function removeTransitionClass(el: ElementWithTransition, cls: string) {
el.classList.remove(cls)
if (el._vtc) {
el._vtc.delete(cls)
if (!el._vtc!.size) {
el._vtc = undefined
export function removeTransitionClass(el: Element, cls: string) {
cls.split(/\s+/).forEach(c => c && el.classList.remove(c))
const { _vtc } = el as ElementWithTransition
if (_vtc) {
_vtc.delete(cls)
if (!_vtc!.size) {
;(el as ElementWithTransition)._vtc = undefined
}
}
}

View File

@@ -9,6 +9,7 @@ import {
} from './Transition'
import {
Fragment,
Comment,
VNode,
warn,
resolveTransitionHooks,
@@ -35,6 +36,12 @@ export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
}
const TransitionGroupImpl = {
props: {
...TransitionPropsValidators,
tag: String,
moveClass: String
},
setup(props: TransitionGroupProps, { slots }: SetupContext) {
const instance = getCurrentInstance()!
const state = useTransitionState()
@@ -52,8 +59,8 @@ const TransitionGroupImpl = {
hasMove =
hasMove === null
? (hasMove = hasCSSTransform(
prevChildren[0].el,
instance.vnode.el,
prevChildren[0].el as ElementWithTransition,
instance.vnode.el as Node,
moveClass
))
: hasMove
@@ -71,17 +78,17 @@ const TransitionGroupImpl = {
forceReflow()
movedChildren.forEach(c => {
const el = c.el
const el = c.el as ElementWithTransition
const style = el.style
addTransitionClass(el, moveClass)
style.transform = style.WebkitTransform = style.transitionDuration = ''
const cb = (el._moveCb = (e: TransitionEvent) => {
style.transform = style.webkitTransform = style.transitionDuration = ''
const cb = ((el as any)._moveCb = (e: TransitionEvent) => {
if (e && e.target !== el) {
return
}
if (!e || /transform$/.test(e.propertyName)) {
el.removeEventListener('transitionend', cb)
el._moveCb = null
;(el as any)._moveCb = null
removeTransitionClass(el, moveClass)
}
})
@@ -108,7 +115,7 @@ const TransitionGroupImpl = {
child,
resolveTransitionHooks(child, cssTransitionProps, state, instance)
)
} else if (__DEV__) {
} else if (__DEV__ && child.type !== Comment) {
warn(`<TransitionGroup> children must be keyed.`)
}
}
@@ -120,7 +127,7 @@ const TransitionGroupImpl = {
child,
resolveTransitionHooks(child, cssTransitionProps, state, instance)
)
positionMap.set(child, child.el.getBoundingClientRect())
positionMap.set(child, (child.el as Element).getBoundingClientRect())
}
}
@@ -129,32 +136,27 @@ const TransitionGroupImpl = {
}
}
// remove mode props as TransitionGroup doesn't support it
delete TransitionGroupImpl.props.mode
export const TransitionGroup = (TransitionGroupImpl as unknown) as {
new (): {
$props: TransitionGroupProps
}
}
if (__DEV__) {
const props = ((TransitionGroup as any).props = {
...TransitionPropsValidators,
tag: String,
moveClass: String
})
delete props.mode
}
function callPendingCbs(c: VNode) {
if (c.el._moveCb) {
c.el._moveCb()
const el = c.el as any
if (el._moveCb) {
el._moveCb()
}
if (c.el._enterCb) {
c.el._enterCb()
if (el._enterCb) {
el._enterCb()
}
}
function recordPosition(c: VNode) {
newPositionMap.set(c, c.el.getBoundingClientRect())
newPositionMap.set(c, (c.el as Element).getBoundingClientRect())
}
function applyTranslation(c: VNode): VNode | undefined {
@@ -163,8 +165,8 @@ function applyTranslation(c: VNode): VNode | undefined {
const dx = oldPos.left - newPos.left
const dy = oldPos.top - newPos.top
if (dx || dy) {
const s = c.el.style
s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`
const s = (c.el as HTMLElement).style
s.transform = s.webkitTransform = `translate(${dx}px,${dy}px)`
s.transitionDuration = '0s'
return c
}
@@ -187,9 +189,11 @@ function hasCSSTransform(
// is applied.
const clone = el.cloneNode() as HTMLElement
if (el._vtc) {
el._vtc.forEach(cls => clone.classList.remove(cls))
el._vtc.forEach(cls => {
cls.split(/\s+/).forEach(c => c && clone.classList.remove(c))
})
}
clone.classList.add(moveClass)
moveClass.split(/\s+/).forEach(c => c && clone.classList.add(c))
clone.style.display = 'none'
const container = (root.nodeType === 1
? root

View File

@@ -1,20 +1,25 @@
import {
ObjectDirective,
VNode,
DirectiveHook,
DirectiveBinding,
warn
} from '@vue/runtime-core'
import { addEventListener } from '../modules/events'
import { isArray, isObject } from '@vue/shared'
import { isArray, looseEqual, looseIndexOf, invokeArrayFns } from '@vue/shared'
const getModelAssigner = (vnode: VNode): ((value: any) => void) =>
vnode.props!['onUpdate:modelValue']
type AssignerFn = (value: any) => void
function onCompositionStart(e: CompositionEvent) {
const getModelAssigner = (vnode: VNode): AssignerFn => {
const fn = vnode.props!['onUpdate:modelValue']
return isArray(fn) ? value => invokeArrayFns(fn, value) : fn
}
function onCompositionStart(e: Event) {
;(e.target as any).composing = true
}
function onCompositionEnd(e: CompositionEvent) {
function onCompositionEnd(e: Event) {
const target = e.target as any
if (target.composing) {
target.composing = false
@@ -33,14 +38,16 @@ function toNumber(val: string): number | string {
return isNaN(n) ? val : n
}
type ModelDirective<T> = ObjectDirective<T & { _assign: AssignerFn }>
// 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: ModelDirective<
HTMLInputElement | HTMLTextAreaElement
> = {
beforeMount(el, { value, modifiers: { lazy, trim, number } }, vnode) {
el.value = value
const assign = getModelAssigner(vnode)
el._assign = getModelAssigner(vnode)
const castToNumber = number || el.type === 'number'
addEventListener(el, lazy ? 'change' : 'input', () => {
let domValue: string | number = el.value
@@ -49,7 +56,7 @@ export const vModelText: ObjectDirective<
} else if (castToNumber) {
domValue = toNumber(domValue)
}
assign(domValue)
el._assign(domValue)
})
if (trim) {
addEventListener(el, 'change', () => {
@@ -66,7 +73,8 @@ export const vModelText: ObjectDirective<
addEventListener(el, 'change', onCompositionEnd)
}
},
beforeUpdate(el, { value, oldValue, modifiers: { trim, number } }) {
beforeUpdate(el, { value, oldValue, modifiers: { trim, number } }, vnode) {
el._assign = getModelAssigner(vnode)
if (value === oldValue) {
return
}
@@ -82,14 +90,15 @@ export const vModelText: ObjectDirective<
}
}
export const vModelCheckbox: ObjectDirective<HTMLInputElement> = {
export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
beforeMount(el, binding, vnode) {
setChecked(el, binding, vnode)
const assign = getModelAssigner(vnode)
el._assign = getModelAssigner(vnode)
addEventListener(el, 'change', () => {
const modelValue = (el as any)._modelValue
const elementValue = getValue(el)
const checked = el.checked
const assign = el._assign
if (isArray(modelValue)) {
const index = looseIndexOf(modelValue, elementValue)
const found = index !== -1
@@ -105,7 +114,10 @@ export const vModelCheckbox: ObjectDirective<HTMLInputElement> = {
}
})
},
beforeUpdate: setChecked
beforeUpdate(el, binding, vnode) {
el._assign = getModelAssigner(vnode)
setChecked(el, binding, vnode)
}
}
function setChecked(
@@ -123,33 +135,37 @@ function setChecked(
}
}
export const vModelRadio: ObjectDirective<HTMLInputElement> = {
export const vModelRadio: ModelDirective<HTMLInputElement> = {
beforeMount(el, { value }, vnode) {
el.checked = looseEqual(value, vnode.props!.value)
const assign = getModelAssigner(vnode)
el._assign = getModelAssigner(vnode)
addEventListener(el, 'change', () => {
assign(getValue(el))
el._assign(getValue(el))
})
},
beforeUpdate(el, { value, oldValue }, vnode) {
el._assign = getModelAssigner(vnode)
if (value !== oldValue) {
el.checked = looseEqual(value, vnode.props!.value)
}
}
}
export const vModelSelect: ObjectDirective<HTMLSelectElement> = {
export const vModelSelect: ModelDirective<HTMLSelectElement> = {
// use mounted & updated because <select> relies on its children <option>s.
mounted(el, { value }, vnode) {
setSelected(el, value)
const assign = getModelAssigner(vnode)
el._assign = getModelAssigner(vnode)
addEventListener(el, 'change', () => {
const selectedVal = Array.prototype.filter
.call(el.options, (o: HTMLOptionElement) => o.selected)
.map(getValue)
assign(el.multiple ? selectedVal : selectedVal[0])
el._assign(el.multiple ? selectedVal : selectedVal[0])
})
},
beforeUpdate(el, _binding, vnode) {
el._assign = getModelAssigner(vnode)
},
updated(el, { value }) {
setSelected(el, value)
}
@@ -182,47 +198,6 @@ function setSelected(el: HTMLSelectElement, value: any) {
}
}
function looseEqual(a: any, b: any): boolean {
if (a === b) return true
const isObjectA = isObject(a)
const isObjectB = isObject(b)
if (isObjectA && isObjectB) {
try {
const isArrayA = isArray(a)
const isArrayB = isArray(b)
if (isArrayA && isArrayB) {
return (
a.length === b.length &&
a.every((e: any, i: any) => looseEqual(e, b[i]))
)
} else if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime()
} else if (!isArrayA && !isArrayB) {
const keysA = Object.keys(a)
const keysB = Object.keys(b)
return (
keysA.length === keysB.length &&
keysA.every(key => looseEqual(a[key], b[key]))
)
} else {
/* istanbul ignore next */
return false
}
} catch (e) {
/* istanbul ignore next */
return false
}
} else if (!isObjectA && !isObjectB) {
return String(a) === String(b)
} else {
return false
}
}
function looseIndexOf(arr: any[], val: any): number {
return arr.findIndex(item => looseEqual(item, val))
}
// retrieve raw value set via :value bindings
function getValue(el: HTMLOptionElement | HTMLInputElement) {
return '_value' in el ? (el as any)._value : el.value
@@ -259,7 +234,7 @@ function callModelHook(
binding: DirectiveBinding,
vnode: VNode,
prevVNode: VNode | null,
hook: keyof ObjectDirective
hook: 'beforeMount' | 'mounted' | 'beforeUpdate' | 'updated'
) {
let modelToUse: ObjectDirective
switch (el.tagName) {
@@ -281,6 +256,27 @@ function callModelHook(
modelToUse = vModelText
}
}
const fn = modelToUse[hook]
const fn = modelToUse[hook] as DirectiveHook
fn && fn(el, binding, vnode, prevVNode)
}
// SSR vnode transforms
if (__NODE_JS__) {
vModelText.getSSRProps = ({ value }) => ({ value })
vModelRadio.getSSRProps = ({ value }, vnode) => {
if (vnode.props && looseEqual(vnode.props.value, value)) {
return { checked: true }
}
}
vModelCheckbox.getSSRProps = ({ value }, vnode) => {
if (isArray(value)) {
if (vnode.props && looseIndexOf(value, vnode.props.value) > -1) {
return { checked: true }
}
} else if (value) {
return { checked: true }
}
}
}

View File

@@ -6,7 +6,7 @@ type KeyedEvent = KeyboardEvent | MouseEvent | TouchEvent
const modifierGuards: Record<
string,
(e: Event, modifiers?: string[]) => void | boolean
(e: Event, modifiers: string[]) => void | boolean
> = {
stop: e => e.stopPropagation(),
prevent: e => e.preventDefault(),
@@ -18,7 +18,7 @@ const modifierGuards: Record<
left: e => 'button' in e && (e as MouseEvent).button !== 0,
middle: e => 'button' in e && (e as MouseEvent).button !== 1,
right: e => 'button' in e && (e as MouseEvent).button !== 2,
exact: (e, modifiers: string[]) =>
exact: (e, modifiers) =>
systemModifiers.some(m => (e as any)[`${m}Key`] && !modifiers.includes(m))
}

View File

@@ -40,6 +40,14 @@ export const vShow: ObjectDirective<VShowElement> = {
}
}
if (__NODE_JS__) {
vShow.getSSRProps = ({ value }) => {
if (!value) {
return { style: { display: 'none' } }
}
}
}
function setDisplay(el: VShowElement, value: unknown): void {
el.style.display = value ? el._vod : 'none'
}

View File

@@ -1,58 +1,112 @@
import {
createRenderer,
createHydrationRenderer,
warn,
RootRenderFunction,
CreateAppFunction,
Renderer,
HydrationRenderer,
App,
RootRenderFunction
RootHydrateFunction
} from '@vue/runtime-core'
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
// Importing from the compiler, will be tree-shaken in prod
import { isFunction, isString, isHTMLTag, isSVGTag } from '@vue/shared'
const { render: baseRender, createApp: baseCreateApp } = createRenderer({
const rendererOptions = {
patchProp,
...nodeOps
})
}
// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
let renderer: Renderer | HydrationRenderer
let enabledHydration = false
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions))
}
function ensureHydrationRenderer() {
renderer = enabledHydration
? renderer
: createHydrationRenderer(rendererOptions)
enabledHydration = true
return renderer as HydrationRenderer
}
// use explicit type casts here to avoid import() calls in rolled-up d.ts
export const render = baseRender as RootRenderFunction<Node, Element>
export const render = ((...args) => {
ensureRenderer().render(...args)
}) as RootRenderFunction<Element>
export const createApp = (): App<Element> => {
const app = baseCreateApp()
export const hydrate = ((...args) => {
ensureHydrationRenderer().hydrate(...args)
}) as RootHydrateFunction
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
if (__DEV__) {
// Inject `isNativeTag`
// this is used for component name validation (dev only)
Object.defineProperty(app.config, 'isNativeTag', {
value: (tag: string) => isHTMLTag(tag) || isSVGTag(tag),
writable: false
})
injectNativeTagCheck(app)
}
const mount = app.mount
app.mount = (component, container, props): any => {
if (isString(container)) {
container = document.querySelector(container)!
if (!container) {
__DEV__ &&
warn(`Failed to mount app: mount target selector returned null.`)
return
}
}
if (
__RUNTIME_COMPILE__ &&
!isFunction(component) &&
!component.render &&
!component.template
) {
const { mount } = app
app.mount = (containerOrSelector: Element | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// clear content before mounting
container.innerHTML = ''
return mount(component, container, props)
const proxy = mount(container)
container.removeAttribute('v-cloak')
return proxy
}
return app
}) as CreateAppFunction<Element>
export const createSSRApp = ((...args) => {
const app = ensureHydrationRenderer().createApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
}
const { mount } = app
app.mount = (containerOrSelector: Element | string): any => {
const container = normalizeContainer(containerOrSelector)
if (container) {
return mount(container, true)
}
}
return app
}) as CreateAppFunction<Element>
function injectNativeTagCheck(app: App) {
// Inject `isNativeTag`
// this is used for component name validation (dev only)
Object.defineProperty(app.config, 'isNativeTag', {
value: (tag: string) => isHTMLTag(tag) || isSVGTag(tag),
writable: false
})
}
function normalizeContainer(container: Element | string): Element | null {
if (isString(container)) {
const res = document.querySelector(container)
if (__DEV__ && !res) {
warn(`Failed to mount app: mount target selector returned null.`)
}
return res
}
return container
}
// DOM-only runtime directive helpers

View File

@@ -1,7 +1,27 @@
export function patchAttr(el: Element, key: string, value: any) {
if (value == null) {
el.removeAttribute(key)
import { isSpecialBooleanAttr } from '@vue/shared'
export const xlinkNS = 'http://www.w3.org/1999/xlink'
export function patchAttr(
el: Element,
key: string,
value: any,
isSVG: boolean
) {
if (isSVG && key.indexOf('xlink:') === 0) {
if (value == null) {
el.removeAttributeNS(xlinkNS, key.slice(6, key.length))
} else {
el.setAttributeNS(xlinkNS, key, value)
}
} else {
el.setAttribute(key, value)
// note we are only checking boolean attributes that don't have a
// correspoding dom prop of the same name here.
const isBoolean = isSpecialBooleanAttr(key)
if (value == null || (isBoolean && value === false)) {
el.removeAttribute(key)
} else {
el.setAttribute(key, isBoolean ? '' : value)
}
}
}

View File

@@ -12,9 +12,9 @@ export function patchClass(el: Element, value: string | null, isSVG: boolean) {
} else {
// if this is an element during a transition, take the temporary transition
// classes into account.
const transtionClasses = (el as ElementWithTransition)._vtc
if (transtionClasses) {
value = [value, ...transtionClasses].join(' ')
const transitionClasses = (el as ElementWithTransition)._vtc
if (transitionClasses) {
value = [value, ...transitionClasses].join(' ')
}
el.className = value
}

View File

@@ -1,4 +1,4 @@
import { EMPTY_OBJ } from '@vue/shared'
import { EMPTY_OBJ, isString } from '@vue/shared'
import {
ComponentInternalInstance,
callWithAsyncErrorHandling
@@ -66,11 +66,22 @@ export function removeEventListener(
export function patchEvent(
el: Element,
name: string,
rawName: string,
prevValue: EventValueWithOptions | EventValue | null,
nextValue: EventValueWithOptions | EventValue | null,
instance: ComponentInternalInstance | null = null
) {
// support native onxxx handlers
if (rawName in el) {
if (isString(nextValue)) {
el.setAttribute(rawName, nextValue)
} else {
;(el as any)[rawName] = nextValue
}
return
}
const name = rawName.slice(2).toLowerCase()
const prevOptions = prevValue && 'options' in prevValue && prevValue.options
const nextOptions = nextValue && 'options' in nextValue && nextValue.options
const invoker = prevValue && prevValue.invoker

View File

@@ -1,3 +1,7 @@
// __UNSAFE__
// Reason: potentially setting innerHTML.
// This can come from explicit usage of v-html or innerHTML as a prop in render
// functions. The user is reponsible for using them with only trusted content.
export function patchDOMProp(
el: any,
key: string,
@@ -10,7 +14,7 @@ export function patchDOMProp(
parentSuspense: any,
unmountChildren: any
) {
if ((key === 'innerHTML' || key === 'textContent') && prevChildren != null) {
if ((key === 'innerHTML' || key === 'textContent') && prevChildren) {
unmountChildren(prevChildren, parentComponent, parentSuspense)
el[key] = value == null ? '' : value
return

View File

@@ -1,42 +1,70 @@
const doc = document
import { RendererOptions } from '@vue/runtime-core'
const doc = (typeof document !== 'undefined' ? document : null) as Document
const svgNS = 'http://www.w3.org/2000/svg'
export const nodeOps = {
insert: (child: Node, parent: Node, anchor?: Node) => {
if (anchor != null) {
let tempContainer: HTMLElement
let tempSVGContainer: SVGElement
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
insert: (child, parent, anchor) => {
if (anchor) {
parent.insertBefore(child, anchor)
} else {
parent.appendChild(child)
}
},
remove: (child: Node) => {
remove: child => {
const parent = child.parentNode
if (parent != null) {
if (parent) {
parent.removeChild(child)
}
},
createElement: (tag: string, isSVG?: boolean): Element =>
isSVG ? doc.createElementNS(svgNS, tag) : doc.createElement(tag),
createElement: (tag, isSVG, is): Element =>
isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined),
createText: (text: string): Text => doc.createTextNode(text),
createText: text => doc.createTextNode(text),
createComment: (text: string): Comment => doc.createComment(text),
createComment: text => doc.createComment(text),
setText: (node: Text, text: string) => {
setText: (node, text) => {
node.nodeValue = text
},
setElementText: (el: HTMLElement, text: string) => {
setElementText: (el, text) => {
el.textContent = text
},
parentNode: (node: Node): HTMLElement | null =>
node.parentNode as HTMLElement,
parentNode: node => node.parentNode as Element | null,
nextSibling: (node: Node): Node | null => node.nextSibling,
nextSibling: node => node.nextSibling,
querySelector: (selector: string): Element | null =>
doc.querySelector(selector)
querySelector: selector => doc.querySelector(selector),
setScopeId(el, id) {
el.setAttribute(id, '')
},
cloneNode(el) {
return el.cloneNode(true)
},
// __UNSAFE__
// Reason: innerHTML.
// Static content here can only come from compiled templates.
// As long as the user only uses trusted templates, this is safe.
insertStaticContent(content, parent, anchor, isSVG) {
const temp = isSVG
? tempSVGContainer ||
(tempSVGContainer = doc.createElementNS(svgNS, 'svg'))
: tempContainer || (tempContainer = doc.createElement('div'))
temp.innerHTML = content
const node = temp.children[0]
nodeOps.insert(node, parent, anchor)
return node
}
}

View File

@@ -4,23 +4,19 @@ import { patchAttr } from './modules/attrs'
import { patchDOMProp } from './modules/props'
import { patchEvent } from './modules/events'
import { isOn } from '@vue/shared'
import {
ComponentInternalInstance,
SuspenseBoundary,
VNode
} from '@vue/runtime-core'
import { RendererOptions } from '@vue/runtime-core'
export function patchProp(
el: Element,
key: string,
nextValue: any,
prevValue: any,
isSVG: boolean,
prevChildren?: VNode[],
parentComponent?: ComponentInternalInstance,
parentSuspense?: SuspenseBoundary<Node, Element>,
unmountChildren?: any
) {
export const patchProp: RendererOptions<Node, Element>['patchProp'] = (
el,
key,
prevValue,
nextValue,
isSVG = false,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
) => {
switch (key) {
// special
case 'class':
@@ -29,19 +25,12 @@ export function patchProp(
case 'style':
patchStyle(el, prevValue, nextValue)
break
case 'modelValue':
case 'onUpdate:modelValue':
// Do nothing. This is handled by v-model directives.
break
default:
if (isOn(key)) {
patchEvent(
el,
key.slice(2).toLowerCase(),
prevValue,
nextValue,
parentComponent
)
// ignore v-model listeners
if (key.indexOf('onUpdate:') < 0) {
patchEvent(el, key, prevValue, nextValue, parentComponent)
}
} else if (!isSVG && key in el) {
patchDOMProp(
el,
@@ -62,7 +51,7 @@ export function patchProp(
} else if (key === 'false-value') {
;(el as any)._falseValue = nextValue
}
patchAttr(el, key, nextValue)
patchAttr(el, key, nextValue, isSVG)
}
break
}