Merge remote-tracking branch 'github/master' into changing_unwrap_ref
This commit is contained in:
commit
6a66b7b60a
41
CHANGELOG.md
41
CHANGELOG.md
@ -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))
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "3.0.0-alpha.11",
|
||||
"version": "3.0.0-alpha.12",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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('&ersand;', {
|
||||
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: '&ersand;'
|
||||
}
|
||||
})
|
||||
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="&ersand;" b="&ersand;" c="&!"></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: '&ersand;',
|
||||
loc: {
|
||||
start: { offset: 7, line: 1, column: 8 },
|
||||
end: { offset: 20, line: 1, column: 21 },
|
||||
source: '"&ersand;"'
|
||||
}
|
||||
})
|
||||
expect(text2).toStrictEqual({
|
||||
type: NodeTypes.TEXT,
|
||||
content: '&ersand;',
|
||||
loc: {
|
||||
start: { offset: 23, line: 1, column: 24 },
|
||||
end: { offset: 37, line: 1, column: 38 },
|
||||
source: '"&ersand;"'
|
||||
}
|
||||
})
|
||||
expect(text3).toStrictEqual({
|
||||
type: NodeTypes.TEXT,
|
||||
content: '&!',
|
||||
loc: {
|
||||
start: { offset: 40, line: 1, column: 41 },
|
||||
end: { offset: 47, line: 1, column: 48 },
|
||||
source: '"&!"'
|
||||
}
|
||||
})
|
||||
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('†', { 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: '†'
|
||||
}
|
||||
})
|
||||
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('&∪︀', {
|
||||
namedCharacterReferences: {
|
||||
'cups;': '\u222A\uFE00' // UNION with serifs
|
||||
},
|
||||
decodeEntities: text => text.replace('∪︀', '\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>c</template>',
|
||||
errors: []
|
||||
},
|
||||
{
|
||||
code: '<template>ÿ</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="c"></template>',
|
||||
errors: []
|
||||
},
|
||||
{
|
||||
code: '<template attr="ÿ"></template>',
|
||||
errors: []
|
||||
}
|
||||
],
|
||||
CDATA_IN_HTML_CONTENT: [
|
||||
{
|
||||
code: '<template><![CDATA[cdata]]></template>',
|
||||
@ -1825,37 +1660,6 @@ foo
|
||||
errors: []
|
||||
}
|
||||
],
|
||||
CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE: [
|
||||
{
|
||||
code: '<template>�</template>',
|
||||
errors: [
|
||||
{
|
||||
type: ErrorCodes.CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE,
|
||||
loc: { offset: 10, line: 1, column: 11 }
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
CONTROL_CHARACTER_REFERENCE: [
|
||||
{
|
||||
code: '<template></template>',
|
||||
errors: [
|
||||
{
|
||||
type: ErrorCodes.CONTROL_CHARACTER_REFERENCE,
|
||||
loc: { offset: 10, line: 1, column: 11 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
code: '<template></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>&</template>',
|
||||
options: { namedCharacterReferences: { amp: '&' } },
|
||||
errors: [
|
||||
{
|
||||
type: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
|
||||
loc: { offset: 14, line: 1, column: 15 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
code: '<template>(</template>',
|
||||
errors: [
|
||||
{
|
||||
type: ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE,
|
||||
loc: { offset: 14, line: 1, column: 15 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
code: '<template>@</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></template>',
|
||||
errors: [
|
||||
{
|
||||
type: ErrorCodes.NONCHARACTER_CHARACTER_REFERENCE,
|
||||
loc: { offset: 10, line: 1, column: 11 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
code: '<template></template>',
|
||||
errors: [
|
||||
{
|
||||
type: ErrorCodes.NONCHARACTER_CHARACTER_REFERENCE,
|
||||
loc: { offset: 10, line: 1, column: 11 }
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
NULL_CHARACTER_REFERENCE: [
|
||||
{
|
||||
code: '<template>�</template>',
|
||||
errors: [
|
||||
{
|
||||
type: ErrorCodes.NULL_CHARACTER_REFERENCE,
|
||||
loc: { offset: 10, line: 1, column: 11 }
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
SURROGATE_CHARACTER_REFERENCE: [
|
||||
{
|
||||
code: '<template>�</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>",
|
||||
|
@ -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))
|
||||
|
@ -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",
|
||||
|
@ -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 '<' 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]:
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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,129 +818,22 @@ 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 {
|
||||
const { column, line, offset } = context
|
||||
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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(' should not be condensed', () => {
|
||||
const nbsp = String.fromCharCode(160)
|
||||
const ast = parse(`foo 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('&ersand;', 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: '&ersand;'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
|
||||
test('HTML entities compatibility in attribute', () => {
|
||||
const ast = parse(
|
||||
'<div a="&ersand;" b="&ersand;" c="&!"></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: '&ersand;',
|
||||
loc: {
|
||||
start: { offset: 7, line: 1, column: 8 },
|
||||
end: { offset: 20, line: 1, column: 21 },
|
||||
source: '"&ersand;"'
|
||||
}
|
||||
})
|
||||
expect(text2).toStrictEqual({
|
||||
type: NodeTypes.TEXT,
|
||||
content: '&ersand;',
|
||||
loc: {
|
||||
start: { offset: 23, line: 1, column: 24 },
|
||||
end: { offset: 37, line: 1, column: 38 },
|
||||
source: '"&ersand;"'
|
||||
}
|
||||
})
|
||||
expect(text3).toStrictEqual({
|
||||
type: NodeTypes.TEXT,
|
||||
content: '&!',
|
||||
loc: {
|
||||
start: { offset: 40, line: 1, column: 41 },
|
||||
end: { offset: 47, line: 1, column: 48 },
|
||||
source: '"&!"'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('Some control character reference should be replaced.', () => {
|
||||
const ast = parse('†', 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: '†'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
133
packages/compiler-dom/src/decodeHtml.ts
Normal file
133
packages/compiler-dom/src/decodeHtml.ts
Normal 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
|
||||
}
|
6
packages/compiler-dom/src/decodeHtmlBrowser.ts
Normal file
6
packages/compiler-dom/src/decodeHtmlBrowser.ts
Normal 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
|
||||
}
|
@ -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,
|
||||
|
@ -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`)) {
|
@ -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
|
||||
)
|
||||
}
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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()
|
||||
})
|
||||
})
|
@ -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)
|
||||
})
|
||||
|
@ -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 })
|
||||
})
|
||||
|
47
packages/runtime-core/__tests__/componentSlots.spec.ts
Normal file
47
packages/runtime-core/__tests__/componentSlots.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 []
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
@ -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('')
|
||||
})
|
||||
})
|
37
packages/runtime-dom/__tests__/patchAttrs.spec.ts
Normal file
37
packages/runtime-dom/__tests__/patchAttrs.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
31
packages/runtime-dom/__tests__/patchClass.spec.ts
Normal file
31
packages/runtime-dom/__tests__/patchClass.spec.ts
Normal 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')
|
||||
})
|
||||
})
|
@ -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)
|
||||
})
|
||||
})
|
78
packages/runtime-dom/__tests__/patchProps.spec.ts
Normal file
78
packages/runtime-dom/__tests__/patchProps.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
@ -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')
|
||||
})
|
||||
})
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vue/template-explorer",
|
||||
"version": "3.0.0-alpha.11",
|
||||
"version": "3.0.0-alpha.12",
|
||||
"private": true,
|
||||
"buildOptions": {
|
||||
"formats": [
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user