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

This commit is contained in:
pikax 2020-04-13 18:32:14 +01:00
commit 6a66b7b60a
54 changed files with 1175 additions and 2676 deletions

View File

@ -1,3 +1,42 @@
# [3.0.0-alpha.12](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.11...v3.0.0-alpha.12) (2020-04-08)
### Bug Fixes
* **compiler:** should not condense ` ` ([8c17535](https://github.com/vuejs/vue-next/commit/8c17535a470501f7f4ec3747cd3de25d9169c505)), closes [#945](https://github.com/vuejs/vue-next/issues/945)
* **compiler:** should only strip leading newline directly in pre tag ([be666eb](https://github.com/vuejs/vue-next/commit/be666ebd59027eb2fc96595c1a6054ecf62832e8))
* **compiler:** support full range of entity decoding in browser builds ([1f6e72b](https://github.com/vuejs/vue-next/commit/1f6e72b11051561abe270fa233cf52d5aba01d6b))
* **compiler-core:** elements with dynamic keys should be forced into blocks ([d531686](https://github.com/vuejs/vue-next/commit/d531686f9154c2ef7f1d877c275df62a8d8da2a5)), closes [#916](https://github.com/vuejs/vue-next/issues/916)
* **reactivity:** track reactive keys in raw collection types ([5dcc645](https://github.com/vuejs/vue-next/commit/5dcc645fc068f9a467fa31ba2d3c2a59e68a9fd7)), closes [#919](https://github.com/vuejs/vue-next/issues/919)
* **runtime-core:** fix globalProperties in check on instance render proxy ([c28a919](https://github.com/vuejs/vue-next/commit/c28a9196b2165e8ce274b2708d6d772024c2933a))
* **runtime-core:** set fragment root children should also update dynamicChildren ([#944](https://github.com/vuejs/vue-next/issues/944)) ([a27e9ee](https://github.com/vuejs/vue-next/commit/a27e9ee9aea3487ef3ef0c8a5df53227fc172886)), closes [#943](https://github.com/vuejs/vue-next/issues/943)
* **runtime-dom:** fix getModelAssigner order in vModelCheckbox ([#926](https://github.com/vuejs/vue-next/issues/926)) ([da1fb7a](https://github.com/vuejs/vue-next/commit/da1fb7afef75470826501fe6e9d81e5af296fea7))
* **runtime-dom:** support native onxxx handlers ([2302dea](https://github.com/vuejs/vue-next/commit/2302dea1624d4b964fed71e30089426212091c11)), closes [#927](https://github.com/vuejs/vue-next/issues/927)
* **slots:** should update compiled dynamic slots ([8444078](https://github.com/vuejs/vue-next/commit/84440780f9e45aa5b060180078b769f27757c7bd))
* **transition:** fix dynamic transition update on nested HOCs ([b8da8b2](https://github.com/vuejs/vue-next/commit/b8da8b2dfac96558df1d038aac3bbe63bd42a8ce))
* **transition:** should ship props declarations in production ([4227831](https://github.com/vuejs/vue-next/commit/42278317e15a202e4e1c8f7084eafa7bb13f1ade))
* **types:** accept generic Component type in h() ([c1d5928](https://github.com/vuejs/vue-next/commit/c1d5928f3b240a4a69bcd8d88494e4fe8d2e625b)), closes [#922](https://github.com/vuejs/vue-next/issues/922)
* **v-model:** handle dynamic assigners and array assigners ([f42d11e](https://github.com/vuejs/vue-next/commit/f42d11e8e19f7356f4e1629cd07c774c9af39288)), closes [#923](https://github.com/vuejs/vue-next/issues/923)
### Features
* **asyncComponent:** add `onError` option for defineAsyncComponent ([e804463](https://github.com/vuejs/vue-next/commit/e80446349215159c002223a41baeb5a8bc0f444c))
* **runtime-core:** improve component public instance proxy inspection ([899287a](https://github.com/vuejs/vue-next/commit/899287ad35d8b74e76a71f39772a92f261dfa4f8))
### BREAKING CHANGES
* **compiler:** compiler options have been adjusted.
- new option `decodeEntities` is added.
- `namedCharacterReferences` option has been removed.
- `maxCRNameLength` option has been rmeoved.
* **asyncComponent:** `retryWhen` and `maxRetries` options for
`defineAsyncComponent` has been replaced by the more flexible `onError`
option, per https://github.com/vuejs/rfcs/pull/148
# [3.0.0-alpha.11](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.10...v3.0.0-alpha.11) (2020-04-04)
@ -11,7 +50,7 @@
* **reactivity:** scheduled effect should not execute if stopped ([0764c33](https://github.com/vuejs/vue-next/commit/0764c33d3da8c06d472893a4e451e33394726a42)), closes [#910](https://github.com/vuejs/vue-next/issues/910)
* **runtime-core:** support attr merging on child with root level comments ([e42cb54](https://github.com/vuejs/vue-next/commit/e42cb543947d4286115b6adae6e8a5741d909f14)), closes [#904](https://github.com/vuejs/vue-next/issues/904)
* **runtime-dom:** v-cloak should be removed after compile on the root element ([#893](https://github.com/vuejs/vue-next/issues/893)) ([0ed147d](https://github.com/vuejs/vue-next/commit/0ed147d33610b86af72cbadcc4b32e6069bcaf08)), closes [#890](https://github.com/vuejs/vue-next/issues/890)
* **runtome-dom:** properly support creating customized built-in element ([b1d0b04](https://github.com/vuejs/vue-next/commit/b1d0b046afb1e8f4640d8d80b6eeaf9f89e892f7))
* **runtime-dom:** properly support creating customized built-in element ([b1d0b04](https://github.com/vuejs/vue-next/commit/b1d0b046afb1e8f4640d8d80b6eeaf9f89e892f7))
* **transition:** warn only when there is more than one rendered child ([#903](https://github.com/vuejs/vue-next/issues/903)) ([37b1dc8](https://github.com/vuejs/vue-next/commit/37b1dc8242608b072d14fd2a5e52f5d40829ea52))
* **types:** allow use PropType with Function ([#915](https://github.com/vuejs/vue-next/issues/915)) ([026eb72](https://github.com/vuejs/vue-next/commit/026eb729f3d1566e95f2f4253d76c20e86d1ec9b)), closes [#748](https://github.com/vuejs/vue-next/issues/748)
* **types:** export missing types from runtime-core ([#889](https://github.com/vuejs/vue-next/issues/889)) ([412ec86](https://github.com/vuejs/vue-next/commit/412ec86128fa33fa41ce435c493fd8275a785fea))

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"workspaces": [
"packages/*"
],

View File

@ -9,7 +9,6 @@ import {
NodeTypes,
Position,
TextNode,
AttributeNode,
InterpolationNode
} from '../src/ast'
@ -163,114 +162,6 @@ describe('compiler: parse', () => {
}
})
})
test('HTML entities compatibility in text (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).', () => {
const spy = jest.fn()
const ast = baseParse('&ampersand;', {
namedCharacterReferences: { amp: '&' },
onError: spy
})
const text = ast.children[0] as TextNode
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: '&ersand;',
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 11, line: 1, column: 12 },
source: '&ampersand;'
}
})
expect(spy.mock.calls).toMatchObject([
[
{
code: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
loc: {
start: { offset: 4, line: 1, column: 5 }
}
}
]
])
})
test('HTML entities compatibility in attribute (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).', () => {
const spy = jest.fn()
const ast = baseParse(
'<div a="&ampersand;" b="&amp;ersand;" c="&amp!"></div>',
{
namedCharacterReferences: { amp: '&', 'amp;': '&' },
onError: spy
}
)
const element = ast.children[0] as ElementNode
const text1 = (element.props[0] as AttributeNode).value
const text2 = (element.props[1] as AttributeNode).value
const text3 = (element.props[2] as AttributeNode).value
expect(text1).toStrictEqual({
type: NodeTypes.TEXT,
content: '&ampersand;',
loc: {
start: { offset: 7, line: 1, column: 8 },
end: { offset: 20, line: 1, column: 21 },
source: '"&ampersand;"'
}
})
expect(text2).toStrictEqual({
type: NodeTypes.TEXT,
content: '&ersand;',
loc: {
start: { offset: 23, line: 1, column: 24 },
end: { offset: 37, line: 1, column: 38 },
source: '"&amp;ersand;"'
}
})
expect(text3).toStrictEqual({
type: NodeTypes.TEXT,
content: '&!',
loc: {
start: { offset: 40, line: 1, column: 41 },
end: { offset: 47, line: 1, column: 48 },
source: '"&amp!"'
}
})
expect(spy.mock.calls).toMatchObject([
[
{
code: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
loc: {
start: { offset: 45, line: 1, column: 46 }
}
}
]
])
})
test('Some control character reference should be replaced.', () => {
const spy = jest.fn()
const ast = baseParse('&#x86;', { onError: spy })
const text = ast.children[0] as TextNode
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: '†',
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 6, line: 1, column: 7 },
source: '&#x86;'
}
})
expect(spy.mock.calls).toMatchObject([
[
{
code: ErrorCodes.CONTROL_CHARACTER_REFERENCE,
loc: {
start: { offset: 0, line: 1, column: 1 }
}
}
]
])
})
})
describe('Interpolation', () => {
@ -1652,12 +1543,10 @@ foo
expect(baz.loc.end).toEqual({ line: 2, column: 28, offset })
})
describe('namedCharacterReferences option', () => {
describe('decodeEntities option', () => {
test('use the given map', () => {
const ast: any = baseParse('&amp;&cups;', {
namedCharacterReferences: {
'cups;': '\u222A\uFE00' // UNION with serifs
},
decodeEntities: text => text.replace('&cups;', '\u222A\uFE00'),
onError: () => {} // Ignore errors
})
@ -1756,60 +1645,6 @@ foo
errors: []
}
],
ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE: [
{
code: '<template>&#a;</template>',
errors: [
{
type: ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
loc: { offset: 10, line: 1, column: 11 }
}
]
},
{
code: '<template>&#xg;</template>',
errors: [
{
type: ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
loc: { offset: 10, line: 1, column: 11 }
}
]
},
{
code: '<template>&#99;</template>',
errors: []
},
{
code: '<template>&#xff;</template>',
errors: []
},
{
code: '<template attr="&#a;"></template>',
errors: [
{
type: ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
loc: { offset: 16, line: 1, column: 17 }
}
]
},
{
code: '<template attr="&#xg;"></template>',
errors: [
{
type: ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
loc: { offset: 16, line: 1, column: 17 }
}
]
},
{
code: '<template attr="&#99;"></template>',
errors: []
},
{
code: '<template attr="&#xff;"></template>',
errors: []
}
],
CDATA_IN_HTML_CONTENT: [
{
code: '<template><![CDATA[cdata]]></template>',
@ -1825,37 +1660,6 @@ foo
errors: []
}
],
CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE: [
{
code: '<template>&#1234567;</template>',
errors: [
{
type: ErrorCodes.CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE,
loc: { offset: 10, line: 1, column: 11 }
}
]
}
],
CONTROL_CHARACTER_REFERENCE: [
{
code: '<template>&#0003;</template>',
errors: [
{
type: ErrorCodes.CONTROL_CHARACTER_REFERENCE,
loc: { offset: 10, line: 1, column: 11 }
}
]
},
{
code: '<template>&#x7F;</template>',
errors: [
{
type: ErrorCodes.CONTROL_CHARACTER_REFERENCE,
loc: { offset: 10, line: 1, column: 11 }
}
]
}
],
DUPLICATE_ATTRIBUTE: [
{
code: '<template><div id="" id=""></div></template>',
@ -2412,36 +2216,6 @@ foo
]
}
],
MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE: [
{
code: '<template>&amp</template>',
options: { namedCharacterReferences: { amp: '&' } },
errors: [
{
type: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
loc: { offset: 14, line: 1, column: 15 }
}
]
},
{
code: '<template>&#40</template>',
errors: [
{
type: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
loc: { offset: 14, line: 1, column: 15 }
}
]
},
{
code: '<template>&#x40</template>',
errors: [
{
type: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
loc: { offset: 15, line: 1, column: 16 }
}
]
}
],
MISSING_WHITESPACE_BETWEEN_ATTRIBUTES: [
{
code: '<template><div id="foo"class="bar"></div></template>',
@ -2500,48 +2274,6 @@ foo
]
}
],
NONCHARACTER_CHARACTER_REFERENCE: [
{
code: '<template>&#xFFFE;</template>',
errors: [
{
type: ErrorCodes.NONCHARACTER_CHARACTER_REFERENCE,
loc: { offset: 10, line: 1, column: 11 }
}
]
},
{
code: '<template>&#x1FFFF;</template>',
errors: [
{
type: ErrorCodes.NONCHARACTER_CHARACTER_REFERENCE,
loc: { offset: 10, line: 1, column: 11 }
}
]
}
],
NULL_CHARACTER_REFERENCE: [
{
code: '<template>&#0000;</template>',
errors: [
{
type: ErrorCodes.NULL_CHARACTER_REFERENCE,
loc: { offset: 10, line: 1, column: 11 }
}
]
}
],
SURROGATE_CHARACTER_REFERENCE: [
{
code: '<template>&#xD800;</template>',
errors: [
{
type: ErrorCodes.SURROGATE_CHARACTER_REFERENCE,
loc: { offset: 10, line: 1, column: 11 }
}
]
}
],
UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME: [
{
code: "<template><div a\"bc=''></div></template>",

View File

@ -739,6 +739,15 @@ describe('compiler: element transform', () => {
expect(node.dynamicProps).toBe(`["foo", "baz"]`)
})
// should treat `class` and `style` as PROPS
test('PROPS on component', () => {
const { node } = parseWithBind(
`<Foo :id="foo" :class="cls" :style="styl" />`
)
expect(node.patchFlag).toBe(genFlagText(PatchFlags.PROPS))
expect(node.dynamicProps).toBe(`["id", "class", "style"]`)
})
test('FULL_PROPS (v-bind)', () => {
const { node } = parseWithBind(`<div v-bind="foo" />`)
expect(node.patchFlag).toBe(genFlagText(PatchFlags.FULL_PROPS))

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-core",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"description": "@vue/compiler-core",
"main": "index.js",
"module": "dist/compiler-core.esm-bundler.js",
@ -30,7 +30,7 @@
},
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-core#readme",
"dependencies": {
"@vue/shared": "3.0.0-alpha.11",
"@vue/shared": "3.0.0-alpha.12",
"@babel/parser": "^7.8.6",
"@babel/types": "^7.8.6",
"estree-walker": "^0.8.1",

View File

@ -32,10 +32,7 @@ export function createCompilerError<T extends number>(
export const enum ErrorCodes {
// parse errors
ABRUPT_CLOSING_OF_EMPTY_COMMENT,
ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE,
CDATA_IN_HTML_CONTENT,
CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE,
CONTROL_CHARACTER_REFERENCE,
DUPLICATE_ATTRIBUTE,
END_TAG_WITH_ATTRIBUTES,
END_TAG_WITH_TRAILING_SOLIDUS,
@ -49,12 +46,8 @@ export const enum ErrorCodes {
INVALID_FIRST_CHARACTER_OF_TAG_NAME,
MISSING_ATTRIBUTE_VALUE,
MISSING_END_TAG_NAME,
MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
MISSING_WHITESPACE_BETWEEN_ATTRIBUTES,
NESTED_COMMENT,
NONCHARACTER_CHARACTER_REFERENCE,
NULL_CHARACTER_REFERENCE,
SURROGATE_CHARACTER_REFERENCE,
UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME,
@ -101,14 +94,8 @@ export const enum ErrorCodes {
export const errorMessages: { [code: number]: string } = {
// parse errors
[ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT]: 'Illegal comment.',
[ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE]:
'Illegal numeric character reference: invalid character.',
[ErrorCodes.CDATA_IN_HTML_CONTENT]:
'CDATA section is allowed only in XML context.',
[ErrorCodes.CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE]:
'Illegal numeric character reference: too big.',
[ErrorCodes.CONTROL_CHARACTER_REFERENCE]:
'Illegal numeric character reference: control character.',
[ErrorCodes.DUPLICATE_ATTRIBUTE]: 'Duplicate attribute.',
[ErrorCodes.END_TAG_WITH_ATTRIBUTES]: 'End tag cannot have attributes.',
[ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS]: "Illegal '/' in tags.",
@ -124,17 +111,9 @@ export const errorMessages: { [code: number]: string } = {
"Illegal tag name. Use '&lt;' to print '<'.",
[ErrorCodes.MISSING_ATTRIBUTE_VALUE]: 'Attribute value was expected.',
[ErrorCodes.MISSING_END_TAG_NAME]: 'End tag name was expected.',
[ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE]:
'Semicolon was expected.',
[ErrorCodes.MISSING_WHITESPACE_BETWEEN_ATTRIBUTES]:
'Whitespace was expected.',
[ErrorCodes.NESTED_COMMENT]: "Unexpected '<!--' in comment.",
[ErrorCodes.NONCHARACTER_CHARACTER_REFERENCE]:
'Illegal numeric character reference: non character.',
[ErrorCodes.NULL_CHARACTER_REFERENCE]:
'Illegal numeric character reference: null character.',
[ErrorCodes.SURROGATE_CHARACTER_REFERENCE]:
'Illegal numeric character reference: non-pair surrogate.',
[ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME]:
'Attribute name cannot contain U+0022 ("), U+0027 (\'), and U+003C (<).',
[ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE]:

View File

@ -26,13 +26,7 @@ export interface ParserOptions {
parent: ElementNode | undefined
) => TextModes
delimiters?: [string, string] // ['{{', '}}']
// Map to HTML entities. E.g., `{ "amp;": "&" }`
// The full set is https://html.spec.whatwg.org/multipage/named-characters.html#named-character-references
namedCharacterReferences?: Record<string, string>
// this number is based on the map above, but it should be pre-computed
// to avoid the cost on every parse() call.
maxCRNameLength?: number
decodeEntities?: (rawText: string, asAttr: boolean) => string
onError?: (error: CompilerError) => void
}

View File

@ -30,6 +30,18 @@ type OptionalOptions = 'isNativeTag' | 'isBuiltInComponent'
type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
Pick<ParserOptions, OptionalOptions>
// The default decoder only provides escapes for characters reserved as part of
// the tempalte syntax, and is only used if the custom renderer did not provide
// a platform-specific decoder.
const decodeRE = /&(gt|lt|amp|apos|quot);/g
const decodeMap: Record<string, string> = {
gt: '>',
lt: '<',
amp: '&',
apos: "'",
quot: '"'
}
export const defaultParserOptions: MergedParserOptions = {
delimiters: [`{{`, `}}`],
getNamespace: () => Namespaces.HTML,
@ -37,14 +49,8 @@ export const defaultParserOptions: MergedParserOptions = {
isVoidTag: NO,
isPreTag: NO,
isCustomElement: NO,
namedCharacterReferences: {
'gt;': '>',
'lt;': '<',
'amp;': '&',
'apos;': "'",
'quot;': '"'
},
maxCRNameLength: 5,
decodeEntities: (rawText: string): string =>
rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
onError: defaultOnError
}
@ -57,7 +63,7 @@ export const enum TextModes {
ATTRIBUTE_VALUE
}
interface ParserContext {
export interface ParserContext {
options: MergedParserOptions
readonly originalSource: string
source: string
@ -194,7 +200,7 @@ function parseChildren(
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.type === NodeTypes.TEXT) {
if (!node.content.trim()) {
if (!/[^\t\r\n\f ]/.test(node.content)) {
const prev = nodes[i - 1]
const next = nodes[i + 1]
// If:
@ -219,11 +225,11 @@ function parseChildren(
node.content = ' '
}
} else {
node.content = node.content.replace(/\s+/g, ' ')
node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
}
}
}
} else {
} else if (parent && context.options.isPreTag(parent.tag)) {
// remove leading newline per html spec
// https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
const first = nodes[0]
@ -812,128 +818,21 @@ function parseTextData(
length: number,
mode: TextModes
): string {
let rawText = context.source.slice(0, length)
const rawText = context.source.slice(0, length)
advanceBy(context, length)
if (
mode === TextModes.RAWTEXT ||
mode === TextModes.CDATA ||
rawText.indexOf('&') === -1
) {
advanceBy(context, length)
return rawText
}
} else {
// DATA or RCDATA containing "&"". Entity decoding required.
const end = context.offset + length
let decodedText = ''
function advance(length: number) {
advanceBy(context, length)
rawText = rawText.slice(length)
}
while (context.offset < end) {
const head = /&(?:#x?)?/i.exec(rawText)
if (!head || context.offset + head.index >= end) {
const remaining = end - context.offset
decodedText += rawText.slice(0, remaining)
advance(remaining)
break
}
// Advance to the "&".
decodedText += rawText.slice(0, head.index)
advance(head.index)
if (head[0] === '&') {
// Named character reference.
let name = ''
let value: string | undefined = undefined
if (/[0-9a-z]/i.test(rawText[1])) {
for (
let length = context.options.maxCRNameLength;
!value && length > 0;
--length
) {
name = rawText.substr(1, length)
value = context.options.namedCharacterReferences[name]
}
if (value) {
const semi = name.endsWith(';')
if (
mode === TextModes.ATTRIBUTE_VALUE &&
!semi &&
/[=a-z0-9]/i.test(rawText[name.length + 1] || '')
) {
decodedText += '&' + name
advance(1 + name.length)
} else {
decodedText += value
advance(1 + name.length)
if (!semi) {
emitError(
context,
ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE
return context.options.decodeEntities(
rawText,
mode === TextModes.ATTRIBUTE_VALUE
)
}
}
} else {
decodedText += '&' + name
advance(1 + name.length)
}
} else {
decodedText += '&'
advance(1)
}
} else {
// Numeric character reference.
const hex = head[0] === '&#x'
const pattern = hex ? /^&#x([0-9a-f]+);?/i : /^&#([0-9]+);?/
const body = pattern.exec(rawText)
if (!body) {
decodedText += head[0]
emitError(
context,
ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE
)
advance(head[0].length)
} else {
// https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
let cp = Number.parseInt(body[1], hex ? 16 : 10)
if (cp === 0) {
emitError(context, ErrorCodes.NULL_CHARACTER_REFERENCE)
cp = 0xfffd
} else if (cp > 0x10ffff) {
emitError(
context,
ErrorCodes.CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE
)
cp = 0xfffd
} else if (cp >= 0xd800 && cp <= 0xdfff) {
emitError(context, ErrorCodes.SURROGATE_CHARACTER_REFERENCE)
cp = 0xfffd
} else if ((cp >= 0xfdd0 && cp <= 0xfdef) || (cp & 0xfffe) === 0xfffe) {
emitError(context, ErrorCodes.NONCHARACTER_CHARACTER_REFERENCE)
} else if (
(cp >= 0x01 && cp <= 0x08) ||
cp === 0x0b ||
(cp >= 0x0d && cp <= 0x1f) ||
(cp >= 0x7f && cp <= 0x9f)
) {
emitError(context, ErrorCodes.CONTROL_CHARACTER_REFERENCE)
cp = CCR_REPLACEMENTS[cp] || cp
}
decodedText += String.fromCodePoint(cp)
advance(body[0].length)
if (!body![0].endsWith(';')) {
emitError(
context,
ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE
)
}
}
}
}
return decodedText
}
function getCursor(context: ParserContext): Position {
@ -1052,34 +951,3 @@ function startsWithEndTagOpen(source: string, tag: string): boolean {
/[\t\n\f />]/.test(source[2 + tag.length] || '>')
)
}
// https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
const CCR_REPLACEMENTS: { [key: number]: number | undefined } = {
0x80: 0x20ac,
0x82: 0x201a,
0x83: 0x0192,
0x84: 0x201e,
0x85: 0x2026,
0x86: 0x2020,
0x87: 0x2021,
0x88: 0x02c6,
0x89: 0x2030,
0x8a: 0x0160,
0x8b: 0x2039,
0x8c: 0x0152,
0x8e: 0x017d,
0x91: 0x2018,
0x92: 0x2019,
0x93: 0x201c,
0x94: 0x201d,
0x95: 0x2022,
0x96: 0x2013,
0x97: 0x2014,
0x98: 0x02dc,
0x99: 0x2122,
0x9a: 0x0161,
0x9b: 0x203a,
0x9c: 0x0153,
0x9e: 0x017e,
0x9f: 0x0178
}

View File

@ -289,9 +289,9 @@ export function buildProps(
}
if (name === 'ref') {
hasRef = true
} else if (name === 'class') {
} else if (name === 'class' && !isComponent) {
hasClassBinding = true
} else if (name === 'style') {
} else if (name === 'style' && !isComponent) {
hasStyleBinding = true
} else if (name !== 'key' && !dynamicPropNames.includes(name)) {
dynamicPropNames.push(name)

View File

@ -5,12 +5,10 @@ import {
TextNode,
ErrorCodes,
ElementTypes,
InterpolationNode
InterpolationNode,
AttributeNode
} from '@vue/compiler-core'
import {
parserOptionsMinimal as parserOptions,
DOMNamespaces
} from '../src/parserOptionsMinimal'
import { parserOptions, DOMNamespaces } from '../src/parserOptions'
describe('DOM parser', () => {
describe('Text', () => {
@ -141,11 +139,104 @@ describe('DOM parser', () => {
// #908
test('<pre> tag should remove leading newline', () => {
const rawText = `\nhello`
const rawText = `\nhello<div>\nbye</div>`
const ast = parse(`<pre>${rawText}</pre>`, parserOptions)
expect((ast.children[0] as ElementNode).children[0]).toMatchObject({
expect((ast.children[0] as ElementNode).children).toMatchObject([
{
type: NodeTypes.TEXT,
content: rawText.slice(1)
content: `hello`
},
{
type: NodeTypes.ELEMENT,
children: [
{
type: NodeTypes.TEXT,
// should not remove the leading newline for nested elements
content: `\nbye`
}
]
}
])
})
// #945
test('&nbsp; should not be condensed', () => {
const nbsp = String.fromCharCode(160)
const ast = parse(`foo&nbsp;&nbsp;bar`, parserOptions)
expect(ast.children[0]).toMatchObject({
type: NodeTypes.TEXT,
content: `foo${nbsp}${nbsp}bar`
})
})
// https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
test('HTML entities compatibility in text', () => {
const ast = parse('&ampersand;', parserOptions)
const text = ast.children[0] as TextNode
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: '&ersand;',
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 11, line: 1, column: 12 },
source: '&ampersand;'
}
})
})
// https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
test('HTML entities compatibility in attribute', () => {
const ast = parse(
'<div a="&ampersand;" b="&amp;ersand;" c="&amp!"></div>',
parserOptions
)
const element = ast.children[0] as ElementNode
const text1 = (element.props[0] as AttributeNode).value
const text2 = (element.props[1] as AttributeNode).value
const text3 = (element.props[2] as AttributeNode).value
expect(text1).toStrictEqual({
type: NodeTypes.TEXT,
content: '&ampersand;',
loc: {
start: { offset: 7, line: 1, column: 8 },
end: { offset: 20, line: 1, column: 21 },
source: '"&ampersand;"'
}
})
expect(text2).toStrictEqual({
type: NodeTypes.TEXT,
content: '&ersand;',
loc: {
start: { offset: 23, line: 1, column: 24 },
end: { offset: 37, line: 1, column: 38 },
source: '"&amp;ersand;"'
}
})
expect(text3).toStrictEqual({
type: NodeTypes.TEXT,
content: '&!',
loc: {
start: { offset: 40, line: 1, column: 41 },
end: { offset: 47, line: 1, column: 48 },
source: '"&amp!"'
}
})
})
test('Some control character reference should be replaced.', () => {
const ast = parse('&#x86;', parserOptions)
const text = ast.children[0] as TextNode
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: '†',
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 6, line: 1, column: 7 },
source: '&#x86;'
}
})
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-dom",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"description": "@vue/compiler-dom",
"main": "index.js",
"module": "dist/compiler-dom.esm-bundler.js",
@ -34,7 +34,7 @@
},
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-dom#readme",
"dependencies": {
"@vue/shared": "3.0.0-alpha.11",
"@vue/compiler-core": "3.0.0-alpha.11"
"@vue/shared": "3.0.0-alpha.12",
"@vue/compiler-core": "3.0.0-alpha.12"
}
}

View File

@ -0,0 +1,133 @@
import { ParserOptions } from '@vue/compiler-core'
import namedCharacterReferences from './namedChars.json'
// lazy compute this to make this file tree-shakable for browser
let maxCRNameLength: number
export const decodeHtml: ParserOptions['decodeEntities'] = (
rawText,
asAttr
) => {
let offset = 0
const end = rawText.length
let decodedText = ''
function advance(length: number) {
offset += length
rawText = rawText.slice(length)
}
while (offset < end) {
const head = /&(?:#x?)?/i.exec(rawText)
if (!head || offset + head.index >= end) {
const remaining = end - offset
decodedText += rawText.slice(0, remaining)
advance(remaining)
break
}
// Advance to the "&".
decodedText += rawText.slice(0, head.index)
advance(head.index)
if (head[0] === '&') {
// Named character reference.
let name = ''
let value: string | undefined = undefined
if (/[0-9a-z]/i.test(rawText[1])) {
if (!maxCRNameLength) {
maxCRNameLength = Object.keys(namedCharacterReferences).reduce(
(max, name) => Math.max(max, name.length),
0
)
}
for (let length = maxCRNameLength; !value && length > 0; --length) {
name = rawText.substr(1, length)
value = (namedCharacterReferences as Record<string, string>)[name]
}
if (value) {
const semi = name.endsWith(';')
if (
asAttr &&
!semi &&
/[=a-z0-9]/i.test(rawText[name.length + 1] || '')
) {
decodedText += '&' + name
advance(1 + name.length)
} else {
decodedText += value
advance(1 + name.length)
}
} else {
decodedText += '&' + name
advance(1 + name.length)
}
} else {
decodedText += '&'
advance(1)
}
} else {
// Numeric character reference.
const hex = head[0] === '&#x'
const pattern = hex ? /^&#x([0-9a-f]+);?/i : /^&#([0-9]+);?/
const body = pattern.exec(rawText)
if (!body) {
decodedText += head[0]
advance(head[0].length)
} else {
// https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
let cp = Number.parseInt(body[1], hex ? 16 : 10)
if (cp === 0) {
cp = 0xfffd
} else if (cp > 0x10ffff) {
cp = 0xfffd
} else if (cp >= 0xd800 && cp <= 0xdfff) {
cp = 0xfffd
} else if ((cp >= 0xfdd0 && cp <= 0xfdef) || (cp & 0xfffe) === 0xfffe) {
// noop
} else if (
(cp >= 0x01 && cp <= 0x08) ||
cp === 0x0b ||
(cp >= 0x0d && cp <= 0x1f) ||
(cp >= 0x7f && cp <= 0x9f)
) {
cp = CCR_REPLACEMENTS[cp] || cp
}
decodedText += String.fromCodePoint(cp)
advance(body[0].length)
}
}
}
return decodedText
}
// https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
const CCR_REPLACEMENTS: { [key: number]: number | undefined } = {
0x80: 0x20ac,
0x82: 0x201a,
0x83: 0x0192,
0x84: 0x201e,
0x85: 0x2026,
0x86: 0x2020,
0x87: 0x2021,
0x88: 0x02c6,
0x89: 0x2030,
0x8a: 0x0160,
0x8b: 0x2039,
0x8c: 0x0152,
0x8e: 0x017d,
0x91: 0x2018,
0x92: 0x2019,
0x93: 0x201c,
0x94: 0x201d,
0x95: 0x2022,
0x96: 0x2013,
0x97: 0x2014,
0x98: 0x02dc,
0x99: 0x2122,
0x9a: 0x0161,
0x9b: 0x203a,
0x9c: 0x0153,
0x9e: 0x017e,
0x9f: 0x0178
}

View File

@ -0,0 +1,6 @@
let decoder: HTMLDivElement
export function decodeHtmlBrowser(raw: string): string {
;(decoder || (decoder = document.createElement('div'))).innerHTML = raw
return decoder.textContent as string
}

View File

@ -9,8 +9,7 @@ import {
NodeTransform,
DirectiveTransform
} from '@vue/compiler-core'
import { parserOptionsMinimal } from './parserOptionsMinimal'
import { parserOptionsStandard } from './parserOptionsStandard'
import { parserOptions } from './parserOptions'
import { transformStyle } from './transforms/transformStyle'
import { transformVHtml } from './transforms/vHtml'
import { transformVText } from './transforms/vText'
@ -20,9 +19,7 @@ import { transformShow } from './transforms/vShow'
import { warnTransitionChildren } from './transforms/warnTransitionChildren'
import { stringifyStatic } from './transforms/stringifyStatic'
export const parserOptions = __BROWSER__
? parserOptionsMinimal
: parserOptionsStandard
export { parserOptions }
export const DOMNodeTransforms: NodeTransform[] = [
transformStyle,

View File

@ -8,6 +8,8 @@ import {
} from '@vue/compiler-core'
import { makeMap, isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
import { decodeHtml } from './decodeHtml'
import { decodeHtmlBrowser } from './decodeHtmlBrowser'
const isRawTextContainer = /*#__PURE__*/ makeMap(
'style,iframe,script,noscript',
@ -20,10 +22,11 @@ export const enum DOMNamespaces {
MATH_ML
}
export const parserOptionsMinimal: ParserOptions = {
export const parserOptions: ParserOptions = {
isVoidTag,
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
isPreTag: tag => tag === 'pre',
decodeEntities: __BROWSER__ ? decodeHtmlBrowser : decodeHtml,
isBuiltInComponent: (tag: string): symbol | undefined => {
if (isBuiltInType(tag, `Transition`)) {

View File

@ -1,15 +0,0 @@
import { ParserOptions } from '@vue/compiler-core'
import { parserOptionsMinimal } from './parserOptionsMinimal'
import namedCharacterReferences from './namedChars.json'
export const parserOptionsStandard: ParserOptions = {
// extends the minimal options with more spec-compliant overrides
...parserOptionsMinimal,
// https://html.spec.whatwg.org/multipage/named-characters.html#named-character-references
namedCharacterReferences,
maxCRNameLength: /*#__PURE__*/ Object.keys(namedCharacterReferences).reduce(
(max, name) => Math.max(max, name.length),
0
)
}

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-sfc",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"description": "@vue/compiler-sfc",
"main": "dist/compiler-sfc.cjs.js",
"types": "dist/compiler-sfc.d.ts",
@ -27,13 +27,13 @@
},
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-sfc#readme",
"peerDependencies": {
"vue": "3.0.0-alpha.11"
"vue": "3.0.0-alpha.12"
},
"dependencies": {
"@vue/shared": "3.0.0-alpha.11",
"@vue/compiler-core": "3.0.0-alpha.11",
"@vue/compiler-dom": "3.0.0-alpha.11",
"@vue/compiler-ssr": "3.0.0-alpha.11",
"@vue/shared": "3.0.0-alpha.12",
"@vue/compiler-core": "3.0.0-alpha.12",
"@vue/compiler-dom": "3.0.0-alpha.12",
"@vue/compiler-ssr": "3.0.0-alpha.12",
"consolidate": "^0.15.1",
"hash-sum": "^2.0.0",
"lru-cache": "^5.1.1",

View File

@ -1,6 +1,6 @@
{
"name": "@vue/compiler-ssr",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"description": "@vue/compiler-ssr",
"main": "dist/compiler-ssr.cjs.js",
"types": "dist/compiler-ssr.d.ts",
@ -27,7 +27,7 @@
},
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/compiler-ssr#readme",
"dependencies": {
"@vue/shared": "3.0.0-alpha.11",
"@vue/compiler-dom": "3.0.0-alpha.11"
"@vue/shared": "3.0.0-alpha.12",
"@vue/compiler-dom": "3.0.0-alpha.12"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@vue/reactivity",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"description": "@vue/reactivity",
"main": "index.js",
"module": "dist/reactivity.esm-bundler.js",
@ -34,6 +34,6 @@
},
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/reactivity#readme",
"dependencies": {
"@vue/shared": "3.0.0-alpha.11"
"@vue/shared": "3.0.0-alpha.12"
}
}

View File

@ -101,7 +101,7 @@ function toProxyRef<T extends object, K extends keyof T>(
// corner case when use narrows type
// Ex. type RelativePath = string & { __brand: unknown }
// RelativePath extends object -> true
type BaseTypes = string | number | boolean
type BaseTypes = string | number | boolean | Node | Window
// Super simple tuple checker
type Tupple<T extends Array<any>> = T[0] extends T[1]

View File

@ -1,146 +0,0 @@
import {
h,
ref,
render,
nodeOps,
nextTick,
defineComponent
} from '@vue/runtime-test'
describe('renderer: component', () => {
test.todo('should work')
test.todo('shouldUpdateComponent')
test.todo('componentProxy')
describe('componentProps', () => {
test.todo('should work')
test('should convert empty booleans to true', () => {
let b1: any, b2: any, b3: any
const Comp = defineComponent({
props: {
b1: Boolean,
b2: [Boolean, String],
b3: [String, Boolean]
},
setup(props) {
;({ b1, b2, b3 } = props)
return () => ''
}
})
render(
h(Comp, <any>{ b1: '', b2: '', b3: '' }),
nodeOps.createElement('div')
)
expect(b1).toBe(true)
expect(b2).toBe(true)
expect(b3).toBe('')
})
})
describe('slots', () => {
test('should respect $stable flag', async () => {
const flag1 = ref(1)
const flag2 = ref(2)
const spy = jest.fn()
const Child = () => {
spy()
return 'child'
}
const App = {
setup() {
return () => [
flag1.value,
h(
Child,
{ n: flag2.value },
{
foo: () => 'foo',
$stable: true
}
)
]
}
}
render(h(App), nodeOps.createElement('div'))
expect(spy).toHaveBeenCalledTimes(1)
// parent re-render, props didn't change, slots are stable
// -> child should not update
flag1.value++
await nextTick()
expect(spy).toHaveBeenCalledTimes(1)
// parent re-render, props changed
// -> child should update
flag2.value++
await nextTick()
expect(spy).toHaveBeenCalledTimes(2)
})
})
test('emit', async () => {
let noMatchEmitResult: any
let singleEmitResult: any
let multiEmitResult: any
const Child = defineComponent({
setup(_, { emit }) {
noMatchEmitResult = emit('foo')
singleEmitResult = emit('bar')
multiEmitResult = emit('baz')
return () => h('div')
}
})
const App = {
setup() {
return () =>
h(Child, {
// emit triggering single handler
onBar: () => 1,
// emit triggering multiple handlers
onBaz: [() => Promise.resolve(2), () => Promise.resolve(3)]
})
}
}
render(h(App), nodeOps.createElement('div'))
// assert return values from emit
expect(noMatchEmitResult).toMatchObject([])
expect(singleEmitResult).toMatchObject([1])
expect(await Promise.all(multiEmitResult)).toMatchObject([2, 3])
})
// for v-model:foo-bar usage in DOM templates
test('emit update:xxx events should trigger kebab-case equivalent', () => {
const Child = defineComponent({
setup(_, { emit }) {
emit('update:fooBar', 1)
return () => h('div')
}
})
const handler = jest.fn()
const App = {
setup() {
return () =>
h(Child, {
'onUpdate:foo-bar': handler
})
}
}
render(h(App), nodeOps.createElement('div'))
expect(handler).toHaveBeenCalled()
})
})

View File

@ -5,28 +5,33 @@ import { mockWarn } from '@vue/shared'
import { render, defineComponent, h, nodeOps } from '@vue/runtime-test'
import { isEmitListener } from '../src/componentEmits'
describe('emits option', () => {
describe('component: emit', () => {
mockWarn()
test('trigger both raw event and capitalize handlers', () => {
test('trigger handlers', () => {
const Foo = defineComponent({
render() {},
created() {
// the `emit` function is bound on component instances
this.$emit('foo')
this.$emit('bar')
this.$emit('!baz')
}
})
const onfoo = jest.fn()
const onBar = jest.fn()
const Comp = () => h(Foo, { onfoo, onBar })
const onBaz = jest.fn()
const Comp = () => h(Foo, { onfoo, onBar, ['on!baz']: onBaz })
render(h(Comp), nodeOps.createElement('div'))
expect(onfoo).toHaveBeenCalled()
expect(onfoo).not.toHaveBeenCalled()
// only capitalized or special chars are considerd event listeners
expect(onBar).toHaveBeenCalled()
expect(onBaz).toHaveBeenCalled()
})
// for v-model:foo-bar usage in DOM templates
test('trigger hyphendated events for update:xxx events', () => {
const Foo = defineComponent({
render() {},
@ -49,6 +54,33 @@ describe('emits option', () => {
expect(barSpy).toHaveBeenCalled()
})
test('should trigger array of listeners', async () => {
const Child = defineComponent({
setup(_, { emit }) {
emit('foo', 1)
return () => h('div')
}
})
const fn1 = jest.fn()
const fn2 = jest.fn()
const App = {
setup() {
return () =>
h(Child, {
onFoo: [fn1, fn2]
})
}
}
render(h(App), nodeOps.createElement('div'))
expect(fn1).toHaveBeenCalledTimes(1)
expect(fn1).toHaveBeenCalledWith(1)
expect(fn2).toHaveBeenCalledTimes(1)
expect(fn1).toHaveBeenCalledWith(1)
})
test('warning for undeclared event (array)', () => {
const Foo = defineComponent({
emits: ['foo'],
@ -97,9 +129,9 @@ describe('emits option', () => {
test('isEmitListener', () => {
expect(isEmitListener(['click'], 'onClick')).toBe(true)
expect(isEmitListener(['click'], 'onclick')).toBe(true)
expect(isEmitListener(['click'], 'onclick')).toBe(false)
expect(isEmitListener({ click: null }, 'onClick')).toBe(true)
expect(isEmitListener({ click: null }, 'onclick')).toBe(true)
expect(isEmitListener({ click: null }, 'onclick')).toBe(false)
expect(isEmitListener(['click'], 'onBlick')).toBe(false)
expect(isEmitListener({ click: null }, 'onBlick')).toBe(false)
})

View File

@ -20,7 +20,7 @@ describe('component props', () => {
let proxy: any
const Comp = defineComponent({
props: ['foo'],
props: ['fooBar'],
render() {
props = this.$props
attrs = this.$attrs
@ -29,18 +29,25 @@ describe('component props', () => {
})
const root = nodeOps.createElement('div')
render(h(Comp, { foo: 1, bar: 2 }), root)
expect(proxy.foo).toBe(1)
expect(props).toEqual({ foo: 1 })
render(h(Comp, { fooBar: 1, bar: 2 }), root)
expect(proxy.fooBar).toBe(1)
expect(props).toEqual({ fooBar: 1 })
expect(attrs).toEqual({ bar: 2 })
render(h(Comp, { foo: 2, bar: 3, baz: 4 }), root)
expect(proxy.foo).toBe(2)
expect(props).toEqual({ foo: 2 })
// test passing kebab-case and resolving to camelCase
render(h(Comp, { 'foo-bar': 2, bar: 3, baz: 4 }), root)
expect(proxy.fooBar).toBe(2)
expect(props).toEqual({ fooBar: 2 })
expect(attrs).toEqual({ bar: 3, baz: 4 })
// test updating kebab-case should not delete it (#955)
render(h(Comp, { 'foo-bar': 3, bar: 3, baz: 4 }), root)
expect(proxy.fooBar).toBe(3)
expect(props).toEqual({ fooBar: 3 })
expect(attrs).toEqual({ bar: 3, baz: 4 })
render(h(Comp, { qux: 5 }), root)
expect(proxy.foo).toBeUndefined()
expect(proxy.fooBar).toBeUndefined()
expect(props).toEqual({})
expect(attrs).toEqual({ qux: 5 })
})

View File

@ -0,0 +1,47 @@
import { ref, render, h, nodeOps, nextTick } from '@vue/runtime-test'
describe('component: slots', () => {
// TODO more tests for slots normalization etc.
test('should respect $stable flag', async () => {
const flag1 = ref(1)
const flag2 = ref(2)
const spy = jest.fn()
const Child = () => {
spy()
return 'child'
}
const App = {
setup() {
return () => [
flag1.value,
h(
Child,
{ n: flag2.value },
{
foo: () => 'foo',
$stable: true
}
)
]
}
}
render(h(App), nodeOps.createElement('div'))
expect(spy).toHaveBeenCalledTimes(1)
// parent re-render, props didn't change, slots are stable
// -> child should not update
flag1.value++
await nextTick()
expect(spy).toHaveBeenCalledTimes(1)
// parent re-render, props changed
// -> child should update
flag2.value++
await nextTick()
expect(spy).toHaveBeenCalledTimes(2)
})
})

View File

@ -416,21 +416,16 @@ describe('error handling', () => {
}
}
let res: any
const Child = {
props: ['onFoo'],
setup(props: any, { emit }: any) {
res = emit('foo')
emit('foo')
return () => null
}
}
render(h(Comp), nodeOps.createElement('div'))
try {
await Promise.all(res)
} catch (e) {
expect(e).toBe(err)
}
await nextTick()
expect(fn).toHaveBeenCalledWith(err, 'component event handler')
})
@ -438,6 +433,12 @@ describe('error handling', () => {
const err = new Error('foo')
const fn = jest.fn()
const res: Promise<any>[] = []
const createAsyncHandler = (p: Promise<any>) => () => {
res.push(p)
return p
}
const Comp = {
setup() {
onErrorCaptured((err, instance, info) => {
@ -446,15 +447,17 @@ describe('error handling', () => {
})
return () =>
h(Child, {
onFoo: [() => Promise.reject(err), () => Promise.resolve(1)]
onFoo: [
createAsyncHandler(Promise.reject(err)),
createAsyncHandler(Promise.resolve(1))
]
})
}
}
let res: any
const Child = {
setup(props: any, { emit }: any) {
res = emit('foo')
emit('foo')
return () => null
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@vue/runtime-core",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"description": "@vue/runtime-core",
"main": "index.js",
"module": "dist/runtime-core.esm-bundler.js",
@ -31,7 +31,7 @@
},
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/runtime-core#readme",
"dependencies": {
"@vue/shared": "3.0.0-alpha.11",
"@vue/reactivity": "3.0.0-alpha.11"
"@vue/shared": "3.0.0-alpha.12",
"@vue/reactivity": "3.0.0-alpha.12"
}
}

View File

@ -517,7 +517,7 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
return new Proxy(instance.slots, slotsHandlers)
},
get emit() {
return instance.emit
return (event: string, ...args: any[]) => instance.emit(event, ...args)
}
})
} else {

View File

@ -28,12 +28,12 @@ export type EmitFn<
Options = ObjectEmitsOptions,
Event extends keyof Options = keyof Options
> = Options extends any[]
? (event: Options[0], ...args: any[]) => unknown[]
? (event: Options[0], ...args: any[]) => void
: UnionToIntersection<
{
[key in Event]: Options[key] extends ((...args: infer Args) => any)
? (event: key, ...args: Args) => unknown[]
: (event: key, ...args: any[]) => unknown[]
? (event: key, ...args: Args) => void
: (event: key, ...args: any[]) => void
}[Event]
>
@ -41,7 +41,7 @@ export function emit(
instance: ComponentInternalInstance,
event: string,
...args: any[]
): any[] {
) {
const props = instance.vnode.props || EMPTY_OBJ
if (__DEV__) {
@ -66,23 +66,20 @@ export function emit(
}
}
let handler = props[`on${event}`] || props[`on${capitalize(event)}`]
let handler = props[`on${capitalize(event)}`]
// for v-model update:xxx events, also trigger kebab-case equivalent
// for props passed via kebab-case
if (!handler && event.indexOf('update:') === 0) {
event = hyphenate(event)
handler = props[`on${event}`] || props[`on${capitalize(event)}`]
handler = props[`on${capitalize(event)}`]
}
if (handler) {
const res = callWithAsyncErrorHandling(
callWithAsyncErrorHandling(
handler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
return isArray(res) ? res : [res]
} else {
return []
}
}

View File

@ -177,8 +177,15 @@ export function updateProps(
setFullProps(instance, rawProps, props, attrs)
// in case of dynamic props, check if we need to delete keys from
// the props object
let kebabKey: string
for (const key in rawCurrentProps) {
if (!rawProps || !hasOwn(rawProps, key)) {
if (
!rawProps ||
(!hasOwn(rawProps, key) &&
// it's possible the original props was passed in as kebab-case
// and converted to camelCase (#955)
((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey)))
) {
delete props[key]
}
}

View File

@ -236,14 +236,7 @@ export function shouldUpdateComponent(
if (patchFlag & PatchFlags.FULL_PROPS) {
// presence of this flag indicates props are always non-null
return hasPropsChanged(prevProps!, nextProps!)
} else {
if (patchFlag & PatchFlags.CLASS) {
return prevProps!.class !== nextProps!.class
}
if (patchFlag & PatchFlags.STYLE) {
return hasPropsChanged(prevProps!.style, nextProps!.style)
}
if (patchFlag & PatchFlags.PROPS) {
} else if (patchFlag & PatchFlags.PROPS) {
const dynamicProps = nextVNode.dynamicProps!
for (let i = 0; i < dynamicProps.length; i++) {
const key = dynamicProps[i]
@ -252,7 +245,6 @@ export function shouldUpdateComponent(
}
}
}
}
} else if (!optimized) {
// this path is only taken by manually written render functions
// so presence of any children leads to a forced update

View File

@ -968,38 +968,7 @@ function baseCreateRenderer(
)
}
} else {
const instance = (n2.component = n1.component)!
if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) {
if (
__FEATURE_SUSPENSE__ &&
instance.asyncDep &&
!instance.asyncResolved
) {
// async & still pending - just update props and slots
// since the component's reactive effect for render isn't set-up yet
if (__DEV__) {
pushWarningContext(n2)
}
updateComponentPreRender(instance, n2, optimized)
if (__DEV__) {
popWarningContext()
}
return
} else {
// normal update
instance.next = n2
// in case the child component is also queued, remove it to avoid
// double updating the same child component in the same flush.
invalidateJob(instance.update)
// instance.update is the reactive effect runner.
instance.update()
}
} else {
// no update needed. just copy over properties
n2.component = n1.component
n2.el = n1.el
}
updateComponent(n1, n2, parentComponent, optimized)
}
}
@ -1077,6 +1046,45 @@ function baseCreateRenderer(
}
}
const updateComponent = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
optimized: boolean
) => {
const instance = (n2.component = n1.component)!
if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) {
if (
__FEATURE_SUSPENSE__ &&
instance.asyncDep &&
!instance.asyncResolved
) {
// async & still pending - just update props and slots
// since the component's reactive effect for render isn't set-up yet
if (__DEV__) {
pushWarningContext(n2)
}
updateComponentPreRender(instance, n2, optimized)
if (__DEV__) {
popWarningContext()
}
return
} else {
// normal update
instance.next = n2
// in case the child component is also queued, remove it to avoid
// double updating the same child component in the same flush.
invalidateJob(instance.update)
// instance.update is the reactive effect runner.
instance.update()
}
} else {
// no update needed. just copy over properties
n2.component = n1.component
n2.el = n1.el
}
}
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,

View File

@ -1,27 +0,0 @@
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,188 +0,0 @@
// https://github.com/vuejs/vue/blob/dev/test/unit/features/directives/class.spec.js
import { h, render, defineComponent } from '../../src'
type ClassItem = {
value: string | object | string[]
}
function assertClass(assertions: Array<Array<any>>) {
const root = document.createElement('div')
const dynamic = { value: '' }
const wrapper = () => h('div', { class: ['foo', dynamic.value] })
for (const [input, expected] of assertions) {
if (typeof input === 'function') {
input(dynamic.value)
} else {
dynamic.value = input
}
render(wrapper(), root)
expect(root.children[0].className).toBe(expected)
}
}
describe('class', () => {
test('plain string', () => {
assertClass([
['bar', 'foo bar'],
['baz qux', 'foo baz qux'],
['qux', 'foo qux'],
[undefined, 'foo']
])
})
test('object value', () => {
assertClass([
[{ bar: true, baz: false }, 'foo bar'],
[{ baz: true }, 'foo baz'],
[null, 'foo'],
[{ 'bar baz': true, qux: false }, 'foo bar baz'],
[{ qux: true }, 'foo qux']
])
})
test('array value', () => {
assertClass([
[['bar', 'baz'], 'foo bar baz'],
[['qux', 'baz'], 'foo qux baz'],
[['w', 'x y z'], 'foo w x y z'],
[undefined, 'foo'],
[['bar'], 'foo bar'],
[(val: Array<any>) => val.push('baz'), 'foo bar baz']
])
})
test('array of mixed values', () => {
assertClass([
[['x', { y: true, z: true }], 'foo x y z'],
[['x', { y: true, z: false }], 'foo x y'],
[['f', { z: true }], 'foo f z'],
[['l', 'f', { n: true, z: true }], 'foo l f n z'],
[['x', {}], 'foo x'],
[undefined, 'foo']
])
})
test('class merge between parent and child', () => {
const root = document.createElement('div')
const childClass: ClassItem = { value: 'd' }
const child = {
render: () => h('div', { class: ['c', childClass.value] })
}
const parentClass: ClassItem = { value: 'b' }
const parent = {
render: () => h(child, { class: ['a', parentClass.value] })
}
render(h(parent), root)
expect(root.children[0].className).toBe('c d a b')
parentClass.value = 'e'
// the `foo` here is just for forcing parent to be updated
// (otherwise it's skipped since its props never change)
render(h(parent, { foo: 1 }), root)
expect(root.children[0].className).toBe('c d a e')
parentClass.value = 'f'
render(h(parent, { foo: 2 }), root)
expect(root.children[0].className).toBe('c d a f')
parentClass.value = { foo: true }
childClass.value = ['bar', 'baz']
render(h(parent, { foo: 3 }), root)
expect(root.children[0].className).toBe('c bar baz a foo')
})
test('class merge between multiple nested components sharing same element', () => {
const component1 = defineComponent({
render() {
return this.$slots.default!()[0]
}
})
const component2 = defineComponent({
render() {
return this.$slots.default!()[0]
}
})
const component3 = defineComponent({
render() {
return h(
'div',
{
class: 'staticClass'
},
[this.$slots.default!()]
)
}
})
const root = document.createElement('div')
const componentClass1 = { value: 'componentClass1' }
const componentClass2 = { value: 'componentClass2' }
const componentClass3 = { value: 'componentClass3' }
const wrapper = () =>
h(component1, { class: componentClass1.value }, () => [
h(component2, { class: componentClass2.value }, () => [
h(component3, { class: componentClass3.value }, () => ['some text'])
])
])
render(wrapper(), root)
expect(root.children[0].className).toBe(
'staticClass componentClass3 componentClass2 componentClass1'
)
componentClass1.value = 'c1'
render(wrapper(), root)
expect(root.children[0].className).toBe(
'staticClass componentClass3 componentClass2 c1'
)
componentClass2.value = 'c2'
render(wrapper(), root)
expect(root.children[0].className).toBe('staticClass componentClass3 c2 c1')
componentClass3.value = 'c3'
render(wrapper(), root)
expect(root.children[0].className).toBe('staticClass c3 c2 c1')
})
test('deep update', () => {
const root = document.createElement('div')
const test = {
a: true,
b: false
}
const wrapper = () => h('div', { class: test })
render(wrapper(), root)
expect(root.children[0].className).toBe('a')
test.b = true
render(wrapper(), root)
expect(root.children[0].className).toBe('a b')
})
// a vdom patch edge case where the user has several un-keyed elements of the
// same tag next to each other, and toggling them.
test('properly remove staticClass for toggling un-keyed children', () => {
const root = document.createElement('div')
const ok = { value: true }
const wrapper = () =>
h('div', [ok.value ? h('div', { class: 'a' }) : h('div')])
render(wrapper(), root)
expect(root.children[0].children[0].className).toBe('a')
ok.value = false
render(wrapper(), root)
expect(root.children[0].children[0].className).toBe('')
})
})

View File

@ -0,0 +1,37 @@
import { patchProp } from '../src/patchProp'
import { xlinkNS } from '../src/modules/attrs'
describe('runtime-dom: attrs patching', () => {
test('xlink attributes', () => {
const el = document.createElementNS('http://www.w3.org/2000/svg', 'use')
patchProp(el, 'xlink:href', null, 'a', true)
expect(el.getAttributeNS(xlinkNS, 'href')).toBe('a')
patchProp(el, 'xlink:href', 'a', null, true)
expect(el.getAttributeNS(xlinkNS, 'href')).toBe(null)
})
test('boolean attributes', () => {
const el = document.createElement('input')
patchProp(el, 'readonly', null, true)
expect(el.getAttribute('readonly')).toBe('')
patchProp(el, 'readonly', true, false)
expect(el.getAttribute('readonly')).toBe(null)
})
test('attributes', () => {
const el = document.createElement('div')
patchProp(el, 'foo', null, 'a')
expect(el.getAttribute('foo')).toBe('a')
patchProp(el, 'foo', 'a', null)
expect(el.getAttribute('foo')).toBe(null)
})
// #949
test('onxxx but non-listener attributes', () => {
const el = document.createElement('div')
patchProp(el, 'onwards', null, 'a')
expect(el.getAttribute('onwards')).toBe('a')
patchProp(el, 'onwards', 'a', null)
expect(el.getAttribute('onwards')).toBe(null)
})
})

View File

@ -0,0 +1,31 @@
import { patchProp } from '../src/patchProp'
import { ElementWithTransition } from '../src/components/Transition'
import { svgNS } from '../src/nodeOps'
describe('runtime-dom: class patching', () => {
test('basics', () => {
const el = document.createElement('div')
patchProp(el, 'class', null, 'foo')
expect(el.className).toBe('foo')
patchProp(el, 'class', null, null)
expect(el.className).toBe('')
})
test('transition class', () => {
const el = document.createElement('div') as ElementWithTransition
el._vtc = new Set(['bar', 'baz'])
patchProp(el, 'class', null, 'foo')
expect(el.className).toBe('foo bar baz')
patchProp(el, 'class', null, null)
expect(el.className).toBe('bar baz')
delete el._vtc
patchProp(el, 'class', null, 'foo')
expect(el.className).toBe('foo')
})
test('svg', () => {
const el = document.createElementNS(svgNS, 'svg')
patchProp(el, 'class', null, 'foo', true)
expect(el.getAttribute('class')).toBe('foo')
})
})

View File

@ -1,13 +1,13 @@
import { patchEvent } from '../../src/modules/events'
import { patchProp } from '../src/patchProp'
const timeout = () => new Promise(r => setTimeout(r))
describe(`events`, () => {
describe(`runtime-dom: events patching`, () => {
it('should assign event handler', async () => {
const el = document.createElement('div')
const event = new Event('click')
const fn = jest.fn()
patchEvent(el, 'onClick', null, fn, null)
patchProp(el, 'onClick', null, fn)
el.dispatchEvent(event)
await timeout()
el.dispatchEvent(event)
@ -22,9 +22,9 @@ describe(`events`, () => {
const event = new Event('click')
const prevFn = jest.fn()
const nextFn = jest.fn()
patchEvent(el, 'onClick', null, prevFn, null)
patchProp(el, 'onClick', null, prevFn)
el.dispatchEvent(event)
patchEvent(el, 'onClick', prevFn, nextFn, null)
patchProp(el, 'onClick', prevFn, nextFn)
await timeout()
el.dispatchEvent(event)
await timeout()
@ -39,7 +39,7 @@ describe(`events`, () => {
const event = new Event('click')
const fn1 = jest.fn()
const fn2 = jest.fn()
patchEvent(el, 'onClick', null, [fn1, fn2], null)
patchProp(el, 'onClick', null, [fn1, fn2])
el.dispatchEvent(event)
await timeout()
expect(fn1).toHaveBeenCalledTimes(1)
@ -50,8 +50,8 @@ describe(`events`, () => {
const el = document.createElement('div')
const event = new Event('click')
const fn = jest.fn()
patchEvent(el, 'onClick', null, fn, null)
patchEvent(el, 'onClick', fn, null, null)
patchProp(el, 'onClick', null, fn)
patchProp(el, 'onClick', fn, null)
el.dispatchEvent(event)
await timeout()
expect(fn).not.toHaveBeenCalled()
@ -67,7 +67,7 @@ describe(`events`, () => {
once: true
}
}
patchEvent(el, 'onClick', null, nextValue, null)
patchProp(el, 'onClick', null, nextValue)
el.dispatchEvent(event)
await timeout()
el.dispatchEvent(event)
@ -86,8 +86,8 @@ describe(`events`, () => {
once: true
}
}
patchEvent(el, 'onClick', null, prevFn, null)
patchEvent(el, 'onClick', prevFn, nextValue, null)
patchProp(el, 'onClick', null, prevFn)
patchProp(el, 'onClick', prevFn, nextValue)
el.dispatchEvent(event)
await timeout()
el.dispatchEvent(event)
@ -106,8 +106,8 @@ describe(`events`, () => {
once: true
}
}
patchEvent(el, 'onClick', null, nextValue, null)
patchEvent(el, 'onClick', nextValue, null, null)
patchProp(el, 'onClick', null, nextValue)
patchProp(el, 'onClick', nextValue, null)
el.dispatchEvent(event)
await timeout()
el.dispatchEvent(event)
@ -115,21 +115,23 @@ describe(`events`, () => {
expect(fn).not.toHaveBeenCalled()
})
it('should assign native onclick attribute', async () => {
it('should support native onclick', 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)
// string should be set as attribute
const fn = ((window as any).__globalSpy = jest.fn())
patchProp(el, 'onclick', null, '__globalSpy(1)')
el.dispatchEvent(event)
await timeout()
expect(fn).toHaveBeenCalledTimes(1)
delete (window as any).__globalSpy
expect(fn).toHaveBeenCalledWith(1)
const fn2 = jest.fn()
patchEvent(el, 'onclick', null, fn2)
patchProp(el, 'onclick', '__globalSpy(1)', fn2)
el.dispatchEvent(event)
await timeout()
expect(fn).toHaveBeenCalledTimes(1)
expect(fn2).toHaveBeenCalledTimes(1)
expect(fn2).toHaveBeenCalledWith(event)
})
})

View File

@ -0,0 +1,78 @@
import { patchProp } from '../src/patchProp'
import { render, h } from '../src'
describe('runtime-dom: props patching', () => {
test('basic', () => {
const el = document.createElement('div')
patchProp(el, 'id', null, 'foo')
expect(el.id).toBe('foo')
patchProp(el, 'id', null, null)
expect(el.id).toBe('')
})
test('value', () => {
const el = document.createElement('input')
patchProp(el, 'value', null, 'foo')
expect(el.value).toBe('foo')
patchProp(el, 'value', null, null)
expect(el.value).toBe('')
const obj = {}
patchProp(el, 'value', null, obj)
expect(el.value).toBe(obj.toString())
expect((el as any)._value).toBe(obj)
})
test('boolean prop', () => {
const el = document.createElement('select')
patchProp(el, 'multiple', null, '')
expect(el.multiple).toBe(true)
patchProp(el, 'multiple', null, null)
expect(el.multiple).toBe(false)
})
test('innerHTML unmount prev children', () => {
const fn = jest.fn()
const comp = {
render: () => 'foo',
unmounted: fn
}
const root = document.createElement('div')
render(h('div', null, [h(comp)]), root)
expect(root.innerHTML).toBe(`<div>foo</div>`)
render(h('div', { innerHTML: 'bar' }), root)
expect(root.innerHTML).toBe(`<div>bar</div>`)
expect(fn).toHaveBeenCalled()
})
// #954
test('(svg) innerHTML unmount prev children', () => {
const fn = jest.fn()
const comp = {
render: () => 'foo',
unmounted: fn
}
const root = document.createElement('div')
render(h('div', null, [h(comp)]), root)
expect(root.innerHTML).toBe(`<div>foo</div>`)
render(h('svg', { innerHTML: '<g></g>' }), root)
expect(root.innerHTML).toBe(`<svg><g></g></svg>`)
expect(fn).toHaveBeenCalled()
})
test('textContent unmount prev children', () => {
const fn = jest.fn()
const comp = {
render: () => 'foo',
unmounted: fn
}
const root = document.createElement('div')
render(h('div', null, [h(comp)]), root)
expect(root.innerHTML).toBe(`<div>foo</div>`)
render(h('div', { textContent: 'bar' }), root)
expect(root.innerHTML).toBe(`<div>bar</div>`)
expect(fn).toHaveBeenCalled()
})
})

View File

@ -1,39 +1,39 @@
import { patchStyle } from '../../src/modules/style'
import { patchProp } from '../src/patchProp'
describe(`module style`, () => {
describe(`runtime-dom: style patching`, () => {
it('string', () => {
const el = document.createElement('div')
patchStyle(el, {}, 'color:red')
patchProp(el, 'style', {}, 'color:red')
expect(el.style.cssText.replace(/\s/g, '')).toBe('color:red;')
})
it('plain object', () => {
const el = document.createElement('div')
patchStyle(el, {}, { color: 'red' })
patchProp(el, 'style', {}, { color: 'red' })
expect(el.style.cssText.replace(/\s/g, '')).toBe('color:red;')
})
it('camelCase', () => {
const el = document.createElement('div')
patchStyle(el, {}, { marginRight: '10px' })
patchProp(el, 'style', {}, { marginRight: '10px' })
expect(el.style.cssText.replace(/\s/g, '')).toBe('margin-right:10px;')
})
it('remove if falsy value', () => {
const el = document.createElement('div')
patchStyle(el, { color: 'red' }, { color: undefined })
patchProp(el, 'style', { color: 'red' }, { color: undefined })
expect(el.style.cssText.replace(/\s/g, '')).toBe('')
})
it('!important', () => {
const el = document.createElement('div')
patchStyle(el, {}, { color: 'red !important' })
patchProp(el, 'style', {}, { color: 'red !important' })
expect(el.style.cssText.replace(/\s/g, '')).toBe('color:red!important;')
})
it('camelCase with !important', () => {
const el = document.createElement('div')
patchStyle(el, {}, { marginRight: '10px !important' })
patchProp(el, 'style', {}, { marginRight: '10px !important' })
expect(el.style.cssText.replace(/\s/g, '')).toBe(
'margin-right:10px!important;'
)
@ -41,7 +41,7 @@ describe(`module style`, () => {
it('object with multiple entries', () => {
const el = document.createElement('div')
patchStyle(el, {}, { color: 'red', marginRight: '10px' })
patchProp(el, 'style', {}, { color: 'red', marginRight: '10px' })
expect(el.style.getPropertyValue('color')).toBe('red')
expect(el.style.getPropertyValue('margin-right')).toBe('10px')
})
@ -65,13 +65,13 @@ describe(`module style`, () => {
it('CSS custom properties', () => {
const el = mockElementWithStyle()
patchStyle(el as any, {}, { '--theme': 'red' } as any)
patchProp(el as any, 'style', {}, { '--theme': 'red' } as any)
expect(el.style.getPropertyValue('--theme')).toBe('red')
})
it('auto vendor prefixing', () => {
const el = mockElementWithStyle()
patchStyle(el as any, {}, { transition: 'all 1s' })
patchProp(el as any, 'style', {}, { transition: 'all 1s' })
expect(el.style.WebkitTransition).toBe('all 1s')
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@vue/runtime-dom",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"description": "@vue/runtime-dom",
"main": "index.js",
"module": "dist/runtime-dom.esm-bundler.js",
@ -37,8 +37,8 @@
},
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/runtime-dom#readme",
"dependencies": {
"@vue/shared": "3.0.0-alpha.11",
"@vue/runtime-core": "3.0.0-alpha.11",
"@vue/shared": "3.0.0-alpha.12",
"@vue/runtime-core": "3.0.0-alpha.12",
"csstype": "^2.6.8"
}
}

View File

@ -6,15 +6,18 @@ export function patchClass(el: Element, value: string | null, isSVG: boolean) {
if (value == null) {
value = ''
}
// directly setting className should be faster than setAttribute in theory
if (isSVG) {
el.setAttribute('class', value)
} else {
// directly setting className should be faster than setAttribute in theory
// if this is an element during a transition, take the temporary transition
// classes into account.
const transitionClasses = (el as ElementWithTransition)._vtc
if (transitionClasses) {
value = [value, ...transitionClasses].join(' ')
value = (value
? [value, ...transitionClasses]
: [...transitionClasses]
).join(' ')
}
el.className = value
}

View File

@ -1,4 +1,4 @@
import { EMPTY_OBJ, isString } from '@vue/shared'
import { EMPTY_OBJ } from '@vue/shared'
import {
ComponentInternalInstance,
callWithAsyncErrorHandling
@ -71,16 +71,6 @@ export function patchEvent(
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

View File

@ -14,8 +14,10 @@ export function patchDOMProp(
parentSuspense: any,
unmountChildren: any
) {
if ((key === 'innerHTML' || key === 'textContent') && prevChildren) {
if (key === 'innerHTML' || key === 'textContent') {
if (prevChildren) {
unmountChildren(prevChildren, parentComponent, parentSuspense)
}
el[key] = value == null ? '' : value
return
}

View File

@ -1,7 +1,8 @@
import { RendererOptions } from '@vue/runtime-core'
export const svgNS = 'http://www.w3.org/2000/svg'
const doc = (typeof document !== 'undefined' ? document : null) as Document
const svgNS = 'http://www.w3.org/2000/svg'
let tempContainer: HTMLElement
let tempSVGContainer: SVGElement

View File

@ -3,9 +3,11 @@ import { patchStyle } from './modules/style'
import { patchAttr } from './modules/attrs'
import { patchDOMProp } from './modules/props'
import { patchEvent } from './modules/events'
import { isOn } from '@vue/shared'
import { isOn, isString, isFunction } from '@vue/shared'
import { RendererOptions } from '@vue/runtime-core'
const nativeOnRE = /^on[a-z]/
export const patchProp: RendererOptions<Node, Element>['patchProp'] = (
el,
key,
@ -31,7 +33,18 @@ export const patchProp: RendererOptions<Node, Element>['patchProp'] = (
if (key.indexOf('onUpdate:') < 0) {
patchEvent(el, key, prevValue, nextValue, parentComponent)
}
} else if (!isSVG && key in el) {
} else if (
isSVG
? // most keys must be set as attribute on svg elements to work
// ...except innerHTML
key === 'innerHTML' ||
// or native onclick with function values
(key in el && nativeOnRE.test(key) && isFunction(nextValue))
: // for normal html elements, set as a property if it exists
key in el &&
// except native onclick with string values
!(nativeOnRE.test(key) && isString(nextValue))
) {
patchDOMProp(
el,
key,

View File

@ -1,6 +1,6 @@
{
"name": "@vue/runtime-test",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"description": "@vue/runtime-test",
"private": true,
"main": "index.js",
@ -30,7 +30,7 @@
},
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/runtime-test#readme",
"dependencies": {
"@vue/shared": "3.0.0-alpha.11",
"@vue/runtime-core": "3.0.0-alpha.11"
"@vue/shared": "3.0.0-alpha.12",
"@vue/runtime-core": "3.0.0-alpha.12"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@vue/server-renderer",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"description": "@vue/server-renderer",
"main": "index.js",
"types": "dist/server-renderer.d.ts",
@ -27,10 +27,10 @@
},
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/server-renderer#readme",
"peerDependencies": {
"vue": "3.0.0-alpha.11"
"vue": "3.0.0-alpha.12"
},
"dependencies": {
"@vue/shared": "3.0.0-alpha.11",
"@vue/compiler-ssr": "3.0.0-alpha.11"
"@vue/shared": "3.0.0-alpha.12",
"@vue/compiler-ssr": "3.0.0-alpha.12"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@vue/shared",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"description": "internal utils shared across @vue packages",
"main": "index.js",
"module": "dist/shared.esm-bundler.js",

View File

@ -24,7 +24,8 @@ export const NOOP = () => {}
*/
export const NO = () => false
export const isOn = (key: string) => key[0] === 'o' && key[1] === 'n'
const onRE = /^on[^a-z]/
export const isOn = (key: string) => onRE.test(key)
export const extend = <T extends object, U extends object>(
a: T,

View File

@ -1,6 +1,6 @@
{
"name": "@vue/size-check",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"private": true,
"buildOptions": {
"name": "Vue",

View File

@ -1,6 +1,6 @@
{
"name": "@vue/template-explorer",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"private": true,
"buildOptions": {
"formats": [

View File

@ -1,6 +1,6 @@
{
"name": "vue",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"description": "vue",
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",
@ -36,9 +36,9 @@
},
"homepage": "https://github.com/vuejs/vue-next/tree/master/packages/vue#readme",
"dependencies": {
"@vue/shared": "3.0.0-alpha.11",
"@vue/compiler-dom": "3.0.0-alpha.11",
"@vue/runtime-dom": "3.0.0-alpha.11"
"@vue/shared": "3.0.0-alpha.12",
"@vue/compiler-dom": "3.0.0-alpha.12",
"@vue/runtime-dom": "3.0.0-alpha.12"
},
"devDependencies": {
"lodash": "^4.17.15",

View File

@ -1,7 +1,7 @@
import { expectType } from 'tsd'
import { Ref, ref, isRef, unref, UnwrapRef } from './index'
function foo(arg: number | Ref<number>) {
function plainType(arg: number | Ref<number>) {
// ref coercing
const coerced = ref(arg)
expectType<Ref<number>>(coerced)
@ -31,4 +31,26 @@ function foo(arg: number | Ref<number>) {
expectType<Ref<HTMLElement> | Ref<null>>(ref<HTMLElement | null>(null))
}
foo(1)
plainType(1)
function bailType(arg: HTMLElement | Ref<HTMLElement>) {
// ref coercing
const coerced = ref(arg)
expectType<Ref<HTMLElement>>(coerced)
// isRef as type guard
if (isRef(arg)) {
expectType<Ref<HTMLElement>>(arg)
}
// ref unwrapping
expectType<HTMLElement>(unref(arg))
// ref inner type should be unwrapped
const nestedRef = ref({ foo: ref(document.createElement('DIV')) })
expectType<Ref<{ foo: HTMLElement }>>(nestedRef)
expectType<{ foo: HTMLElement }>(nestedRef.value)
}
const el = document.createElement('DIV')
bailType(el)

733
yarn.lock

File diff suppressed because it is too large Load Diff