mirror of https://github.com/vuejs/core.git
chore: Merge branch 'main' into minor
This commit is contained in:
commit
eaf5455d77
|
@ -44,7 +44,7 @@ This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
|
||||||
|
|
||||||
### Full Message Format
|
### 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>
|
<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:
|
The subject contains a succinct description of the change:
|
||||||
|
|
||||||
* use the imperative, present tense: "change" not "changed" nor "changes"
|
- use the imperative, present tense: "change" not "changed" nor "changes"
|
||||||
* don't capitalize the first letter
|
- don't capitalize the first letter
|
||||||
* no dot (.) at the end
|
- no dot (.) at the end
|
||||||
|
|
||||||
### Body
|
### Body
|
||||||
|
|
||||||
|
|
|
@ -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.
|
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
|
### Pull Request Checklist
|
||||||
|
|
||||||
- Vue core has two primary work branches: `main` and `minor`.
|
- Vue core has two primary work branches: `main` and `minor`.
|
||||||
|
|
|
@ -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`.
|
- 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
|
- Performance
|
||||||
|
|
||||||
- Be careful about code changes in "hot paths", in particular the Virtual DOM renderer (`runtime-core/src/renderer.ts`) and component instantiation code.
|
- 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
|
- Potential Breakage
|
||||||
|
|
|
@ -3,7 +3,7 @@ on:
|
||||||
tags:
|
tags:
|
||||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
|
|
||||||
name: Create Release
|
name: Create GH Release for Tag
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
jobs:
|
jobs:
|
|
@ -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.
|
|
@ -4,6 +4,7 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- minor
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
|
@ -58,7 +58,7 @@ jobs:
|
||||||
- name: Download Previous Size Data
|
- name: Download Previous Size Data
|
||||||
uses: dawidd6/action-download-artifact@v6
|
uses: dawidd6/action-download-artifact@v6
|
||||||
with:
|
with:
|
||||||
branch: main
|
branch: ${{ github.base_ref }}
|
||||||
workflow: size-data.yml
|
workflow: size-data.yml
|
||||||
event: push
|
event: push
|
||||||
name: size-data
|
name: size-data
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
dist
|
dist
|
||||||
*.md
|
|
||||||
*.html
|
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
CHANGELOG*.md
|
||||||
|
|
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -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)
|
# [3.5.0-alpha.5](https://github.com/vuejs/core/compare/v3.4.35...v3.5.0-alpha.5) (2024-07-31)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
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)
|
- Mix - [@mnixry](https://github.com/mnixry)
|
||||||
- Aviv Keller - [@RedYetiDev](https://github.com/redyetidev) | [LinkedIn](https://www.linkedin.com/in/redyetidev) <redyetidev@gmail.com>
|
- Aviv Keller - [@RedYetiDev](https://github.com/redyetidev) | [LinkedIn](https://www.linkedin.com/in/redyetidev) <redyetidev@gmail.com>
|
||||||
|
|
16
package.json
16
package.json
|
@ -66,9 +66,9 @@
|
||||||
"@rollup/plugin-json": "^6.1.0",
|
"@rollup/plugin-json": "^6.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@rollup/plugin-replace": "5.0.4",
|
"@rollup/plugin-replace": "5.0.4",
|
||||||
"@swc/core": "^1.7.3",
|
"@swc/core": "^1.7.6",
|
||||||
"@types/hash-sum": "^1.0.2",
|
"@types/hash-sum": "^1.0.2",
|
||||||
"@types/node": "^20.14.13",
|
"@types/node": "^20.14.14",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/serve-handler": "^6.1.4",
|
"@types/serve-handler": "^6.1.4",
|
||||||
"@vitest/coverage-istanbul": "^1.6.0",
|
"@vitest/coverage-istanbul": "^1.6.0",
|
||||||
|
@ -82,19 +82,19 @@
|
||||||
"eslint-plugin-vitest": "^0.5.4",
|
"eslint-plugin-vitest": "^0.5.4",
|
||||||
"estree-walker": "catalog:",
|
"estree-walker": "catalog:",
|
||||||
"jsdom": "^24.1.1",
|
"jsdom": "^24.1.1",
|
||||||
"lint-staged": "^15.2.7",
|
"lint-staged": "^15.2.8",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"magic-string": "^0.30.10",
|
"magic-string": "^0.30.11",
|
||||||
"markdown-table": "^3.0.3",
|
"markdown-table": "^3.0.3",
|
||||||
"marked": "^12.0.2",
|
"marked": "^13.0.3",
|
||||||
"npm-run-all2": "^6.2.2",
|
"npm-run-all2": "^6.2.2",
|
||||||
"picocolors": "^1.0.1",
|
"picocolors": "^1.0.1",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"pug": "^3.0.3",
|
"pug": "^3.0.3",
|
||||||
"puppeteer": "~22.14.0",
|
"puppeteer": "~22.15.0",
|
||||||
"rimraf": "^5.0.9",
|
"rimraf": "^6.0.1",
|
||||||
"rollup": "^4.19.1",
|
"rollup": "^4.20.0",
|
||||||
"rollup-plugin-dts": "^6.1.1",
|
"rollup-plugin-dts": "^6.1.1",
|
||||||
"rollup-plugin-esbuild": "^6.1.1",
|
"rollup-plugin-esbuild": "^6.1.1",
|
||||||
"rollup-plugin-polyfill-node": "^0.13.0",
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
||||||
|
|
|
@ -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
|
"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
|
||||||
|
|
||||||
return function render(_ctx, _cache, $props, $setup, $data, $options) {
|
return function render(_ctx, _cache) {
|
||||||
return (_openBlock(), _createElementBlock("div", {
|
return (_openBlock(), _createElementBlock("div", {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
for (let i = 0; i < _ctx.list.length; i++) {
|
for (var i = 0; i < _ctx.list.length; i++) {
|
||||||
_ctx.log(i)
|
_ctx.log(i)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
_ctx.error(i)
|
||||||
|
}
|
||||||
}, null, 8 /* PROPS */, ["onClick"]))
|
}, 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
|
"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
|
||||||
|
|
||||||
return function render(_ctx, _cache, $props, $setup, $data, $options) {
|
return function render(_ctx, _cache) {
|
||||||
return (_openBlock(), _createElementBlock("div", {
|
return (_openBlock(), _createElementBlock("div", {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
for (const x in _ctx.list) {
|
try {} catch (err) { console.error(err) }
|
||||||
_ctx.log(x)
|
console.log(_ctx.err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, null, 8 /* PROPS */, ["onClick"]))
|
}, 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
|
"const { openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
|
||||||
|
|
||||||
return function render(_ctx, _cache, $props, $setup, $data, $options) {
|
return function render(_ctx, _cache) {
|
||||||
return (_openBlock(), _createElementBlock("div", {
|
return (_openBlock(), _createElementBlock("div", {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
for (const x of _ctx.list) {
|
try {
|
||||||
_ctx.log(x)
|
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"]))
|
}, null, 8 /* PROPS */, ["onClick"]))
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -27,6 +27,10 @@ function parseWithExpressionTransform(
|
||||||
return ast.children[0]
|
return ast.children[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compile(template: string) {
|
||||||
|
return baseCompile(template, { prefixIdentifiers: true })
|
||||||
|
}
|
||||||
|
|
||||||
describe('compiler: expression transform', () => {
|
describe('compiler: expression transform', () => {
|
||||||
test('interpolation (root)', () => {
|
test('interpolation (root)', () => {
|
||||||
const node = parseWithExpressionTransform(`{{ foo }}`) as InterpolationNode
|
const node = parseWithExpressionTransform(`{{ foo }}`) as InterpolationNode
|
||||||
|
@ -291,6 +295,7 @@ describe('compiler: expression transform', () => {
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not prefix an object property key', () => {
|
test('should not prefix an object property key', () => {
|
||||||
const node = parseWithExpressionTransform(
|
const node = parseWithExpressionTransform(
|
||||||
`{{ { foo() { baz() }, value: bar } }}`,
|
`{{ { 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', () => {
|
describe('ES Proposals support', () => {
|
||||||
test('bigInt', () => {
|
test('bigInt', () => {
|
||||||
const node = parseWithExpressionTransform(
|
const node = parseWithExpressionTransform(
|
||||||
|
@ -555,42 +644,6 @@ describe('compiler: expression transform', () => {
|
||||||
expect(code).toMatchSnapshot()
|
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', () => {
|
test('inline mode', () => {
|
||||||
const { code } = compileWithBindingMetadata(
|
const { code } = compileWithBindingMetadata(
|
||||||
`<div>{{ props }} {{ setup }} {{ setupConst }} {{ data }} {{ options }} {{ isNaN }}</div>`,
|
`<div>{{ props }} {{ setup }} {{ setupConst }} {{ data }} {{ options }} {{ isNaN }}</div>`,
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "catalog:",
|
"@babel/parser": "catalog:",
|
||||||
"@vue/shared": "workspace:*",
|
"@vue/shared": "workspace:*",
|
||||||
"entities": "^4.5.0",
|
"entities": "^5.0.0",
|
||||||
"estree-walker": "catalog:",
|
"estree-walker": "catalog:",
|
||||||
"source-map-js": "catalog:"
|
"source-map-js": "catalog:"
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
// do not import runtime methods
|
// do not import runtime methods
|
||||||
import type {
|
import type {
|
||||||
BlockStatement,
|
BlockStatement,
|
||||||
|
ForInStatement,
|
||||||
|
ForOfStatement,
|
||||||
|
ForStatement,
|
||||||
Function,
|
Function,
|
||||||
Identifier,
|
Identifier,
|
||||||
Node,
|
Node,
|
||||||
|
@ -77,6 +80,14 @@ export function walkIdentifiers(
|
||||||
markScopeIdentifier(node, id, knownIds),
|
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) {
|
leave(node: Node & { scopeIds?: Set<string> }, parent: Node | null) {
|
||||||
|
@ -192,18 +203,36 @@ export function walkBlockDeclarations(
|
||||||
) {
|
) {
|
||||||
if (stmt.declare || !stmt.id) continue
|
if (stmt.declare || !stmt.id) continue
|
||||||
onIdent(stmt.id)
|
onIdent(stmt.id)
|
||||||
} else if (
|
} else if (isForStatement(stmt)) {
|
||||||
stmt.type === 'ForOfStatement' ||
|
walkForStatement(stmt, true, onIdent)
|
||||||
stmt.type === 'ForInStatement' ||
|
}
|
||||||
stmt.type === 'ForStatement'
|
}
|
||||||
) {
|
}
|
||||||
const variable = stmt.type === 'ForStatement' ? stmt.init : stmt.left
|
|
||||||
if (variable && variable.type === 'VariableDeclaration') {
|
function isForStatement(
|
||||||
for (const decl of variable.declarations) {
|
stmt: Node,
|
||||||
for (const id of extractIdentifiers(decl.id)) {
|
): stmt is ForStatement | ForOfStatement | ForInStatement {
|
||||||
onIdent(id)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ import {
|
||||||
isSimpleIdentifier,
|
isSimpleIdentifier,
|
||||||
isStaticArgOf,
|
isStaticArgOf,
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import { decodeHTML } from 'entities/lib/decode.js'
|
import { decodeHTML } from 'entities/dist/decode.js'
|
||||||
import {
|
import {
|
||||||
type ParserOptions as BabelOptions,
|
type ParserOptions as BabelOptions,
|
||||||
parse,
|
parse,
|
||||||
|
|
|
@ -36,7 +36,7 @@ import {
|
||||||
EntityDecoder,
|
EntityDecoder,
|
||||||
fromCodePoint,
|
fromCodePoint,
|
||||||
htmlDecodeTree,
|
htmlDecodeTree,
|
||||||
} from 'entities/lib/decode.js'
|
} from 'entities/dist/decode.js'
|
||||||
|
|
||||||
export enum ParseMode {
|
export enum ParseMode {
|
||||||
BASE,
|
BASE,
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
"hash-sum": "^2.0.0",
|
"hash-sum": "^2.0.0",
|
||||||
"lru-cache": "10.1.0",
|
"lru-cache": "10.1.0",
|
||||||
"merge-source-map": "^1.1.0",
|
"merge-source-map": "^1.1.0",
|
||||||
"minimatch": "^9.0.5",
|
"minimatch": "~9.0.5",
|
||||||
"postcss-modules": "^6.0.0",
|
"postcss-modules": "^6.0.0",
|
||||||
"postcss-selector-parser": "^6.1.1",
|
"postcss-selector-parser": "^6.1.1",
|
||||||
"pug": "^3.0.3",
|
"pug": "^3.0.3",
|
||||||
|
|
|
@ -288,12 +288,27 @@ describe('ssr: element', () => {
|
||||||
}></div>\`"
|
}></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(`
|
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\${
|
"\`<div\${
|
||||||
_ssrRenderAttrs(_ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.z, "x", { y: true }))
|
_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 />`))
|
expect(getCompiledString(`<div class="foo" v-xxx />`))
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"\`<div\${
|
"\`<div\${
|
||||||
_ssrRenderAttrs(_mergeProps({ class: "foo" }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
|
_ssrRenderAttrs(_temp0 = _mergeProps({ class: "foo" }, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
|
||||||
}></div>\`"
|
}>\${
|
||||||
|
("textContent" in _temp0) ? _ssrInterpolate(_temp0.textContent) : _temp0.innerHTML ?? ''
|
||||||
|
}</div>\`"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('custom dir with v-bind', () => {
|
test('custom dir with v-bind', () => {
|
||||||
expect(getCompiledString(`<div :title="foo" :class="bar" v-xxx />`))
|
expect(getCompiledString(`<div :title="foo" :class="bar" v-xxx />`))
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"\`<div\${
|
"\`<div\${
|
||||||
_ssrRenderAttrs(_mergeProps({
|
_ssrRenderAttrs(_temp0 = _mergeProps({
|
||||||
title: _ctx.foo,
|
title: _ctx.foo,
|
||||||
class: _ctx.bar
|
class: _ctx.bar
|
||||||
}, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
|
}, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
|
||||||
}></div>\`"
|
}>\${
|
||||||
`)
|
("textContent" in _temp0) ? _ssrInterpolate(_temp0.textContent) : _temp0.innerHTML ?? ''
|
||||||
|
}</div>\`"
|
||||||
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('custom dir with object v-bind', () => {
|
test('custom dir with object v-bind', () => {
|
||||||
expect(getCompiledString(`<div v-bind="x" v-xxx />`))
|
expect(getCompiledString(`<div v-bind="x" v-xxx />`))
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"\`<div\${
|
"\`<div\${
|
||||||
_ssrRenderAttrs(_mergeProps(_ctx.x, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
|
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.x, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
|
||||||
}></div>\`"
|
}>\${
|
||||||
`)
|
("textContent" in _temp0) ? _ssrInterpolate(_temp0.textContent) : _temp0.innerHTML ?? ''
|
||||||
|
}</div>\`"
|
||||||
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('custom dir with object v-bind + normal bindings', () => {
|
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" />`),
|
getCompiledString(`<div v-bind="x" class="foo" v-xxx title="bar" />`),
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
"\`<div\${
|
"\`<div\${
|
||||||
_ssrRenderAttrs(_mergeProps(_ctx.x, {
|
_ssrRenderAttrs(_temp0 = _mergeProps(_ctx.x, {
|
||||||
class: "foo",
|
class: "foo",
|
||||||
title: "bar"
|
title: "bar"
|
||||||
}, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
|
}, _ssrGetDirectiveProps(_ctx, _directive_xxx)))
|
||||||
}></div>\`"
|
}>\${
|
||||||
|
("textContent" in _temp0) ? _ssrInterpolate(_temp0.textContent) : _temp0.innerHTML ?? ''
|
||||||
|
}</div>\`"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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', () => {
|
test('with dynamic tag', () => {
|
||||||
expect(
|
expect(
|
||||||
compile(
|
compile(
|
||||||
|
|
|
@ -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) {
|
if (needTagForRuntime) {
|
||||||
|
|
|
@ -108,7 +108,7 @@ export function ssrProcessTransitionGroup(
|
||||||
context.pushStringPart(` ${scopeId}`)
|
context.pushStringPart(` ${scopeId}`)
|
||||||
}
|
}
|
||||||
context.pushStringPart(`>`)
|
context.pushStringPart(`>`)
|
||||||
processChildren(node, context, false, true)
|
processChildren(node, context, false, true, true)
|
||||||
context.pushStringPart(`</${tag.value!.content}>`)
|
context.pushStringPart(`</${tag.value!.content}>`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -182,6 +182,11 @@ describe('allow getter and setter types to be unrelated', <T>() => {
|
||||||
const d = {} as T
|
const d = {} as T
|
||||||
const e = ref(d)
|
const e = ref(d)
|
||||||
e.value = d
|
e.value = d
|
||||||
|
|
||||||
|
const f = ref(ref(0))
|
||||||
|
expectType<number>(f.value)
|
||||||
|
// @ts-expect-error
|
||||||
|
f.value = ref(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
|
|
|
@ -43,7 +43,8 @@ describe('defineProps w/ generics', () => {
|
||||||
test()
|
test()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('defineProps w/ type declaration + withDefaults', () => {
|
describe('defineProps w/ type declaration + withDefaults', <T extends
|
||||||
|
string>() => {
|
||||||
const res = withDefaults(
|
const res = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
number?: number
|
number?: number
|
||||||
|
@ -56,6 +57,7 @@ describe('defineProps w/ type declaration + withDefaults', () => {
|
||||||
z?: string
|
z?: string
|
||||||
bool?: boolean
|
bool?: boolean
|
||||||
boolAndUndefined: boolean | undefined
|
boolAndUndefined: boolean | undefined
|
||||||
|
foo?: T
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
number: 123,
|
number: 123,
|
||||||
|
@ -65,6 +67,7 @@ describe('defineProps w/ type declaration + withDefaults', () => {
|
||||||
genStr: () => '',
|
genStr: () => '',
|
||||||
y: undefined,
|
y: undefined,
|
||||||
z: 'string',
|
z: 'string',
|
||||||
|
foo: '' as any,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -81,6 +84,7 @@ describe('defineProps w/ type declaration + withDefaults', () => {
|
||||||
expectType<string | undefined>(res.x)
|
expectType<string | undefined>(res.x)
|
||||||
expectType<string | undefined>(res.y)
|
expectType<string | undefined>(res.y)
|
||||||
expectType<string>(res.z)
|
expectType<string>(res.z)
|
||||||
|
expectType<T>(res.foo)
|
||||||
|
|
||||||
expectType<boolean>(res.bool)
|
expectType<boolean>(res.bool)
|
||||||
expectType<boolean>(res.boolAndUndefined)
|
expectType<boolean>(res.boolAndUndefined)
|
||||||
|
@ -138,6 +142,31 @@ describe('defineProps w/ object union + withDefaults', () => {
|
||||||
>(props)
|
>(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
|
describe('defineProps w/ generic type declaration + withDefaults', <T extends
|
||||||
number, TA extends {
|
number, TA extends {
|
||||||
a: string
|
a: string
|
||||||
|
|
|
@ -73,7 +73,7 @@ class BaseReactiveHandler implements ProxyHandler<Target> {
|
||||||
: reactiveMap
|
: reactiveMap
|
||||||
).get(target) ||
|
).get(target) ||
|
||||||
// receiver is not the reactive proxy, but has the same prototype
|
// 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)
|
Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)
|
||||||
) {
|
) {
|
||||||
return target
|
return target
|
||||||
|
|
|
@ -52,7 +52,9 @@ export function isRef(r: any): r is Ref {
|
||||||
* @param value - The object to wrap in the ref.
|
* @param value - The object to wrap in the ref.
|
||||||
* @see {@link https://vuejs.org/api/reactivity-core.html#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<T = any>(): Ref<T | undefined>
|
||||||
export function ref(value?: unknown) {
|
export function ref(value?: unknown) {
|
||||||
return createRef(value, false)
|
return createRef(value, false)
|
||||||
|
|
|
@ -13,7 +13,7 @@ const { render, createApp } = createRenderer({
|
||||||
patchProp,
|
patchProp,
|
||||||
insert,
|
insert,
|
||||||
remove,
|
remove,
|
||||||
createElement
|
createElement,
|
||||||
// ...
|
// ...
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -116,12 +116,25 @@ describe('api: createApp', () => {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
setup() {
|
setup() {
|
||||||
provide('foo', 'should not be seen')
|
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')
|
return () => h('div')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
app.provide('foo', 1)
|
app.provide('foo', 1)
|
||||||
|
|
||||||
expect(app.runWithContext(() => inject('foo'))).toBe(1)
|
expect(app.runWithContext(() => inject('foo'))).toBe(1)
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
app.mount(root)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
app.runWithContext(() => {
|
app.runWithContext(() => {
|
||||||
|
|
|
@ -155,12 +155,12 @@ describe('component: emit', () => {
|
||||||
render() {},
|
render() {},
|
||||||
created() {
|
created() {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
this.$emit('bar')
|
this.$emit('bar-baz')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
render(h(Foo), nodeOps.createElement('div'))
|
render(h(Foo), nodeOps.createElement('div'))
|
||||||
expect(
|
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()
|
).toHaveBeenWarned()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -172,12 +172,12 @@ describe('component: emit', () => {
|
||||||
render() {},
|
render() {},
|
||||||
created() {
|
created() {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
this.$emit('bar')
|
this.$emit('bar-baz')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
render(h(Foo), nodeOps.createElement('div'))
|
render(h(Foo), nodeOps.createElement('div'))
|
||||||
expect(
|
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()
|
).toHaveBeenWarned()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -197,6 +197,22 @@ describe('component: emit', () => {
|
||||||
).not.toHaveBeenWarned()
|
).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', () => {
|
test('validator warning', () => {
|
||||||
const Foo = defineComponent({
|
const Foo = defineComponent({
|
||||||
emits: {
|
emits: {
|
||||||
|
|
|
@ -213,7 +213,7 @@ describe('component: slots', () => {
|
||||||
expect(instance.slots.default()).toMatchObject([normalizeVNode('footer')])
|
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 flag1 = ref(1)
|
||||||
const flag2 = ref(2)
|
const flag2 = ref(2)
|
||||||
const spy = vi.fn()
|
const spy = vi.fn()
|
||||||
|
@ -255,6 +255,48 @@ describe('component: slots', () => {
|
||||||
expect(spy).toHaveBeenCalledTimes(2)
|
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', () => {
|
test('should not warn when mounting another app in setup', () => {
|
||||||
const Comp = {
|
const Comp = {
|
||||||
setup(_: any, { slots }: any) {
|
setup(_: any, { slots }: any) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
KeepAlive,
|
KeepAlive,
|
||||||
Suspense,
|
Suspense,
|
||||||
type SuspenseProps,
|
type SuspenseProps,
|
||||||
|
createCommentVNode,
|
||||||
h,
|
h,
|
||||||
nextTick,
|
nextTick,
|
||||||
nodeOps,
|
nodeOps,
|
||||||
|
@ -2035,7 +2036,7 @@ describe('Suspense', () => {
|
||||||
expect(serializeInner(root)).toBe(`<div>sync</div>`)
|
expect(serializeInner(root)).toBe(`<div>sync</div>`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// #10899
|
// #10899 / #11427
|
||||||
test('KeepAlive + Suspense switch before branch resolves', async () => {
|
test('KeepAlive + Suspense switch before branch resolves', async () => {
|
||||||
const Async1 = defineAsyncComponent({
|
const Async1 = defineAsyncComponent({
|
||||||
render() {
|
render() {
|
||||||
|
@ -2052,14 +2053,20 @@ describe('Suspense', () => {
|
||||||
const root = nodeOps.createElement('div')
|
const root = nodeOps.createElement('div')
|
||||||
const App = {
|
const App = {
|
||||||
render() {
|
render() {
|
||||||
return h(KeepAlive, null, {
|
return h(
|
||||||
default: () => {
|
KeepAlive,
|
||||||
return h(Suspense, null, {
|
{
|
||||||
default: h(components[viewRef.value]),
|
max: 1,
|
||||||
fallback: h('div', 'loading'),
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
{
|
||||||
|
default: () => {
|
||||||
|
return h(Suspense, null, {
|
||||||
|
default: h(components[viewRef.value]),
|
||||||
|
fallback: h('div', 'loading'),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
render(h(App), root)
|
render(h(App), root)
|
||||||
|
@ -2085,6 +2092,35 @@ describe('Suspense', () => {
|
||||||
expect(serializeInner(root)).toBe(`<div>async2</div>`)
|
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
|
// #6416 follow up / #10017
|
||||||
test('Suspense patched during HOC async component re-mount', async () => {
|
test('Suspense patched during HOC async component re-mount', async () => {
|
||||||
const key = ref('k')
|
const key = ref('k')
|
||||||
|
|
|
@ -1388,6 +1388,26 @@ describe('SSR hydration', () => {
|
||||||
expect((container.firstChild!.firstChild as any)._value).toBe(true)
|
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
|
// #5728
|
||||||
test('empty text node in slot', () => {
|
test('empty text node in slot', () => {
|
||||||
const Comp = {
|
const Comp = {
|
||||||
|
|
|
@ -58,11 +58,14 @@ export function inject(
|
||||||
// #2400
|
// #2400
|
||||||
// to support `app.use` plugins,
|
// to support `app.use` plugins,
|
||||||
// fallback to appContext's `provides` if the instance is at root
|
// fallback to appContext's `provides` if the instance is at root
|
||||||
const provides = instance
|
// #11488, in a nested createApp, prioritize using the provides from currentApp
|
||||||
? instance.parent == null
|
const provides = currentApp
|
||||||
? instance.vnode.appContext && instance.vnode.appContext.provides
|
? currentApp._context.provides
|
||||||
: instance.parent.provides
|
: instance
|
||||||
: currentApp!._context.provides
|
? instance.parent == null
|
||||||
|
? instance.vnode.appContext && instance.vnode.appContext.provides
|
||||||
|
: instance.parent.provides
|
||||||
|
: undefined
|
||||||
|
|
||||||
if (provides && (key as string | symbol) in provides) {
|
if (provides && (key as string | symbol) in provides) {
|
||||||
// TS doesn't allow symbol as index type
|
// TS doesn't allow symbol as index type
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
type IfAny,
|
||||||
type LooseRequired,
|
type LooseRequired,
|
||||||
type Prettify,
|
type Prettify,
|
||||||
type UnionToIntersection,
|
type UnionToIntersection,
|
||||||
|
@ -329,7 +330,7 @@ type PropsWithDefaults<
|
||||||
> = Readonly<MappedOmit<T, keyof Defaults>> & {
|
> = Readonly<MappedOmit<T, keyof Defaults>> & {
|
||||||
readonly [K in keyof Defaults]-?: K extends keyof T
|
readonly [K in keyof Defaults]-?: K extends keyof T
|
||||||
? Defaults[K] extends undefined
|
? Defaults[K] extends undefined
|
||||||
? T[K]
|
? IfAny<Defaults[K], NotUndefined<T[K]>, T[K]>
|
||||||
: NotUndefined<T[K]>
|
: NotUndefined<T[K]>
|
||||||
: never
|
: never
|
||||||
} & {
|
} & {
|
||||||
|
|
|
@ -122,10 +122,10 @@ export function emit(
|
||||||
event.startsWith(compatModelEventPrefix))
|
event.startsWith(compatModelEventPrefix))
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (!propsOptions || !(toHandlerKey(event) in propsOptions)) {
|
if (!propsOptions || !(toHandlerKey(camelize(event)) in propsOptions)) {
|
||||||
warn(
|
warn(
|
||||||
`Component emitted event "${event}" but it is neither declared in ` +
|
`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 {
|
} else {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
} from '../component'
|
} from '../component'
|
||||||
import {
|
import {
|
||||||
|
Comment,
|
||||||
type VNode,
|
type VNode,
|
||||||
type VNodeProps,
|
type VNodeProps,
|
||||||
cloneVNode,
|
cloneVNode,
|
||||||
|
@ -205,7 +206,7 @@ const KeepAliveImpl: ComponentOptions = {
|
||||||
|
|
||||||
function pruneCacheEntry(key: CacheKey) {
|
function pruneCacheEntry(key: CacheKey) {
|
||||||
const cached = cache.get(key) as VNode
|
const cached = cache.get(key) as VNode
|
||||||
if (!current || !isSameVNodeType(cached, current)) {
|
if (cached && (!current || !isSameVNodeType(cached, current))) {
|
||||||
unmount(cached)
|
unmount(cached)
|
||||||
} else if (current) {
|
} else if (current) {
|
||||||
// current active instance should no longer be kept-alive.
|
// current active instance should no longer be kept-alive.
|
||||||
|
@ -287,6 +288,12 @@ const KeepAliveImpl: ComponentOptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
let vnode = getInnerChild(rawVNode)
|
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
|
const comp = vnode.type as ConcreteComponent
|
||||||
|
|
||||||
// for async components, name check should be based in its loaded
|
// for async components, name check should be based in its loaded
|
||||||
|
|
|
@ -125,7 +125,7 @@ export type DirectiveArguments = Array<
|
||||||
| [Directive | undefined]
|
| [Directive | undefined]
|
||||||
| [Directive | undefined, any]
|
| [Directive | undefined, any]
|
||||||
| [Directive | undefined, any, string]
|
| [Directive | undefined, any, string]
|
||||||
| [Directive | undefined, any, string, DirectiveModifiers]
|
| [Directive | undefined, any, string | undefined, DirectiveModifiers]
|
||||||
>
|
>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -98,7 +98,7 @@ export function renderSlot(
|
||||||
return rendered
|
return rendered
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidVNode(vnodes: VNodeArrayChildren) {
|
export function ensureValidVNode(vnodes: VNodeArrayChildren) {
|
||||||
return vnodes.some(child => {
|
return vnodes.some(child => {
|
||||||
if (!isVNode(child)) return true
|
if (!isVNode(child)) return true
|
||||||
if (child.type === Comment) return false
|
if (child.type === Comment) return false
|
||||||
|
|
|
@ -451,6 +451,7 @@ export function createHydrationFunctions(
|
||||||
!optimized ||
|
!optimized ||
|
||||||
patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.NEED_HYDRATION)
|
patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.NEED_HYDRATION)
|
||||||
) {
|
) {
|
||||||
|
const isCustomElement = el.tagName.includes('-')
|
||||||
for (const key in props) {
|
for (const key in props) {
|
||||||
// check hydration mismatch
|
// check hydration mismatch
|
||||||
if (
|
if (
|
||||||
|
@ -467,7 +468,8 @@ export function createHydrationFunctions(
|
||||||
(key.endsWith('value') || key === 'indeterminate')) ||
|
(key.endsWith('value') || key === 'indeterminate')) ||
|
||||||
(isOn(key) && !isReservedProp(key)) ||
|
(isOn(key) && !isReservedProp(key)) ||
|
||||||
// force hydrate v-bind with .prop modifiers
|
// force hydrate v-bind with .prop modifiers
|
||||||
key[0] === '.'
|
key[0] === '.' ||
|
||||||
|
isCustomElement
|
||||||
) {
|
) {
|
||||||
patchProp(el, key, null, props[key], undefined, parentComponent)
|
patchProp(el, key, null, props[key], undefined, parentComponent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -397,6 +397,7 @@ import {
|
||||||
import { renderComponentRoot } from './componentRenderUtils'
|
import { renderComponentRoot } from './componentRenderUtils'
|
||||||
import { setCurrentRenderingInstance } from './componentRenderContext'
|
import { setCurrentRenderingInstance } from './componentRenderContext'
|
||||||
import { isVNode, normalizeVNode } from './vnode'
|
import { isVNode, normalizeVNode } from './vnode'
|
||||||
|
import { ensureValidVNode } from './helpers/renderSlot'
|
||||||
|
|
||||||
const _ssrUtils = {
|
const _ssrUtils = {
|
||||||
createComponentInstance,
|
createComponentInstance,
|
||||||
|
@ -406,6 +407,7 @@ const _ssrUtils = {
|
||||||
isVNode,
|
isVNode,
|
||||||
normalizeVNode,
|
normalizeVNode,
|
||||||
getComponentPublicInstance,
|
getComponentPublicInstance,
|
||||||
|
ensureValidVNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2356,13 +2356,13 @@ function baseCreateRenderer(
|
||||||
namespace,
|
namespace,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
container._vnode = vnode
|
||||||
if (!isFlushing) {
|
if (!isFlushing) {
|
||||||
isFlushing = true
|
isFlushing = true
|
||||||
flushPreFlushCbs()
|
flushPreFlushCbs()
|
||||||
flushPostFlushCbs()
|
flushPostFlushCbs()
|
||||||
isFlushing = false
|
isFlushing = false
|
||||||
}
|
}
|
||||||
container._vnode = vnode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const internals: RendererInternals = {
|
const internals: RendererInternals = {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
# @vue/runtime-dom
|
# @vue/runtime-dom
|
||||||
|
|
||||||
``` js
|
```js
|
||||||
import { h, createApp } from '@vue/runtime-dom'
|
import { h, createApp } from '@vue/runtime-dom'
|
||||||
|
|
||||||
const RootComponent = {
|
const RootComponent = {
|
||||||
render() {
|
render() {
|
||||||
return h('div', 'hello world')
|
return h('div', 'hello world')
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
createApp(RootComponent).mount('#app')
|
createApp(RootComponent).mount('#app')
|
||||||
|
|
|
@ -114,6 +114,7 @@ describe('defineCustomElement', () => {
|
||||||
myInputEl.removeAttribute('value')
|
myInputEl.removeAttribute('value')
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(inputEl.value).toBe('')
|
expect(inputEl.value).toBe('')
|
||||||
|
app.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not unmount on move', async () => {
|
test('should not unmount on move', async () => {
|
||||||
|
@ -1158,4 +1159,33 @@ describe('defineCustomElement', () => {
|
||||||
expect(e.shadowRoot?.innerHTML).toBe('<div>app-injected</div>')
|
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',
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
Static,
|
Static,
|
||||||
type VNode,
|
type VNode,
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
|
onBeforeMount,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUnmounted,
|
onUnmounted,
|
||||||
warn,
|
warn,
|
||||||
|
@ -46,8 +47,11 @@ export function useCssVars(getter: (ctx: any) => Record<string, string>) {
|
||||||
updateTeleports(vars)
|
updateTeleports(vars)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onBeforeMount(() => {
|
||||||
watchPostEffect(setVars)
|
watchPostEffect(setVars)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
const ob = new MutationObserver(setVars)
|
const ob = new MutationObserver(setVars)
|
||||||
ob.observe(instance.subTree.el!.parentNode, { childList: true })
|
ob.observe(instance.subTree.el!.parentNode, { childList: true })
|
||||||
onUnmounted(() => ob.disconnect())
|
onUnmounted(() => ob.disconnect())
|
||||||
|
|
|
@ -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.
|
It can also be used as a reference for implementing a custom renderer.
|
||||||
|
|
||||||
``` js
|
```js
|
||||||
import { h, render, nodeOps, dumpOps } from '@vue/runtime-test'
|
import { h, render, nodeOps, dumpOps } from '@vue/runtime-test'
|
||||||
|
|
||||||
const App = {
|
const App = {
|
||||||
|
|
|
@ -200,17 +200,4 @@ describe('test renderer', () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(serialize(root)).toBe(`<div><span>1, 2</span></div>`)
|
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()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
```ts
|
```ts
|
||||||
function renderToString(
|
function renderToString(
|
||||||
input: App | VNode,
|
input: App | VNode,
|
||||||
context?: SSRContext
|
context?: SSRContext,
|
||||||
): Promise<string>
|
): Promise<string>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ const { renderToString } = require('@vue/server-renderer')
|
||||||
|
|
||||||
const app = createSSRApp({
|
const app = createSSRApp({
|
||||||
data: () => ({ msg: 'hello' }),
|
data: () => ({ msg: 'hello' }),
|
||||||
template: `<div>{{ msg }}</div>`
|
template: `<div>{{ msg }}</div>`,
|
||||||
})
|
})
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
|
@ -74,7 +74,7 @@ Render and pipe to an existing [Node.js Writable stream](https://nodejs.org/api/
|
||||||
function pipeToNodeWritable(
|
function pipeToNodeWritable(
|
||||||
input: App | VNode,
|
input: App | VNode,
|
||||||
context: SSRContext = {},
|
context: SSRContext = {},
|
||||||
writable: Writable
|
writable: Writable,
|
||||||
): void
|
): void
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ Renders input as a [Web ReadableStream](https://developer.mozilla.org/en-US/docs
|
||||||
```ts
|
```ts
|
||||||
function renderToWebStream(
|
function renderToWebStream(
|
||||||
input: App | VNode,
|
input: App | VNode,
|
||||||
context?: SSRContext
|
context?: SSRContext,
|
||||||
): ReadableStream
|
): ReadableStream
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ Render and pipe to an existing [Web WritableStream](https://developer.mozilla.or
|
||||||
function pipeToWebWritable(
|
function pipeToWebWritable(
|
||||||
input: App | VNode,
|
input: App | VNode,
|
||||||
context: SSRContext = {},
|
context: SSRContext = {},
|
||||||
writable: WritableStream
|
writable: WritableStream,
|
||||||
): void
|
): void
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@ Renders input in streaming mode using a simple readable interface.
|
||||||
function renderToSimpleStream(
|
function renderToSimpleStream(
|
||||||
input: App | VNode,
|
input: App | VNode,
|
||||||
context: SSRContext,
|
context: SSRContext,
|
||||||
options: SimpleReadable
|
options: SimpleReadable,
|
||||||
): SimpleReadable
|
): SimpleReadable
|
||||||
|
|
||||||
interface SimpleReadable {
|
interface SimpleReadable {
|
||||||
|
@ -172,7 +172,7 @@ renderToSimpleStream(
|
||||||
},
|
},
|
||||||
destroy(err) {
|
destroy(err) {
|
||||||
// error encountered
|
// error encountered
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
|
@ -153,4 +153,54 @@ describe('ssr: slot', () => {
|
||||||
),
|
),
|
||||||
).toBe(`<div><p>1</p><p>2</p></div>`)
|
).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>`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { ComponentInternalInstance, Slots } from 'vue'
|
import { type ComponentInternalInstance, type Slots, ssrUtils } from 'vue'
|
||||||
import {
|
import {
|
||||||
type Props,
|
type Props,
|
||||||
type PushFn,
|
type PushFn,
|
||||||
|
@ -7,6 +7,8 @@ import {
|
||||||
} from '../render'
|
} from '../render'
|
||||||
import { isArray } from '@vue/shared'
|
import { isArray } from '@vue/shared'
|
||||||
|
|
||||||
|
const { ensureValidVNode } = ssrUtils
|
||||||
|
|
||||||
export type SSRSlots = Record<string, SSRSlot>
|
export type SSRSlots = Record<string, SSRSlot>
|
||||||
export type SSRSlot = (
|
export type SSRSlot = (
|
||||||
props: Props,
|
props: Props,
|
||||||
|
@ -61,8 +63,18 @@ export function ssrRenderSlotInner(
|
||||||
slotScopeId ? ' ' + slotScopeId : '',
|
slotScopeId ? ' ' + slotScopeId : '',
|
||||||
)
|
)
|
||||||
if (isArray(ret)) {
|
if (isArray(ret)) {
|
||||||
// normal slot
|
const validSlotContent = ensureValidVNode(ret)
|
||||||
renderVNodeChildren(push, ret, parentComponent, slotScopeId)
|
if (validSlotContent) {
|
||||||
|
// normal slot
|
||||||
|
renderVNodeChildren(
|
||||||
|
push,
|
||||||
|
validSlotContent,
|
||||||
|
parentComponent,
|
||||||
|
slotScopeId,
|
||||||
|
)
|
||||||
|
} else if (fallbackRenderFn) {
|
||||||
|
fallbackRenderFn()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// ssr slot.
|
// ssr slot.
|
||||||
// check if the slot renders all comments, in which case use the fallback
|
// check if the slot renders all comments, in which case use the fallback
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
@ -7,13 +7,16 @@
|
||||||
<link rel="icon" type="image/svg" href="/logo.svg" />
|
<link rel="icon" type="image/svg" href="/logo.svg" />
|
||||||
<title>Vue SFC Playground</title>
|
<title>Vue SFC Playground</title>
|
||||||
<script>
|
<script>
|
||||||
const savedPreferDark = localStorage.getItem('vue-sfc-playground-prefer-dark')
|
const savedPreferDark = localStorage.getItem(
|
||||||
if (
|
'vue-sfc-playground-prefer-dark',
|
||||||
savedPreferDark === 'true' ||
|
)
|
||||||
(!savedPreferDark && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
if (
|
||||||
) {
|
savedPreferDark === 'true' ||
|
||||||
|
(!savedPreferDark &&
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
|
) {
|
||||||
document.documentElement.classList.add('dark')
|
document.documentElement.classList.add('dark')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
"serve": "vite preview"
|
"serve": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.1.1",
|
"@vitejs/plugin-vue": "^5.1.2",
|
||||||
"vite": "catalog:"
|
"vite": "catalog:"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.1.1",
|
"@vitejs/plugin-vue": "^5.1.2",
|
||||||
"vite": "^5.3.5"
|
"vite": "^5.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<title>Vue Template Explorer</title>
|
<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
|
||||||
<link rel="stylesheet" href="./style.css">
|
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="header"></div>
|
||||||
<div id="source" class="editor"></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 src="https://unpkg.com/monaco-editor@0.20.0/min/vs/loader.js"></script>
|
||||||
<script>
|
<script>
|
||||||
require.config({
|
require.config({
|
||||||
paths: {
|
paths: {
|
||||||
'vs': 'https://unpkg.com/monaco-editor@0.20.0/min/vs'
|
vs: 'https://unpkg.com/monaco-editor@0.20.0/min/vs',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<script src="./dist/template-explorer.global.js"></script>
|
<script src="./dist/template-explorer.global.js"></script>
|
||||||
<script>
|
<script>
|
||||||
require(['vs/editor/editor.main'], init /* injected by build */)
|
require(['vs/editor/editor.main'], init /* injected by build */)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<title>Vue Template Explorer</title>
|
<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
|
||||||
<link rel="stylesheet" href="./style.css">
|
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="header"></div>
|
||||||
<div id="source" class="editor"></div>
|
<div id="source" class="editor"></div>
|
||||||
|
@ -8,13 +12,13 @@
|
||||||
|
|
||||||
<script src="./node_modules/monaco-editor/min/vs/loader.js"></script>
|
<script src="./node_modules/monaco-editor/min/vs/loader.js"></script>
|
||||||
<script>
|
<script>
|
||||||
require.config({
|
require.config({
|
||||||
paths: {
|
paths: {
|
||||||
'vs': './node_modules/monaco-editor/min/vs'
|
vs: './node_modules/monaco-editor/min/vs',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<script src="./dist/template-explorer.global.js"></script>
|
<script src="./dist/template-explorer.global.js"></script>
|
||||||
<script>
|
<script>
|
||||||
require(['vs/editor/editor.main'], init /* injected by build */)
|
require(['vs/editor/editor.main'], init /* injected by build */)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -84,12 +84,12 @@ The following workflow walks through the steps of migrating an actual Vue 2 app
|
||||||
...options,
|
...options,
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
compatConfig: {
|
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 = {
|
module.exports = {
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
vue: '@vue/compat'
|
vue: '@vue/compat',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -114,13 +114,13 @@ The following workflow walks through the steps of migrating an actual Vue 2 app
|
||||||
options: {
|
options: {
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
compatConfig: {
|
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 {
|
export default {
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
vue: '@vue/compat'
|
vue: '@vue/compat',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue({
|
vue({
|
||||||
template: {
|
template: {
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
compatConfig: {
|
compatConfig: {
|
||||||
MODE: 2
|
MODE: 2,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -207,7 +207,7 @@ import { configureCompat } from 'vue'
|
||||||
// disable compat for certain features
|
// disable compat for certain features
|
||||||
configureCompat({
|
configureCompat({
|
||||||
FEATURE_ID_A: false,
|
FEATURE_ID_A: false,
|
||||||
FEATURE_ID_B: false
|
FEATURE_ID_B: false,
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -221,7 +221,7 @@ import { configureCompat } from 'vue'
|
||||||
configureCompat({
|
configureCompat({
|
||||||
MODE: 3,
|
MODE: 3,
|
||||||
FEATURE_ID_A: true,
|
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 {
|
export default {
|
||||||
compatConfig: {
|
compatConfig: {
|
||||||
MODE: 3, // opt-in to Vue 3 behavior for this component only
|
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
|
> Should be fixed upfront or will likely lead to errors
|
||||||
|
|
||||||
| ID | Type | Description | Docs |
|
| 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) |
|
| 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) |
|
| 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_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_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_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) | | |
|
| 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
|
### 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) |
|
| 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) |
|
| 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) |
|
| 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) | |
|
| 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 | |
|
| 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_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 |
|
| CONFIG_SILENT | ◐ | `config.silent` removed |
|
||||||
|
|
||||||
### Compat only (no warning)
|
### 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) |
|
| TRANSITION_CLASSES | ⭘ | Transition enter/leave classes changed | [link](https://v3-migration.vuejs.org/breaking-changes/transition.html) |
|
||||||
|
|
||||||
### Fully Compatible
|
### Fully Compatible
|
||||||
|
|
||||||
| ID | Type | Description | Docs |
|
| ID | Type | Description | Docs |
|
||||||
| ---------------------------- | ---- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
| ---------------------------- | ---- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| GLOBAL_MOUNT | ✔ | new Vue() -> createApp | [link](https://v3-migration.vuejs.org/breaking-changes/global-api.html#mounting-app-instance) |
|
| 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_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_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_SET | ✔ | `Vue.set` removed (no longer needed) | |
|
||||||
| GLOBAL_DELETE | ✔ | `Vue.delete` 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) |
|
| 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_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"` | |
|
| CONFIG_WHITESPACE | ✔ | In Vue 3 whitespace defaults to `"condense"` | |
|
||||||
| INSTANCE_SET | ✔ | `vm.$set` removed (no longer needed) | |
|
| INSTANCE_SET | ✔ | `vm.$set` removed (no longer needed) | |
|
||||||
| INSTANCE_DELETE | ✔ | `vm.$delete` 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_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_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_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_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_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) |
|
| 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_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_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_BEFORE_DESTROY | ✔ | `beforeDestroy` -> `beforeUnmount` | |
|
||||||
| OPTIONS_DESTROYED | ✔ | `destroyed` -> `unmounted` | |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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_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) |
|
| 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) |
|
| 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_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_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) |
|
| 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) |
|
| 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) |
|
| 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_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_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_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_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_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_V_FOR_REF | ✔ | `ref` in `v-for` (compiler support) | |
|
||||||
| COMPILER_NATIVE_TEMPLATE | ✔ | `<template>` with no special directives now renders as native element | |
|
| COMPILER_NATIVE_TEMPLATE | ✔ | `<template>` with no special directives now renders as native element | |
|
||||||
| COMPILER_FILTERS | ✔ | filters (compiler support) | |
|
| COMPILER_FILTERS | ✔ | filters (compiler support) | |
|
||||||
|
|
|
@ -5,8 +5,9 @@
|
||||||
### From CDN or without a Bundler
|
### From CDN or without a Bundler
|
||||||
|
|
||||||
- **`vue(.runtime).global(.prod).js`**:
|
- **`vue(.runtime).global(.prod).js`**:
|
||||||
|
|
||||||
- For direct use via `<script src="...">` in the browser. Exposes the `Vue` global.
|
- 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:
|
- 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.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.
|
- **`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:
|
`esm-bundler` builds of Vue expose global feature flags that can be overwritten at compile time:
|
||||||
|
|
||||||
- `__VUE_OPTIONS_API__`
|
- `__VUE_OPTIONS_API__`
|
||||||
|
|
||||||
- Default: `true`
|
- Default: `true`
|
||||||
- Enable / disable Options API support
|
- Enable / disable Options API support
|
||||||
|
|
||||||
- `__VUE_PROD_DEVTOOLS__`
|
- `__VUE_PROD_DEVTOOLS__`
|
||||||
|
|
||||||
- Default: `false`
|
- Default: `false`
|
||||||
- Enable / disable devtools support in production
|
- Enable / disable devtools support in production
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,14 @@
|
||||||
-webkit-transition: -webkit-transform 50ms ease;
|
-webkit-transition: -webkit-transform 50ms ease;
|
||||||
transition: transform 50ms ease;
|
transition: transform 50ms ease;
|
||||||
}
|
}
|
||||||
.v-appear, .v-enter, .v-leave-active,
|
.v-appear,
|
||||||
.test-appear, .test-enter, .test-leave-active,
|
.v-enter,
|
||||||
.hello, .bye.active,
|
.v-leave-active,
|
||||||
|
.test-appear,
|
||||||
|
.test-enter,
|
||||||
|
.test-leave-active,
|
||||||
|
.hello,
|
||||||
|
.bye.active,
|
||||||
.changed-enter {
|
.changed-enter {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
@ -33,19 +38,35 @@
|
||||||
-webkit-animation: test-leave 100ms;
|
-webkit-animation: test-leave 100ms;
|
||||||
}
|
}
|
||||||
@keyframes test-enter {
|
@keyframes test-enter {
|
||||||
from { opacity: 0 }
|
from {
|
||||||
to { opacity: 1 }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@-webkit-keyframes test-enter {
|
@-webkit-keyframes test-enter {
|
||||||
from { opacity: 0 }
|
from {
|
||||||
to { opacity: 1 }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@keyframes test-leave {
|
@keyframes test-leave {
|
||||||
from { opacity: 1 }
|
from {
|
||||||
to { opacity: 0 }
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@-webkit-keyframes test-leave {
|
@-webkit-keyframes test-leave {
|
||||||
from { opacity: 1 }
|
from {
|
||||||
to { opacity: 0 }
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,59 +3,68 @@
|
||||||
<div id="demo">
|
<div id="demo">
|
||||||
<h1>Latest Vue.js Commits</h1>
|
<h1>Latest Vue.js Commits</h1>
|
||||||
<template v-for="branch in branches">
|
<template v-for="branch in branches">
|
||||||
<input type="radio"
|
<input
|
||||||
|
type="radio"
|
||||||
:id="branch"
|
:id="branch"
|
||||||
:value="branch"
|
:value="branch"
|
||||||
name="branch"
|
name="branch"
|
||||||
v-model="currentBranch">
|
v-model="currentBranch"
|
||||||
|
/>
|
||||||
<label :for="branch">{{ branch }}</label>
|
<label :for="branch">{{ branch }}</label>
|
||||||
</template>
|
</template>
|
||||||
<p>vuejs/core@{{ currentBranch }}</p>
|
<p>vuejs/core@{{ currentBranch }}</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="{ html_url, sha, author, commit } in commits">
|
<li v-for="{ html_url, sha, author, commit } in commits">
|
||||||
<a :href="html_url" target="_blank" class="commit">{{ sha.slice(0, 7) }}</a>
|
<a :href="html_url" target="_blank" class="commit"
|
||||||
- <span class="message">{{ truncate(commit.message) }}</span><br>
|
>{{ sha.slice(0, 7) }}</a
|
||||||
by <span class="author"><a :href="author.html_url" target="_blank">{{ commit.author.name }}</a></span>
|
>
|
||||||
|
- <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>
|
at <span class="date">{{ formatDate(commit.author.date) }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<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({
|
Vue.createApp({
|
||||||
data: () => ({
|
data: () => ({
|
||||||
branches: ['main', 'v2-compat'],
|
branches: ['main', 'v2-compat'],
|
||||||
currentBranch: 'main',
|
currentBranch: 'main',
|
||||||
commits: null
|
commits: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.fetchData()
|
this.fetchData()
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
currentBranch: 'fetchData'
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
fetchData() {
|
|
||||||
fetch(`${API_URL}${this.currentBranch}`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
this.commits = data
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
truncate(v) {
|
|
||||||
const newline = v.indexOf('\n')
|
watch: {
|
||||||
return newline > 0 ? v.slice(0, newline) : v
|
currentBranch: 'fetchData',
|
||||||
},
|
},
|
||||||
formatDate(v) {
|
|
||||||
return v.replace(/T|Z/g, ' ')
|
methods: {
|
||||||
}
|
fetchData() {
|
||||||
}
|
fetch(`${API_URL}${this.currentBranch}`)
|
||||||
}).mount('#demo')
|
.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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -70,7 +79,8 @@ Vue.createApp({
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.author, .date {
|
.author,
|
||||||
|
.date {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -26,142 +26,141 @@
|
||||||
</script>
|
</script>
|
||||||
<!-- DemoGrid component script -->
|
<!-- DemoGrid component script -->
|
||||||
<script>
|
<script>
|
||||||
const DemoGrid = {
|
const DemoGrid = {
|
||||||
template: '#grid-template',
|
template: '#grid-template',
|
||||||
props: {
|
props: {
|
||||||
data: Array,
|
data: Array,
|
||||||
columns: Array,
|
columns: Array,
|
||||||
filterKey: String
|
filterKey: String,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
sortKey: '',
|
sortKey: '',
|
||||||
sortOrders: this.columns.reduce((o, key) => (o[key] = 1, o), {})
|
sortOrders: this.columns.reduce((o, key) => ((o[key] = 1), o), {}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
filteredData() {
|
filteredData() {
|
||||||
const sortKey = this.sortKey
|
const sortKey = this.sortKey
|
||||||
const filterKey = this.filterKey && this.filterKey.toLowerCase()
|
const filterKey = this.filterKey && this.filterKey.toLowerCase()
|
||||||
const order = this.sortOrders[sortKey] || 1
|
const order = this.sortOrders[sortKey] || 1
|
||||||
let data = this.data
|
let data = this.data
|
||||||
if (filterKey) {
|
if (filterKey) {
|
||||||
data = data.filter(row => {
|
data = data.filter(row => {
|
||||||
return Object.keys(row).some(key => {
|
return Object.keys(row).some(key => {
|
||||||
return String(row[key]).toLowerCase().indexOf(filterKey) > -1
|
return String(row[key]).toLowerCase().indexOf(filterKey) > -1
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (sortKey) {
|
if (sortKey) {
|
||||||
data = data.slice().sort((a, b) => {
|
data = data.slice().sort((a, b) => {
|
||||||
a = a[sortKey]
|
a = a[sortKey]
|
||||||
b = b[sortKey]
|
b = b[sortKey]
|
||||||
return (a === b ? 0 : a > b ? 1 : -1) * order
|
return (a === b ? 0 : a > b ? 1 : -1) * order
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
sortBy(key) {
|
sortBy(key) {
|
||||||
this.sortKey = key
|
this.sortKey = key
|
||||||
this.sortOrders[key] = this.sortOrders[key] * -1
|
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>
|
</script>
|
||||||
|
|
||||||
<!-- App template (in DOM) -->
|
<!-- App template (in DOM) -->
|
||||||
<div id="demo">
|
<div id="demo">
|
||||||
<form id="search">
|
<form id="search">Search <input name="query" v-model="searchQuery" /></form>
|
||||||
Search <input name="query" v-model="searchQuery">
|
<demo-grid :data="gridData" :columns="gridColumns" :filter-key="searchQuery">
|
||||||
</form>
|
|
||||||
<demo-grid
|
|
||||||
:data="gridData"
|
|
||||||
:columns="gridColumns"
|
|
||||||
:filter-key="searchQuery">
|
|
||||||
</demo-grid>
|
</demo-grid>
|
||||||
</div>
|
</div>
|
||||||
<!-- App script -->
|
<!-- App script -->
|
||||||
<script>
|
<script>
|
||||||
Vue.createApp({
|
Vue.createApp({
|
||||||
components: {
|
components: {
|
||||||
DemoGrid
|
DemoGrid,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
gridColumns: ['name', 'power'],
|
gridColumns: ['name', 'power'],
|
||||||
gridData: [
|
gridData: [
|
||||||
{ name: 'Chuck Norris', power: Infinity },
|
{ name: 'Chuck Norris', power: Infinity },
|
||||||
{ name: 'Bruce Lee', power: 9000 },
|
{ name: 'Bruce Lee', power: 9000 },
|
||||||
{ name: 'Jackie Chan', power: 7000 },
|
{ name: 'Jackie Chan', power: 7000 },
|
||||||
{ name: 'Jet Li', power: 8000 }
|
{ name: 'Jet Li', power: 8000 },
|
||||||
]
|
],
|
||||||
})
|
}),
|
||||||
}).mount('#demo')
|
}).mount('#demo')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Helvetica Neue, Arial, sans-serif;
|
font-family:
|
||||||
font-size: 14px;
|
Helvetica Neue,
|
||||||
color: #444;
|
Arial,
|
||||||
}
|
sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border: 2px solid #42b983;
|
border: 2px solid #42b983;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background-color: #42b983;
|
background-color: #42b983;
|
||||||
color: rgba(255,255,255,0.66);
|
color: rgba(255, 255, 255, 0.66);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
th,
|
||||||
min-width: 120px;
|
td {
|
||||||
padding: 10px 20px;
|
min-width: 120px;
|
||||||
}
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
th.active {
|
th.active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
th.active .arrow {
|
th.active .arrow {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
opacity: 0.66;
|
opacity: 0.66;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow.asc {
|
.arrow.asc {
|
||||||
border-left: 4px solid transparent;
|
border-left: 4px solid transparent;
|
||||||
border-right: 4px solid transparent;
|
border-right: 4px solid transparent;
|
||||||
border-bottom: 4px solid #fff;
|
border-bottom: 4px solid #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow.dsc {
|
.arrow.dsc {
|
||||||
border-left: 4px solid transparent;
|
border-left: 4px solid transparent;
|
||||||
border-right: 4px solid transparent;
|
border-right: 4px solid transparent;
|
||||||
border-top: 4px solid #fff;
|
border-top: 4px solid #fff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,55 +8,58 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
Vue.createApp({
|
Vue.createApp({
|
||||||
data: () => ({
|
data: () => ({
|
||||||
input: '# hello'
|
input: '# hello',
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
compiledMarkdown() {
|
compiledMarkdown() {
|
||||||
return marked.marked(this.input, { sanitize: true })
|
return marked.marked(this.input, { sanitize: true })
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
update: _.debounce(function (e) {
|
update: _.debounce(function (e) {
|
||||||
this.input = e.target.value
|
this.input = e.target.value
|
||||||
}, 50)
|
}, 50),
|
||||||
}
|
},
|
||||||
}).mount('#editor')
|
}).mount('#editor')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html, body, #editor {
|
html,
|
||||||
margin: 0;
|
body,
|
||||||
height: 100%;
|
#editor {
|
||||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
margin: 0;
|
||||||
color: #333;
|
height: 100%;
|
||||||
}
|
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
textarea, #editor div {
|
textarea,
|
||||||
display: inline-block;
|
#editor div {
|
||||||
overflow: auto;
|
display: inline-block;
|
||||||
width: 50%;
|
overflow: auto;
|
||||||
height: 100%;
|
width: 50%;
|
||||||
vertical-align: top;
|
height: 100%;
|
||||||
-webkit-box-sizing: border-box;
|
vertical-align: top;
|
||||||
-moz-box-sizing: border-box;
|
-webkit-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
padding: 0 20px;
|
box-sizing: border-box;
|
||||||
}
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
border: none;
|
border: none;
|
||||||
border-right: 1px solid #ccc;
|
border-right: 1px solid #ccc;
|
||||||
resize: none;
|
resize: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: 'Monaco', courier, monospace;
|
font-family: 'Monaco', courier, monospace;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
color: #f66;
|
color: #f66;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,37 +1,33 @@
|
||||||
<script src="../../dist/vue.global.js"></script>
|
<script src="../../dist/vue.global.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// math helper...
|
// math helper...
|
||||||
function valueToPoint (value, index, total) {
|
function valueToPoint(value, index, total) {
|
||||||
var x = 0
|
var x = 0
|
||||||
var y = -value * 0.8
|
var y = -value * 0.8
|
||||||
var angle = Math.PI * 2 / total * index
|
var angle = ((Math.PI * 2) / total) * index
|
||||||
var cos = Math.cos(angle)
|
var cos = Math.cos(angle)
|
||||||
var sin = Math.sin(angle)
|
var sin = Math.sin(angle)
|
||||||
var tx = x * cos - y * sin + 100
|
var tx = x * cos - y * sin + 100
|
||||||
var ty = x * sin + y * cos + 100
|
var ty = x * sin + y * cos + 100
|
||||||
return {
|
return {
|
||||||
x: tx,
|
x: tx,
|
||||||
y: ty
|
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<!-- template for the polygraph component. -->
|
<!-- template for the polygraph component. -->
|
||||||
|
@ -49,23 +45,25 @@ const AxisLabel = {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const Polygraph = {
|
const Polygraph = {
|
||||||
props: ['stats'],
|
props: ['stats'],
|
||||||
template: '#polygraph-template',
|
template: '#polygraph-template',
|
||||||
computed: {
|
computed: {
|
||||||
// a computed property for the polygon's points
|
// a computed property for the polygon's points
|
||||||
points() {
|
points() {
|
||||||
const total = this.stats.length
|
const total = this.stats.length
|
||||||
return this.stats.map((stat, i) => {
|
return this.stats
|
||||||
const point = valueToPoint(stat.value, i, total)
|
.map((stat, i) => {
|
||||||
return point.x + ',' + point.y
|
const point = valueToPoint(stat.value, i, total)
|
||||||
}).join(' ')
|
return point.x + ',' + point.y
|
||||||
}
|
})
|
||||||
},
|
.join(' ')
|
||||||
components: {
|
},
|
||||||
AxisLabel
|
},
|
||||||
|
components: {
|
||||||
|
AxisLabel,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- demo root element -->
|
<!-- demo root element -->
|
||||||
|
@ -77,86 +75,92 @@ const Polygraph = {
|
||||||
<!-- controls -->
|
<!-- controls -->
|
||||||
<div v-for="stat in stats">
|
<div v-for="stat in stats">
|
||||||
<label>{{stat.label}}</label>
|
<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>
|
<span>{{stat.value}}</span>
|
||||||
<button @click="remove(stat)" class="remove">X</button>
|
<button @click="remove(stat)" class="remove">X</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="add">
|
<form id="add">
|
||||||
<input name="newlabel" v-model="newLabel">
|
<input name="newlabel" v-model="newLabel" />
|
||||||
<button @click="add">Add a Stat</button>
|
<button @click="add">Add a Stat</button>
|
||||||
</form>
|
</form>
|
||||||
<pre id="raw">{{ stats }}</pre>
|
<pre id="raw">{{ stats }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const globalStats = [
|
const globalStats = [
|
||||||
{ label: 'A', value: 100 },
|
{ label: 'A', value: 100 },
|
||||||
{ label: 'B', value: 100 },
|
{ label: 'B', value: 100 },
|
||||||
{ label: 'C', value: 100 },
|
{ label: 'C', value: 100 },
|
||||||
{ label: 'D', value: 100 },
|
{ label: 'D', value: 100 },
|
||||||
{ label: 'E', value: 100 },
|
{ label: 'E', value: 100 },
|
||||||
{ label: 'F', value: 100 }
|
{ label: 'F', value: 100 },
|
||||||
]
|
]
|
||||||
|
|
||||||
Vue.createApp({
|
Vue.createApp({
|
||||||
components: {
|
components: {
|
||||||
Polygraph
|
Polygraph,
|
||||||
},
|
|
||||||
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) {
|
data: () => ({
|
||||||
if (this.stats.length > 3) {
|
newLabel: '',
|
||||||
this.stats.splice(this.stats.indexOf(stat), 1)
|
stats: globalStats,
|
||||||
} else {
|
}),
|
||||||
alert('Can\'t delete more!')
|
methods: {
|
||||||
}
|
add(e) {
|
||||||
}
|
e.preventDefault()
|
||||||
}
|
if (!this.newLabel) return
|
||||||
}).mount('#demo')
|
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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Helvetica Neue, Arial, sans-serif;
|
font-family:
|
||||||
}
|
Helvetica Neue,
|
||||||
|
Arial,
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
polygon {
|
polygon {
|
||||||
fill: #42b983;
|
fill: #42b983;
|
||||||
opacity: .75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
circle {
|
circle {
|
||||||
fill: transparent;
|
fill: transparent;
|
||||||
stroke: #999;
|
stroke: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
text {
|
text {
|
||||||
font-family: Helvetica Neue, Arial, sans-serif;
|
font-family:
|
||||||
|
Helvetica Neue,
|
||||||
|
Arial,
|
||||||
|
sans-serif;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
fill: #666;
|
fill: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#raw {
|
#raw {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 300px;
|
left: 300px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,49 +1,81 @@
|
||||||
<script src="../../dist/vue.global.js"></script>
|
<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">
|
<div id="app">
|
||||||
<section class="todoapp">
|
<section class="todoapp">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<h1>todos</h1>
|
<h1>todos</h1>
|
||||||
<input class="new-todo"
|
<input
|
||||||
autofocus autocomplete="off"
|
class="new-todo"
|
||||||
placeholder="What needs to be done?"
|
autofocus
|
||||||
v-model="newTodo"
|
autocomplete="off"
|
||||||
@keyup.enter="addTodo">
|
placeholder="What needs to be done?"
|
||||||
|
v-model="newTodo"
|
||||||
|
@keyup.enter="addTodo"
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
<section class="main" v-show="todos.length">
|
<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>
|
<label for="toggle-all">Mark all as complete</label>
|
||||||
<ul class="todo-list">
|
<ul class="todo-list">
|
||||||
<li v-for="todo in filteredTodos"
|
<li
|
||||||
class="todo"
|
v-for="todo in filteredTodos"
|
||||||
:key="todo.id"
|
class="todo"
|
||||||
:class="{ completed: todo.completed, editing: todo === editedTodo }">
|
:key="todo.id"
|
||||||
|
:class="{ completed: todo.completed, editing: todo === editedTodo }"
|
||||||
|
>
|
||||||
<div class="view">
|
<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>
|
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
|
||||||
<button class="destroy" @click="removeTodo(todo)"></button>
|
<button class="destroy" @click="removeTodo(todo)"></button>
|
||||||
</div>
|
</div>
|
||||||
<input class="edit" type="text"
|
<input
|
||||||
v-model="todo.title"
|
class="edit"
|
||||||
v-todo-focus="todo === editedTodo"
|
type="text"
|
||||||
@blur="doneEdit(todo)"
|
v-model="todo.title"
|
||||||
@keyup.enter="doneEdit(todo)"
|
v-todo-focus="todo === editedTodo"
|
||||||
@keyup.escape="cancelEdit(todo)"
|
@blur="doneEdit(todo)"
|
||||||
>
|
@keyup.enter="doneEdit(todo)"
|
||||||
|
@keyup.escape="cancelEdit(todo)"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<footer class="footer" v-show="todos.length">
|
<footer class="footer" v-show="todos.length">
|
||||||
<span class="todo-count">
|
<span class="todo-count">
|
||||||
<strong>{{ remaining }}</strong> <span>{{ pluralize(remaining) }} left</span>
|
<strong>{{ remaining }}</strong>
|
||||||
</span>
|
<span>{{ pluralize(remaining) }} left</span>
|
||||||
|
</span>
|
||||||
<ul class="filters">
|
<ul class="filters">
|
||||||
<li><a href="#/all" :class="{ selected: visibility === 'all' }">All</a></li>
|
<li>
|
||||||
<li><a href="#/active" :class="{ selected: visibility === 'active' }">Active</a></li>
|
<a href="#/all" :class="{ selected: visibility === 'all' }">All</a>
|
||||||
<li><a href="#/completed" :class="{ selected: visibility === 'completed' }">Completed</a></li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#/active" :class="{ selected: visibility === 'active' }"
|
||||||
|
>Active</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#/completed"
|
||||||
|
:class="{ selected: visibility === 'completed' }"
|
||||||
|
>Completed</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</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
|
Clear completed
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -51,146 +83,146 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const STORAGE_KEY = 'todos-vuejs-3.x'
|
const STORAGE_KEY = 'todos-vuejs-3.x'
|
||||||
const todoStorage = {
|
const todoStorage = {
|
||||||
fetch() {
|
fetch() {
|
||||||
const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
|
const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
|
||||||
todos.forEach((todo, index) => {
|
todos.forEach((todo, index) => {
|
||||||
todo.id = 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
|
|
||||||
})
|
})
|
||||||
this.newTodo = ''
|
todoStorage.uid = todos.length
|
||||||
|
return todos
|
||||||
},
|
},
|
||||||
|
save(todos) {
|
||||||
removeTodo(todo) {
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
|
||||||
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')
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
|
@ -22,43 +22,42 @@
|
||||||
</script>
|
</script>
|
||||||
<!-- item script -->
|
<!-- item script -->
|
||||||
<script>
|
<script>
|
||||||
const TreeItem = {
|
const TreeItem = {
|
||||||
name: 'TreeItem', // necessary for self-reference
|
name: 'TreeItem', // necessary for self-reference
|
||||||
template: '#item-template',
|
template: '#item-template',
|
||||||
props: {
|
props: {
|
||||||
model: Object
|
model: Object,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
open: false
|
open: false,
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isFolder() {
|
|
||||||
return this.model.children &&
|
|
||||||
this.model.children.length
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggle() {
|
|
||||||
if (this.isFolder) {
|
|
||||||
this.open = !this.open
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
changeType() {
|
computed: {
|
||||||
if (!this.isFolder) {
|
isFolder() {
|
||||||
this.model.children = []
|
return this.model.children && this.model.children.length
|
||||||
this.addChild()
|
},
|
||||||
this.open = true
|
},
|
||||||
}
|
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>
|
</script>
|
||||||
|
|
||||||
<p>(You can double click on an item to turn it into a folder.)</p>
|
<p>(You can double click on an item to turn it into a folder.)</p>
|
||||||
|
@ -69,43 +68,37 @@ const TreeItem = {
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const treeData = {
|
const treeData = {
|
||||||
name: 'My Tree',
|
name: 'My Tree',
|
||||||
children: [
|
children: [
|
||||||
{ name: 'hello' },
|
{ name: 'hello' },
|
||||||
{ name: 'wat' },
|
{ name: 'wat' },
|
||||||
{
|
{
|
||||||
name: 'child folder',
|
name: 'child folder',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'child folder',
|
name: 'child folder',
|
||||||
children: [
|
children: [{ name: 'hello' }, { name: 'wat' }],
|
||||||
{ name: 'hello' },
|
},
|
||||||
{ name: 'wat' }
|
{ name: 'hello' },
|
||||||
]
|
{ name: 'wat' },
|
||||||
},
|
{
|
||||||
{ name: 'hello' },
|
name: 'child folder',
|
||||||
{ name: 'wat' },
|
children: [{ name: 'hello' }, { name: 'wat' }],
|
||||||
{
|
},
|
||||||
name: 'child folder',
|
],
|
||||||
children: [
|
},
|
||||||
{ name: 'hello' },
|
],
|
||||||
{ name: 'wat' }
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.createApp({
|
Vue.createApp({
|
||||||
components: {
|
components: {
|
||||||
TreeItem
|
TreeItem,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
treeData
|
treeData,
|
||||||
})
|
}),
|
||||||
}).mount('#demo')
|
}).mount('#demo')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -3,58 +3,67 @@
|
||||||
<div id="demo">
|
<div id="demo">
|
||||||
<h1>Latest Vue.js Commits</h1>
|
<h1>Latest Vue.js Commits</h1>
|
||||||
<template v-for="branch in branches">
|
<template v-for="branch in branches">
|
||||||
<input type="radio"
|
<input
|
||||||
|
type="radio"
|
||||||
:id="branch"
|
:id="branch"
|
||||||
:value="branch"
|
:value="branch"
|
||||||
name="branch"
|
name="branch"
|
||||||
v-model="currentBranch">
|
v-model="currentBranch"
|
||||||
|
/>
|
||||||
<label :for="branch">{{ branch }}</label>
|
<label :for="branch">{{ branch }}</label>
|
||||||
</template>
|
</template>
|
||||||
<p>vuejs/core@{{ currentBranch }}</p>
|
<p>vuejs/core@{{ currentBranch }}</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="{ html_url, sha, author, commit } in commits">
|
<li v-for="{ html_url, sha, author, commit } in commits">
|
||||||
<a :href="html_url" target="_blank" class="commit">{{ sha.slice(0, 7) }}</a>
|
<a :href="html_url" target="_blank" class="commit"
|
||||||
- <span class="message">{{ truncate(commit.message) }}</span><br>
|
>{{ sha.slice(0, 7) }}</a
|
||||||
by <span class="author"><a :href="author.html_url" target="_blank">{{ commit.author.name }}</a></span>
|
>
|
||||||
|
- <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>
|
at <span class="date">{{ formatDate(commit.author.date) }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const { createApp, ref, watchEffect } = Vue
|
const { createApp, ref, watchEffect } = Vue
|
||||||
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=`
|
||||||
|
|
||||||
const truncate = v => {
|
const truncate = v => {
|
||||||
const newline = v.indexOf('\n')
|
const newline = v.indexOf('\n')
|
||||||
return newline > 0 ? v.slice(0, newline) : v
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -69,7 +78,8 @@ createApp({
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.author, .date {
|
.author,
|
||||||
|
.date {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -26,148 +26,147 @@
|
||||||
</script>
|
</script>
|
||||||
<!-- DemoGrid component script -->
|
<!-- DemoGrid component script -->
|
||||||
<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 = {
|
const DemoGrid = {
|
||||||
template: '#grid-template',
|
template: '#grid-template',
|
||||||
props: {
|
props: {
|
||||||
data: Array,
|
data: Array,
|
||||||
columns: Array,
|
columns: Array,
|
||||||
filterKey: String
|
filterKey: String,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
sortKey: '',
|
sortKey: '',
|
||||||
sortOrders: props.columns.reduce((o, key) => (o[key] = 1, o), {})
|
sortOrders: props.columns.reduce((o, key) => ((o[key] = 1), o), {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredData = computed(() => {
|
const filteredData = computed(() => {
|
||||||
let { data, filterKey } = props
|
let { data, filterKey } = props
|
||||||
if (filterKey) {
|
if (filterKey) {
|
||||||
filterKey = filterKey.toLowerCase()
|
filterKey = filterKey.toLowerCase()
|
||||||
data = data.filter(row => {
|
data = data.filter(row => {
|
||||||
return Object.keys(row).some(key => {
|
return Object.keys(row).some(key => {
|
||||||
return String(row[key]).toLowerCase().indexOf(filterKey) > -1
|
return String(row[key]).toLowerCase().indexOf(filterKey) > -1
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
const { sortKey } = state
|
||||||
const { sortKey } = state
|
if (sortKey) {
|
||||||
if (sortKey) {
|
const order = state.sortOrders[sortKey]
|
||||||
const order = state.sortOrders[sortKey]
|
data = data.slice().sort((a, b) => {
|
||||||
data = data.slice().sort((a, b) => {
|
a = a[sortKey]
|
||||||
a = a[sortKey]
|
b = b[sortKey]
|
||||||
b = b[sortKey]
|
return (a === b ? 0 : a > b ? 1 : -1) * order
|
||||||
return (a === b ? 0 : a > b ? 1 : -1) * order
|
})
|
||||||
})
|
}
|
||||||
}
|
return data
|
||||||
return data
|
})
|
||||||
})
|
|
||||||
|
|
||||||
function sortBy(key) {
|
function sortBy(key) {
|
||||||
state.sortKey = key
|
state.sortKey = key
|
||||||
state.sortOrders[key] *= -1
|
state.sortOrders[key] *= -1
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
filteredData,
|
filteredData,
|
||||||
sortBy,
|
sortBy,
|
||||||
capitalize
|
capitalize,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- App template (in DOM) -->
|
<!-- App template (in DOM) -->
|
||||||
<div id="demo">
|
<div id="demo">
|
||||||
<form id="search">
|
<form id="search">Search <input name="query" v-model="searchQuery" /></form>
|
||||||
Search <input name="query" v-model="searchQuery">
|
<demo-grid :data="gridData" :columns="gridColumns" :filter-key="searchQuery">
|
||||||
</form>
|
|
||||||
<demo-grid
|
|
||||||
:data="gridData"
|
|
||||||
:columns="gridColumns"
|
|
||||||
:filter-key="searchQuery">
|
|
||||||
</demo-grid>
|
</demo-grid>
|
||||||
</div>
|
</div>
|
||||||
<!-- App script -->
|
<!-- App script -->
|
||||||
<script>
|
<script>
|
||||||
Vue.createApp({
|
Vue.createApp({
|
||||||
components: {
|
components: {
|
||||||
DemoGrid
|
DemoGrid,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
gridColumns: ['name', 'power'],
|
gridColumns: ['name', 'power'],
|
||||||
gridData: [
|
gridData: [
|
||||||
{ name: 'Chuck Norris', power: Infinity },
|
{ name: 'Chuck Norris', power: Infinity },
|
||||||
{ name: 'Bruce Lee', power: 9000 },
|
{ name: 'Bruce Lee', power: 9000 },
|
||||||
{ name: 'Jackie Chan', power: 7000 },
|
{ name: 'Jackie Chan', power: 7000 },
|
||||||
{ name: 'Jet Li', power: 8000 }
|
{ name: 'Jet Li', power: 8000 },
|
||||||
]
|
],
|
||||||
})
|
}),
|
||||||
}).mount('#demo')
|
}).mount('#demo')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Helvetica Neue, Arial, sans-serif;
|
font-family:
|
||||||
font-size: 14px;
|
Helvetica Neue,
|
||||||
color: #444;
|
Arial,
|
||||||
}
|
sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border: 2px solid #42b983;
|
border: 2px solid #42b983;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background-color: #42b983;
|
background-color: #42b983;
|
||||||
color: rgba(255,255,255,0.66);
|
color: rgba(255, 255, 255, 0.66);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
th,
|
||||||
min-width: 120px;
|
td {
|
||||||
padding: 10px 20px;
|
min-width: 120px;
|
||||||
}
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
th.active {
|
th.active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
th.active .arrow {
|
th.active .arrow {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
opacity: 0.66;
|
opacity: 0.66;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow.asc {
|
.arrow.asc {
|
||||||
border-left: 4px solid transparent;
|
border-left: 4px solid transparent;
|
||||||
border-right: 4px solid transparent;
|
border-right: 4px solid transparent;
|
||||||
border-bottom: 4px solid #fff;
|
border-bottom: 4px solid #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow.dsc {
|
.arrow.dsc {
|
||||||
border-left: 4px solid transparent;
|
border-left: 4px solid transparent;
|
||||||
border-right: 4px solid transparent;
|
border-right: 4px solid transparent;
|
||||||
border-top: 4px solid #fff;
|
border-top: 4px solid #fff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,55 +8,62 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const { ref, computed } = Vue
|
const { ref, computed } = Vue
|
||||||
|
|
||||||
Vue.createApp({
|
Vue.createApp({
|
||||||
setup() {
|
setup() {
|
||||||
const input = ref('# hello')
|
const input = ref('# hello')
|
||||||
const output = computed(() => marked.marked(input.value, { sanitize: true }))
|
const output = computed(() =>
|
||||||
const update = _.debounce(e => { input.value = e.target.value }, 50)
|
marked.marked(input.value, { sanitize: true }),
|
||||||
|
)
|
||||||
|
const update = _.debounce(e => {
|
||||||
|
input.value = e.target.value
|
||||||
|
}, 50)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
update
|
update,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}).mount('#editor')
|
}).mount('#editor')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html, body, #editor {
|
html,
|
||||||
margin: 0;
|
body,
|
||||||
height: 100%;
|
#editor {
|
||||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
margin: 0;
|
||||||
color: #333;
|
height: 100%;
|
||||||
}
|
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
textarea, #editor div {
|
textarea,
|
||||||
display: inline-block;
|
#editor div {
|
||||||
overflow: auto;
|
display: inline-block;
|
||||||
width: 50%;
|
overflow: auto;
|
||||||
height: 100%;
|
width: 50%;
|
||||||
vertical-align: top;
|
height: 100%;
|
||||||
-webkit-box-sizing: border-box;
|
vertical-align: top;
|
||||||
-moz-box-sizing: border-box;
|
-webkit-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
padding: 0 20px;
|
box-sizing: border-box;
|
||||||
}
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
border: none;
|
border: none;
|
||||||
border-right: 1px solid #ccc;
|
border-right: 1px solid #ccc;
|
||||||
resize: none;
|
resize: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: 'Monaco', courier, monospace;
|
font-family: 'Monaco', courier, monospace;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
color: #f66;
|
color: #f66;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,39 +1,37 @@
|
||||||
<script src="../../dist/vue.global.js"></script>
|
<script src="../../dist/vue.global.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const { ref, reactive, computed, createApp } = Vue
|
const { ref, reactive, computed, createApp } = Vue
|
||||||
|
|
||||||
// math helper...
|
// math helper...
|
||||||
function valueToPoint (value, index, total) {
|
function valueToPoint(value, index, total) {
|
||||||
var x = 0
|
var x = 0
|
||||||
var y = -value * 0.8
|
var y = -value * 0.8
|
||||||
var angle = Math.PI * 2 / total * index
|
var angle = ((Math.PI * 2) / total) * index
|
||||||
var cos = Math.cos(angle)
|
var cos = Math.cos(angle)
|
||||||
var sin = Math.sin(angle)
|
var sin = Math.sin(angle)
|
||||||
var tx = x * cos - y * sin + 100
|
var tx = x * cos - y * sin + 100
|
||||||
var ty = x * sin + y * cos + 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) {
|
|
||||||
return {
|
return {
|
||||||
point: computed(() => valueToPoint(
|
x: tx,
|
||||||
+props.stat.value + 10,
|
y: ty,
|
||||||
props.index,
|
|
||||||
props.total
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<!-- template for the polygraph component. -->
|
<!-- template for the polygraph component. -->
|
||||||
|
@ -51,24 +49,26 @@ const AxisLabel = {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const Polygraph = {
|
const Polygraph = {
|
||||||
props: ['stats'],
|
props: ['stats'],
|
||||||
template: '#polygraph-template',
|
template: '#polygraph-template',
|
||||||
setup(props) {
|
setup(props) {
|
||||||
return {
|
return {
|
||||||
points: computed(() => {
|
points: computed(() => {
|
||||||
const total = props.stats.length
|
const total = props.stats.length
|
||||||
return props.stats.map((stat, i) => {
|
return props.stats
|
||||||
const point = valueToPoint(stat.value, i, total)
|
.map((stat, i) => {
|
||||||
return point.x + ',' + point.y
|
const point = valueToPoint(stat.value, i, total)
|
||||||
}).join(' ')
|
return point.x + ',' + point.y
|
||||||
})
|
})
|
||||||
}
|
.join(' ')
|
||||||
},
|
}),
|
||||||
components: {
|
}
|
||||||
AxisLabel
|
},
|
||||||
|
components: {
|
||||||
|
AxisLabel,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- demo root element -->
|
<!-- demo root element -->
|
||||||
|
@ -80,93 +80,99 @@ const Polygraph = {
|
||||||
<!-- controls -->
|
<!-- controls -->
|
||||||
<div v-for="stat in stats">
|
<div v-for="stat in stats">
|
||||||
<label>{{stat.label}}</label>
|
<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>
|
<span>{{stat.value}}</span>
|
||||||
<button @click="remove(stat)" class="remove">X</button>
|
<button @click="remove(stat)" class="remove">X</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="add">
|
<form id="add">
|
||||||
<input name="newlabel" v-model="newLabel">
|
<input name="newlabel" v-model="newLabel" />
|
||||||
<button @click="add">Add a Stat</button>
|
<button @click="add">Add a Stat</button>
|
||||||
</form>
|
</form>
|
||||||
<pre id="raw">{{ stats }}</pre>
|
<pre id="raw">{{ stats }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const globalStats = [
|
const globalStats = [
|
||||||
{ label: 'A', value: 100 },
|
{ label: 'A', value: 100 },
|
||||||
{ label: 'B', value: 100 },
|
{ label: 'B', value: 100 },
|
||||||
{ label: 'C', value: 100 },
|
{ label: 'C', value: 100 },
|
||||||
{ label: 'D', value: 100 },
|
{ label: 'D', value: 100 },
|
||||||
{ label: 'E', value: 100 },
|
{ label: 'E', value: 100 },
|
||||||
{ label: 'F', value: 100 }
|
{ label: 'F', value: 100 },
|
||||||
]
|
]
|
||||||
|
|
||||||
createApp({
|
createApp({
|
||||||
components: {
|
components: {
|
||||||
Polygraph
|
Polygraph,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const newLabel = ref('')
|
const newLabel = ref('')
|
||||||
const stats = reactive(globalStats)
|
const stats = reactive(globalStats)
|
||||||
|
|
||||||
function add(e) {
|
function add(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!newLabel.value) return
|
if (!newLabel.value) return
|
||||||
stats.push({
|
stats.push({
|
||||||
label: newLabel.value,
|
label: newLabel.value,
|
||||||
value: 100
|
value: 100,
|
||||||
})
|
})
|
||||||
newLabel.value = ''
|
newLabel.value = ''
|
||||||
}
|
|
||||||
|
|
||||||
function remove(stat) {
|
|
||||||
if (stats.length > 3) {
|
|
||||||
stats.splice(stats.indexOf(stat), 1)
|
|
||||||
} else {
|
|
||||||
alert('Can\'t delete more!')
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
function remove(stat) {
|
||||||
newLabel,
|
if (stats.length > 3) {
|
||||||
stats,
|
stats.splice(stats.indexOf(stat), 1)
|
||||||
add,
|
} else {
|
||||||
remove
|
alert("Can't delete more!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).mount('#demo')
|
|
||||||
|
return {
|
||||||
|
newLabel,
|
||||||
|
stats,
|
||||||
|
add,
|
||||||
|
remove,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}).mount('#demo')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Helvetica Neue, Arial, sans-serif;
|
font-family:
|
||||||
}
|
Helvetica Neue,
|
||||||
|
Arial,
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
polygon {
|
polygon {
|
||||||
fill: #42b983;
|
fill: #42b983;
|
||||||
opacity: .75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
circle {
|
circle {
|
||||||
fill: transparent;
|
fill: transparent;
|
||||||
stroke: #999;
|
stroke: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
text {
|
text {
|
||||||
font-family: Helvetica Neue, Arial, sans-serif;
|
font-family:
|
||||||
|
Helvetica Neue,
|
||||||
|
Arial,
|
||||||
|
sans-serif;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
fill: #666;
|
fill: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#raw {
|
#raw {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 300px;
|
left: 300px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,51 +1,86 @@
|
||||||
<script src="../../dist/vue.global.js"></script>
|
<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">
|
<div id="app">
|
||||||
<section class="todoapp">
|
<section class="todoapp">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<h1>todos</h1>
|
<h1>todos</h1>
|
||||||
<input class="new-todo"
|
<input
|
||||||
autofocus autocomplete="off"
|
class="new-todo"
|
||||||
placeholder="What needs to be done?"
|
autofocus
|
||||||
v-model="state.newTodo"
|
autocomplete="off"
|
||||||
@keyup.enter="addTodo">
|
placeholder="What needs to be done?"
|
||||||
|
v-model="state.newTodo"
|
||||||
|
@keyup.enter="addTodo"
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
<section class="main" v-show="state.todos.length">
|
<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>
|
<label for="toggle-all">Mark all as complete</label>
|
||||||
<ul class="todo-list">
|
<ul class="todo-list">
|
||||||
<li v-for="todo in state.filteredTodos"
|
<li
|
||||||
class="todo"
|
v-for="todo in state.filteredTodos"
|
||||||
:key="todo.id"
|
class="todo"
|
||||||
:class="{ completed: todo.completed, editing: todo === state.editedTodo }">
|
:key="todo.id"
|
||||||
|
:class="{ completed: todo.completed, editing: todo === state.editedTodo }"
|
||||||
|
>
|
||||||
<div class="view">
|
<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>
|
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
|
||||||
<button class="destroy" @click="removeTodo(todo)"></button>
|
<button class="destroy" @click="removeTodo(todo)"></button>
|
||||||
</div>
|
</div>
|
||||||
<input class="edit" type="text"
|
<input
|
||||||
v-model="todo.title"
|
class="edit"
|
||||||
v-todo-focus="todo === state.editedTodo"
|
type="text"
|
||||||
@blur="doneEdit(todo)"
|
v-model="todo.title"
|
||||||
@keyup.enter="doneEdit(todo)"
|
v-todo-focus="todo === state.editedTodo"
|
||||||
@keyup.escape="cancelEdit(todo)"
|
@blur="doneEdit(todo)"
|
||||||
>
|
@keyup.enter="doneEdit(todo)"
|
||||||
|
@keyup.escape="cancelEdit(todo)"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<footer class="footer" v-show="state.todos.length">
|
<footer class="footer" v-show="state.todos.length">
|
||||||
<span class="todo-count">
|
<span class="todo-count">
|
||||||
<strong>{{ state.remaining }}</strong>
|
<strong>{{ state.remaining }}</strong>
|
||||||
<span>{{ state.remainingText }}</span>
|
<span>{{ state.remainingText }}</span>
|
||||||
</span>
|
</span>
|
||||||
<ul class="filters">
|
<ul class="filters">
|
||||||
<li><a href="#/all" :class="{ selected: state.visibility === 'all' }">All</a></li>
|
<li>
|
||||||
<li><a href="#/active" :class="{ selected: state.visibility === 'active' }">Active</a></li>
|
<a href="#/all" :class="{ selected: state.visibility === 'all' }"
|
||||||
<li><a href="#/completed" :class="{ selected: state.visibility === 'completed' }">Completed</a></li>
|
>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>
|
</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
|
Clear completed
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -53,154 +88,155 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<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 STORAGE_KEY = 'todos-vuejs-3.x'
|
||||||
const todoStorage = {
|
const todoStorage = {
|
||||||
fetch () {
|
fetch() {
|
||||||
const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
|
const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
|
||||||
todos.forEach((todo, index) => {
|
todos.forEach((todo, index) => {
|
||||||
todo.id = index
|
todo.id = index
|
||||||
})
|
})
|
||||||
todoStorage.uid = todos.length
|
todoStorage.uid = todos.length
|
||||||
return todos
|
return todos
|
||||||
},
|
},
|
||||||
save (todos) {
|
save(todos) {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const filters = {
|
const filters = {
|
||||||
all (todos) {
|
all(todos) {
|
||||||
return todos
|
return todos
|
||||||
},
|
},
|
||||||
active (todos) {
|
active(todos) {
|
||||||
return todos.filter((todo) => {
|
return todos.filter(todo => {
|
||||||
return !todo.completed
|
return !todo.completed
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
completed (todos) {
|
completed(todos) {
|
||||||
return todos.filter(function (todo) {
|
return todos.filter(function (todo) {
|
||||||
return todo.completed
|
return todo.completed
|
||||||
})
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function pluralize (n) {
|
function pluralize(n) {
|
||||||
return n === 1 ? 'item' : 'items'
|
return n === 1 ? 'item' : 'items'
|
||||||
}
|
}
|
||||||
|
|
||||||
createApp({
|
createApp({
|
||||||
setup () {
|
setup() {
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
todos: todoStorage.fetch(),
|
todos: todoStorage.fetch(),
|
||||||
editedTodo: null,
|
editedTodo: null,
|
||||||
newTodo: '',
|
newTodo: '',
|
||||||
beforeEditCache: '',
|
beforeEditCache: '',
|
||||||
visibility: 'all',
|
visibility: 'all',
|
||||||
remaining: computed(() => {
|
remaining: computed(() => {
|
||||||
return filters.active(state.todos).length
|
return filters.active(state.todos).length
|
||||||
}),
|
}),
|
||||||
remainingText: computed(() => {
|
remainingText: computed(() => {
|
||||||
return ` ${pluralize(state.remaining)} left`
|
return ` ${pluralize(state.remaining)} left`
|
||||||
}),
|
}),
|
||||||
filteredTodos: computed(() => {
|
filteredTodos: computed(() => {
|
||||||
return filters[state.visibility](state.todos)
|
return filters[state.visibility](state.todos)
|
||||||
}),
|
}),
|
||||||
allDone: computed({
|
allDone: computed({
|
||||||
get: function () {
|
get: function () {
|
||||||
return state.remaining === 0
|
return state.remaining === 0
|
||||||
},
|
},
|
||||||
set: function (value) {
|
set: function (value) {
|
||||||
state.todos.forEach((todo) => {
|
state.todos.forEach(todo => {
|
||||||
todo.completed = value
|
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 () {
|
function addTodo() {
|
||||||
const value = state.newTodo && state.newTodo.trim()
|
const value = state.newTodo && state.newTodo.trim()
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return
|
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) {
|
function removeTodo(todo) {
|
||||||
state.todos.splice(state.todos.indexOf(todo), 1)
|
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
|
|
||||||
}
|
}
|
||||||
state.editedTodo = null
|
|
||||||
todo.title = todo.title.trim()
|
function editTodo(todo) {
|
||||||
if (!todo.title) {
|
state.beforeEditCache = todo.title
|
||||||
removeTodo(todo)
|
state.editedTodo = todo
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function cancelEdit (todo) {
|
function doneEdit(todo) {
|
||||||
state.editedTodo = null
|
if (!state.editedTodo) {
|
||||||
todo.title = state.beforeEditCache
|
return
|
||||||
}
|
}
|
||||||
|
state.editedTodo = null
|
||||||
function removeCompleted () {
|
todo.title = todo.title.trim()
|
||||||
state.todos = filters.active(state.todos)
|
if (!todo.title) {
|
||||||
}
|
removeTodo(todo)
|
||||||
|
}
|
||||||
return {
|
|
||||||
state,
|
|
||||||
addTodo,
|
|
||||||
removeTodo,
|
|
||||||
editTodo,
|
|
||||||
doneEdit,
|
|
||||||
cancelEdit,
|
|
||||||
removeCompleted
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
directives: {
|
|
||||||
'todo-focus': (el, { value }) => {
|
|
||||||
if (value) {
|
|
||||||
el.focus()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
function cancelEdit(todo) {
|
||||||
}).mount('#app')
|
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>
|
</script>
|
||||||
|
|
|
@ -22,46 +22,46 @@
|
||||||
</script>
|
</script>
|
||||||
<!-- item script -->
|
<!-- item script -->
|
||||||
<script>
|
<script>
|
||||||
const { reactive, computed, toRefs } = Vue
|
const { reactive, computed, toRefs } = Vue
|
||||||
|
|
||||||
const TreeItem = {
|
const TreeItem = {
|
||||||
name: 'TreeItem', // necessary for self-reference
|
name: 'TreeItem', // necessary for self-reference
|
||||||
template: '#item-template',
|
template: '#item-template',
|
||||||
props: {
|
props: {
|
||||||
model: Object
|
model: Object,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
open: false,
|
open: false,
|
||||||
isFolder: computed(() => {
|
isFolder: computed(() => {
|
||||||
return props.model.children && props.model.children.length
|
return props.model.children && props.model.children.length
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
state.open = !state.open
|
state.open = !state.open
|
||||||
}
|
|
||||||
|
|
||||||
function changeType() {
|
|
||||||
if (!state.isFolder) {
|
|
||||||
props.model.children = []
|
|
||||||
addChild()
|
|
||||||
state.open = true
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function addChild() {
|
function changeType() {
|
||||||
props.model.children.push({ name: 'new stuff' })
|
if (!state.isFolder) {
|
||||||
}
|
props.model.children = []
|
||||||
|
addChild()
|
||||||
|
state.open = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
function addChild() {
|
||||||
...toRefs(state),
|
props.model.children.push({ name: 'new stuff' })
|
||||||
toggle,
|
}
|
||||||
changeType,
|
|
||||||
addChild
|
return {
|
||||||
}
|
...toRefs(state),
|
||||||
|
toggle,
|
||||||
|
changeType,
|
||||||
|
addChild,
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p>(You can double click on an item to turn it into a folder.)</p>
|
<p>(You can double click on an item to turn it into a folder.)</p>
|
||||||
|
@ -72,43 +72,37 @@ const TreeItem = {
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const treeData = {
|
const treeData = {
|
||||||
name: 'My Tree',
|
name: 'My Tree',
|
||||||
children: [
|
children: [
|
||||||
{ name: 'hello' },
|
{ name: 'hello' },
|
||||||
{ name: 'wat' },
|
{ name: 'wat' },
|
||||||
{
|
{
|
||||||
name: 'child folder',
|
name: 'child folder',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'child folder',
|
name: 'child folder',
|
||||||
children: [
|
children: [{ name: 'hello' }, { name: 'wat' }],
|
||||||
{ name: 'hello' },
|
},
|
||||||
{ name: 'wat' }
|
{ name: 'hello' },
|
||||||
]
|
{ name: 'wat' },
|
||||||
},
|
{
|
||||||
{ name: 'hello' },
|
name: 'child folder',
|
||||||
{ name: 'wat' },
|
children: [{ name: 'hello' }, { name: 'wat' }],
|
||||||
{
|
},
|
||||||
name: 'child folder',
|
],
|
||||||
children: [
|
},
|
||||||
{ name: 'hello' },
|
],
|
||||||
{ name: 'wat' }
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.createApp({
|
Vue.createApp({
|
||||||
components: {
|
components: {
|
||||||
TreeItem
|
TreeItem,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
treeData
|
treeData,
|
||||||
})
|
}),
|
||||||
}).mount('#demo')
|
}).mount('#demo')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -6,78 +6,83 @@
|
||||||
<button @click="reset">reset</button>
|
<button @click="reset">reset</button>
|
||||||
<button @click="shuffle">shuffle</button>
|
<button @click="shuffle">shuffle</button>
|
||||||
<transition-group tag="ul" name="fade" class="container">
|
<transition-group tag="ul" name="fade" class="container">
|
||||||
<item v-for="item in items"
|
<item
|
||||||
|
v-for="item in items"
|
||||||
class="item"
|
class="item"
|
||||||
:msg="item"
|
:msg="item"
|
||||||
:key="item"
|
:key="item"
|
||||||
@rm="remove(item)">
|
@rm="remove(item)"
|
||||||
|
>
|
||||||
</item>
|
</item>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const getInitialItems = () => [1, 2, 3, 4, 5]
|
const getInitialItems = () => [1, 2, 3, 4, 5]
|
||||||
let id = getInitialItems().length + 1
|
let id = getInitialItems().length + 1
|
||||||
|
|
||||||
const Item = {
|
const Item = {
|
||||||
props: ['msg'],
|
props: ['msg'],
|
||||||
template: `<div>{{ msg }} <button @click="$emit('rm')">x</button></div>`
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.item {
|
.item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
background-color: #f3f3f3;
|
background-color: #f3f3f3;
|
||||||
border: 1px solid #666;
|
border: 1px solid #666;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
/* 1. declare transition */
|
/* 1. declare transition */
|
||||||
.fade-move, .fade-enter-active, .fade-leave-active {
|
.fade-move,
|
||||||
transition: all .5s cubic-bezier(.55,0,.1,1);
|
.fade-enter-active,
|
||||||
}
|
.fade-leave-active {
|
||||||
/* 2. declare enter from and leave to state */
|
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
|
||||||
.fade-enter-from, .fade-leave-to {
|
}
|
||||||
opacity: 0;
|
/* 2. declare enter from and leave to state */
|
||||||
transform: scaleY(0.01) translate(30px, 0);
|
.fade-enter-from,
|
||||||
}
|
.fade-leave-to {
|
||||||
/* 3. ensure leaving items are taken out of layout flow so that moving
|
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. */
|
animations can be calculated correctly. */
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -33,10 +33,10 @@
|
||||||
</transition>
|
</transition>
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
const Modal = {
|
const Modal = {
|
||||||
template: '#modal-template',
|
template: '#modal-template',
|
||||||
props: ['show']
|
props: ['show'],
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- modal container that lives outside of app root -->
|
<!-- modal container that lives outside of app root -->
|
||||||
|
@ -56,57 +56,57 @@ const Modal = {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
Vue.createApp({
|
Vue.createApp({
|
||||||
components: { Modal },
|
components: { Modal },
|
||||||
data: () => ({
|
data: () => ({
|
||||||
showModal: false
|
showModal: false,
|
||||||
})
|
}),
|
||||||
}).mount('#app')
|
}).mount('#app')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.modal-mask {
|
.modal-mask {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 9998;
|
z-index: 9998;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(0, 0, 0, .5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
display: table;
|
display: table;
|
||||||
transition: opacity .3s ease;
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-wrapper {
|
.modal-wrapper {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-container {
|
.modal-container {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
margin: 0px auto;
|
margin: 0px auto;
|
||||||
padding: 20px 30px;
|
padding: 20px 30px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
|
||||||
transition: all .3s ease;
|
transition: all 0.3s ease;
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header h3 {
|
.modal-header h3 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
color: #42b983;
|
color: #42b983;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-default-button {
|
.modal-default-button {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The following styles are auto-applied to elements with
|
* The following styles are auto-applied to elements with
|
||||||
* transition="modal" when their visibility is toggled
|
* transition="modal" when their visibility is toggled
|
||||||
* by Vue.js.
|
* by Vue.js.
|
||||||
|
@ -115,17 +115,17 @@ Vue.createApp({
|
||||||
* these styles.
|
* these styles.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.modal-enter-from {
|
.modal-enter-from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-leave-to {
|
.modal-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-enter-from .modal-container,
|
.modal-enter-from .modal-container,
|
||||||
.modal-leave-to .modal-container {
|
.modal-leave-to .modal-container {
|
||||||
-webkit-transform: scale(1.1);
|
-webkit-transform: scale(1.1);
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
765
pnpm-lock.yaml
765
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -253,7 +253,7 @@ function createConfig(format, output, plugins = []) {
|
||||||
'source-map-js',
|
'source-map-js',
|
||||||
'@babel/parser',
|
'@babel/parser',
|
||||||
'estree-walker',
|
'estree-walker',
|
||||||
'entities/lib/decode.js',
|
'entities/dist/decode.js',
|
||||||
]
|
]
|
||||||
|
|
||||||
if (isGlobalBuild || isBrowserESMBuild || isCompatPackage) {
|
if (isGlobalBuild || isBrowserESMBuild || isCompatPackage) {
|
||||||
|
|
|
@ -218,6 +218,12 @@ async function main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
if (versionIncrements.includes(targetVersion)) {
|
||||||
|
// @ts-expect-error
|
||||||
|
targetVersion = inc(targetVersion)
|
||||||
|
}
|
||||||
|
|
||||||
if (!semver.valid(targetVersion)) {
|
if (!semver.valid(targetVersion)) {
|
||||||
throw new Error(`invalid target version: ${targetVersion}`)
|
throw new Error(`invalid target version: ${targetVersion}`)
|
||||||
}
|
}
|
||||||
|
@ -246,15 +252,23 @@ async function main() {
|
||||||
let isCIPassed = await getCIResult()
|
let isCIPassed = await getCIResult()
|
||||||
skipTests ||= isCIPassed
|
skipTests ||= isCIPassed
|
||||||
|
|
||||||
if (isCIPassed && !skipPrompts) {
|
if (isCIPassed) {
|
||||||
/** @type {{ yes: boolean }} */
|
if (!skipPrompts) {
|
||||||
const { yes: promptSkipTests } = await prompt({
|
/** @type {{ yes: boolean }} */
|
||||||
type: 'confirm',
|
const { yes: promptSkipTests } = await prompt({
|
||||||
name: 'yes',
|
type: 'confirm',
|
||||||
message: `CI for this commit passed. Skip local tests?`,
|
name: 'yes',
|
||||||
})
|
message: `CI for this commit passed. Skip local tests?`,
|
||||||
|
})
|
||||||
skipTests = promptSkipTests
|
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') {
|
if (branch !== 'main') {
|
||||||
additionalPublishFlags.push('--publish-branch', branch)
|
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) {
|
for (const pkg of packages) {
|
||||||
await publishPackage(pkg, targetVersion, additionalPublishFlags)
|
await publishPackage(pkg, targetVersion, additionalPublishFlags)
|
||||||
|
@ -514,7 +533,7 @@ async function publishPackage(pkgName, version, additionalFlags) {
|
||||||
)
|
)
|
||||||
console.log(pico.green(`Successfully published ${pkgName}@${version}`))
|
console.log(pico.green(`Successfully published ${pkgName}@${version}`))
|
||||||
} catch (/** @type {any} */ e) {
|
} catch (/** @type {any} */ e) {
|
||||||
if (e.stderr.match(/previously published/)) {
|
if (e.message?.match(/previously published/)) {
|
||||||
console.log(pico.red(`Skipping already published: ${pkgName}`))
|
console.log(pico.red(`Skipping already published: ${pkgName}`))
|
||||||
} else {
|
} else {
|
||||||
throw e
|
throw e
|
||||||
|
|
|
@ -95,8 +95,17 @@ export async function exec(command, args, options) {
|
||||||
const ok = code === 0
|
const ok = code === 0
|
||||||
const stderr = Buffer.concat(stderrChunks).toString().trim()
|
const stderr = Buffer.concat(stderrChunks).toString().trim()
|
||||||
const stdout = Buffer.concat(stdoutChunks).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}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue