chore: Merge branch 'main' into minor

This commit is contained in:
Evan You 2024-08-07 18:25:58 +08:00
commit eaf5455d77
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
78 changed files with 2545 additions and 1883 deletions

View File

@ -44,7 +44,7 @@ This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
### Full Message Format
A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**:
A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**:
```
<type>(<scope>): <subject>
@ -74,9 +74,9 @@ The scope could be anything specifying the place of the commit change. For examp
The subject contains a succinct description of the change:
* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize the first letter
* no dot (.) at the end
- use the imperative, present tense: "change" not "changed" nor "changes"
- don't capitalize the first letter
- no dot (.) at the end
### Body

View File

@ -35,7 +35,6 @@ Hi! I'm really excited that you are interested in contributing to Vue.js. Before
Another aspect of it is that large scale stylistic changes result in massive diffs that touch multiple files, adding noise to the git history and makes tracing behavior changes across commits more cumbersome.
### Pull Request Checklist
- Vue core has two primary work branches: `main` and `minor`.

View File

@ -80,6 +80,7 @@ Depending on the type of the PR, different considerations need to be taken into
- Make sure it doesn't accidentally cause dev-only or compiler-only code branches to be included in the runtime build. Notable case is that some functions in @vue/shared are compiler-only and should not be used in runtime code, e.g. `isHTMLTag` and `isSVGTag`.
- Performance
- Be careful about code changes in "hot paths", in particular the Virtual DOM renderer (`runtime-core/src/renderer.ts`) and component instantiation code.
- Potential Breakage

View File

@ -3,7 +3,7 @@ on:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
name: Create Release
name: Create GH Release for Tag
permissions: {}
jobs:

94
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,94 @@
name: Release
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to publish'
required: true
default: 'main'
type: choice
options:
- main
- minor
bump:
description: 'Bump version'
required: true
default: 'patch'
type: choice
options:
- patch
- minor
- prepatch
- preminor
- custom
custom_version:
description: 'Custom version'
required: false
default: ''
type: string
jobs:
release:
# prevents this action from running on forks
if: github.repository == 'vuejs/core'
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
# Use Release environment for deployment protection
environment: Release
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.branch }}
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- name: Install deps
run: pnpm install
- name: Configure git user as vue bot
run: |
git config user.name "vue-bot"
git config user.email "<bot@vuejs.org>"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
git_user_signingkey: true
git_commit_gpgsign: true
- name: Run release script
id: release
run: |
pnpm release ${{ inputs.bump != 'custom' && inputs.bump || inputs.custom_version }} --skipPrompts
RELEASE_TAG=$(git describe --tags --abbrev=0)
echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Push tags
run: git push -u origin ${{ inputs.branch }} --follow-tags
- name: Create Release for Tag
id: release_tag
uses: yyx990803/release-tag@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.release.outputs.tag }}
body: |
For stable releases, please refer to [CHANGELOG.md](https://github.com/vuejs/core/blob/main/CHANGELOG.md) for details.
For pre-releases, please refer to [CHANGELOG.md](https://github.com/vuejs/core/blob/minor/CHANGELOG.md) of the `minor` branch.

View File

@ -4,6 +4,7 @@ on:
push:
branches:
- main
- minor
pull_request:
branches:
- main

View File

@ -58,7 +58,7 @@ jobs:
- name: Download Previous Size Data
uses: dawidd6/action-download-artifact@v6
with:
branch: main
branch: ${{ github.base_ref }}
workflow: size-data.yml
event: push
name: size-data

View File

@ -1,4 +1,3 @@
dist
*.md
*.html
pnpm-lock.yaml
CHANGELOG*.md

View File

@ -1,3 +1,16 @@
## [3.4.36](https://github.com/vuejs/core/compare/v3.4.35...v3.4.36) (2024-08-06)
### Bug Fixes
* **compiler-core:** fix expression transform for try...catch block params ([077a1ae](https://github.com/vuejs/core/commit/077a1aeb3c222b729a7e190f46864656ecc65325)), closes [#11465](https://github.com/vuejs/core/issues/11465) [#11467](https://github.com/vuejs/core/issues/11467)
* **compiler-core:** properly handle for loop variable declarations in expression transforms ([67bb820](https://github.com/vuejs/core/commit/67bb820904d53480fa37536fc3cb4109a4c6d3e2)), ref [#11467](https://github.com/vuejs/core/issues/11467)
* **compiler-ssr:** don't render v-if comments in TransitionGroup + static tag ([#11515](https://github.com/vuejs/core/issues/11515)) ([275354c](https://github.com/vuejs/core/commit/275354caba295a6fb50695b70e97888a33c504e0)), closes [#11514](https://github.com/vuejs/core/issues/11514)
* **hydration:** force hydrate custom element dynamic props ([7d473b7](https://github.com/vuejs/core/commit/7d473b7721b423050dba62823b16f3d39e640567)), closes [#7203](https://github.com/vuejs/core/issues/7203) [#8038](https://github.com/vuejs/core/issues/8038)
* **ssr:** respect textContent/innerHTML from getSSRProps in optimized SSR output ([79602f9](https://github.com/vuejs/core/commit/79602f9ecd9559954f844774a90286305b13e056)), closes [#8112](https://github.com/vuejs/core/issues/8112)
* **types/withDefaults:** ensure default values of type `any` do not include `undefined` ([#11490](https://github.com/vuejs/core/issues/11490)) ([4592b63](https://github.com/vuejs/core/commit/4592b63c6a8a3d69bfe4ac1f9458b4a86a9676a4))
# [3.5.0-alpha.5](https://github.com/vuejs/core/compare/v3.4.35...v3.5.0-alpha.5) (2024-07-31)

View File

@ -10,6 +10,6 @@ Please note that we do not consider XSS via template expressions a valid attack
We would like to thank the following security researchers for responsibly disclosing security issues to us.
- Jeet Pal - [@jeetpal2007](https://github.com/jeetpal2007) | [Email](jeetpal2007@gmail.com) | [LinkedIn](https://in.linkedin.com/in/jeet-pal-22601a290 )
- Jeet Pal - [@jeetpal2007](https://github.com/jeetpal2007) | [Email](jeetpal2007@gmail.com) | [LinkedIn](https://in.linkedin.com/in/jeet-pal-22601a290)
- Mix - [@mnixry](https://github.com/mnixry)
- Aviv Keller - [@RedYetiDev](https://github.com/redyetidev) | [LinkedIn](https://www.linkedin.com/in/redyetidev) <redyetidev@gmail.com>

View File

@ -66,9 +66,9 @@
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "5.0.4",
"@swc/core": "^1.7.3",
"@swc/core": "^1.7.6",
"@types/hash-sum": "^1.0.2",
"@types/node": "^20.14.13",
"@types/node": "^20.14.14",
"@types/semver": "^7.5.8",
"@types/serve-handler": "^6.1.4",
"@vitest/coverage-istanbul": "^1.6.0",
@ -82,19 +82,19 @@
"eslint-plugin-vitest": "^0.5.4",
"estree-walker": "catalog:",
"jsdom": "^24.1.1",
"lint-staged": "^15.2.7",
"lint-staged": "^15.2.8",
"lodash": "^4.17.21",
"magic-string": "^0.30.10",
"magic-string": "^0.30.11",
"markdown-table": "^3.0.3",
"marked": "^12.0.2",
"marked": "^13.0.3",
"npm-run-all2": "^6.2.2",
"picocolors": "^1.0.1",
"prettier": "^3.3.3",
"pretty-bytes": "^6.1.1",
"pug": "^3.0.3",
"puppeteer": "~22.14.0",
"rimraf": "^5.0.9",
"rollup": "^4.19.1",
"puppeteer": "~22.15.0",
"rimraf": "^6.0.1",
"rollup": "^4.20.0",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-polyfill-node": "^0.13.0",

View File

@ -14,44 +14,92 @@ return function render(_ctx, _cache, $props, $setup, $data, $options) {
}"
`;
exports[`compiler: expression transform > bindingMetadata > should not prefix temp variable of for loop 1`] = `
exports[`compiler: expression transform > should allow leak of var declarations in for loop 1`] = `
"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", {
onClick: () => {
for (let i = 0; i < _ctx.list.length; i++) {
_ctx.log(i)
}
for (var i = 0; i < _ctx.list.length; i++) {
_ctx.log(i)
}
_ctx.error(i)
}
}, null, 8 /* PROPS */, ["onClick"]))
}"
`;
exports[`compiler: expression transform > bindingMetadata > should not prefix temp variable of for...in 1`] = `
exports[`compiler: expression transform > should not prefix catch block param 1`] = `
"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", {
onClick: () => {
for (const x in _ctx.list) {
_ctx.log(x)
}
}
try {} catch (err) { console.error(err) }
console.log(_ctx.err)
}
}, null, 8 /* PROPS */, ["onClick"]))
}"
`;
exports[`compiler: expression transform > bindingMetadata > should not prefix temp variable of for...of 1`] = `
exports[`compiler: expression transform > should not prefix destructured catch block param 1`] = `
"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", {
onClick: () => {
for (const x of _ctx.list) {
_ctx.log(x)
}
try {
throw new Error('sup?')
} catch ({ message: { length } }) {
console.error(length)
}
console.log(_ctx.length)
}
}, null, 8 /* PROPS */, ["onClick"]))
}"
`;
exports[`compiler: expression transform > should not prefix temp variable of for loop 1`] = `
"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", {
onClick: () => {
for (let i = 0; i < _ctx.list.length; i++) {
_ctx.log(i)
}
_ctx.error(_ctx.i)
}
}, null, 8 /* PROPS */, ["onClick"]))
}"
`;
exports[`compiler: expression transform > should not prefix temp variable of for...in 1`] = `
"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", {
onClick: () => {
for (const x in _ctx.list) {
_ctx.log(x)
}
_ctx.error(_ctx.x)
}
}, null, 8 /* PROPS */, ["onClick"]))
}"
`;
exports[`compiler: expression transform > should not prefix temp variable of for...of 1`] = `
"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", {
onClick: () => {
for (const x of _ctx.list) {
_ctx.log(x)
}
_ctx.error(_ctx.x)
}
}, null, 8 /* PROPS */, ["onClick"]))
}"
`;

View File

@ -27,6 +27,10 @@ function parseWithExpressionTransform(
return ast.children[0]
}
function compile(template: string) {
return baseCompile(template, { prefixIdentifiers: true })
}
describe('compiler: expression transform', () => {
test('interpolation (root)', () => {
const node = parseWithExpressionTransform(`{{ foo }}`) as InterpolationNode
@ -291,6 +295,7 @@ describe('compiler: expression transform', () => {
],
})
})
test('should not prefix an object property key', () => {
const node = parseWithExpressionTransform(
`{{ { foo() { baz() }, value: bar } }}`,
@ -457,6 +462,90 @@ describe('compiler: expression transform', () => {
})
})
test('should not prefix temp variable of for...in', () => {
const { code } = compile(
`<div @click="() => {
for (const x in list) {
log(x)
}
error(x)
}"/>`,
)
expect(code).not.toMatch(`log(_ctx.x)`)
expect(code).toMatch(`error(_ctx.x)`)
expect(code).toMatchSnapshot()
})
test('should not prefix temp variable of for...of', () => {
const { code } = compile(
`<div @click="() => {
for (const x of list) {
log(x)
}
error(x)
}"/>`,
)
expect(code).not.toMatch(`log(_ctx.x)`)
expect(code).toMatch(`error(_ctx.x)`)
expect(code).toMatchSnapshot()
})
test('should not prefix temp variable of for loop', () => {
const { code } = compile(
`<div @click="() => {
for (let i = 0; i < list.length; i++) {
log(i)
}
error(i)
}"/>`,
)
expect(code).not.toMatch(`log(_ctx.i)`)
expect(code).toMatch(`error(_ctx.i)`)
expect(code).toMatchSnapshot()
})
test('should allow leak of var declarations in for loop', () => {
const { code } = compile(
`<div @click="() => {
for (var i = 0; i < list.length; i++) {
log(i)
}
error(i)
}"/>`,
)
expect(code).not.toMatch(`log(_ctx.i)`)
expect(code).not.toMatch(`error(_ctx.i)`)
expect(code).toMatchSnapshot()
})
test('should not prefix catch block param', () => {
const { code } = compile(
`<div @click="() => {
try {} catch (err) { console.error(err) }
console.log(err)
}"/>`,
)
expect(code).not.toMatch(`console.error(_ctx.err)`)
expect(code).toMatch(`console.log(_ctx.err)`)
expect(code).toMatchSnapshot()
})
test('should not prefix destructured catch block param', () => {
const { code } = compile(
`<div @click="() => {
try {
throw new Error('sup?')
} catch ({ message: { length } }) {
console.error(length)
}
console.log(length)
}"/>`,
)
expect(code).not.toMatch(`console.error(_ctx.length)`)
expect(code).toMatch(`console.log(_ctx.length)`)
expect(code).toMatchSnapshot()
})
describe('ES Proposals support', () => {
test('bigInt', () => {
const node = parseWithExpressionTransform(
@ -555,42 +644,6 @@ describe('compiler: expression transform', () => {
expect(code).toMatchSnapshot()
})
test('should not prefix temp variable of for...in', () => {
const { code } = compileWithBindingMetadata(
`<div @click="() => {
for (const x in list) {
log(x)
}
}"/>`,
)
expect(code).not.toMatch(`_ctx.x`)
expect(code).toMatchSnapshot()
})
test('should not prefix temp variable of for...of', () => {
const { code } = compileWithBindingMetadata(
`<div @click="() => {
for (const x of list) {
log(x)
}
}"/>`,
)
expect(code).not.toMatch(`_ctx.x`)
expect(code).toMatchSnapshot()
})
test('should not prefix temp variable of for loop', () => {
const { code } = compileWithBindingMetadata(
`<div @click="() => {
for (let i = 0; i < list.length; i++) {
log(i)
}
}"/>`,
)
expect(code).not.toMatch(`_ctx.i`)
expect(code).toMatchSnapshot()
})
test('inline mode', () => {
const { code } = compileWithBindingMetadata(
`<div>{{ props }} {{ setup }} {{ setupConst }} {{ data }} {{ options }} {{ isNaN }}</div>`,

View File

@ -48,7 +48,7 @@
"dependencies": {
"@babel/parser": "catalog:",
"@vue/shared": "workspace:*",
"entities": "^4.5.0",
"entities": "^5.0.0",
"estree-walker": "catalog:",
"source-map-js": "catalog:"
},

View File

@ -2,6 +2,9 @@
// do not import runtime methods
import type {
BlockStatement,
ForInStatement,
ForOfStatement,
ForStatement,
Function,
Identifier,
Node,
@ -77,6 +80,14 @@ export function walkIdentifiers(
markScopeIdentifier(node, id, knownIds),
)
}
} else if (node.type === 'CatchClause' && node.param) {
for (const id of extractIdentifiers(node.param)) {
markScopeIdentifier(node, id, knownIds)
}
} else if (isForStatement(node)) {
walkForStatement(node, false, id =>
markScopeIdentifier(node, id, knownIds),
)
}
},
leave(node: Node & { scopeIds?: Set<string> }, parent: Node | null) {
@ -192,18 +203,36 @@ export function walkBlockDeclarations(
) {
if (stmt.declare || !stmt.id) continue
onIdent(stmt.id)
} else if (
stmt.type === 'ForOfStatement' ||
stmt.type === 'ForInStatement' ||
stmt.type === 'ForStatement'
) {
const variable = stmt.type === 'ForStatement' ? stmt.init : stmt.left
if (variable && variable.type === 'VariableDeclaration') {
for (const decl of variable.declarations) {
for (const id of extractIdentifiers(decl.id)) {
onIdent(id)
}
}
} else if (isForStatement(stmt)) {
walkForStatement(stmt, true, onIdent)
}
}
}
function isForStatement(
stmt: Node,
): stmt is ForStatement | ForOfStatement | ForInStatement {
return (
stmt.type === 'ForOfStatement' ||
stmt.type === 'ForInStatement' ||
stmt.type === 'ForStatement'
)
}
function walkForStatement(
stmt: ForStatement | ForOfStatement | ForInStatement,
isVar: boolean,
onIdent: (id: Identifier) => void,
) {
const variable = stmt.type === 'ForStatement' ? stmt.init : stmt.left
if (
variable &&
variable.type === 'VariableDeclaration' &&
(variable.kind === 'var' ? isVar : !isVar)
) {
for (const decl of variable.declarations) {
for (const id of extractIdentifiers(decl.id)) {
onIdent(id)
}
}
}

View File

@ -44,7 +44,7 @@ import {
isSimpleIdentifier,
isStaticArgOf,
} from './utils'
import { decodeHTML } from 'entities/lib/decode.js'
import { decodeHTML } from 'entities/dist/decode.js'
import {
type ParserOptions as BabelOptions,
parse,

View File

@ -36,7 +36,7 @@ import {
EntityDecoder,
fromCodePoint,
htmlDecodeTree,
} from 'entities/lib/decode.js'
} from 'entities/dist/decode.js'
export enum ParseMode {
BASE,

View File

@ -1 +1 @@
# @vue/compiler-dom
# @vue/compiler-dom

View File

@ -58,7 +58,7 @@
"hash-sum": "^2.0.0",
"lru-cache": "10.1.0",
"merge-source-map": "^1.1.0",
"minimatch": "^9.0.5",
"minimatch": "~9.0.5",
"postcss-modules": "^6.0.0",
"postcss-selector-parser": "^6.1.1",
"pug": "^3.0.3",

View File

@ -1 +1 @@
# @vue/compiler-ssr
# @vue/compiler-ssr

View File

@ -288,12 +288,27 @@ describe('ssr: element', () => {
}></div>\`"
`)
})
})
test('custom dir', () => {
describe('custom directives', () => {
// #8112 should respect textContent / innerHTML from directive getSSRProps
// if the element has no children
test('custom dir without children', () => {
expect(getCompiledString(`<div v-xxx:x.y="z" />`)).toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_temp0 = _ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.z, "x", { y: true }))
}>\${
("textContent" in _temp0) ? _ssrInterpolate(_temp0.textContent) : _temp0.innerHTML ?? ''
}</div>\`"
`)
})
test('custom dir with children', () => {
expect(getCompiledString(`<div v-xxx:x.y="z">hello</div>`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.z, "x", { y: true }))
}></div>\`"
}>hello</div>\`"
`)
})
@ -301,30 +316,36 @@ describe('ssr: element', () => {
expect(getCompiledString(`<div class="foo" v-xxx />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps({ class: "foo" }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}></div>\`"
_ssrRenderAttrs(_temp0 = _mergeProps({ class: "foo" }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}>\${
("textContent" in _temp0) ? _ssrInterpolate(_temp0.textContent) : _temp0.innerHTML ?? ''
}</div>\`"
`)
})
test('custom dir with v-bind', () => {
expect(getCompiledString(`<div :title="foo" :class="bar" v-xxx />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps({
title: _ctx.foo,
class: _ctx.bar
}, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}></div>\`"
`)
"\`<div\${
_ssrRenderAttrs(_temp0 = _mergeProps({
title: _ctx.foo,
class: _ctx.bar
}, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}>\${
("textContent" in _temp0) ? _ssrInterpolate(_temp0.textContent) : _temp0.innerHTML ?? ''
}</div>\`"
`)
})
test('custom dir with object v-bind', () => {
expect(getCompiledString(`<div v-bind="x" v-xxx />`))
.toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps(_ctx.x, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}></div>\`"
`)
"\`<div\${
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.x, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}>\${
("textContent" in _temp0) ? _ssrInterpolate(_temp0.textContent) : _temp0.innerHTML ?? ''
}</div>\`"
`)
})
test('custom dir with object v-bind + normal bindings', () => {
@ -332,11 +353,13 @@ describe('ssr: element', () => {
getCompiledString(`<div v-bind="x" class="foo" v-xxx title="bar" />`),
).toMatchInlineSnapshot(`
"\`<div\${
_ssrRenderAttrs(_mergeProps(_ctx.x, {
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.x, {
class: "foo",
title: "bar"
}, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
}></div>\`"
}>\${
("textContent" in _temp0) ? _ssrInterpolate(_temp0.textContent) : _temp0.innerHTML ?? ''
}</div>\`"
`)
})
})

View File

@ -38,6 +38,28 @@ describe('transition-group', () => {
`)
})
// #11514
test('with static tag + comment', () => {
expect(
compile(
`<transition-group tag="ul"><div v-for="i in list"/><div v-if="false"></div></transition-group>`,
).code,
).toMatchInlineSnapshot(`
"const { ssrRenderAttrs: _ssrRenderAttrs, ssrRenderList: _ssrRenderList } = require("vue/server-renderer")
return function ssrRender(_ctx, _push, _parent, _attrs) {
_push(\`<ul\${_ssrRenderAttrs(_attrs)}>\`)
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
if (false) {
_push(\`<div></div>\`)
}
_push(\`</ul>\`)
}"
`)
})
test('with dynamic tag', () => {
expect(
compile(

View File

@ -163,6 +163,25 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
]),
]
}
} else if (directives.length && !node.children.length) {
const tempId = `_temp${context.temps++}`
propsExp.arguments = [
createAssignmentExpression(
createSimpleExpression(tempId, false),
mergedProps,
),
]
rawChildrenMap.set(
node,
createConditionalExpression(
createSimpleExpression(`"textContent" in ${tempId}`, false),
createCallExpression(context.helper(SSR_INTERPOLATE), [
createSimpleExpression(`${tempId}.textContent`, false),
]),
createSimpleExpression(`${tempId}.innerHTML ?? ''`, false),
false,
),
)
}
if (needTagForRuntime) {

View File

@ -108,7 +108,7 @@ export function ssrProcessTransitionGroup(
context.pushStringPart(` ${scopeId}`)
}
context.pushStringPart(`>`)
processChildren(node, context, false, true)
processChildren(node, context, false, true, true)
context.pushStringPart(`</${tag.value!.content}>`)
}
} else {

View File

@ -182,6 +182,11 @@ describe('allow getter and setter types to be unrelated', <T>() => {
const d = {} as T
const e = ref(d)
e.value = d
const f = ref(ref(0))
expectType<number>(f.value)
// @ts-expect-error
f.value = ref(1)
})
// computed

View File

@ -43,7 +43,8 @@ describe('defineProps w/ generics', () => {
test()
})
describe('defineProps w/ type declaration + withDefaults', () => {
describe('defineProps w/ type declaration + withDefaults', <T extends
string>() => {
const res = withDefaults(
defineProps<{
number?: number
@ -56,6 +57,7 @@ describe('defineProps w/ type declaration + withDefaults', () => {
z?: string
bool?: boolean
boolAndUndefined: boolean | undefined
foo?: T
}>(),
{
number: 123,
@ -65,6 +67,7 @@ describe('defineProps w/ type declaration + withDefaults', () => {
genStr: () => '',
y: undefined,
z: 'string',
foo: '' as any,
},
)
@ -81,6 +84,7 @@ describe('defineProps w/ type declaration + withDefaults', () => {
expectType<string | undefined>(res.x)
expectType<string | undefined>(res.y)
expectType<string>(res.z)
expectType<T>(res.foo)
expectType<boolean>(res.bool)
expectType<boolean>(res.boolAndUndefined)
@ -138,6 +142,31 @@ describe('defineProps w/ object union + withDefaults', () => {
>(props)
})
describe('defineProps w/ generic discriminate union + withDefaults', () => {
interface B {
b?: string
}
interface S<T> extends B {
mode: 'single'
v: T
}
interface M<T> extends B {
mode: 'multiple'
v: T[]
}
type Props = S<string> | M<string>
const props = withDefaults(defineProps<Props>(), {
b: 'b',
})
if (props.mode === 'single') {
expectType<string>(props.v)
}
if (props.mode === 'multiple') {
expectType<string[]>(props.v)
}
})
describe('defineProps w/ generic type declaration + withDefaults', <T extends
number, TA extends {
a: string

View File

@ -73,7 +73,7 @@ class BaseReactiveHandler implements ProxyHandler<Target> {
: reactiveMap
).get(target) ||
// receiver is not the reactive proxy, but has the same prototype
// this means the reciever is a user proxy of the reactive proxy
// this means the receiver is a user proxy of the reactive proxy
Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)
) {
return target

View File

@ -52,7 +52,9 @@ export function isRef(r: any): r is Ref {
* @param value - The object to wrap in the ref.
* @see {@link https://vuejs.org/api/reactivity-core.html#ref}
*/
export function ref<T>(value: T): Ref<UnwrapRef<T>, UnwrapRef<T> | T>
export function ref<T>(
value: T,
): [T] extends [Ref] ? IfAny<T, Ref<T>, T> : Ref<UnwrapRef<T>, UnwrapRef<T> | T>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value, false)

View File

@ -13,7 +13,7 @@ const { render, createApp } = createRenderer({
patchProp,
insert,
remove,
createElement
createElement,
// ...
})

View File

@ -116,12 +116,25 @@ describe('api: createApp', () => {
const app = createApp({
setup() {
provide('foo', 'should not be seen')
// nested createApp
const childApp = createApp({
setup() {
provide('foo', 'foo from child')
},
})
childApp.provide('foo', 2)
expect(childApp.runWithContext(() => inject('foo'))).toBe(2)
return () => h('div')
},
})
app.provide('foo', 1)
expect(app.runWithContext(() => inject('foo'))).toBe(1)
const root = nodeOps.createElement('div')
app.mount(root)
expect(
app.runWithContext(() => {

View File

@ -155,12 +155,12 @@ describe('component: emit', () => {
render() {},
created() {
// @ts-expect-error
this.$emit('bar')
this.$emit('bar-baz')
},
})
render(h(Foo), nodeOps.createElement('div'))
expect(
`Component emitted event "bar" but it is neither declared`,
`Component emitted event "bar-baz" but it is neither declared in the emits option nor as an "onBarBaz" prop`,
).toHaveBeenWarned()
})
@ -172,12 +172,12 @@ describe('component: emit', () => {
render() {},
created() {
// @ts-expect-error
this.$emit('bar')
this.$emit('bar-baz')
},
})
render(h(Foo), nodeOps.createElement('div'))
expect(
`Component emitted event "bar" but it is neither declared`,
`Component emitted event "bar-baz" but it is neither declared in the emits option nor as an "onBarBaz" prop`,
).toHaveBeenWarned()
})
@ -197,6 +197,22 @@ describe('component: emit', () => {
).not.toHaveBeenWarned()
})
test('should not warn if has equivalent onXXX prop with kebab-cased event', () => {
const Foo = defineComponent({
props: ['onFooBar'],
emits: [],
render() {},
created() {
// @ts-expect-error
this.$emit('foo-bar')
},
})
render(h(Foo), nodeOps.createElement('div'))
expect(
`Component emitted event "foo-bar" but it is neither declared`,
).not.toHaveBeenWarned()
})
test('validator warning', () => {
const Foo = defineComponent({
emits: {

View File

@ -213,7 +213,7 @@ describe('component: slots', () => {
expect(instance.slots.default()).toMatchObject([normalizeVNode('footer')])
})
test('should respect $stable flag', async () => {
test('should respect $stable flag with a value of true', async () => {
const flag1 = ref(1)
const flag2 = ref(2)
const spy = vi.fn()
@ -255,6 +255,48 @@ describe('component: slots', () => {
expect(spy).toHaveBeenCalledTimes(2)
})
test('should respect $stable flag with a value of false', async () => {
const flag1 = ref(1)
const flag2 = ref(2)
const spy = vi.fn()
const Child = () => {
spy()
return 'child'
}
const App = {
setup() {
return () => [
flag1.value,
h(
Child,
{ n: flag2.value },
{
foo: () => 'foo',
$stable: false,
},
),
]
},
}
render(h(App), nodeOps.createElement('div'))
expect(spy).toHaveBeenCalledTimes(1)
// parent re-render, props didn't change, slots are not stable
// -> child should update
flag1.value++
await nextTick()
expect(spy).toHaveBeenCalledTimes(2)
// parent re-render, props changed
// -> child should update
flag2.value++
await nextTick()
expect(spy).toHaveBeenCalledTimes(3)
})
test('should not warn when mounting another app in setup', () => {
const Comp = {
setup(_: any, { slots }: any) {

View File

@ -8,6 +8,7 @@ import {
KeepAlive,
Suspense,
type SuspenseProps,
createCommentVNode,
h,
nextTick,
nodeOps,
@ -2035,7 +2036,7 @@ describe('Suspense', () => {
expect(serializeInner(root)).toBe(`<div>sync</div>`)
})
// #10899
// #10899 / #11427
test('KeepAlive + Suspense switch before branch resolves', async () => {
const Async1 = defineAsyncComponent({
render() {
@ -2052,14 +2053,20 @@ describe('Suspense', () => {
const root = nodeOps.createElement('div')
const App = {
render() {
return h(KeepAlive, null, {
default: () => {
return h(Suspense, null, {
default: h(components[viewRef.value]),
fallback: h('div', 'loading'),
})
return h(
KeepAlive,
{
max: 1,
},
})
{
default: () => {
return h(Suspense, null, {
default: h(components[viewRef.value]),
fallback: h('div', 'loading'),
})
},
},
)
},
}
render(h(App), root)
@ -2085,6 +2092,35 @@ describe('Suspense', () => {
expect(serializeInner(root)).toBe(`<div>async2</div>`)
})
test('KeepAlive + Suspense + comment slot', async () => {
const toggle = ref(false)
const Async = defineAsyncComponent({
render() {
return h('div', 'async1')
},
})
const App = {
render() {
return h(KeepAlive, null, {
default: () => {
return h(Suspense, null, {
default: toggle.value ? h(Async) : createCommentVNode('v-if'),
})
},
})
},
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe(`<!--v-if-->`)
toggle.value = true
await nextTick()
await Promise.all(deps)
expect(serializeInner(root)).toBe(`<div>async1</div>`)
})
// #6416 follow up / #10017
test('Suspense patched during HOC async component re-mount', async () => {
const key = ref('k')

View File

@ -1388,6 +1388,26 @@ describe('SSR hydration', () => {
expect((container.firstChild!.firstChild as any)._value).toBe(true)
})
// #7203
test('force hydrate custom element with dynamic props', () => {
class MyElement extends HTMLElement {
foo = ''
constructor() {
super()
}
}
customElements.define('my-element-7203', MyElement)
const msg = ref('bar')
const container = document.createElement('div')
container.innerHTML = '<my-element-7203></my-element-7203>'
const app = createSSRApp({
render: () => h('my-element-7203', { foo: msg.value }),
})
app.mount(container)
expect((container.firstChild as any).foo).toBe(msg.value)
})
// #5728
test('empty text node in slot', () => {
const Comp = {

View File

@ -58,11 +58,14 @@ export function inject(
// #2400
// to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root
const provides = instance
? instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
: currentApp!._context.provides
// #11488, in a nested createApp, prioritize using the provides from currentApp
const provides = currentApp
? currentApp._context.provides
: instance
? instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
: undefined
if (provides && (key as string | symbol) in provides) {
// TS doesn't allow symbol as index type

View File

@ -1,4 +1,5 @@
import {
type IfAny,
type LooseRequired,
type Prettify,
type UnionToIntersection,
@ -329,7 +330,7 @@ type PropsWithDefaults<
> = Readonly<MappedOmit<T, keyof Defaults>> & {
readonly [K in keyof Defaults]-?: K extends keyof T
? Defaults[K] extends undefined
? T[K]
? IfAny<Defaults[K], NotUndefined<T[K]>, T[K]>
: NotUndefined<T[K]>
: never
} & {

View File

@ -122,10 +122,10 @@ export function emit(
event.startsWith(compatModelEventPrefix))
)
) {
if (!propsOptions || !(toHandlerKey(event) in propsOptions)) {
if (!propsOptions || !(toHandlerKey(camelize(event)) in propsOptions)) {
warn(
`Component emitted event "${event}" but it is neither declared in ` +
`the emits option nor as an "${toHandlerKey(event)}" prop.`,
`the emits option nor as an "${toHandlerKey(camelize(event))}" prop.`,
)
}
} else {

View File

@ -8,6 +8,7 @@ import {
getCurrentInstance,
} from '../component'
import {
Comment,
type VNode,
type VNodeProps,
cloneVNode,
@ -205,7 +206,7 @@ const KeepAliveImpl: ComponentOptions = {
function pruneCacheEntry(key: CacheKey) {
const cached = cache.get(key) as VNode
if (!current || !isSameVNodeType(cached, current)) {
if (cached && (!current || !isSameVNodeType(cached, current))) {
unmount(cached)
} else if (current) {
// current active instance should no longer be kept-alive.
@ -287,6 +288,12 @@ const KeepAliveImpl: ComponentOptions = {
}
let vnode = getInnerChild(rawVNode)
// #6028 Suspense ssContent maybe a comment VNode, should avoid caching it
if (vnode.type === Comment) {
current = null
return vnode
}
const comp = vnode.type as ConcreteComponent
// for async components, name check should be based in its loaded

View File

@ -125,7 +125,7 @@ export type DirectiveArguments = Array<
| [Directive | undefined]
| [Directive | undefined, any]
| [Directive | undefined, any, string]
| [Directive | undefined, any, string, DirectiveModifiers]
| [Directive | undefined, any, string | undefined, DirectiveModifiers]
>
/**

View File

@ -98,7 +98,7 @@ export function renderSlot(
return rendered
}
function ensureValidVNode(vnodes: VNodeArrayChildren) {
export function ensureValidVNode(vnodes: VNodeArrayChildren) {
return vnodes.some(child => {
if (!isVNode(child)) return true
if (child.type === Comment) return false

View File

@ -451,6 +451,7 @@ export function createHydrationFunctions(
!optimized ||
patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.NEED_HYDRATION)
) {
const isCustomElement = el.tagName.includes('-')
for (const key in props) {
// check hydration mismatch
if (
@ -467,7 +468,8 @@ export function createHydrationFunctions(
(key.endsWith('value') || key === 'indeterminate')) ||
(isOn(key) && !isReservedProp(key)) ||
// force hydrate v-bind with .prop modifiers
key[0] === '.'
key[0] === '.' ||
isCustomElement
) {
patchProp(el, key, null, props[key], undefined, parentComponent)
}

View File

@ -397,6 +397,7 @@ import {
import { renderComponentRoot } from './componentRenderUtils'
import { setCurrentRenderingInstance } from './componentRenderContext'
import { isVNode, normalizeVNode } from './vnode'
import { ensureValidVNode } from './helpers/renderSlot'
const _ssrUtils = {
createComponentInstance,
@ -406,6 +407,7 @@ const _ssrUtils = {
isVNode,
normalizeVNode,
getComponentPublicInstance,
ensureValidVNode,
}
/**

View File

@ -2356,13 +2356,13 @@ function baseCreateRenderer(
namespace,
)
}
container._vnode = vnode
if (!isFlushing) {
isFlushing = true
flushPreFlushCbs()
flushPostFlushCbs()
isFlushing = false
}
container._vnode = vnode
}
const internals: RendererInternals = {

View File

@ -1,12 +1,12 @@
# @vue/runtime-dom
``` js
```js
import { h, createApp } from '@vue/runtime-dom'
const RootComponent = {
render() {
return h('div', 'hello world')
}
},
}
createApp(RootComponent).mount('#app')

View File

@ -114,6 +114,7 @@ describe('defineCustomElement', () => {
myInputEl.removeAttribute('value')
await nextTick()
expect(inputEl.value).toBe('')
app.unmount()
})
test('should not unmount on move', async () => {
@ -1158,4 +1159,33 @@ describe('defineCustomElement', () => {
expect(e.shadowRoot?.innerHTML).toBe('<div>app-injected</div>')
})
})
// #9885
test('avoid double mount when prop is set immediately after mount', () => {
customElements.define(
'my-input-dupe',
defineCustomElement({
props: {
value: String,
},
render() {
return 'hello'
},
}),
)
createApp({
render() {
return h('div', [
h('my-input-dupe', {
onVnodeMounted(vnode) {
vnode.el!.value = 'fesfes'
},
}),
])
},
}).mount(container)
expect(container.children[0].children[0].shadowRoot?.innerHTML).toBe(
'hello',
)
})
})

View File

@ -3,6 +3,7 @@ import {
Static,
type VNode,
getCurrentInstance,
onBeforeMount,
onMounted,
onUnmounted,
warn,
@ -46,8 +47,11 @@ export function useCssVars(getter: (ctx: any) => Record<string, string>) {
updateTeleports(vars)
}
onMounted(() => {
onBeforeMount(() => {
watchPostEffect(setVars)
})
onMounted(() => {
const ob = new MutationObserver(setVars)
ob.observe(instance.subTree.el!.parentNode, { childList: true })
onUnmounted(() => ob.disconnect())

View File

@ -4,7 +4,7 @@ This is for Vue's own internal tests only - it ensures logic tested using this p
It can also be used as a reference for implementing a custom renderer.
``` js
```js
import { h, render, nodeOps, dumpOps } from '@vue/runtime-test'
const App = {

View File

@ -200,17 +200,4 @@ describe('test renderer', () => {
await nextTick()
expect(serialize(root)).toBe(`<div><span>1, 2</span></div>`)
})
it('should mock warn', () => {
console.warn('warn!!!')
expect('warn!!!').toHaveBeenWarned()
expect('warn!!!').toHaveBeenWarnedTimes(1)
console.warn('warn!!!')
expect('warn!!!').toHaveBeenWarnedTimes(2)
console.warn('warning')
expect('warn!!!').toHaveBeenWarnedTimes(2)
expect('warning').toHaveBeenWarnedLast()
})
})

View File

@ -11,7 +11,7 @@
```ts
function renderToString(
input: App | VNode,
context?: SSRContext
context?: SSRContext,
): Promise<string>
```
@ -23,7 +23,7 @@ const { renderToString } = require('@vue/server-renderer')
const app = createSSRApp({
data: () => ({ msg: 'hello' }),
template: `<div>{{ msg }}</div>`
template: `<div>{{ msg }}</div>`,
})
;(async () => {
@ -74,7 +74,7 @@ Render and pipe to an existing [Node.js Writable stream](https://nodejs.org/api/
function pipeToNodeWritable(
input: App | VNode,
context: SSRContext = {},
writable: Writable
writable: Writable,
): void
```
@ -94,7 +94,7 @@ Renders input as a [Web ReadableStream](https://developer.mozilla.org/en-US/docs
```ts
function renderToWebStream(
input: App | VNode,
context?: SSRContext
context?: SSRContext,
): ReadableStream
```
@ -117,7 +117,7 @@ Render and pipe to an existing [Web WritableStream](https://developer.mozilla.or
function pipeToWebWritable(
input: App | VNode,
context: SSRContext = {},
writable: WritableStream
writable: WritableStream,
): void
```
@ -144,7 +144,7 @@ Renders input in streaming mode using a simple readable interface.
function renderToSimpleStream(
input: App | VNode,
context: SSRContext,
options: SimpleReadable
options: SimpleReadable,
): SimpleReadable
interface SimpleReadable {
@ -172,7 +172,7 @@ renderToSimpleStream(
},
destroy(err) {
// error encountered
}
}
},
},
)
```

View File

@ -153,4 +153,54 @@ describe('ssr: slot', () => {
),
).toBe(`<div><p>1</p><p>2</p></div>`)
})
// #11326
test('dynamic component slot', async () => {
expect(
await renderToString(
createApp({
components: {
ButtonComp: {
template: `<component is="button"><slot/></component>`,
},
Wrap: {
template: `<div><slot/></div>`,
},
},
template: `<ButtonComp><Wrap><div v-if="false">hello</div></Wrap></ButtonComp>`,
}),
),
).toBe(`<button><!--[--><div><!--[--><!--]--></div><!--]--></button>`)
expect(
await renderToString(
createApp({
components: {
ButtonComp: {
template: `<component is="button"><slot/></component>`,
},
Wrap: {
template: `<div><slot/></div>`,
},
},
template: `<ButtonComp><Wrap><div v-if="true">hello</div></Wrap></ButtonComp>`,
}),
),
).toBe(
`<button><!--[--><div><!--[--><div>hello</div><!--]--></div><!--]--></button>`,
)
expect(
await renderToString(
createApp({
components: {
ButtonComp: {
template: `<component is="button"><slot/></component>`,
},
},
template: `<ButtonComp><template v-if="false">hello</template></ButtonComp>`,
}),
),
).toBe(`<button><!--[--><!--]--></button>`)
})
})

View File

@ -1,4 +1,4 @@
import type { ComponentInternalInstance, Slots } from 'vue'
import { type ComponentInternalInstance, type Slots, ssrUtils } from 'vue'
import {
type Props,
type PushFn,
@ -7,6 +7,8 @@ import {
} from '../render'
import { isArray } from '@vue/shared'
const { ensureValidVNode } = ssrUtils
export type SSRSlots = Record<string, SSRSlot>
export type SSRSlot = (
props: Props,
@ -61,8 +63,18 @@ export function ssrRenderSlotInner(
slotScopeId ? ' ' + slotScopeId : '',
)
if (isArray(ret)) {
// normal slot
renderVNodeChildren(push, ret, parentComponent, slotScopeId)
const validSlotContent = ensureValidVNode(ret)
if (validSlotContent) {
// normal slot
renderVNodeChildren(
push,
validSlotContent,
parentComponent,
slotScopeId,
)
} else if (fallbackRenderFn) {
fallbackRenderFn()
}
} else {
// ssr slot.
// check if the slot renders all comments, in which case use the fallback

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@ -7,13 +7,16 @@
<link rel="icon" type="image/svg" href="/logo.svg" />
<title>Vue SFC Playground</title>
<script>
const savedPreferDark = localStorage.getItem('vue-sfc-playground-prefer-dark')
if (
savedPreferDark === 'true' ||
(!savedPreferDark && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
const savedPreferDark = localStorage.getItem(
'vue-sfc-playground-prefer-dark',
)
if (
savedPreferDark === 'true' ||
(!savedPreferDark &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark')
}
}
</script>
<script type="module" src="/src/main.ts"></script>
</head>

View File

@ -9,7 +9,7 @@
"serve": "vite preview"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.1",
"@vitejs/plugin-vue": "^5.1.2",
"vite": "catalog:"
},
"dependencies": {

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />

View File

@ -11,7 +11,7 @@
"vue": "^3.4.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.1",
"@vitejs/plugin-vue": "^5.1.2",
"vite": "^5.3.5"
}
}

View File

@ -1,6 +1,10 @@
<title>Vue Template Explorer</title>
<link rel="stylesheet" data-name="vs/editor/editor.main" href="https://unpkg.com/monaco-editor@0.20.0/min/vs/editor/editor.main.css">
<link rel="stylesheet" href="./style.css">
<link
rel="stylesheet"
data-name="vs/editor/editor.main"
href="https://unpkg.com/monaco-editor@0.20.0/min/vs/editor/editor.main.css"
/>
<link rel="stylesheet" href="./style.css" />
<div id="header"></div>
<div id="source" class="editor"></div>
@ -8,13 +12,13 @@
<script src="https://unpkg.com/monaco-editor@0.20.0/min/vs/loader.js"></script>
<script>
require.config({
paths: {
'vs': 'https://unpkg.com/monaco-editor@0.20.0/min/vs'
}
})
require.config({
paths: {
vs: 'https://unpkg.com/monaco-editor@0.20.0/min/vs',
},
})
</script>
<script src="./dist/template-explorer.global.js"></script>
<script>
require(['vs/editor/editor.main'], init /* injected by build */)
require(['vs/editor/editor.main'], init /* injected by build */)
</script>

View File

@ -1,6 +1,10 @@
<title>Vue Template Explorer</title>
<link rel="stylesheet" data-name="vs/editor/editor.main" href="./node_modules/monaco-editor/min/vs/editor/editor.main.css">
<link rel="stylesheet" href="./style.css">
<link
rel="stylesheet"
data-name="vs/editor/editor.main"
href="./node_modules/monaco-editor/min/vs/editor/editor.main.css"
/>
<link rel="stylesheet" href="./style.css" />
<div id="header"></div>
<div id="source" class="editor"></div>
@ -8,13 +12,13 @@
<script src="./node_modules/monaco-editor/min/vs/loader.js"></script>
<script>
require.config({
paths: {
'vs': './node_modules/monaco-editor/min/vs'
}
})
require.config({
paths: {
vs: './node_modules/monaco-editor/min/vs',
},
})
</script>
<script src="./dist/template-explorer.global.js"></script>
<script>
require(['vs/editor/editor.main'], init /* injected by build */)
require(['vs/editor/editor.main'], init /* injected by build */)
</script>

View File

@ -84,12 +84,12 @@ The following workflow walks through the steps of migrating an actual Vue 2 app
...options,
compilerOptions: {
compatConfig: {
MODE: 2
}
}
MODE: 2,
},
},
}
})
}
},
}
```
@ -103,8 +103,8 @@ The following workflow walks through the steps of migrating an actual Vue 2 app
module.exports = {
resolve: {
alias: {
vue: '@vue/compat'
}
vue: '@vue/compat',
},
},
module: {
rules: [
@ -114,13 +114,13 @@ The following workflow walks through the steps of migrating an actual Vue 2 app
options: {
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}
}
]
}
MODE: 2,
},
},
},
},
],
},
}
```
@ -134,20 +134,20 @@ The following workflow walks through the steps of migrating an actual Vue 2 app
export default {
resolve: {
alias: {
vue: '@vue/compat'
}
vue: '@vue/compat',
},
},
plugins: [
vue({
template: {
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}
})
]
MODE: 2,
},
},
},
}),
],
}
```
@ -207,7 +207,7 @@ import { configureCompat } from 'vue'
// disable compat for certain features
configureCompat({
FEATURE_ID_A: false,
FEATURE_ID_B: false
FEATURE_ID_B: false,
})
```
@ -221,7 +221,7 @@ import { configureCompat } from 'vue'
configureCompat({
MODE: 3,
FEATURE_ID_A: true,
FEATURE_ID_B: true
FEATURE_ID_B: true,
})
```
@ -233,8 +233,8 @@ A component can use the `compatConfig` option, which expects the same options as
export default {
compatConfig: {
MODE: 3, // opt-in to Vue 3 behavior for this component only
FEATURE_ID_A: true // features can also be toggled at component level
}
FEATURE_ID_A: true, // features can also be toggled at component level
},
// ...
}
```
@ -256,73 +256,73 @@ Features that start with `COMPILER_` are compiler-specific: if you are using the
> Should be fixed upfront or will likely lead to errors
| ID | Type | Description | Docs |
| ------------------------------------- | ---- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| GLOBAL_MOUNT_CONTAINER | ⨂ | Mounted application does not replace the element it's mounted to | [link](https://v3-migration.vuejs.org/breaking-changes/mount-changes.html) |
| CONFIG_DEVTOOLS | ⨂ | production devtools is now a build-time flag | [link](https://github.com/vuejs/core/tree/main/packages/vue#bundler-build-feature-flags) |
| COMPILER_V_IF_V_FOR_PRECEDENCE | ⨂ | `v-if` and `v-for` precedence when used on the same element has changed | [link](https://v3-migration.vuejs.org/breaking-changes/v-if-v-for.html) |
| COMPILER_V_IF_SAME_KEY | ⨂ | `v-if` branches can no longer have the same key | [link](https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#on-conditional-branches) |
| COMPILER_V_FOR_TEMPLATE_KEY_PLACEMENT | ⨂ | `<template v-for>` key should now be placed on `<template>` | [link](https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#with-template-v-for) |
| COMPILER_SFC_FUNCTIONAL | ⨂ | `<template functional>` is no longer supported in SFCs | [link](https://v3-migration.vuejs.org/breaking-changes/functional-components.html#single-file-components-sfcs) | | |
| ID | Type | Description | Docs |
| ------------------------------------- | ---- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| GLOBAL_MOUNT_CONTAINER | ⨂ | Mounted application does not replace the element it's mounted to | [link](https://v3-migration.vuejs.org/breaking-changes/mount-changes.html) |
| CONFIG_DEVTOOLS | ⨂ | production devtools is now a build-time flag | [link](https://github.com/vuejs/core/tree/main/packages/vue#bundler-build-feature-flags) |
| COMPILER_V_IF_V_FOR_PRECEDENCE | ⨂ | `v-if` and `v-for` precedence when used on the same element has changed | [link](https://v3-migration.vuejs.org/breaking-changes/v-if-v-for.html) |
| COMPILER_V_IF_SAME_KEY | ⨂ | `v-if` branches can no longer have the same key | [link](https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#on-conditional-branches) |
| COMPILER_V_FOR_TEMPLATE_KEY_PLACEMENT | ⨂ | `<template v-for>` key should now be placed on `<template>` | [link](https://v3-migration.vuejs.org/breaking-changes/key-attribute.html#with-template-v-for) |
| COMPILER_SFC_FUNCTIONAL | ⨂ | `<template functional>` is no longer supported in SFCs | [link](https://v3-migration.vuejs.org/breaking-changes/functional-components.html#single-file-components-sfcs) |
### Partially Compatible with Caveats
| ID | Type | Description | Docs |
| ------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
| ID | Type | Description | Docs |
| ------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| CONFIG_IGNORED_ELEMENTS | ◐ | `config.ignoredElements` is now `config.compilerOptions.isCustomElement` (only in browser compiler build). If using build setup, `isCustomElement` must be passed via build configuration. | [link](https://v3-migration.vuejs.org/breaking-changes/global-api.html#config-ignoredelements-is-now-config-iscustomelement) |
| COMPILER_INLINE_TEMPLATE | ◐ | `inline-template` removed (compat only supported in browser compiler build) | [link](https://v3-migration.vuejs.org/breaking-changes/inline-template-attribute.html) |
| PROPS_DEFAULT_THIS | ◐ | props default factory no longer have access to `this` (in compat mode, `this` is not a real instance - it only exposes props, `$options` and injections) | [link](https://v3-migration.vuejs.org/breaking-changes/props-default-this.html) |
| INSTANCE_DESTROY | ◐ | `$destroy` instance method removed (in compat mode, only supported on root instance) | |
| GLOBAL_PRIVATE_UTIL | ◐ | `Vue.util` is private and no longer available | |
| INSTANCE_DESTROY | ◐ | `$destroy` instance method removed (in compat mode, only supported on root instance) | |
| GLOBAL_PRIVATE_UTIL | ◐ | `Vue.util` is private and no longer available | |
| CONFIG_PRODUCTION_TIP | ◐ | `config.productionTip` no longer necessary | [link](https://v3-migration.vuejs.org/breaking-changes/global-api.html#config-productiontip-removed) |
| CONFIG_SILENT | ◐ | `config.silent` removed |
### Compat only (no warning)
| ID | Type | Description | Docs |
| ------------------ | ---- | ------------------------------------- | ---------------------------------------- |
| ID | Type | Description | Docs |
| ------------------ | ---- | -------------------------------------- | ----------------------------------------------------------------------- |
| TRANSITION_CLASSES | ⭘ | Transition enter/leave classes changed | [link](https://v3-migration.vuejs.org/breaking-changes/transition.html) |
### Fully Compatible
| ID | Type | Description | Docs |
| ---------------------------- | ---- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| GLOBAL_MOUNT | ✔ | new Vue() -> createApp | [link](https://v3-migration.vuejs.org/breaking-changes/global-api.html#mounting-app-instance) |
| GLOBAL_EXTEND | ✔ | Vue.extend removed (use `defineComponent` or `extends` option) | [link](https://v3-migration.vuejs.org/breaking-changes/global-api.html#vue-extend-replaced-by-definecomponent) |
| GLOBAL_PROTOTYPE | ✔ | `Vue.prototype` -> `app.config.globalProperties` | [link](https://v3-migration.vuejs.org/breaking-changes/global-api.html#vue-prototype-replaced-by-config-globalproperties) |
| GLOBAL_SET | ✔ | `Vue.set` removed (no longer needed) | |
| GLOBAL_DELETE | ✔ | `Vue.delete` removed (no longer needed) | |
| GLOBAL_OBSERVABLE | ✔ | `Vue.observable` removed (use `reactive`) | [link](https://vuejs.org/api/reactivity-core.html#reactive) |
| CONFIG_KEY_CODES | ✔ | config.keyCodes removed | [link](https://v3-migration.vuejs.org/breaking-changes/keycode-modifiers.html) |
| CONFIG_WHITESPACE | ✔ | In Vue 3 whitespace defaults to `"condense"` | |
| INSTANCE_SET | ✔ | `vm.$set` removed (no longer needed) | |
| INSTANCE_DELETE | ✔ | `vm.$delete` removed (no longer needed) | |
| INSTANCE_EVENT_EMITTER | ✔ | `vm.$on`, `vm.$off`, `vm.$once` removed | [link](https://v3-migration.vuejs.org/breaking-changes/events-api.html) |
| INSTANCE_EVENT_HOOKS | ✔ | Instance no longer emits `hook:x` events | [link](https://v3-migration.vuejs.org/breaking-changes/vnode-lifecycle-events.html) |
| INSTANCE_CHILDREN | ✔ | `vm.$children` removed | [link](https://v3-migration.vuejs.org/breaking-changes/children.html) |
| INSTANCE_LISTENERS | ✔ | `vm.$listeners` removed | [link](https://v3-migration.vuejs.org/breaking-changes/listeners-removed.html) |
| INSTANCE_SCOPED_SLOTS | ✔ | `vm.$scopedSlots` removed; `vm.$slots` now exposes functions | [link](https://v3-migration.vuejs.org/breaking-changes/slots-unification.html) |
| INSTANCE_ATTRS_CLASS_STYLE | ✔ | `$attrs` now includes `class` and `style` | [link](https://v3-migration.vuejs.org/breaking-changes/attrs-includes-class-style.html) |
| OPTIONS_DATA_FN | ✔ | `data` must be a function in all cases | [link](https://v3-migration.vuejs.org/breaking-changes/data-option.html) |
| OPTIONS_DATA_MERGE | ✔ | `data` from mixin or extension is now shallow merged | [link](https://v3-migration.vuejs.org/breaking-changes/data-option.html) |
| OPTIONS_BEFORE_DESTROY | ✔ | `beforeDestroy` -> `beforeUnmount` | |
| OPTIONS_DESTROYED | ✔ | `destroyed` -> `unmounted` | |
| WATCH_ARRAY | ✔ | watching an array no longer triggers on mutation unless deep | [link](https://v3-migration.vuejs.org/breaking-changes/watch.html) |
| V_ON_KEYCODE_MODIFIER | ✔ | `v-on` no longer supports keyCode modifiers | [link](https://v3-migration.vuejs.org/breaking-changes/keycode-modifiers.html) |
| CUSTOM_DIR | ✔ | Custom directive hook names changed | [link](https://v3-migration.vuejs.org/breaking-changes/custom-directives.html) |
| ATTR_FALSE_VALUE | ✔ | No longer removes attribute if binding value is boolean `false` | [link](https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html) |
| ATTR_ENUMERATED_COERCION | ✔ | No longer special case enumerated attributes | [link](https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html) |
| TRANSITION_GROUP_ROOT | ✔ | `<transition-group>` no longer renders a root element by default | [link](https://v3-migration.vuejs.org/breaking-changes/transition-group.html) |
| COMPONENT_ASYNC | ✔ | Async component API changed (now requires `defineAsyncComponent`) | [link](https://v3-migration.vuejs.org/breaking-changes/async-components.html) |
| COMPONENT_FUNCTIONAL | ✔ | Functional component API changed (now must be plain functions) | [link](https://v3-migration.vuejs.org/breaking-changes/functional-components.html) |
| COMPONENT_V_MODEL | ✔ | Component v-model reworked | [link](https://v3-migration.vuejs.org/breaking-changes/v-model.html) |
| RENDER_FUNCTION | ✔ | Render function API changed | [link](https://v3-migration.vuejs.org/breaking-changes/render-function-api.html) |
| FILTERS | ✔ | Filters removed (this option affects only runtime filter APIs) | [link](https://v3-migration.vuejs.org/breaking-changes/filters.html) |
| COMPILER_IS_ON_ELEMENT | ✔ | `is` usage is now restricted to `<component>` only | [link](https://v3-migration.vuejs.org/breaking-changes/custom-elements-interop.html) |
| COMPILER_V_BIND_SYNC | ✔ | `v-bind.sync` replaced by `v-model` with arguments | [link](https://v3-migration.vuejs.org/breaking-changes/v-model.html) |
| COMPILER_V_BIND_PROP | ✔ | `v-bind.prop` modifier removed | |
| COMPILER_V_BIND_OBJECT_ORDER | ✔ | `v-bind="object"` is now order sensitive | [link](https://v3-migration.vuejs.org/breaking-changes/v-bind.html) |
| COMPILER_V_ON_NATIVE | ✔ | `v-on.native` modifier removed | [link](https://v3-migration.vuejs.org/breaking-changes/v-on-native-modifier-removed.html) |
| COMPILER_V_FOR_REF | ✔ | `ref` in `v-for` (compiler support) | |
| COMPILER_NATIVE_TEMPLATE | ✔ | `<template>` with no special directives now renders as native element | |
| COMPILER_FILTERS | ✔ | filters (compiler support) | |
| ID | Type | Description | Docs |
| ---------------------------- | ---- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| GLOBAL_MOUNT | ✔ | new Vue() -> createApp | [link](https://v3-migration.vuejs.org/breaking-changes/global-api.html#mounting-app-instance) |
| GLOBAL_EXTEND | ✔ | Vue.extend removed (use `defineComponent` or `extends` option) | [link](https://v3-migration.vuejs.org/breaking-changes/global-api.html#vue-extend-replaced-by-definecomponent) |
| GLOBAL_PROTOTYPE | ✔ | `Vue.prototype` -> `app.config.globalProperties` | [link](https://v3-migration.vuejs.org/breaking-changes/global-api.html#vue-prototype-replaced-by-config-globalproperties) |
| GLOBAL_SET | ✔ | `Vue.set` removed (no longer needed) | |
| GLOBAL_DELETE | ✔ | `Vue.delete` removed (no longer needed) | |
| GLOBAL_OBSERVABLE | ✔ | `Vue.observable` removed (use `reactive`) | [link](https://vuejs.org/api/reactivity-core.html#reactive) |
| CONFIG_KEY_CODES | ✔ | config.keyCodes removed | [link](https://v3-migration.vuejs.org/breaking-changes/keycode-modifiers.html) |
| CONFIG_WHITESPACE | ✔ | In Vue 3 whitespace defaults to `"condense"` | |
| INSTANCE_SET | ✔ | `vm.$set` removed (no longer needed) | |
| INSTANCE_DELETE | ✔ | `vm.$delete` removed (no longer needed) | |
| INSTANCE_EVENT_EMITTER | ✔ | `vm.$on`, `vm.$off`, `vm.$once` removed | [link](https://v3-migration.vuejs.org/breaking-changes/events-api.html) |
| INSTANCE_EVENT_HOOKS | ✔ | Instance no longer emits `hook:x` events | [link](https://v3-migration.vuejs.org/breaking-changes/vnode-lifecycle-events.html) |
| INSTANCE_CHILDREN | ✔ | `vm.$children` removed | [link](https://v3-migration.vuejs.org/breaking-changes/children.html) |
| INSTANCE_LISTENERS | ✔ | `vm.$listeners` removed | [link](https://v3-migration.vuejs.org/breaking-changes/listeners-removed.html) |
| INSTANCE_SCOPED_SLOTS | ✔ | `vm.$scopedSlots` removed; `vm.$slots` now exposes functions | [link](https://v3-migration.vuejs.org/breaking-changes/slots-unification.html) |
| INSTANCE_ATTRS_CLASS_STYLE | ✔ | `$attrs` now includes `class` and `style` | [link](https://v3-migration.vuejs.org/breaking-changes/attrs-includes-class-style.html) |
| OPTIONS_DATA_FN | ✔ | `data` must be a function in all cases | [link](https://v3-migration.vuejs.org/breaking-changes/data-option.html) |
| OPTIONS_DATA_MERGE | ✔ | `data` from mixin or extension is now shallow merged | [link](https://v3-migration.vuejs.org/breaking-changes/data-option.html) |
| OPTIONS_BEFORE_DESTROY | ✔ | `beforeDestroy` -> `beforeUnmount` | |
| OPTIONS_DESTROYED | ✔ | `destroyed` -> `unmounted` | |
| WATCH_ARRAY | ✔ | watching an array no longer triggers on mutation unless deep | [link](https://v3-migration.vuejs.org/breaking-changes/watch.html) |
| V_ON_KEYCODE_MODIFIER | ✔ | `v-on` no longer supports keyCode modifiers | [link](https://v3-migration.vuejs.org/breaking-changes/keycode-modifiers.html) |
| CUSTOM_DIR | ✔ | Custom directive hook names changed | [link](https://v3-migration.vuejs.org/breaking-changes/custom-directives.html) |
| ATTR_FALSE_VALUE | ✔ | No longer removes attribute if binding value is boolean `false` | [link](https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html) |
| ATTR_ENUMERATED_COERCION | ✔ | No longer special case enumerated attributes | [link](https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html) |
| TRANSITION_GROUP_ROOT | ✔ | `<transition-group>` no longer renders a root element by default | [link](https://v3-migration.vuejs.org/breaking-changes/transition-group.html) |
| COMPONENT_ASYNC | ✔ | Async component API changed (now requires `defineAsyncComponent`) | [link](https://v3-migration.vuejs.org/breaking-changes/async-components.html) |
| COMPONENT_FUNCTIONAL | ✔ | Functional component API changed (now must be plain functions) | [link](https://v3-migration.vuejs.org/breaking-changes/functional-components.html) |
| COMPONENT_V_MODEL | ✔ | Component v-model reworked | [link](https://v3-migration.vuejs.org/breaking-changes/v-model.html) |
| RENDER_FUNCTION | ✔ | Render function API changed | [link](https://v3-migration.vuejs.org/breaking-changes/render-function-api.html) |
| FILTERS | ✔ | Filters removed (this option affects only runtime filter APIs) | [link](https://v3-migration.vuejs.org/breaking-changes/filters.html) |
| COMPILER_IS_ON_ELEMENT | ✔ | `is` usage is now restricted to `<component>` only | [link](https://v3-migration.vuejs.org/breaking-changes/custom-elements-interop.html) |
| COMPILER_V_BIND_SYNC | ✔ | `v-bind.sync` replaced by `v-model` with arguments | [link](https://v3-migration.vuejs.org/breaking-changes/v-model.html) |
| COMPILER_V_BIND_PROP | ✔ | `v-bind.prop` modifier removed | |
| COMPILER_V_BIND_OBJECT_ORDER | ✔ | `v-bind="object"` is now order sensitive | [link](https://v3-migration.vuejs.org/breaking-changes/v-bind.html) |
| COMPILER_V_ON_NATIVE | ✔ | `v-on.native` modifier removed | [link](https://v3-migration.vuejs.org/breaking-changes/v-on-native-modifier-removed.html) |
| COMPILER_V_FOR_REF | ✔ | `ref` in `v-for` (compiler support) | |
| COMPILER_NATIVE_TEMPLATE | ✔ | `<template>` with no special directives now renders as native element | |
| COMPILER_FILTERS | ✔ | filters (compiler support) | |

View File

@ -5,8 +5,9 @@
### From CDN or without a Bundler
- **`vue(.runtime).global(.prod).js`**:
- For direct use via `<script src="...">` in the browser. Exposes the `Vue` global.
- Note that global builds are not [UMD](https://github.com/umdjs/umd) builds. They are built as [IIFEs](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) and is only meant for direct use via `<script src="...">`.
- Note that global builds are not [UMD](https://github.com/umdjs/umd) builds. They are built as [IIFEs](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) and is only meant for direct use via `<script src="...">`.
- In-browser template compilation:
- **`vue.global.js`** is the "full" build that includes both the compiler and the runtime so it supports compiling templates on the fly.
- **`vue.runtime.global.js`** contains only the runtime and requires templates to be pre-compiled during a build step.
@ -38,10 +39,12 @@
`esm-bundler` builds of Vue expose global feature flags that can be overwritten at compile time:
- `__VUE_OPTIONS_API__`
- Default: `true`
- Enable / disable Options API support
- `__VUE_PROD_DEVTOOLS__`
- Default: `false`
- Enable / disable devtools support in production

View File

@ -10,9 +10,14 @@
-webkit-transition: -webkit-transform 50ms ease;
transition: transform 50ms ease;
}
.v-appear, .v-enter, .v-leave-active,
.test-appear, .test-enter, .test-leave-active,
.hello, .bye.active,
.v-appear,
.v-enter,
.v-leave-active,
.test-appear,
.test-enter,
.test-leave-active,
.hello,
.bye.active,
.changed-enter {
opacity: 0;
}
@ -33,19 +38,35 @@
-webkit-animation: test-leave 100ms;
}
@keyframes test-enter {
from { opacity: 0 }
to { opacity: 1 }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@-webkit-keyframes test-enter {
from { opacity: 0 }
to { opacity: 1 }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes test-leave {
from { opacity: 1 }
to { opacity: 0 }
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@-webkit-keyframes test-leave {
from { opacity: 1 }
to { opacity: 0 }
from {
opacity: 1;
}
to {
opacity: 0;
}
}
</style>

View File

@ -3,59 +3,68 @@
<div id="demo">
<h1>Latest Vue.js Commits</h1>
<template v-for="branch in branches">
<input type="radio"
<input
type="radio"
:id="branch"
:value="branch"
name="branch"
v-model="currentBranch">
v-model="currentBranch"
/>
<label :for="branch">{{ branch }}</label>
</template>
<p>vuejs/core@{{ currentBranch }}</p>
<ul>
<li v-for="{ html_url, sha, author, commit } in commits">
<a :href="html_url" target="_blank" class="commit">{{ sha.slice(0, 7) }}</a>
- <span class="message">{{ truncate(commit.message) }}</span><br>
by <span class="author"><a :href="author.html_url" target="_blank">{{ commit.author.name }}</a></span>
<a :href="html_url" target="_blank" class="commit"
>{{ sha.slice(0, 7) }}</a
>
- <span class="message">{{ truncate(commit.message) }}</span><br />
by
<span class="author"
><a :href="author.html_url" target="_blank"
>{{ commit.author.name }}</a
></span
>
at <span class="date">{{ formatDate(commit.author.date) }}</span>
</li>
</ul>
</div>
<script>
const API_URL = `https://api.github.com/repos/vuejs/core/commits?per_page=3&sha=`
const API_URL = `https://api.github.com/repos/vuejs/core/commits?per_page=3&sha=`
Vue.createApp({
data: () => ({
branches: ['main', 'v2-compat'],
currentBranch: 'main',
commits: null
}),
Vue.createApp({
data: () => ({
branches: ['main', 'v2-compat'],
currentBranch: 'main',
commits: null,
}),
created() {
this.fetchData()
},
watch: {
currentBranch: 'fetchData'
},
methods: {
fetchData() {
fetch(`${API_URL}${this.currentBranch}`)
.then(res => res.json())
.then(data => {
this.commits = data
})
created() {
this.fetchData()
},
truncate(v) {
const newline = v.indexOf('\n')
return newline > 0 ? v.slice(0, newline) : v
watch: {
currentBranch: 'fetchData',
},
formatDate(v) {
return v.replace(/T|Z/g, ' ')
}
}
}).mount('#demo')
methods: {
fetchData() {
fetch(`${API_URL}${this.currentBranch}`)
.then(res => res.json())
.then(data => {
this.commits = data
})
},
truncate(v) {
const newline = v.indexOf('\n')
return newline > 0 ? v.slice(0, newline) : v
},
formatDate(v) {
return v.replace(/T|Z/g, ' ')
},
},
}).mount('#demo')
</script>
<style>
@ -70,7 +79,8 @@ Vue.createApp({
line-height: 1.5em;
margin-bottom: 20px;
}
.author, .date {
.author,
.date {
font-weight: bold;
}
</style>

View File

@ -26,142 +26,141 @@
</script>
<!-- DemoGrid component script -->
<script>
const DemoGrid = {
template: '#grid-template',
props: {
data: Array,
columns: Array,
filterKey: String
},
data() {
return {
sortKey: '',
sortOrders: this.columns.reduce((o, key) => (o[key] = 1, o), {})
}
},
computed: {
filteredData() {
const sortKey = this.sortKey
const filterKey = this.filterKey && this.filterKey.toLowerCase()
const order = this.sortOrders[sortKey] || 1
let data = this.data
if (filterKey) {
data = data.filter(row => {
return Object.keys(row).some(key => {
return String(row[key]).toLowerCase().indexOf(filterKey) > -1
})
})
}
if (sortKey) {
data = data.slice().sort((a, b) => {
a = a[sortKey]
b = b[sortKey]
return (a === b ? 0 : a > b ? 1 : -1) * order
})
}
return data
}
},
methods: {
sortBy(key) {
this.sortKey = key
this.sortOrders[key] = this.sortOrders[key] * -1
const DemoGrid = {
template: '#grid-template',
props: {
data: Array,
columns: Array,
filterKey: String,
},
data() {
return {
sortKey: '',
sortOrders: this.columns.reduce((o, key) => ((o[key] = 1), o), {}),
}
},
computed: {
filteredData() {
const sortKey = this.sortKey
const filterKey = this.filterKey && this.filterKey.toLowerCase()
const order = this.sortOrders[sortKey] || 1
let data = this.data
if (filterKey) {
data = data.filter(row => {
return Object.keys(row).some(key => {
return String(row[key]).toLowerCase().indexOf(filterKey) > -1
})
})
}
if (sortKey) {
data = data.slice().sort((a, b) => {
a = a[sortKey]
b = b[sortKey]
return (a === b ? 0 : a > b ? 1 : -1) * order
})
}
return data
},
},
methods: {
sortBy(key) {
this.sortKey = key
this.sortOrders[key] = this.sortOrders[key] * -1
},
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
},
},
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
}
</script>
<!-- App template (in DOM) -->
<div id="demo">
<form id="search">
Search <input name="query" v-model="searchQuery">
</form>
<demo-grid
:data="gridData"
:columns="gridColumns"
:filter-key="searchQuery">
<form id="search">Search <input name="query" v-model="searchQuery" /></form>
<demo-grid :data="gridData" :columns="gridColumns" :filter-key="searchQuery">
</demo-grid>
</div>
<!-- App script -->
<script>
Vue.createApp({
components: {
DemoGrid
},
data: () => ({
searchQuery: '',
gridColumns: ['name', 'power'],
gridData: [
{ name: 'Chuck Norris', power: Infinity },
{ name: 'Bruce Lee', power: 9000 },
{ name: 'Jackie Chan', power: 7000 },
{ name: 'Jet Li', power: 8000 }
]
})
}).mount('#demo')
Vue.createApp({
components: {
DemoGrid,
},
data: () => ({
searchQuery: '',
gridColumns: ['name', 'power'],
gridData: [
{ name: 'Chuck Norris', power: Infinity },
{ name: 'Bruce Lee', power: 9000 },
{ name: 'Jackie Chan', power: 7000 },
{ name: 'Jet Li', power: 8000 },
],
}),
}).mount('#demo')
</script>
<style>
body {
font-family: Helvetica Neue, Arial, sans-serif;
font-size: 14px;
color: #444;
}
body {
font-family:
Helvetica Neue,
Arial,
sans-serif;
font-size: 14px;
color: #444;
}
table {
border: 2px solid #42b983;
border-radius: 3px;
background-color: #fff;
}
table {
border: 2px solid #42b983;
border-radius: 3px;
background-color: #fff;
}
th {
background-color: #42b983;
color: rgba(255,255,255,0.66);
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
th {
background-color: #42b983;
color: rgba(255, 255, 255, 0.66);
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
td {
background-color: #f9f9f9;
}
td {
background-color: #f9f9f9;
}
th, td {
min-width: 120px;
padding: 10px 20px;
}
th,
td {
min-width: 120px;
padding: 10px 20px;
}
th.active {
color: #fff;
}
th.active {
color: #fff;
}
th.active .arrow {
opacity: 1;
}
th.active .arrow {
opacity: 1;
}
.arrow {
display: inline-block;
vertical-align: middle;
width: 0;
height: 0;
margin-left: 5px;
opacity: 0.66;
}
.arrow {
display: inline-block;
vertical-align: middle;
width: 0;
height: 0;
margin-left: 5px;
opacity: 0.66;
}
.arrow.asc {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid #fff;
}
.arrow.asc {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid #fff;
}
.arrow.dsc {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #fff;
}
.arrow.dsc {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #fff;
}
</style>

View File

@ -8,55 +8,58 @@
</div>
<script>
Vue.createApp({
data: () => ({
input: '# hello'
}),
computed: {
compiledMarkdown() {
return marked.marked(this.input, { sanitize: true })
}
},
methods: {
update: _.debounce(function (e) {
this.input = e.target.value
}, 50)
}
}).mount('#editor')
Vue.createApp({
data: () => ({
input: '# hello',
}),
computed: {
compiledMarkdown() {
return marked.marked(this.input, { sanitize: true })
},
},
methods: {
update: _.debounce(function (e) {
this.input = e.target.value
}, 50),
},
}).mount('#editor')
</script>
<style>
html, body, #editor {
margin: 0;
height: 100%;
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #333;
}
html,
body,
#editor {
margin: 0;
height: 100%;
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #333;
}
textarea, #editor div {
display: inline-block;
overflow: auto;
width: 50%;
height: 100%;
vertical-align: top;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 0 20px;
}
textarea,
#editor div {
display: inline-block;
overflow: auto;
width: 50%;
height: 100%;
vertical-align: top;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 0 20px;
}
textarea {
border: none;
border-right: 1px solid #ccc;
resize: none;
outline: none;
background-color: #f6f6f6;
font-size: 14px;
font-family: 'Monaco', courier, monospace;
padding: 20px;
}
textarea {
border: none;
border-right: 1px solid #ccc;
resize: none;
outline: none;
background-color: #f6f6f6;
font-size: 14px;
font-family: 'Monaco', courier, monospace;
padding: 20px;
}
code {
color: #f66;
}
code {
color: #f66;
}
</style>

View File

@ -1,37 +1,33 @@
<script src="../../dist/vue.global.js"></script>
<script>
// math helper...
function valueToPoint (value, index, total) {
var x = 0
var y = -value * 0.8
var angle = Math.PI * 2 / total * index
var cos = Math.cos(angle)
var sin = Math.sin(angle)
var tx = x * cos - y * sin + 100
var ty = x * sin + y * cos + 100
return {
x: tx,
y: ty
}
}
const AxisLabel = {
template: '<text :x="point.x" :y="point.y">{{stat.label}}</text>',
props: {
stat: Object,
index: Number,
total: Number
},
computed: {
point: function () {
return valueToPoint(
+this.stat.value + 10,
this.index,
this.total
)
// math helper...
function valueToPoint(value, index, total) {
var x = 0
var y = -value * 0.8
var angle = ((Math.PI * 2) / total) * index
var cos = Math.cos(angle)
var sin = Math.sin(angle)
var tx = x * cos - y * sin + 100
var ty = x * sin + y * cos + 100
return {
x: tx,
y: ty,
}
}
}
const AxisLabel = {
template: '<text :x="point.x" :y="point.y">{{stat.label}}</text>',
props: {
stat: Object,
index: Number,
total: Number,
},
computed: {
point: function () {
return valueToPoint(+this.stat.value + 10, this.index, this.total)
},
},
}
</script>
<!-- template for the polygraph component. -->
@ -49,23 +45,25 @@ const AxisLabel = {
</script>
<script>
const Polygraph = {
props: ['stats'],
template: '#polygraph-template',
computed: {
// a computed property for the polygon's points
points() {
const total = this.stats.length
return this.stats.map((stat, i) => {
const point = valueToPoint(stat.value, i, total)
return point.x + ',' + point.y
}).join(' ')
}
},
components: {
AxisLabel
const Polygraph = {
props: ['stats'],
template: '#polygraph-template',
computed: {
// a computed property for the polygon's points
points() {
const total = this.stats.length
return this.stats
.map((stat, i) => {
const point = valueToPoint(stat.value, i, total)
return point.x + ',' + point.y
})
.join(' ')
},
},
components: {
AxisLabel,
},
}
}
</script>
<!-- demo root element -->
@ -77,86 +75,92 @@ const Polygraph = {
<!-- controls -->
<div v-for="stat in stats">
<label>{{stat.label}}</label>
<input type="range" v-model="stat.value" min="0" max="100">
<input type="range" v-model="stat.value" min="0" max="100" />
<span>{{stat.value}}</span>
<button @click="remove(stat)" class="remove">X</button>
</div>
<form id="add">
<input name="newlabel" v-model="newLabel">
<input name="newlabel" v-model="newLabel" />
<button @click="add">Add a Stat</button>
</form>
<pre id="raw">{{ stats }}</pre>
</div>
<script>
const globalStats = [
{ label: 'A', value: 100 },
{ label: 'B', value: 100 },
{ label: 'C', value: 100 },
{ label: 'D', value: 100 },
{ label: 'E', value: 100 },
{ label: 'F', value: 100 }
]
const globalStats = [
{ label: 'A', value: 100 },
{ label: 'B', value: 100 },
{ label: 'C', value: 100 },
{ label: 'D', value: 100 },
{ label: 'E', value: 100 },
{ label: 'F', value: 100 },
]
Vue.createApp({
components: {
Polygraph
},
data: () => ({
newLabel: '',
stats: globalStats
}),
methods: {
add(e) {
e.preventDefault()
if (!this.newLabel) return
this.stats.push({
label: this.newLabel,
value: 100
})
this.newLabel = ''
Vue.createApp({
components: {
Polygraph,
},
remove(stat) {
if (this.stats.length > 3) {
this.stats.splice(this.stats.indexOf(stat), 1)
} else {
alert('Can\'t delete more!')
}
}
}
}).mount('#demo')
data: () => ({
newLabel: '',
stats: globalStats,
}),
methods: {
add(e) {
e.preventDefault()
if (!this.newLabel) return
this.stats.push({
label: this.newLabel,
value: 100,
})
this.newLabel = ''
},
remove(stat) {
if (this.stats.length > 3) {
this.stats.splice(this.stats.indexOf(stat), 1)
} else {
alert("Can't delete more!")
}
},
},
}).mount('#demo')
</script>
<style>
body {
font-family: Helvetica Neue, Arial, sans-serif;
}
body {
font-family:
Helvetica Neue,
Arial,
sans-serif;
}
polygon {
polygon {
fill: #42b983;
opacity: .75;
}
opacity: 0.75;
}
circle {
circle {
fill: transparent;
stroke: #999;
}
}
text {
font-family: Helvetica Neue, Arial, sans-serif;
text {
font-family:
Helvetica Neue,
Arial,
sans-serif;
font-size: 10px;
fill: #666;
}
}
label {
label {
display: inline-block;
margin-left: 10px;
width: 20px;
}
}
#raw {
#raw {
position: absolute;
top: 0;
left: 300px;
}
}
</style>

View File

@ -1,49 +1,81 @@
<script src="../../dist/vue.global.js"></script>
<link rel="stylesheet" href="../../../../node_modules/todomvc-app-css/index.css">
<link
rel="stylesheet"
href="../../../../node_modules/todomvc-app-css/index.css"
/>
<div id="app">
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo"
autofocus autocomplete="off"
placeholder="What needs to be done?"
v-model="newTodo"
@keyup.enter="addTodo">
<input
class="new-todo"
autofocus
autocomplete="off"
placeholder="What needs to be done?"
v-model="newTodo"
@keyup.enter="addTodo"
/>
</header>
<section class="main" v-show="todos.length">
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="allDone">
<input
id="toggle-all"
class="toggle-all"
type="checkbox"
v-model="allDone"
/>
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li v-for="todo in filteredTodos"
class="todo"
:key="todo.id"
:class="{ completed: todo.completed, editing: todo === editedTodo }">
<li
v-for="todo in filteredTodos"
class="todo"
:key="todo.id"
:class="{ completed: todo.completed, editing: todo === editedTodo }"
>
<div class="view">
<input class="toggle" type="checkbox" v-model="todo.completed">
<input class="toggle" type="checkbox" v-model="todo.completed" />
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
<button class="destroy" @click="removeTodo(todo)"></button>
</div>
<input class="edit" type="text"
v-model="todo.title"
v-todo-focus="todo === editedTodo"
@blur="doneEdit(todo)"
@keyup.enter="doneEdit(todo)"
@keyup.escape="cancelEdit(todo)"
>
<input
class="edit"
type="text"
v-model="todo.title"
v-todo-focus="todo === editedTodo"
@blur="doneEdit(todo)"
@keyup.enter="doneEdit(todo)"
@keyup.escape="cancelEdit(todo)"
/>
</li>
</ul>
</section>
<footer class="footer" v-show="todos.length">
<span class="todo-count">
<strong>{{ remaining }}</strong> <span>{{ pluralize(remaining) }} left</span>
</span>
<span class="todo-count">
<strong>{{ remaining }}</strong>
<span>{{ pluralize(remaining) }} left</span>
</span>
<ul class="filters">
<li><a href="#/all" :class="{ selected: visibility === 'all' }">All</a></li>
<li><a href="#/active" :class="{ selected: visibility === 'active' }">Active</a></li>
<li><a href="#/completed" :class="{ selected: visibility === 'completed' }">Completed</a></li>
<li>
<a href="#/all" :class="{ selected: visibility === 'all' }">All</a>
</li>
<li>
<a href="#/active" :class="{ selected: visibility === 'active' }"
>Active</a
>
</li>
<li>
<a
href="#/completed"
:class="{ selected: visibility === 'completed' }"
>Completed</a
>
</li>
</ul>
<button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">
<button
class="clear-completed"
@click="removeCompleted"
v-show="todos.length > remaining"
>
Clear completed
</button>
</footer>
@ -51,146 +83,146 @@
</div>
<script>
const STORAGE_KEY = 'todos-vuejs-3.x'
const todoStorage = {
fetch() {
const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
todos.forEach((todo, index) => {
todo.id = index
})
todoStorage.uid = todos.length
return todos
},
save(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
}
}
const filters = {
all(todos) {
return todos
},
active(todos) {
return todos.filter((todo) => {
return !todo.completed
})
},
completed(todos) {
return todos.filter(function (todo) {
return todo.completed
})
}
}
Vue.createApp({
// app initial state
data: () => ({
todos: todoStorage.fetch(),
newTodo: '',
editedTodo: null,
visibility: 'all'
}),
// watch todos change for localStorage persistence
watch: {
todos: {
handler(todos) {
todoStorage.save(todos)
},
deep: true
}
},
mounted() {
window.addEventListener('hashchange', this.onHashChange)
this.onHashChange()
},
computed: {
filteredTodos() {
return filters[this.visibility](this.todos)
},
remaining() {
return filters.active(this.todos).length
},
allDone: {
get() {
return this.remaining === 0
},
set(value) {
this.todos.forEach(function (todo) {
todo.completed = value
})
}
}
},
// methods that implement data logic.
// note there's no DOM manipulation here at all.
methods: {
addTodo() {
var value = this.newTodo && this.newTodo.trim()
if (!value) {
return
}
this.todos.push({
id: todoStorage.uid++,
title: value,
completed: false
const STORAGE_KEY = 'todos-vuejs-3.x'
const todoStorage = {
fetch() {
const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
todos.forEach((todo, index) => {
todo.id = index
})
this.newTodo = ''
todoStorage.uid = todos.length
return todos
},
removeTodo(todo) {
this.todos.splice(this.todos.indexOf(todo), 1)
save(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
},
editTodo(todo) {
this.beforeEditCache = todo.title
this.editedTodo = todo
},
doneEdit(todo) {
if (!this.editedTodo) {
return
}
this.editedTodo = null
todo.title = todo.title.trim()
if (!todo.title) {
this.removeTodo(todo)
}
},
cancelEdit(todo) {
this.editedTodo = null
todo.title = this.beforeEditCache
},
removeCompleted() {
this.todos = filters.active(this.todos)
},
onHashChange() {
var visibility = window.location.hash.replace(/#\/?/, '')
if (filters[visibility]) {
this.visibility = visibility
} else {
window.location.hash = ''
this.visibility = 'all'
}
},
pluralize (n) {
return n === 1 ? 'item' : 'items'
}
},
directives: {
'todo-focus'(el, binding) {
if (binding.value) {
el.focus()
}
}
}
}).mount('#app')
const filters = {
all(todos) {
return todos
},
active(todos) {
return todos.filter(todo => {
return !todo.completed
})
},
completed(todos) {
return todos.filter(function (todo) {
return todo.completed
})
},
}
Vue.createApp({
// app initial state
data: () => ({
todos: todoStorage.fetch(),
newTodo: '',
editedTodo: null,
visibility: 'all',
}),
// watch todos change for localStorage persistence
watch: {
todos: {
handler(todos) {
todoStorage.save(todos)
},
deep: true,
},
},
mounted() {
window.addEventListener('hashchange', this.onHashChange)
this.onHashChange()
},
computed: {
filteredTodos() {
return filters[this.visibility](this.todos)
},
remaining() {
return filters.active(this.todos).length
},
allDone: {
get() {
return this.remaining === 0
},
set(value) {
this.todos.forEach(function (todo) {
todo.completed = value
})
},
},
},
// methods that implement data logic.
// note there's no DOM manipulation here at all.
methods: {
addTodo() {
var value = this.newTodo && this.newTodo.trim()
if (!value) {
return
}
this.todos.push({
id: todoStorage.uid++,
title: value,
completed: false,
})
this.newTodo = ''
},
removeTodo(todo) {
this.todos.splice(this.todos.indexOf(todo), 1)
},
editTodo(todo) {
this.beforeEditCache = todo.title
this.editedTodo = todo
},
doneEdit(todo) {
if (!this.editedTodo) {
return
}
this.editedTodo = null
todo.title = todo.title.trim()
if (!todo.title) {
this.removeTodo(todo)
}
},
cancelEdit(todo) {
this.editedTodo = null
todo.title = this.beforeEditCache
},
removeCompleted() {
this.todos = filters.active(this.todos)
},
onHashChange() {
var visibility = window.location.hash.replace(/#\/?/, '')
if (filters[visibility]) {
this.visibility = visibility
} else {
window.location.hash = ''
this.visibility = 'all'
}
},
pluralize(n) {
return n === 1 ? 'item' : 'items'
},
},
directives: {
'todo-focus'(el, binding) {
if (binding.value) {
el.focus()
}
},
},
}).mount('#app')
</script>

View File

@ -22,43 +22,42 @@
</script>
<!-- item script -->
<script>
const TreeItem = {
name: 'TreeItem', // necessary for self-reference
template: '#item-template',
props: {
model: Object
},
data() {
return {
open: false
}
},
computed: {
isFolder() {
return this.model.children &&
this.model.children.length
}
},
methods: {
toggle() {
if (this.isFolder) {
this.open = !this.open
const TreeItem = {
name: 'TreeItem', // necessary for self-reference
template: '#item-template',
props: {
model: Object,
},
data() {
return {
open: false,
}
},
changeType() {
if (!this.isFolder) {
this.model.children = []
this.addChild()
this.open = true
}
computed: {
isFolder() {
return this.model.children && this.model.children.length
},
},
methods: {
toggle() {
if (this.isFolder) {
this.open = !this.open
}
},
changeType() {
if (!this.isFolder) {
this.model.children = []
this.addChild()
this.open = true
}
},
addChild() {
this.model.children.push({
name: 'new stuff',
})
},
},
addChild() {
this.model.children.push({
name: 'new stuff'
})
}
}
}
</script>
<p>(You can double click on an item to turn it into a folder.)</p>
@ -69,43 +68,37 @@ const TreeItem = {
</ul>
<script>
const treeData = {
name: 'My Tree',
children: [
{ name: 'hello' },
{ name: 'wat' },
{
name: 'child folder',
children: [
{
name: 'child folder',
children: [
{ name: 'hello' },
{ name: 'wat' }
]
},
{ name: 'hello' },
{ name: 'wat' },
{
name: 'child folder',
children: [
{ name: 'hello' },
{ name: 'wat' }
]
}
]
}
]
}
const treeData = {
name: 'My Tree',
children: [
{ name: 'hello' },
{ name: 'wat' },
{
name: 'child folder',
children: [
{
name: 'child folder',
children: [{ name: 'hello' }, { name: 'wat' }],
},
{ name: 'hello' },
{ name: 'wat' },
{
name: 'child folder',
children: [{ name: 'hello' }, { name: 'wat' }],
},
],
},
],
}
Vue.createApp({
components: {
TreeItem
},
data: () => ({
treeData
})
}).mount('#demo')
Vue.createApp({
components: {
TreeItem,
},
data: () => ({
treeData,
}),
}).mount('#demo')
</script>
<style>

View File

@ -3,58 +3,67 @@
<div id="demo">
<h1>Latest Vue.js Commits</h1>
<template v-for="branch in branches">
<input type="radio"
<input
type="radio"
:id="branch"
:value="branch"
name="branch"
v-model="currentBranch">
v-model="currentBranch"
/>
<label :for="branch">{{ branch }}</label>
</template>
<p>vuejs/core@{{ currentBranch }}</p>
<ul>
<li v-for="{ html_url, sha, author, commit } in commits">
<a :href="html_url" target="_blank" class="commit">{{ sha.slice(0, 7) }}</a>
- <span class="message">{{ truncate(commit.message) }}</span><br>
by <span class="author"><a :href="author.html_url" target="_blank">{{ commit.author.name }}</a></span>
<a :href="html_url" target="_blank" class="commit"
>{{ sha.slice(0, 7) }}</a
>
- <span class="message">{{ truncate(commit.message) }}</span><br />
by
<span class="author"
><a :href="author.html_url" target="_blank"
>{{ commit.author.name }}</a
></span
>
at <span class="date">{{ formatDate(commit.author.date) }}</span>
</li>
</ul>
</div>
<script>
const { createApp, ref, watchEffect } = Vue
const API_URL = `https://api.github.com/repos/vuejs/core/commits?per_page=3&sha=`
const { createApp, ref, watchEffect } = Vue
const API_URL = `https://api.github.com/repos/vuejs/core/commits?per_page=3&sha=`
const truncate = v => {
const newline = v.indexOf('\n')
return newline > 0 ? v.slice(0, newline) : v
}
const formatDate = v => v.replace(/T|Z/g, ' ')
createApp({
setup() {
const currentBranch = ref('main')
const commits = ref(null)
watchEffect(() => {
fetch(`${API_URL}${currentBranch.value}`)
.then(res => res.json())
.then(data => {
console.log(data)
commits.value = data
})
})
return {
branches: ['main', 'v2-compat'],
currentBranch,
commits,
truncate,
formatDate
}
const truncate = v => {
const newline = v.indexOf('\n')
return newline > 0 ? v.slice(0, newline) : v
}
}).mount('#demo')
const formatDate = v => v.replace(/T|Z/g, ' ')
createApp({
setup() {
const currentBranch = ref('main')
const commits = ref(null)
watchEffect(() => {
fetch(`${API_URL}${currentBranch.value}`)
.then(res => res.json())
.then(data => {
console.log(data)
commits.value = data
})
})
return {
branches: ['main', 'v2-compat'],
currentBranch,
commits,
truncate,
formatDate,
}
},
}).mount('#demo')
</script>
<style>
@ -69,7 +78,8 @@ createApp({
line-height: 1.5em;
margin-bottom: 20px;
}
.author, .date {
.author,
.date {
font-weight: bold;
}
</style>

View File

@ -26,148 +26,147 @@
</script>
<!-- DemoGrid component script -->
<script>
const { reactive, computed } = Vue
const { reactive, computed } = Vue
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1)
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1)
const DemoGrid = {
template: '#grid-template',
props: {
data: Array,
columns: Array,
filterKey: String
},
setup(props) {
const state = reactive({
sortKey: '',
sortOrders: props.columns.reduce((o, key) => (o[key] = 1, o), {})
})
const DemoGrid = {
template: '#grid-template',
props: {
data: Array,
columns: Array,
filterKey: String,
},
setup(props) {
const state = reactive({
sortKey: '',
sortOrders: props.columns.reduce((o, key) => ((o[key] = 1), o), {}),
})
const filteredData = computed(() => {
let { data, filterKey } = props
if (filterKey) {
filterKey = filterKey.toLowerCase()
data = data.filter(row => {
return Object.keys(row).some(key => {
return String(row[key]).toLowerCase().indexOf(filterKey) > -1
const filteredData = computed(() => {
let { data, filterKey } = props
if (filterKey) {
filterKey = filterKey.toLowerCase()
data = data.filter(row => {
return Object.keys(row).some(key => {
return String(row[key]).toLowerCase().indexOf(filterKey) > -1
})
})
})
}
const { sortKey } = state
if (sortKey) {
const order = state.sortOrders[sortKey]
data = data.slice().sort((a, b) => {
a = a[sortKey]
b = b[sortKey]
return (a === b ? 0 : a > b ? 1 : -1) * order
})
}
return data
})
}
const { sortKey } = state
if (sortKey) {
const order = state.sortOrders[sortKey]
data = data.slice().sort((a, b) => {
a = a[sortKey]
b = b[sortKey]
return (a === b ? 0 : a > b ? 1 : -1) * order
})
}
return data
})
function sortBy(key) {
state.sortKey = key
state.sortOrders[key] *= -1
}
function sortBy(key) {
state.sortKey = key
state.sortOrders[key] *= -1
}
return {
state,
filteredData,
sortBy,
capitalize
}
return {
state,
filteredData,
sortBy,
capitalize,
}
},
}
}
</script>
<!-- App template (in DOM) -->
<div id="demo">
<form id="search">
Search <input name="query" v-model="searchQuery">
</form>
<demo-grid
:data="gridData"
:columns="gridColumns"
:filter-key="searchQuery">
<form id="search">Search <input name="query" v-model="searchQuery" /></form>
<demo-grid :data="gridData" :columns="gridColumns" :filter-key="searchQuery">
</demo-grid>
</div>
<!-- App script -->
<script>
Vue.createApp({
components: {
DemoGrid
},
data: () => ({
searchQuery: '',
gridColumns: ['name', 'power'],
gridData: [
{ name: 'Chuck Norris', power: Infinity },
{ name: 'Bruce Lee', power: 9000 },
{ name: 'Jackie Chan', power: 7000 },
{ name: 'Jet Li', power: 8000 }
]
})
}).mount('#demo')
Vue.createApp({
components: {
DemoGrid,
},
data: () => ({
searchQuery: '',
gridColumns: ['name', 'power'],
gridData: [
{ name: 'Chuck Norris', power: Infinity },
{ name: 'Bruce Lee', power: 9000 },
{ name: 'Jackie Chan', power: 7000 },
{ name: 'Jet Li', power: 8000 },
],
}),
}).mount('#demo')
</script>
<style>
body {
font-family: Helvetica Neue, Arial, sans-serif;
font-size: 14px;
color: #444;
}
body {
font-family:
Helvetica Neue,
Arial,
sans-serif;
font-size: 14px;
color: #444;
}
table {
border: 2px solid #42b983;
border-radius: 3px;
background-color: #fff;
}
table {
border: 2px solid #42b983;
border-radius: 3px;
background-color: #fff;
}
th {
background-color: #42b983;
color: rgba(255,255,255,0.66);
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
th {
background-color: #42b983;
color: rgba(255, 255, 255, 0.66);
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
td {
background-color: #f9f9f9;
}
td {
background-color: #f9f9f9;
}
th, td {
min-width: 120px;
padding: 10px 20px;
}
th,
td {
min-width: 120px;
padding: 10px 20px;
}
th.active {
color: #fff;
}
th.active {
color: #fff;
}
th.active .arrow {
opacity: 1;
}
th.active .arrow {
opacity: 1;
}
.arrow {
display: inline-block;
vertical-align: middle;
width: 0;
height: 0;
margin-left: 5px;
opacity: 0.66;
}
.arrow {
display: inline-block;
vertical-align: middle;
width: 0;
height: 0;
margin-left: 5px;
opacity: 0.66;
}
.arrow.asc {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid #fff;
}
.arrow.asc {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid #fff;
}
.arrow.dsc {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #fff;
}
.arrow.dsc {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #fff;
}
</style>

View File

@ -8,55 +8,62 @@
</div>
<script>
const { ref, computed } = Vue
const { ref, computed } = Vue
Vue.createApp({
setup() {
const input = ref('# hello')
const output = computed(() => marked.marked(input.value, { sanitize: true }))
const update = _.debounce(e => { input.value = e.target.value }, 50)
Vue.createApp({
setup() {
const input = ref('# hello')
const output = computed(() =>
marked.marked(input.value, { sanitize: true }),
)
const update = _.debounce(e => {
input.value = e.target.value
}, 50)
return {
input,
output,
update
}
}
}).mount('#editor')
return {
input,
output,
update,
}
},
}).mount('#editor')
</script>
<style>
html, body, #editor {
margin: 0;
height: 100%;
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #333;
}
html,
body,
#editor {
margin: 0;
height: 100%;
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #333;
}
textarea, #editor div {
display: inline-block;
overflow: auto;
width: 50%;
height: 100%;
vertical-align: top;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 0 20px;
}
textarea,
#editor div {
display: inline-block;
overflow: auto;
width: 50%;
height: 100%;
vertical-align: top;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 0 20px;
}
textarea {
border: none;
border-right: 1px solid #ccc;
resize: none;
outline: none;
background-color: #f6f6f6;
font-size: 14px;
font-family: 'Monaco', courier, monospace;
padding: 20px;
}
textarea {
border: none;
border-right: 1px solid #ccc;
resize: none;
outline: none;
background-color: #f6f6f6;
font-size: 14px;
font-family: 'Monaco', courier, monospace;
padding: 20px;
}
code {
color: #f66;
}
code {
color: #f66;
}
</style>

View File

@ -1,39 +1,37 @@
<script src="../../dist/vue.global.js"></script>
<script>
const { ref, reactive, computed, createApp } = Vue
const { ref, reactive, computed, createApp } = Vue
// math helper...
function valueToPoint (value, index, total) {
var x = 0
var y = -value * 0.8
var angle = Math.PI * 2 / total * index
var cos = Math.cos(angle)
var sin = Math.sin(angle)
var tx = x * cos - y * sin + 100
var ty = x * sin + y * cos + 100
return {
x: tx,
y: ty
}
}
const AxisLabel = {
template: '<text :x="point.x" :y="point.y">{{stat.label}}</text>',
props: {
stat: Object,
index: Number,
total: Number
},
setup(props) {
// math helper...
function valueToPoint(value, index, total) {
var x = 0
var y = -value * 0.8
var angle = ((Math.PI * 2) / total) * index
var cos = Math.cos(angle)
var sin = Math.sin(angle)
var tx = x * cos - y * sin + 100
var ty = x * sin + y * cos + 100
return {
point: computed(() => valueToPoint(
+props.stat.value + 10,
props.index,
props.total
))
x: tx,
y: ty,
}
}
}
const AxisLabel = {
template: '<text :x="point.x" :y="point.y">{{stat.label}}</text>',
props: {
stat: Object,
index: Number,
total: Number,
},
setup(props) {
return {
point: computed(() =>
valueToPoint(+props.stat.value + 10, props.index, props.total),
),
}
},
}
</script>
<!-- template for the polygraph component. -->
@ -51,24 +49,26 @@ const AxisLabel = {
</script>
<script>
const Polygraph = {
props: ['stats'],
template: '#polygraph-template',
setup(props) {
return {
points: computed(() => {
const total = props.stats.length
return props.stats.map((stat, i) => {
const point = valueToPoint(stat.value, i, total)
return point.x + ',' + point.y
}).join(' ')
})
}
},
components: {
AxisLabel
const Polygraph = {
props: ['stats'],
template: '#polygraph-template',
setup(props) {
return {
points: computed(() => {
const total = props.stats.length
return props.stats
.map((stat, i) => {
const point = valueToPoint(stat.value, i, total)
return point.x + ',' + point.y
})
.join(' ')
}),
}
},
components: {
AxisLabel,
},
}
}
</script>
<!-- demo root element -->
@ -80,93 +80,99 @@ const Polygraph = {
<!-- controls -->
<div v-for="stat in stats">
<label>{{stat.label}}</label>
<input type="range" v-model="stat.value" min="0" max="100">
<input type="range" v-model="stat.value" min="0" max="100" />
<span>{{stat.value}}</span>
<button @click="remove(stat)" class="remove">X</button>
</div>
<form id="add">
<input name="newlabel" v-model="newLabel">
<input name="newlabel" v-model="newLabel" />
<button @click="add">Add a Stat</button>
</form>
<pre id="raw">{{ stats }}</pre>
</div>
<script>
const globalStats = [
{ label: 'A', value: 100 },
{ label: 'B', value: 100 },
{ label: 'C', value: 100 },
{ label: 'D', value: 100 },
{ label: 'E', value: 100 },
{ label: 'F', value: 100 }
]
const globalStats = [
{ label: 'A', value: 100 },
{ label: 'B', value: 100 },
{ label: 'C', value: 100 },
{ label: 'D', value: 100 },
{ label: 'E', value: 100 },
{ label: 'F', value: 100 },
]
createApp({
components: {
Polygraph
},
setup() {
const newLabel = ref('')
const stats = reactive(globalStats)
createApp({
components: {
Polygraph,
},
setup() {
const newLabel = ref('')
const stats = reactive(globalStats)
function add(e) {
e.preventDefault()
if (!newLabel.value) return
stats.push({
label: newLabel.value,
value: 100
})
newLabel.value = ''
}
function remove(stat) {
if (stats.length > 3) {
stats.splice(stats.indexOf(stat), 1)
} else {
alert('Can\'t delete more!')
function add(e) {
e.preventDefault()
if (!newLabel.value) return
stats.push({
label: newLabel.value,
value: 100,
})
newLabel.value = ''
}
}
return {
newLabel,
stats,
add,
remove
}
}
}).mount('#demo')
function remove(stat) {
if (stats.length > 3) {
stats.splice(stats.indexOf(stat), 1)
} else {
alert("Can't delete more!")
}
}
return {
newLabel,
stats,
add,
remove,
}
},
}).mount('#demo')
</script>
<style>
body {
font-family: Helvetica Neue, Arial, sans-serif;
}
body {
font-family:
Helvetica Neue,
Arial,
sans-serif;
}
polygon {
polygon {
fill: #42b983;
opacity: .75;
}
opacity: 0.75;
}
circle {
circle {
fill: transparent;
stroke: #999;
}
}
text {
font-family: Helvetica Neue, Arial, sans-serif;
text {
font-family:
Helvetica Neue,
Arial,
sans-serif;
font-size: 10px;
fill: #666;
}
}
label {
label {
display: inline-block;
margin-left: 10px;
width: 20px;
}
}
#raw {
#raw {
position: absolute;
top: 0;
left: 300px;
}
}
</style>

View File

@ -1,51 +1,86 @@
<script src="../../dist/vue.global.js"></script>
<link rel="stylesheet" href="../../../../node_modules/todomvc-app-css/index.css">
<link
rel="stylesheet"
href="../../../../node_modules/todomvc-app-css/index.css"
/>
<div id="app">
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo"
autofocus autocomplete="off"
placeholder="What needs to be done?"
v-model="state.newTodo"
@keyup.enter="addTodo">
<input
class="new-todo"
autofocus
autocomplete="off"
placeholder="What needs to be done?"
v-model="state.newTodo"
@keyup.enter="addTodo"
/>
</header>
<section class="main" v-show="state.todos.length">
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="state.allDone">
<input
id="toggle-all"
class="toggle-all"
type="checkbox"
v-model="state.allDone"
/>
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li v-for="todo in state.filteredTodos"
class="todo"
:key="todo.id"
:class="{ completed: todo.completed, editing: todo === state.editedTodo }">
<li
v-for="todo in state.filteredTodos"
class="todo"
:key="todo.id"
:class="{ completed: todo.completed, editing: todo === state.editedTodo }"
>
<div class="view">
<input class="toggle" type="checkbox" v-model="todo.completed">
<input class="toggle" type="checkbox" v-model="todo.completed" />
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
<button class="destroy" @click="removeTodo(todo)"></button>
</div>
<input class="edit" type="text"
v-model="todo.title"
v-todo-focus="todo === state.editedTodo"
@blur="doneEdit(todo)"
@keyup.enter="doneEdit(todo)"
@keyup.escape="cancelEdit(todo)"
>
<input
class="edit"
type="text"
v-model="todo.title"
v-todo-focus="todo === state.editedTodo"
@blur="doneEdit(todo)"
@keyup.enter="doneEdit(todo)"
@keyup.escape="cancelEdit(todo)"
/>
</li>
</ul>
</section>
<footer class="footer" v-show="state.todos.length">
<span class="todo-count">
<strong>{{ state.remaining }}</strong>
<span>{{ state.remainingText }}</span>
</span>
<span class="todo-count">
<strong>{{ state.remaining }}</strong>
<span>{{ state.remainingText }}</span>
</span>
<ul class="filters">
<li><a href="#/all" :class="{ selected: state.visibility === 'all' }">All</a></li>
<li><a href="#/active" :class="{ selected: state.visibility === 'active' }">Active</a></li>
<li><a href="#/completed" :class="{ selected: state.visibility === 'completed' }">Completed</a></li>
<li>
<a href="#/all" :class="{ selected: state.visibility === 'all' }"
>All</a
>
</li>
<li>
<a
href="#/active"
:class="{ selected: state.visibility === 'active' }"
>Active</a
>
</li>
<li>
<a
href="#/completed"
:class="{ selected: state.visibility === 'completed' }"
>Completed</a
>
</li>
</ul>
<button class="clear-completed" @click="removeCompleted" v-show="state.todos.length > state.remaining">
<button
class="clear-completed"
@click="removeCompleted"
v-show="state.todos.length > state.remaining"
>
Clear completed
</button>
</footer>
@ -53,154 +88,155 @@
</div>
<script>
const { createApp, reactive, computed, watchEffect, onMounted, onUnmounted } = Vue
const { createApp, reactive, computed, watchEffect, onMounted, onUnmounted } =
Vue
const STORAGE_KEY = 'todos-vuejs-3.x'
const todoStorage = {
fetch () {
const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
todos.forEach((todo, index) => {
todo.id = index
})
todoStorage.uid = todos.length
return todos
},
save (todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
const STORAGE_KEY = 'todos-vuejs-3.x'
const todoStorage = {
fetch() {
const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
todos.forEach((todo, index) => {
todo.id = index
})
todoStorage.uid = todos.length
return todos
},
save(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
},
}
}
const filters = {
all (todos) {
return todos
},
active (todos) {
return todos.filter((todo) => {
return !todo.completed
})
},
completed (todos) {
return todos.filter(function (todo) {
return todo.completed
})
const filters = {
all(todos) {
return todos
},
active(todos) {
return todos.filter(todo => {
return !todo.completed
})
},
completed(todos) {
return todos.filter(function (todo) {
return todo.completed
})
},
}
}
function pluralize (n) {
return n === 1 ? 'item' : 'items'
}
function pluralize(n) {
return n === 1 ? 'item' : 'items'
}
createApp({
setup () {
const state = reactive({
todos: todoStorage.fetch(),
editedTodo: null,
newTodo: '',
beforeEditCache: '',
visibility: 'all',
remaining: computed(() => {
return filters.active(state.todos).length
}),
remainingText: computed(() => {
return ` ${pluralize(state.remaining)} left`
}),
filteredTodos: computed(() => {
return filters[state.visibility](state.todos)
}),
allDone: computed({
get: function () {
return state.remaining === 0
},
set: function (value) {
state.todos.forEach((todo) => {
todo.completed = value
})
createApp({
setup() {
const state = reactive({
todos: todoStorage.fetch(),
editedTodo: null,
newTodo: '',
beforeEditCache: '',
visibility: 'all',
remaining: computed(() => {
return filters.active(state.todos).length
}),
remainingText: computed(() => {
return ` ${pluralize(state.remaining)} left`
}),
filteredTodos: computed(() => {
return filters[state.visibility](state.todos)
}),
allDone: computed({
get: function () {
return state.remaining === 0
},
set: function (value) {
state.todos.forEach(todo => {
todo.completed = value
})
},
}),
})
watchEffect(() => {
todoStorage.save(state.todos)
})
onMounted(() => {
window.addEventListener('hashchange', onHashChange)
onHashChange()
})
onUnmounted(() => {
window.removeEventListener('hashchange', onHashChange)
})
function onHashChange() {
const visibility = window.location.hash.replace(/#\/?/, '')
if (filters[visibility]) {
state.visibility = visibility
} else {
window.location.hash = ''
state.visibility = 'all'
}
})
})
watchEffect(() => {
todoStorage.save(state.todos)
})
onMounted(() => {
window.addEventListener('hashchange', onHashChange)
onHashChange()
})
onUnmounted(() => {
window.removeEventListener('hashchange', onHashChange)
})
function onHashChange () {
const visibility = window.location.hash.replace(/#\/?/, '')
if (filters[visibility]) {
state.visibility = visibility
} else {
window.location.hash = ''
state.visibility = 'all'
}
}
function addTodo () {
const value = state.newTodo && state.newTodo.trim()
if (!value) {
return
function addTodo() {
const value = state.newTodo && state.newTodo.trim()
if (!value) {
return
}
state.todos.push({
id: todoStorage.uid++,
title: value,
completed: false,
})
state.newTodo = ''
}
state.todos.push({
id: todoStorage.uid++,
title: value,
completed: false
})
state.newTodo = ''
}
function removeTodo (todo) {
state.todos.splice(state.todos.indexOf(todo), 1)
}
function editTodo (todo) {
state.beforeEditCache = todo.title
state.editedTodo = todo
}
function doneEdit (todo) {
if (!state.editedTodo) {
return
function removeTodo(todo) {
state.todos.splice(state.todos.indexOf(todo), 1)
}
state.editedTodo = null
todo.title = todo.title.trim()
if (!todo.title) {
removeTodo(todo)
function editTodo(todo) {
state.beforeEditCache = todo.title
state.editedTodo = todo
}
}
function cancelEdit (todo) {
state.editedTodo = null
todo.title = state.beforeEditCache
}
function removeCompleted () {
state.todos = filters.active(state.todos)
}
return {
state,
addTodo,
removeTodo,
editTodo,
doneEdit,
cancelEdit,
removeCompleted
}
},
directives: {
'todo-focus': (el, { value }) => {
if (value) {
el.focus()
function doneEdit(todo) {
if (!state.editedTodo) {
return
}
state.editedTodo = null
todo.title = todo.title.trim()
if (!todo.title) {
removeTodo(todo)
}
}
}
}
}).mount('#app')
function cancelEdit(todo) {
state.editedTodo = null
todo.title = state.beforeEditCache
}
function removeCompleted() {
state.todos = filters.active(state.todos)
}
return {
state,
addTodo,
removeTodo,
editTodo,
doneEdit,
cancelEdit,
removeCompleted,
}
},
directives: {
'todo-focus': (el, { value }) => {
if (value) {
el.focus()
}
},
},
}).mount('#app')
</script>

View File

@ -22,46 +22,46 @@
</script>
<!-- item script -->
<script>
const { reactive, computed, toRefs } = Vue
const { reactive, computed, toRefs } = Vue
const TreeItem = {
name: 'TreeItem', // necessary for self-reference
template: '#item-template',
props: {
model: Object
},
setup(props) {
const state = reactive({
open: false,
isFolder: computed(() => {
return props.model.children && props.model.children.length
const TreeItem = {
name: 'TreeItem', // necessary for self-reference
template: '#item-template',
props: {
model: Object,
},
setup(props) {
const state = reactive({
open: false,
isFolder: computed(() => {
return props.model.children && props.model.children.length
}),
})
})
function toggle() {
state.open = !state.open
}
function changeType() {
if (!state.isFolder) {
props.model.children = []
addChild()
state.open = true
function toggle() {
state.open = !state.open
}
}
function addChild() {
props.model.children.push({ name: 'new stuff' })
}
function changeType() {
if (!state.isFolder) {
props.model.children = []
addChild()
state.open = true
}
}
return {
...toRefs(state),
toggle,
changeType,
addChild
}
function addChild() {
props.model.children.push({ name: 'new stuff' })
}
return {
...toRefs(state),
toggle,
changeType,
addChild,
}
},
}
}
</script>
<p>(You can double click on an item to turn it into a folder.)</p>
@ -72,43 +72,37 @@ const TreeItem = {
</ul>
<script>
const treeData = {
name: 'My Tree',
children: [
{ name: 'hello' },
{ name: 'wat' },
{
name: 'child folder',
children: [
{
name: 'child folder',
children: [
{ name: 'hello' },
{ name: 'wat' }
]
},
{ name: 'hello' },
{ name: 'wat' },
{
name: 'child folder',
children: [
{ name: 'hello' },
{ name: 'wat' }
]
}
]
}
]
}
const treeData = {
name: 'My Tree',
children: [
{ name: 'hello' },
{ name: 'wat' },
{
name: 'child folder',
children: [
{
name: 'child folder',
children: [{ name: 'hello' }, { name: 'wat' }],
},
{ name: 'hello' },
{ name: 'wat' },
{
name: 'child folder',
children: [{ name: 'hello' }, { name: 'wat' }],
},
],
},
],
}
Vue.createApp({
components: {
TreeItem
},
data: () => ({
treeData
})
}).mount('#demo')
Vue.createApp({
components: {
TreeItem,
},
data: () => ({
treeData,
}),
}).mount('#demo')
</script>
<style>

View File

@ -6,78 +6,83 @@
<button @click="reset">reset</button>
<button @click="shuffle">shuffle</button>
<transition-group tag="ul" name="fade" class="container">
<item v-for="item in items"
<item
v-for="item in items"
class="item"
:msg="item"
:key="item"
@rm="remove(item)">
@rm="remove(item)"
>
</item>
</transition-group>
</div>
<script>
const getInitialItems = () => [1, 2, 3, 4, 5]
let id = getInitialItems().length + 1
const getInitialItems = () => [1, 2, 3, 4, 5]
let id = getInitialItems().length + 1
const Item = {
props: ['msg'],
template: `<div>{{ msg }} <button @click="$emit('rm')">x</button></div>`
}
Vue.createApp({
components: {
Item
},
data() {
return {
items: getInitialItems()
}
},
methods: {
insert () {
const i = Math.round(Math.random() * this.items.length)
this.items.splice(i, 0, id++)
},
reset () {
this.items = getInitialItems()
},
shuffle () {
this.items = _.shuffle(this.items)
},
remove (item) {
const i = this.items.indexOf(item)
if (i > -1) {
this.items.splice(i, 1)
}
}
const Item = {
props: ['msg'],
template: `<div>{{ msg }} <button @click="$emit('rm')">x</button></div>`,
}
}).mount('#app')
Vue.createApp({
components: {
Item,
},
data() {
return {
items: getInitialItems(),
}
},
methods: {
insert() {
const i = Math.round(Math.random() * this.items.length)
this.items.splice(i, 0, id++)
},
reset() {
this.items = getInitialItems()
},
shuffle() {
this.items = _.shuffle(this.items)
},
remove(item) {
const i = this.items.indexOf(item)
if (i > -1) {
this.items.splice(i, 1)
}
},
},
}).mount('#app')
</script>
<style>
.container {
position: relative;
padding: 0;
}
.item {
width: 100%;
height: 30px;
background-color: #f3f3f3;
border: 1px solid #666;
box-sizing: border-box;
}
/* 1. declare transition */
.fade-move, .fade-enter-active, .fade-leave-active {
transition: all .5s cubic-bezier(.55,0,.1,1);
}
/* 2. declare enter from and leave to state */
.fade-enter-from, .fade-leave-to {
opacity: 0;
transform: scaleY(0.01) translate(30px, 0);
}
/* 3. ensure leaving items are taken out of layout flow so that moving
.container {
position: relative;
padding: 0;
}
.item {
width: 100%;
height: 30px;
background-color: #f3f3f3;
border: 1px solid #666;
box-sizing: border-box;
}
/* 1. declare transition */
.fade-move,
.fade-enter-active,
.fade-leave-active {
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
}
/* 2. declare enter from and leave to state */
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: scaleY(0.01) translate(30px, 0);
}
/* 3. ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.fade-leave-active {
position: absolute;
}
.fade-leave-active {
position: absolute;
}
</style>

View File

@ -33,10 +33,10 @@
</transition>
</script>
<script>
const Modal = {
template: '#modal-template',
props: ['show']
}
const Modal = {
template: '#modal-template',
props: ['show'],
}
</script>
<!-- modal container that lives outside of app root -->
@ -56,57 +56,57 @@ const Modal = {
</div>
<script>
Vue.createApp({
components: { Modal },
data: () => ({
showModal: false
})
}).mount('#app')
Vue.createApp({
components: { Modal },
data: () => ({
showModal: false,
}),
}).mount('#app')
</script>
<style>
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
display: table;
transition: opacity .3s ease;
}
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: table;
transition: opacity 0.3s ease;
}
.modal-wrapper {
display: table-cell;
vertical-align: middle;
}
.modal-wrapper {
display: table-cell;
vertical-align: middle;
}
.modal-container {
width: 300px;
margin: 0px auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
transition: all .3s ease;
font-family: Helvetica, Arial, sans-serif;
}
.modal-container {
width: 300px;
margin: 0px auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
font-family: Helvetica, Arial, sans-serif;
}
.modal-header h3 {
margin-top: 0;
color: #42b983;
}
.modal-header h3 {
margin-top: 0;
color: #42b983;
}
.modal-body {
margin: 20px 0;
}
.modal-body {
margin: 20px 0;
}
.modal-default-button {
float: right;
}
.modal-default-button {
float: right;
}
/*
/*
* The following styles are auto-applied to elements with
* transition="modal" when their visibility is toggled
* by Vue.js.
@ -115,17 +115,17 @@ Vue.createApp({
* these styles.
*/
.modal-enter-from {
opacity: 0;
}
.modal-enter-from {
opacity: 0;
}
.modal-leave-to {
opacity: 0;
}
.modal-leave-to {
opacity: 0;
}
.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -253,7 +253,7 @@ function createConfig(format, output, plugins = []) {
'source-map-js',
'@babel/parser',
'estree-walker',
'entities/lib/decode.js',
'entities/dist/decode.js',
]
if (isGlobalBuild || isBrowserESMBuild || isCompatPackage) {

View File

@ -218,6 +218,12 @@ async function main() {
}
}
// @ts-expect-error
if (versionIncrements.includes(targetVersion)) {
// @ts-expect-error
targetVersion = inc(targetVersion)
}
if (!semver.valid(targetVersion)) {
throw new Error(`invalid target version: ${targetVersion}`)
}
@ -246,15 +252,23 @@ async function main() {
let isCIPassed = await getCIResult()
skipTests ||= isCIPassed
if (isCIPassed && !skipPrompts) {
/** @type {{ yes: boolean }} */
const { yes: promptSkipTests } = await prompt({
type: 'confirm',
name: 'yes',
message: `CI for this commit passed. Skip local tests?`,
})
skipTests = promptSkipTests
if (isCIPassed) {
if (!skipPrompts) {
/** @type {{ yes: boolean }} */
const { yes: promptSkipTests } = await prompt({
type: 'confirm',
name: 'yes',
message: `CI for this commit passed. Skip local tests?`,
})
skipTests = promptSkipTests
} else {
skipTests = true
}
} else if (skipPrompts) {
throw new Error(
'CI for the latest commit has not passed yet. ' +
'Only run the release workflow after the CI has passed.',
)
}
}
@ -338,6 +352,11 @@ async function main() {
if (branch !== 'main') {
additionalPublishFlags.push('--publish-branch', branch)
}
// add provenance metadata when releasing from CI
// canary release commits are not pushed therefore we don't need to add provenance
if (process.env.CI && !isCanary) {
additionalPublishFlags.push('--provenance')
}
for (const pkg of packages) {
await publishPackage(pkg, targetVersion, additionalPublishFlags)
@ -514,7 +533,7 @@ async function publishPackage(pkgName, version, additionalFlags) {
)
console.log(pico.green(`Successfully published ${pkgName}@${version}`))
} catch (/** @type {any} */ e) {
if (e.stderr.match(/previously published/)) {
if (e.message?.match(/previously published/)) {
console.log(pico.red(`Skipping already published: ${pkgName}`))
} else {
throw e

View File

@ -95,8 +95,17 @@ export async function exec(command, args, options) {
const ok = code === 0
const stderr = Buffer.concat(stderrChunks).toString().trim()
const stdout = Buffer.concat(stdoutChunks).toString().trim()
const result = { ok, code, stderr, stdout }
resolve(result)
if (ok) {
const result = { ok, code, stderr, stdout }
resolve(result)
} else {
reject(
new Error(
`Failed to execute command: ${command} ${args.join(' ')}: ${stderr}`,
),
)
}
})
})
}