├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 0-answer.md │ ├── 0-new.md │ ├── 1-answer.zh-CN.md │ └── 1-new.zh-CN.md └── workflows │ ├── issue-pr.yaml │ ├── labeling.yaml │ ├── pr-merged.yaml │ └── toggle-pr-with-issue.yaml ├── .gitignore ├── LICENSE ├── README.md ├── README.zh-CN.md ├── docs ├── .vitepress │ ├── components.d.ts │ ├── components │ │ └── ChallengeList.vue │ ├── config.ts │ ├── plugins │ │ └── markdownTransform.ts │ └── theme │ │ ├── index.ts │ │ └── styles │ │ └── overrides.css ├── challenges.md ├── getting-started.md ├── guide │ └── contribution.md ├── index.md ├── meta │ ├── challenges-nav-menu.json │ ├── challenges.json │ └── index.ts ├── package.json ├── pnpm-lock.yaml ├── public │ ├── favicon.ico │ └── logo.png ├── questions │ ├── 1-hello-word │ │ └── README.md │ ├── 10-lifecycle │ │ └── README.md │ ├── 11-next-dom-update │ │ └── README.md │ ├── 12-optimize-perf-directive │ │ └── README.md │ ├── 13-dom-portal │ │ └── README.md │ ├── 14-dynamic-css-values │ │ └── README.md │ ├── 15-useToggle │ │ └── README.md │ ├── 16-until │ │ └── README.md │ ├── 17-useCounter │ │ └── README.md │ ├── 18-useLocalStorage │ │ └── README.md │ ├── 19-v-focus │ │ └── README.md │ ├── 2-ref-family │ │ └── README.md │ ├── 20-v-debounce-click │ │ └── README.md │ ├── 208-tree-component │ │ └── README.md │ ├── 21-functional-component │ │ └── README.md │ ├── 218-h-render-function │ │ └── README.md │ ├── 22-custom-element │ │ └── README.md │ ├── 23-custom-ref │ │ └── README.md │ ├── 232-key-modifiers │ │ └── README.md │ ├── 24-v-active-style │ │ └── README.md │ ├── 243-prevent-event-propagation │ │ └── README.md │ ├── 25-useMouse │ │ └── README.md │ ├── 26-v-model │ │ └── README.md │ ├── 27-global-css │ │ └── README.md │ ├── 3-losing-reactivity │ │ └── README.md │ ├── 305-capitalize │ │ └── README.md │ ├── 323-prop-validation │ │ └── README.md │ ├── 4-writable-computed │ │ └── README.md │ ├── 5-watch-family │ │ └── README.md │ ├── 6-shallow-ref │ │ └── README.md │ ├── 7-raw-api │ │ └── README.md │ ├── 8-effect-scope │ │ └── README.md │ └── 9-dependency-injection │ │ └── README.md ├── scripts │ └── update.ts ├── tsconfig.json └── vite.config.ts ├── logo.png ├── netlify.toml ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── questions ├── 1-hello-word │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 10-lifecycle │ ├── App.vue │ ├── Child.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 11-next-dom-update │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 12-optimize-perf-directive │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 13-dom-portal │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 14-dynamic-css-values │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 15-useToggle │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 16-until │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 17-useCounter │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 18-useLocalStorage │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 19-v-focus │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 2-ref-family │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 20-v-debounce-click │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 208-tree-component │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── TreeComponent.vue │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 21-functional-component │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 218-h-render-function │ ├── App.vue │ ├── MyButton.ts │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 22-custom-element │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 23-custom-ref │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 232-key-modifiers │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 24-v-active-style │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 243-prevent-event-propagation │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 25-useMouse │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 26-v-model │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 27-global-css │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 3-losing-reactivity │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 305-capitalize │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 323-prop-validation │ ├── App.vue │ ├── Button.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 4-writable-computed │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 5-watch-family │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 6-shallow-ref │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 7-raw-api │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── 8-effect-scope │ ├── App.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml └── 9-dependency-injection │ ├── App.vue │ ├── Child.vue │ ├── README.md │ ├── README.zh-CN.md │ ├── index.test.ts │ ├── info.yml │ └── info.zh-CN.yml ├── scripts ├── actions │ ├── issue-pr.ts │ ├── labeling.ts │ ├── loader.ts │ └── toggle-pr-with-issue.ts ├── badge.ts ├── build.ts ├── configs.ts ├── loader.ts ├── locales.ts ├── locales │ ├── en.json │ └── zh-CN.json ├── package.json ├── pnpm-lock.yaml ├── readme.ts ├── stackblitz │ ├── index.ts │ ├── package-lock.ts │ └── template.ts ├── types.ts └── utils.ts └── vitest.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | !docs/.vitepress 2 | dist 3 | node_modules 4 | .output 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@webfansplz", 3 | "rules":{ 4 | "no-console":0, 5 | "prefer-const":0, 6 | "quotes":[1,"double"], 7 | "no-prototype-builtins":0, 8 | "vue/no-multiple-template-root":0, 9 | "vue/no-parsing-error": ["error", { 10 | "invalid-first-character-of-tag-name": false 11 | }] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [webfansplz] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/0-answer.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Answers 3 | about: Share answers/solutions to a question 4 | title: "no - " 5 | labels: answer, en 6 | --- 7 | 8 | 19 | 20 | ```vue 21 | // your answers 22 | ``` 23 | 24 | 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/0-new.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Challenge 3 | about: Propose a new challenge, a PR will be auto generated. 4 | title: "" 5 | labels: new-challenge 6 | --- 7 | 8 | > Please follow the template and fill the info. A PR will be auto-generated and always reflect on your changes. 9 | > 10 | > Detailed solution/guide is not required, but please be sure the challenge is solvable. 11 | 12 | ## Info 13 | 14 | Basic info of your challenge questions, 15 | 16 | ```yaml 17 | difficulty: easy # medium / hard / extreme 18 | title: Your Question Name 19 | tags: Directvies, Components # separate by comma 20 | ``` 21 | 22 | 23 | ## Question 24 | 25 | 26 | 27 | Describe and explain your question here. 28 | 29 | 30 | 31 | ## Template 32 | 33 | This is the template for challengers to start the coding. 34 | 35 | If you want to commit multiple files, just create multiple blocks and identify the file name and code block 36 | 37 | 38 | 39 | filename: App.vue 40 | 41 | ```vue 42 | 43 | ``` 44 | 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-answer.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🇨🇳 解法分享 3 | about: 分享你对于某题的解法/答案 4 | title: "no - " 5 | labels: answer, zh-CN 6 | --- 7 | 8 | 19 | 20 | 21 | ```vue 22 | // 你的答案 23 | ``` 24 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-new.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🇨🇳 新题目 3 | about: 新题目提案,PR 会根据 Issue 自动生成。 4 | title: "新题目" 5 | labels: new-challenge, zh-CN 6 | --- 7 | 8 | > 请按照以下的模版填充相应的内容,一个 PR 会自动生成并保持与本 Issue 的内容同步。 9 | 10 | > 你不需要提供详细的答案或教学,但请保证题目可解。 11 | 12 | 13 | ## 基本信息 14 | 15 | ```yaml 16 | # 题目难度 17 | difficulty: easy # medium / hard / extreme 18 | 19 | # 题目标题 20 | title: 你的题目 21 | 22 | # 题目标签 23 | tags: union, array # separate by comma 24 | ``` 25 | 26 | ## 题目 27 | 28 | 29 | 30 | 在这里描述并说明你的题目。 31 | 32 | 33 | 34 | ## 题目模版 35 | 36 | 以下是给予挑战者开始做题的代码模版, 37 | 38 | 如果您想提交多个文件,只需创建多个``,并按照以下格式填写文件名和代码內容 39 | 40 | 41 | 42 | filename: App.vue 43 | 44 | ```vue 45 | 46 | ``` 47 | 48 | -------------------------------------------------------------------------------- /.github/workflows/issue-pr.yaml: -------------------------------------------------------------------------------- 1 | name: Issue to Pull Request 2 | 3 | on: 4 | issues: 5 | types: [opened, edited] 6 | 7 | jobs: 8 | start: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - run: cd scripts && npm i --only=production 13 | - run: cd scripts/actions && npx tsx ./loader.ts ${{github.token}} issue-pr 14 | -------------------------------------------------------------------------------- /.github/workflows/labeling.yaml: -------------------------------------------------------------------------------- 1 | name: Labeling 2 | 3 | on: 4 | issues: 5 | types: [opened, labeled] 6 | 7 | jobs: 8 | start: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - run: cd scripts && npm i --only=production 13 | - run: cd scripts/actions && npx tsx ./loader.ts ${{github.token}} labeling 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/pr-merged.yaml: -------------------------------------------------------------------------------- 1 | name: PR Merged 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | 7 | jobs: 8 | start: 9 | if: github.event.pull_request.merged 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - run: npx pnpm i 14 | - name: 'Update README' 15 | run: npm run build 16 | - uses: EndBug/add-and-commit@v4 # You can change this to use a specific version 17 | with: 18 | ref: 'main' 19 | message: 'chore: update readme' 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | - uses: jsmrcaga/action-netlify-deploy@v1.1.0 23 | with: 24 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 25 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 26 | NETLIFY_DEPLOY_TO_PROD: true 27 | -------------------------------------------------------------------------------- /.github/workflows/toggle-pr-with-issue.yaml: -------------------------------------------------------------------------------- 1 | name: Toggle PR With Issue 2 | 3 | on: 4 | issues: 5 | types: [closed, reopened] 6 | 7 | jobs: 8 | start: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - run: cd scripts && npm i --only=production 13 | - run: cd scripts/actions && npx tsx ./loader.ts ${{github.token}} toggle-pr-with-issue 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 webfansplz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/.vitepress/components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/core/pull/3399 4 | import '@vue/runtime-core' 5 | 6 | export {} 7 | 8 | declare module '@vue/runtime-core' { 9 | export interface GlobalComponents { 10 | ChallengeList: typeof import('./components/ChallengeList.vue')['default'] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/.vitepress/components/ChallengeList.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 18 | 19 | 41 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress" 2 | import { categorys, navMenu } from "../meta" 3 | const defaultSidebar = [ 4 | { 5 | text: "Introduction", 6 | items: [ 7 | { 8 | text: "Getting Started", 9 | link: "/getting-started", 10 | }, 11 | ], 12 | }, 13 | { 14 | text: "Guide", 15 | items: [ 16 | { 17 | text: "How to Contribute", 18 | link: "/guide/contribution", 19 | }, 20 | ], 21 | }, 22 | { 23 | text: "Challenges", 24 | items: categorys, 25 | }, 26 | ] 27 | 28 | const nav = [ 29 | ...defaultSidebar, 30 | ] 31 | 32 | export default defineConfig({ 33 | title: "Vue.js challenges", 34 | description: "Collection of Vue.js challenges", 35 | // appearance: false, 36 | lastUpdated: true, 37 | themeConfig: { 38 | sidebar: { 39 | "/challenges": navMenu, 40 | "/questions/": navMenu, 41 | "/": defaultSidebar, 42 | }, 43 | nav, 44 | socialLinks: [ 45 | { icon: "github", link: "https://github.com/webfansplz/vuejs-challenges" }, 46 | ], 47 | footer: { 48 | copyright: "Copyright © 2022-present webfansplz", 49 | }, 50 | editLink: { 51 | pattern: "https://github.com/webfansplz/vuejs-challenges", 52 | text: "Edit this page on Gitlab", 53 | }, 54 | lastUpdatedText: "Last Updated", 55 | localeLinks: { 56 | text: "English", 57 | items: [ 58 | { text: "简体中文", link: "https://cn-vuejs-challenges.netlify.app" }, 59 | ], 60 | }, 61 | }, 62 | }) 63 | -------------------------------------------------------------------------------- /docs/.vitepress/plugins/markdownTransform.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import type { Plugin } from "vite" 3 | 4 | const HOST = "https://cn-vuejs-challenges.netlify.app" 5 | 6 | export function MarkdownTransform(): Plugin { 7 | return { 8 | name: "vueuse-md-transform", 9 | enforce: "pre", 10 | async transform(code, id) { 11 | if (!id.endsWith(".md")) 12 | return null 13 | 14 | /* eslint-disable prefer-regex-literals */ 15 | const backbadegeRegexp = new RegExp( 16 | "
\"Back\"/", 17 | ) 18 | 19 | const chinesebadegeRegexp = new RegExp( 20 | "\"简体中文\"/", 21 | ) 22 | 23 | const pathInfo = path.parse(path.relative(process.cwd(), id)) 24 | const host = `${HOST}/${path.join(pathInfo.dir, "README.zh-CN")}` 25 | const replaceContent = `\"简体中文\"/` 26 | 27 | code = code.replace(backbadegeRegexp, "").replace(chinesebadegeRegexp, replaceContent) 28 | 29 | return code 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from "vitepress/theme" 2 | import "./styles/overrides.css" 3 | 4 | import type { Theme } from "vitepress" 5 | const define = (value: T): T => value 6 | 7 | export default define({ 8 | ...DefaultTheme, 9 | enhanceApp: ({ app }) => { 10 | // globals.forEach(([name, Comp]) => { 11 | // app.component(name, Comp) 12 | // }) 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/styles/overrides.css: -------------------------------------------------------------------------------- 1 | img { 2 | display: inline !important; 3 | } 4 | 5 | :root { 6 | --vp-home-hero-name-color: transparent; 7 | --vp-home-hero-name-background: -webkit-linear-gradient(315deg, #42d392 25%, #647eff); 8 | --vp-home-hero-image-background-image: linear-gradient(-45deg, 9 | #4acf93 30%, 10 | #c3edda); 11 | --vp-home-hero-image-filter: blur(100px); 12 | } 13 | -------------------------------------------------------------------------------- /docs/challenges.md: -------------------------------------------------------------------------------- 1 | # Challenges 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | 2 | # 📖 Getting Started 3 | 4 | I love `Vue.js` ♥️. It's so Awesome 🚀. It helps me finish my work well and build applications. 5 | 6 | I was looking for a project for everyone to learn `Vue.js` together, and that's why this project was born. I believe we can grow together here, and hope it helps you. 7 | 8 | This project is aimed at helping you better understand `Vue.js`, writing your own utilities, or just having fun with the challenges. 9 | 10 | We are also trying to form a community where you can ask questions and get answers you have faced in the real world - they may become part of the challenges! 11 | 12 | **Let’s go,Have a nice trip ! ♥️** 13 | -------------------------------------------------------------------------------- /docs/guide/contribution.md: -------------------------------------------------------------------------------- 1 | # 🤝 How to Contribute 2 | 3 | There are several ways you can contribute to this project 4 | 5 | - Share your answers/solutions 6 | - Propose new challenges 7 | - Add more test cases to the existing challenges 8 | - Provide learning resources or ideas of how to solve challenges 9 | - Share the problems you have faced in real-world projects, regardless you have the solution or not - the community would help you as well. 10 | - Help others by discussing issues 11 | 12 | Just [open an issue](https://github.com/webfansplz/vuejs-challenges/issues/new/choose) and choose the corresponding template. Thanks! 13 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | sidebar: false 4 | 5 | title: Vue.js challenges 6 | titleTemplate: A Vue.js online challenge platform 7 | 8 | hero: 9 | name: Vue.js challenges 10 | text: Collection of Vue.js challenges 11 | tagline: A Vue.js online challenge platform 12 | image: 13 | src: logo.png 14 | alt: Vue.js challenges 15 | actions: 16 | - theme: brand 17 | text: Get Started 18 | link: /getting-started 19 | - theme: alt 20 | text: Challenges 21 | link: /challenges 22 | - theme: alt 23 | text: View on Github 24 | link: https://github.com/webfansplz/vuejs-challenges 25 | 26 | features: 27 | - title: Online Play 28 | details: Take the challenges based on the online Vue.js SFC Playground 29 | - title: Wide Coverage 30 | details: Reactivity API,Composable Function,Directives,Components 31 | - title: Difficulty level 32 | details: You can choose the challenge from different levels of difficulty 33 | - title: Automated contribution 34 | details: Propose a new challenge in the issue, a PR will be auto generated 35 | --- 36 | -------------------------------------------------------------------------------- /docs/meta/index.ts: -------------------------------------------------------------------------------- 1 | export { challenges, challengesByDifficulty } from "./challenges.json" 2 | export { categorys, navMenu } from "./challenges-nav-menu.json" 3 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "webfansplz", 3 | "description": "", 4 | "devDependencies": { 5 | "sass": "^1.53.0", 6 | "unplugin-vue-components": "^0.21.0", 7 | "vite": "^2.9.13", 8 | "vitepress": "1.0.0-alpha.4" 9 | }, 10 | "license": "MIT", 11 | "private": true, 12 | "scripts": { 13 | "build": "npm run update && vitepress build .", 14 | "dev": "npm run update && vitepress dev . --host", 15 | "serve": "vitepress serve . --port 5001", 16 | "update": "tsx ./scripts/update.ts" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfansplz/vuejs-challenges/b3026f0a34c4a588876c6fd7ad9b49f3a837401e/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfansplz/vuejs-challenges/b3026f0a34c4a588876c6fd7ad9b49f3a837401e/docs/public/logo.png -------------------------------------------------------------------------------- /docs/questions/323-prop-validation/README.md: -------------------------------------------------------------------------------- 1 |

Prop Validation easy #Components

By Lov`u`e @heappynd

Take the Challenge    简体中文

2 | 3 | 4 | Please validate the `type` prop of the `Button` component. it's accept the following strings `primary | ghost | dashed | link | text | default` only and the default value is `default`. 5 | 6 | ```vue 7 | 12 | 13 | 16 | ``` 17 | 18 |
Back Share your Solutions Check out Solutions 19 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "esnext", 5 | "target": "esnext", 6 | "lib": ["DOM", "ESNext"], 7 | "strict": true, 8 | "jsx": "preserve", 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "types": [ 17 | "vite/client", 18 | "vitepress" 19 | ], 20 | "paths": { 21 | "~/*": ["src/*"] 22 | } 23 | }, 24 | "include": [ 25 | "./*.ts", 26 | ".vitepress/**/*.ts", 27 | ".vitepress/**/*.vue" 28 | ], 29 | "exclude": ["dist", "node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path" 2 | import { defineConfig } from "vite" 3 | import Components from "unplugin-vue-components/vite" 4 | import { MarkdownTransform } from "./.vitepress/plugins/markdownTransform" 5 | 6 | export default defineConfig({ 7 | server: { 8 | hmr: { 9 | overlay: false, 10 | }, 11 | fs: { 12 | allow: [resolve(__dirname, "..")], 13 | }, 14 | }, 15 | 16 | plugins: [ 17 | MarkdownTransform(), 18 | Components({ 19 | include: [/\.vue/, /\.md/], 20 | dirs: ".vitepress/components", 21 | dts: ".vitepress/components.d.ts", 22 | }), 23 | ], 24 | 25 | css: { preprocessorOptions: { scss: { charset: false } } }, 26 | }) 27 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webfansplz/vuejs-challenges/b3026f0a34c4a588876c6fd7ad9b49f3a837401e/logo.png -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | NPM_FLAGS = "--version" 3 | NODE_VERSION = "16" 4 | 5 | [build] 6 | publish = "docs/.vitepress/dist" 7 | command = "npx pnpm i --store=node_modules/.pnpm-store && npm run docs:build" 8 | 9 | [[redirects]] 10 | from = "/*" 11 | to = "/index.html" 12 | status = 200 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuejs-challenges", 3 | "description": "Collection of Vue.js challenges", 4 | "keywords": ["Vue.js challenges", "Vue.js", "challenges", "Learn Vue.js"], 5 | "license": "MIT", 6 | "private":true, 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/webfansplz/vuejs-challenges.git" 10 | }, 11 | "author": "", 12 | "main": "index.js", 13 | "scripts": { 14 | "test": "vitest --environment jsdom --ui", 15 | "build": "tsx ./scripts/build.ts", 16 | "docs:dev": "pnpm run -C docs update && pnpm run -C docs dev", 17 | "docs:build": "pnpm run -C docs build", 18 | "build:all": "pnpm run build && pnpm run docs:build", 19 | "docs:serve": "pnpm run -C docs serve", 20 | "lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx,.json --max-warnings 0", 21 | "lint:fix": "eslint --fix . --ext .vue,.js,.ts,.jsx,.tsx,.json" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/webfansplz/vuejs-challenges/issues" 25 | }, 26 | "homepage": "https://github.com/webfansplz/vuejs-challenges#readme", 27 | "devDependencies": { 28 | "@types/fs-extra": "^9.0.13", 29 | "@types/js-yaml": "^4.0.5", 30 | "@types/jsdom": "^16.2.14", 31 | "@types/node": "^18.0.0", 32 | "@vitejs/plugin-vue": "^2.3.3", 33 | "@vitest/ui": "^0.17.0", 34 | "@vue/test-utils": "^2.0.2", 35 | "@webfansplz/eslint-config": "^0.1.0", 36 | "eslint": "^8.18.0", 37 | "fast-glob": "^3.2.11", 38 | "fs-extra": "^10.1.0", 39 | "js-yaml": "^4.1.0", 40 | "jsdom": "^20.0.0", 41 | "pnpm": "7.3.0", 42 | "tsx": "^3.6.0", 43 | "typescript": "^4.7.4", 44 | "vitest": "^0.17.0", 45 | "vue": "^3.2.37" 46 | }, 47 | "stackblitz": { 48 | "startCommand": "npm run test" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - docs 3 | - scripts 4 | -------------------------------------------------------------------------------- /questions/1-hello-word/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /questions/1-hello-word/README.md: -------------------------------------------------------------------------------- 1 |

Hello World warm-up

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | Hello, World! 4 | 5 | In Vue.js Challenges, we use the Vue.js SFC Playground based on [vuejs/repl](https://github.com/vuejs/repl) to code online and provided coding judge by `StackBlitz` & [Vitest](https://github.com/vitest-dev/vitest). 6 | 7 | For this challenge, you will need to change the following code to make the page show "Hello World" correctly. 8 | 9 | ```vue 10 | 14 | 15 | 21 | 22 | ``` 23 | 24 | Click the `Take the Challenge` button to start coding! Happy Hacking! 25 | 26 |
Back Share your Solutions Check out Solutions 27 | -------------------------------------------------------------------------------- /questions/1-hello-word/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

你好 ! 热身

By webfansplz @webfansplz

接受挑战    English

2 | 3 | Hello,World! 4 | 5 | 在这个挑战中,我们使用基于 [vuejs/repl](https://github.com/vuejs/repl) 的`SFC`编码游乐场进行在线编码且通过`StackBlitz` 和 [Vitest](https://github.com/vitest-dev/vitest)进行挑战判断。 6 | 7 | 对于这个挑战,您将需要更改以下代码,以使页面正确显示“Hello World”。 8 | 9 | ```vue 10 | 14 | 15 | 21 | 22 | ``` 23 | 24 | 25 | 点击上方的 `接受挑战` 开始编码!旅途愉快! 26 | 27 |
返回首页 分享你的解答 查看解答 28 | 29 | -------------------------------------------------------------------------------- /questions/1-hello-word/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import HelloWorld from "./App.vue" 5 | 6 | interface HelloWorldProps { 7 | msg: string 8 | } 9 | 10 | describe("HelloWorld", () => { 11 | it("renders a 'Hello World'", () => { 12 | const wrapper = mount(HelloWorld) 13 | const msg = (wrapper.vm as unknown as HelloWorldProps).msg 14 | expect(wrapper.text()).toBe(msg) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /questions/1-hello-word/info.yml: -------------------------------------------------------------------------------- 1 | title: Hello World 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: warm 8 | -------------------------------------------------------------------------------- /questions/1-hello-word/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 你好 ! 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/10-lifecycle/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /questions/10-lifecycle/Child.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /questions/10-lifecycle/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import LifecycleFather from "./App.vue" 5 | import LifecycleChild from "./Child.vue" 6 | 7 | function delay(timeout: number) { 8 | return new Promise((resolve) => { 9 | setTimeout(resolve, timeout) 10 | }) 11 | } 12 | 13 | interface InjectType { 14 | count: number 15 | timer: number 16 | } 17 | 18 | describe("Lifecycle", () => { 19 | it("The timer will work abnormally when the child component is toggled", async() => { 20 | const fatherWrapper = mount(LifecycleFather) 21 | const childWrapper = fatherWrapper.findComponent(LifecycleChild) 22 | const button = fatherWrapper.find("button") 23 | expect(childWrapper.exists()).toBeTruthy() 24 | 25 | await delay(1000) 26 | const firstchildCount = (childWrapper.vm as unknown as InjectType).count 27 | expect(firstchildCount).toMatchInlineSnapshot("1") 28 | 29 | await button.trigger("click") 30 | expect(childWrapper.exists()).not.toBeTruthy() 31 | 32 | await delay(1000) 33 | const secondChildCount = (childWrapper.vm as unknown as InjectType).count 34 | expect(secondChildCount).toMatchInlineSnapshot("1") 35 | 36 | await button.trigger("click") 37 | expect(fatherWrapper.findComponent(LifecycleChild).exists()).toBeTruthy() 38 | 39 | await delay(1000) 40 | const thirdChildCount = (childWrapper.vm as unknown as InjectType).count 41 | expect(thirdChildCount).toMatchInlineSnapshot("2") 42 | 43 | await delay(1000) 44 | const fourthChildCount = (childWrapper.vm as unknown as InjectType).count 45 | expect(fourthChildCount).toMatchInlineSnapshot("3") 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /questions/10-lifecycle/info.yml: -------------------------------------------------------------------------------- 1 | title: Lifecycle Hooks 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: easy 8 | 9 | tags: Composition API,Lifecycle 10 | -------------------------------------------------------------------------------- /questions/10-lifecycle/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 生命周期钩子 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/11-next-dom-update/App.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /questions/11-next-dom-update/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

下一次DOM更新 简单 #Global API:General

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 在`Vue.js`中改变响应式状态时,DOM不会同步更新。 5 | `Vue.js` 提供了一个用于等待下一次DOM更新的方法,让我们开始吧 👇: 6 | 7 | ```vue 8 | 24 | 25 | 30 | 31 | ``` 32 |
返回首页 分享你的解答 查看解答 33 | -------------------------------------------------------------------------------- /questions/11-next-dom-update/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect, vi } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | describe("next-dom-update", () => { 7 | it("should work'", async() => { 8 | let printLog = '' 9 | console.log = vi.fn( 10 | (log: string) => { 11 | printLog = log?.toString()?.trim() 12 | }) 13 | const wrapper = mount(App) 14 | 15 | expect(wrapper.text()).toMatchInlineSnapshot("\"0\"") 16 | 17 | const button = wrapper.find("button") 18 | await button.trigger("click") 19 | 20 | expect(wrapper.text()).toMatchInlineSnapshot('"1"') 21 | expect(printLog).toMatchInlineSnapshot('"true"') 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /questions/11-next-dom-update/info.yml: -------------------------------------------------------------------------------- 1 | title: Next DOM update flush 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: easy 8 | 9 | tags: Global API:General 10 | -------------------------------------------------------------------------------- /questions/11-next-dom-update/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 下一次DOM更新 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/12-optimize-perf-directive/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /questions/12-optimize-perf-directive/README.md: -------------------------------------------------------------------------------- 1 |

Optimize performance directive medium #Directives #Built-ins

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | 4 | `Vue.js` provides a directive that renders the element and the component only once, and skips future updates. 5 | 6 | Do you know what the directive is?. Lets try it 👇: 7 | 8 | ```vue 9 | 18 | 19 | 22 | 23 | ``` 24 | 25 |
Back Share your Solutions Check out Solutions 26 | -------------------------------------------------------------------------------- /questions/12-optimize-perf-directive/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

优化性能的指令 中等 #Directives #Built-ins

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | `Vue.js` 提供了一个指令,以便只渲染一次元素和组件,并且跳过以后的更新。 5 | 6 | 你知道它是什么吗 ? 让我们试试👇: 7 | 8 | ```vue 9 | 18 | 19 | 22 | 23 | ``` 24 |
返回首页 分享你的解答 查看解答 25 | -------------------------------------------------------------------------------- /questions/12-optimize-perf-directive/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import OptimizePerfDirective from "./App.vue" 5 | 6 | function delay(timeout: number) { 7 | return new Promise((resolve) => { 8 | setTimeout(resolve, timeout) 9 | }) 10 | } 11 | 12 | describe("OptimizePerfDirective", () => { 13 | it("should work", async() => { 14 | const wrapper = mount(OptimizePerfDirective) 15 | await delay(1100) 16 | const content = +wrapper.vm.$el.innerHTML.replace(/\D/g, "") 17 | expect(content).toBe(0) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /questions/12-optimize-perf-directive/info.yml: -------------------------------------------------------------------------------- 1 | title: Optimize performance directive 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: medium 8 | 9 | tags: Directives,Built-ins 10 | -------------------------------------------------------------------------------- /questions/12-optimize-perf-directive/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 优化性能的指令 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/13-dom-portal/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /questions/13-dom-portal/README.md: -------------------------------------------------------------------------------- 1 |

DOM Portal easy #Components #Built-ins

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | 4 | `Vue.js` provides a built-in component that renders its slot content to another part of the `DOM`. 5 | 6 | Do you know the built-in component?. Lets try it 👇: 7 | 8 | ```vue 9 | 14 | 15 | 19 | 20 | 21 | ``` 22 | 23 |
Back Share your Solutions Check out Solutions 24 | -------------------------------------------------------------------------------- /questions/13-dom-portal/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

DOM传送门 简单 #Components #Built-ins

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | `Vue.js`提供了一个内置组件,将其插槽内容渲染到另一个DOM,成为该DOM的一部分。 5 | 6 | 你知道它是什么吗 ? 让我们试试👇: 7 | 8 | ```vue 9 | 14 | 15 | 19 | 20 | 21 | ``` 22 |
返回首页 分享你的解答 查看解答 23 | -------------------------------------------------------------------------------- /questions/13-dom-portal/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | describe("DomPortal", () => { 7 | it("render to body", () => { 8 | const wrapper = mount(App) 9 | expect(wrapper.find("span").exists()).toBeFalsy() 10 | expect(document.body.innerHTML).toMatchInlineSnapshot("\"Hello World\"") 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /questions/13-dom-portal/info.yml: -------------------------------------------------------------------------------- 1 | title: DOM Portal 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: easy 8 | 9 | tags: Components,Built-ins 10 | -------------------------------------------------------------------------------- /questions/13-dom-portal/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: DOM传送门 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/14-dynamic-css-values/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | 17 | 23 | -------------------------------------------------------------------------------- /questions/14-dynamic-css-values/README.md: -------------------------------------------------------------------------------- 1 |

Dynamic css values easy #CSS Features

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | 4 | SFC ` 31 | 32 | ``` 33 | 34 |
Back Share your Solutions Check out Solutions 35 | -------------------------------------------------------------------------------- /questions/14-dynamic-css-values/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

动态CSS 简单 #CSS Features

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 5 | 6 | `Vue`单文件组件 ` 33 | 34 | ``` 35 |
返回首页 分享你的解答 查看解答 36 | -------------------------------------------------------------------------------- /questions/14-dynamic-css-values/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest" 2 | 3 | import AppRaw from "./App.vue?raw" 4 | 5 | describe("DomPortal", () => { 6 | it("render to body", () => { 7 | expect(AppRaw).toContain(atob("Y29sb3I6IHYtYmluZCh0aGVtZSk=")) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /questions/14-dynamic-css-values/info.yml: -------------------------------------------------------------------------------- 1 | title: Dynamic css values 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: easy 8 | 9 | tags: CSS Features 10 | -------------------------------------------------------------------------------- /questions/14-dynamic-css-values/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 动态CSS 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/15-useToggle/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | -------------------------------------------------------------------------------- /questions/15-useToggle/README.md: -------------------------------------------------------------------------------- 1 |

useToggle medium #Composable Function

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | 4 | For this challenge, we'll start by creating a composable function. Lets start with `useToggle` 👇: 5 | 6 | 7 | ```vue 8 | 21 | 22 | 28 | 29 | ``` 30 | 31 |
Back Share your Solutions Check out Solutions 32 | -------------------------------------------------------------------------------- /questions/15-useToggle/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

切换器 中等 #Composable Function

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 这个挑战开始,我们将尝试编写可组合函数,让我们从`useToggle`开始 👇: 5 | 6 | ```vue 7 | 20 | 21 | 27 | 28 | ``` 29 |
返回首页 分享你的解答 查看解答 30 | -------------------------------------------------------------------------------- /questions/15-useToggle/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | describe("useToggle", () => { 7 | it("toggle state", async() => { 8 | const wrapper = mount(App) 9 | const p1 = wrapper.findAll("p")[0] 10 | const p2 = wrapper.findAll("p")[1] 11 | expect(p1.text()).toBe("State: OFF") 12 | 13 | await p2.trigger("click") 14 | expect(p1.text()).toBe("State: ON") 15 | 16 | await p2.trigger("click") 17 | expect(p1.text()).toBe("State: OFF") 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /questions/15-useToggle/info.yml: -------------------------------------------------------------------------------- 1 | title: useToggle 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: medium 8 | 9 | tags: Composable Function 10 | -------------------------------------------------------------------------------- /questions/15-useToggle/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 切换器 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/16-until/App.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 36 | -------------------------------------------------------------------------------- /questions/16-until/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

until 中等 #Utility Function

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 有些时候,我们需要依赖于异步的返回结果做一些后续处理,`until`函数在这种场景下非常有用,你能实现它吗 ? 让我们来试试吧 👇: 5 | 6 | 7 | ```vue 8 | 37 | 38 | ``` 39 |
返回首页 分享你的解答 查看解答 40 | -------------------------------------------------------------------------------- /questions/16-until/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect, vi } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | function delay(timeout: number) { 7 | return new Promise((resolve) => { 8 | setTimeout(resolve, timeout) 9 | }) 10 | } 11 | 12 | describe("until", () => { 13 | it("should work", async() => { 14 | const result: string[] = [] 15 | console.log = vi.fn((log: string) => { 16 | result.push(log) 17 | }) 18 | 19 | const wrapper = mount(App) 20 | 21 | await wrapper.find("p").trigger("click") 22 | await delay(4000) 23 | expect(JSON.stringify(result)).toBe("[true]") 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /questions/16-until/info.yml: -------------------------------------------------------------------------------- 1 | title: until 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: medium 8 | 9 | tags: Utility Function 10 | -------------------------------------------------------------------------------- /questions/16-until/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: until 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/17-useCounter/App.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 32 | -------------------------------------------------------------------------------- /questions/17-useCounter/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect, beforeEach } from "vitest" 3 | import type { DOMWrapper } from "@vue/test-utils" 4 | 5 | import App from "./App.vue" 6 | 7 | async function triggerClick(target: DOMWrapper, times = 1) { 8 | for (let i = 0; i < times; i++) 9 | await target.trigger("click") 10 | } 11 | 12 | describe("useCounter", () => { 13 | let wrapper, incBtn, decBtn, resetBtn, text 14 | 15 | beforeEach(() => { 16 | wrapper = mount(App) 17 | incBtn = wrapper.findAll("button")[0] 18 | decBtn = wrapper.findAll("button")[1] 19 | resetBtn = wrapper.findAll("button")[2] 20 | text = wrapper.find("p") 21 | }) 22 | 23 | it("should work", async() => { 24 | expect(text.text()).toBe("Count: 0") 25 | await triggerClick(incBtn, 4) 26 | expect(text.text()).toBe("Count: 4") 27 | await triggerClick(decBtn, 2) 28 | expect(text.text()).toBe("Count: 2") 29 | }) 30 | 31 | it("support min and max", async() => { 32 | expect(text.text()).toBe("Count: 0") 33 | await triggerClick(incBtn, 15) 34 | expect(text.text()).toBe("Count: 10") 35 | await triggerClick(decBtn, 20) 36 | expect(text.text()).toBe("Count: 0") 37 | }) 38 | 39 | it('support reset', async() => { 40 | expect(text.text()).toBe("Count: 0") 41 | await triggerClick(incBtn, 3) 42 | expect(text.text()).toBe("Count: 3") 43 | await triggerClick(resetBtn) 44 | expect(text.text()).toBe("Count: 0") 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /questions/17-useCounter/info.yml: -------------------------------------------------------------------------------- 1 | title: useCounter 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: medium 8 | 9 | tags: Composable Function 10 | -------------------------------------------------------------------------------- /questions/17-useCounter/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 计数器 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/18-useLocalStorage/App.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 32 | -------------------------------------------------------------------------------- /questions/18-useLocalStorage/README.md: -------------------------------------------------------------------------------- 1 |

useLocalStorage medium #Composable Function

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | 4 | We often need to use the `localStorage` API. A composable function will help us use it better. Lets go. 👇: 5 | 6 | 7 | ```vue 8 | 32 | 33 | ``` 34 | 35 |
Back Share your Solutions Check out Solutions 36 | -------------------------------------------------------------------------------- /questions/18-useLocalStorage/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

实现本地存储函数 中等 #Composable Function

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 我们经常需要使用`localStorage`API,一个好用的可组合函数封装将帮助我们更好地使用它,让我们开始吧 👇: 5 | 6 | 7 | ```vue 8 | 31 | 32 | ``` 33 |
返回首页 分享你的解答 查看解答 34 | -------------------------------------------------------------------------------- /questions/18-useLocalStorage/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | describe("useLocalStorage", () => { 7 | it("should work", async() => { 8 | let wrapper = mount(App) 9 | expect(wrapper.find("p").text()).toBe("Counter: 0") 10 | await wrapper.find("button").trigger("click") 11 | expect(wrapper.find("p").text()).toBe("Counter: 1") 12 | 13 | wrapper.unmount() 14 | 15 | wrapper = mount(App) 16 | expect(wrapper.find("p").text()).toBe("Counter: 1") 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /questions/18-useLocalStorage/info.yml: -------------------------------------------------------------------------------- 1 | title: useLocalStorage 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: medium 8 | 9 | tags: Composable Function 10 | -------------------------------------------------------------------------------- /questions/18-useLocalStorage/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 实现本地存储函数 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/19-v-focus/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /questions/19-v-focus/README.md: -------------------------------------------------------------------------------- 1 |

v-focus medium #Directives

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | 4 | For this challenge, we'll start by creating a custom directive. Lets start with `v-focus` 👇: 5 | 6 | ```vue 7 | 27 | 28 | 31 | 32 | ``` 33 | 34 |
Back Share your Solutions Check out Solutions 35 | -------------------------------------------------------------------------------- /questions/19-v-focus/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

切换焦点指令 中等 #Directives

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 这个挑战开始,我们将尝试编写自定义指令,让我们从`v-focus`开始 👇: 5 | 6 | ```vue 7 | 26 | 27 | 30 | 31 | ``` 32 | 33 |
返回首页 分享你的解答 查看解答 34 | -------------------------------------------------------------------------------- /questions/19-v-focus/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | function delay(timeout: number) { 7 | return new Promise((resolve) => { 8 | setTimeout(resolve, timeout) 9 | }) 10 | } 11 | 12 | describe("v-focus", () => { 13 | it("should work", async() => { 14 | const wrapper = mount(App, { 15 | attachTo: document.body, 16 | }) 17 | 18 | expect(wrapper.find("input").element).not.toBe(document.activeElement) 19 | await delay(3000) 20 | expect(wrapper.find("input").element).toBe(document.activeElement) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /questions/19-v-focus/info.yml: -------------------------------------------------------------------------------- 1 | title: v-focus 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: medium 8 | 9 | tags: Directives 10 | -------------------------------------------------------------------------------- /questions/19-v-focus/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 切换焦点指令 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/2-ref-family/App.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 63 | -------------------------------------------------------------------------------- /questions/2-ref-family/index.test.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from "vue" 2 | import { mount } from "@vue/test-utils" 3 | import { describe, it, expect, vi } from "vitest" 4 | 5 | import RefFamily from "./App.vue" 6 | 7 | interface RefFamilyType { 8 | count: Ref 9 | initial: Ref 10 | update: (value: number) => void 11 | initialCount: (value: number | Ref) => number 12 | state: {foo: number; bar: number} 13 | fooRef: Ref 14 | } 15 | 16 | describe("RefFamily", () => { 17 | it("update ref function", () => { 18 | const wrapper = mount(RefFamily); 19 | (wrapper.vm as unknown as RefFamilyType).update(996) 20 | expect((wrapper.vm as unknown as RefFamilyType).count).toBe(996) 21 | }) 22 | 23 | it("should work", () => { 24 | const result: string[] = [] 25 | console.log = vi.fn((log: string) => { 26 | result.push(log) 27 | }) 28 | mount(RefFamily) 29 | expect(JSON.stringify(result)).toBe(JSON.stringify([ 30 | 1, true, true, true, 31 | ])) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /questions/2-ref-family/info.yml: -------------------------------------------------------------------------------- 1 | title: ref family 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: easy 8 | 9 | tags: Composition API,Reactivity:Core 10 | -------------------------------------------------------------------------------- /questions/2-ref-family/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: ref 全家桶 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/20-v-debounce-click/App.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 25 | -------------------------------------------------------------------------------- /questions/20-v-debounce-click/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

防抖点击指令 中等 #Directives

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 在这个挑战中,我们将尝试实现一个防抖点击指令,让我们开始吧 👇: 5 | 6 | ```vue 7 | 25 | 26 | 31 | 32 | ``` 33 | 34 |
返回首页 分享你的解答 查看解答 35 | -------------------------------------------------------------------------------- /questions/20-v-debounce-click/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect, vi } from "vitest" 3 | import type { DOMWrapper } from "@vue/test-utils" 4 | 5 | import App from "./App.vue" 6 | 7 | async function triggerClick(target: DOMWrapper, times = 1) { 8 | for (let i = 0; i < times; i++) 9 | await target.trigger("click") 10 | } 11 | 12 | describe("v-debounce-click", () => { 13 | it("should work", async() => { 14 | const result: string[] = [] 15 | console.log = vi.fn((log: string) => { 16 | result.push(log) 17 | }) 18 | const wrapper = mount(App) 19 | await triggerClick(wrapper.find("button"), 2) 20 | expect(JSON.stringify(result)).toBe("[\"Only triggered once when clicked many times quickly\"]") 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /questions/20-v-debounce-click/info.yml: -------------------------------------------------------------------------------- 1 | title: v-debounce-click 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: medium 8 | 9 | tags: Directives 10 | -------------------------------------------------------------------------------- /questions/20-v-debounce-click/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 防抖点击指令 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/208-tree-component/App.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 44 | -------------------------------------------------------------------------------- /questions/208-tree-component/README.md: -------------------------------------------------------------------------------- 1 |

Tree Component hard #Components

By 木荣 @murongg

Take the Challenge    简体中文

2 | 3 | For this challenge, you need to implement a tree component. Lets go. 4 | 5 | ```vue 6 | 14 | 15 | 18 | ``` 19 | 20 |
Back Share your Solutions Check out Solutions 21 | -------------------------------------------------------------------------------- /questions/208-tree-component/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

树组件 困难 #Components

By 木荣 @murongg

接受挑战    English

2 | 3 | 4 | 在这个挑战中,你需要实现一个树组件,让我们开始吧。 5 | 6 | ```vue 7 | 15 | 16 | 19 | ``` 20 | 21 |
返回首页 分享你的解答 查看解答 22 | -------------------------------------------------------------------------------- /questions/208-tree-component/TreeComponent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /questions/208-tree-component/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import App from "./App.vue" 5 | import Tree from "./TreeComponent.vue" 6 | 7 | describe("TreeComponent", () => { 8 | it("should work", async() => { 9 | const wrapper = mount(App) 10 | const wrapperTree = wrapper.findComponent(Tree) 11 | await wrapper.setProps({ 12 | data: [ 13 | { 14 | key: "1", 15 | title: "Parent 1", 16 | children: [{ 17 | key: "1-1", 18 | title: "child 1", 19 | }, { 20 | key: "1-2", 21 | title: "child 2", 22 | children: [{ 23 | key: "1-2-1", 24 | title: "grandchild 1", 25 | }, { 26 | key: "1-2-2", 27 | title: "grandchild 2", 28 | }], 29 | }], 30 | }, 31 | { 32 | key: "2", 33 | title: "Parent 2", 34 | children: [{ 35 | key: "2-1", 36 | title: "child 1", 37 | children: [{ 38 | key: "2-1-1", 39 | title: "grandchild 1", 40 | }, { 41 | key: "2-1-2", 42 | title: "grandchild 2", 43 | }], 44 | }, { 45 | key: "2-2", 46 | title: "child 2", 47 | }], 48 | }, 49 | { 50 | key: "3", 51 | title: "Parent 3", 52 | children: [{ 53 | key: "3-1", 54 | title: "child 1", 55 | children: [{ 56 | key: "3-1-1", 57 | title: "grandchild 1", 58 | }, { 59 | key: "3-1-2", 60 | title: "grandchild 2", 61 | }], 62 | }], 63 | }, 64 | ], 65 | }) 66 | expect(wrapperTree.text()).toMatchInlineSnapshot("\"Parent 1child 1child 2grandchild 1grandchild 2Parent 2child 1grandchild 1grandchild 2child 2Parent 3child 1grandchild 1grandchild 2\"") 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /questions/208-tree-component/info.yml: -------------------------------------------------------------------------------- 1 | difficulty: hard 2 | title: Tree Component 3 | tags: Components 4 | author: 5 | github: murongg 6 | name: 木荣 7 | 8 | -------------------------------------------------------------------------------- /questions/208-tree-component/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | difficulty: hard 2 | title: 树组件 3 | tags: Components 4 | author: 5 | github: murongg 6 | name: 木荣 7 | 8 | -------------------------------------------------------------------------------- /questions/21-functional-component/App.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 36 | -------------------------------------------------------------------------------- /questions/21-functional-component/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | describe("functional-component", () => { 7 | it("should work", async() => { 8 | const wrapper = mount(App) 9 | const list = wrapper.findComponent({ name: "list-component" }) 10 | expect(list.vm).toBeUndefined() 11 | 12 | const item1 = list.findAll("li")[0] 13 | const item2 = list.findAll("li")[1] 14 | expect(item1.attributes("style")).toBe("color: red;") 15 | expect(item2.attributes("style")).toBeUndefined() 16 | 17 | await item2.trigger("click") 18 | 19 | expect(item1.attributes("style")).toBeUndefined() 20 | expect(item2.attributes("style")).toBe("color: red;") 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /questions/21-functional-component/info.yml: -------------------------------------------------------------------------------- 1 | title: functional component 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: medium 8 | 9 | tags: Components 10 | -------------------------------------------------------------------------------- /questions/21-functional-component/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 函数式组件 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/218-h-render-function/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /questions/218-h-render-function/MyButton.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent } from "vue" 2 | 3 | export default defineComponent({ 4 | name: 'MyButton', 5 | render() { 6 | return h(/** do someting */) 7 | } 8 | }) -------------------------------------------------------------------------------- /questions/218-h-render-function/README.md: -------------------------------------------------------------------------------- 1 |

render function[h()] medium #Components

By 木荣 @murongg

Take the Challenge    简体中文

2 | 3 | 4 | For this challenge, you need use `h` render function to implement a component. 5 | 6 | Note: You should make sure that the props are passed correctly, event is triggered correctly and the slot content is rendered correctly. Lets go. 7 | 8 | ```vue 9 | 15 | 16 | 21 | 22 | ``` 23 |
Back Share your Solutions Check out Solutions 24 | -------------------------------------------------------------------------------- /questions/218-h-render-function/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

渲染函数[h()] 中等 #Components

By 木荣 @murongg

接受挑战    English

2 | 3 | 在这个挑战中,你需要使用`h`渲染函数来实现一个组件。 4 | 5 | 请注意: 你应该确保参数被正确传递、事件被正常触发和插槽内容正常渲染。让我们开始吧。 6 | 7 | ```vue 8 | 14 | 19 | ``` 20 | 21 | 22 |
返回首页 分享你的解答 查看解答 23 | -------------------------------------------------------------------------------- /questions/218-h-render-function/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import MyButton from "./MyButton" 5 | 6 | describe("Render function h()", () => { 7 | it("renders a 'MyButton'", () => { 8 | const wrapper = mount(MyButton) 9 | expect(wrapper.element.tagName.toLocaleLowerCase()).toBe("button") 10 | }) 11 | 12 | it("disabled", async() => { 13 | const wrapper = mount(MyButton, { 14 | props: { 15 | disabled: true, 16 | }, 17 | }) 18 | expect(wrapper.find("button").attributes()).toBeDefined() 19 | 20 | await wrapper.trigger("click") 21 | expect(wrapper.emitted("click")).toBeUndefined() 22 | }) 23 | 24 | it("slot", () => { 25 | const wrapper = mount(MyButton, { 26 | slots: { 27 | default: "my button", 28 | }, 29 | }) 30 | expect(wrapper.text()).toBe("my button") 31 | }) 32 | 33 | it("custom click defined", () => { 34 | const wrapper = mount(MyButton) 35 | wrapper.trigger("click") 36 | expect(wrapper.emitted().customClick || wrapper.emitted()["custom-click"]).toBeTruthy() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /questions/218-h-render-function/info.yml: -------------------------------------------------------------------------------- 1 | difficulty: medium 2 | title: render function[h()] 3 | tags: Components 4 | author: 5 | github: murongg 6 | name: 木荣 7 | 8 | -------------------------------------------------------------------------------- /questions/218-h-render-function/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | difficulty: medium 2 | title: 渲染函数[h()] 3 | tags: Components 4 | author: 5 | github: murongg 6 | name: 木荣 7 | 8 | -------------------------------------------------------------------------------- /questions/22-custom-element/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /questions/22-custom-element/README.md: -------------------------------------------------------------------------------- 1 |

custom element hard #Web Components

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | 4 | Have you heard about `Web Components` ? 5 | 6 | Vue has excellent support for both creating and consuming custom elements. 7 | 8 | For this challenge, you can try it out. Lets go 👇: 9 | 10 | ```vue 11 | 26 | 27 | 30 | 31 | ``` 32 | 33 |
Back Share your Solutions Check out Solutions 34 | -------------------------------------------------------------------------------- /questions/22-custom-element/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

自定义元素 困难 #Web Components

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 你听说过 `Web Components` 吗 ? 5 | 6 | Vue 能很好地解析和创建 `Web Components` 。 7 | 8 | 在这个挑战中,我们将尝试了解它,让我们开始吧 👇: 9 | 10 | ```vue 11 | 26 | 27 | 30 | 31 | ``` 32 |
返回首页 分享你的解答 查看解答 33 | -------------------------------------------------------------------------------- /questions/22-custom-element/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | describe("custom-component", () => { 7 | it("should work", () => { 8 | const wrapper = mount(App, { attachTo: document.body }) 9 | const custom = wrapper.find('vue-js') 10 | expect(custom.exists()).toBeTruthy() 11 | expect(custom.element.shadowRoot?.innerHTML).toBe("Hello Vue.js") 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /questions/22-custom-element/info.yml: -------------------------------------------------------------------------------- 1 | title: custom element 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: hard 8 | 9 | tags: Web Components 10 | -------------------------------------------------------------------------------- /questions/22-custom-element/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 自定义元素 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/23-custom-ref/App.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /questions/23-custom-ref/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

自定义ref 困难 #Composition API #Reactivity:Advanced

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 防抖函数在输入框操作场景中非常有用。 5 | 6 | 一个 防抖的`ref`在`Vue.js`更加灵活,让我们开始吧 👇: 7 | 8 | ```vue 9 | 27 | 28 | 31 | 32 | ``` 33 |
返回首页 分享你的解答 查看解答 34 | -------------------------------------------------------------------------------- /questions/23-custom-ref/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect, vi } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | function delay(timeout: number) { 7 | return new Promise((resolve) => { 8 | setTimeout(resolve, timeout) 9 | }) 10 | } 11 | 12 | describe("custom-ref", () => { 13 | it("should work", async() => { 14 | let printLog = '' 15 | console.log = vi.fn((log: string) => { 16 | printLog = log?.toString()?.trim() 17 | }) 18 | 19 | const wrapper = mount(App) 20 | 21 | await wrapper.find("input").setValue("hello!") 22 | expect(printLog).toMatchInlineSnapshot('""') 23 | await delay(200) 24 | expect(printLog).toMatchInlineSnapshot('"hello!"') 25 | 26 | await wrapper.find("input").setValue("world") 27 | expect(printLog).toMatchInlineSnapshot('"hello!"') 28 | await delay(200) 29 | expect(printLog).toMatchInlineSnapshot('"world"') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /questions/23-custom-ref/info.yml: -------------------------------------------------------------------------------- 1 | title: custom ref 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: hard 8 | 9 | tags: Composition API,Reactivity:Advanced 10 | -------------------------------------------------------------------------------- /questions/23-custom-ref/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 自定义ref 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/232-key-modifiers/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /questions/232-key-modifiers/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

按键修饰符 中等 #Event Handling

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 在监听键盘事件时,我们经常需要检查特定的按键。Vue 允许为 v-on 或者 @ 在监听键盘事件时添加按键修饰符:,例如: 5 | 6 | ```vue 7 | 8 | 9 | ``` 10 | 11 | 在这个挑战中,我们将尝试它,让我们开始吧: 12 | 13 | ```vue 14 | 24 | 25 | ``` 26 | 27 | 28 |
返回首页 分享你的解答 查看解答 29 | -------------------------------------------------------------------------------- /questions/232-key-modifiers/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect, vi } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | describe("key modifiers", () => { 7 | it("should work", async() => { 8 | let printLog = "" 9 | console.log = vi.fn( 10 | (log: string) => { 11 | printLog = log?.toString()?.trim() 12 | }) 13 | const wrapper = mount(App) 14 | const buttons = wrapper.findAll("button") 15 | 16 | await buttons[0].trigger('click') 17 | expect(printLog).toMatchInlineSnapshot('""') 18 | await buttons[0].trigger('click.alt') 19 | expect(printLog).toMatchInlineSnapshot('"onClick1"') 20 | await buttons[0].trigger('click.shift') 21 | expect(printLog).toMatchInlineSnapshot('"onClick1"') 22 | 23 | await buttons[1].trigger('click') 24 | expect(printLog).toMatchInlineSnapshot('"onClick1"') 25 | await buttons[1].trigger('click.shift') 26 | expect(printLog).toMatchInlineSnapshot('"onCtrlClick"') 27 | 28 | await buttons[2].trigger('click') 29 | expect(printLog).toMatchInlineSnapshot('"onClick2"') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /questions/232-key-modifiers/info.yml: -------------------------------------------------------------------------------- 1 | difficulty: medium 2 | title: Key Modifiers 3 | tags: Event Handling 4 | author: 5 | github: webfansplz 6 | name: webfansplz 7 | 8 | -------------------------------------------------------------------------------- /questions/232-key-modifiers/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | difficulty: medium 2 | title: 按键修饰符 3 | tags: Event Handling 4 | author: 5 | github: webfansplz 6 | name: webfansplz 7 | 8 | -------------------------------------------------------------------------------- /questions/24-v-active-style/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | -------------------------------------------------------------------------------- /questions/24-v-active-style/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | describe("v-active-style", () => { 7 | it("should work", async() => { 8 | const wrapper = mount(App) 9 | const list = wrapper.findAll('li') 10 | 11 | expect(list[0].attributes("style")).toBe("color: red;") 12 | expect(list[1].attributes("style")).toBeUndefined() 13 | 14 | await list[1].trigger("click") 15 | 16 | expect(list[0].attributes("style")).toBe("") 17 | expect(list[1].attributes("style")).toBe("color: red;") 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /questions/24-v-active-style/info.yml: -------------------------------------------------------------------------------- 1 | title: v-active-style 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: hard 8 | 9 | tags: Directives 10 | -------------------------------------------------------------------------------- /questions/24-v-active-style/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 激活的样式-指令 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/243-prevent-event-propagation/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /questions/243-prevent-event-propagation/README.md: -------------------------------------------------------------------------------- 1 |

prevent event propagation easy #Event Handling

By 木荣 @murongg

Take the Challenge    简体中文

2 | 3 | 4 | In this challenge,you should make the click event's propagation to be stopped,let's go 👇: 5 | 6 | 7 | ```vue 8 | 19 | 20 | 27 | ``` 28 | 29 | 30 |
Back Share your Solutions Check out Solutions 31 | -------------------------------------------------------------------------------- /questions/243-prevent-event-propagation/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

阻止事件冒泡 简单 #Event Handling

By 木荣 @murongg

接受挑战    English

2 | 3 | 4 | 在这个挑战中,你需要阻止点击事件的冒泡,让我们开始吧。 5 | 6 | ```vue 7 | 18 | 19 | 26 | ``` 27 | 28 | 29 |
返回首页 分享你的解答 查看解答 30 | -------------------------------------------------------------------------------- /questions/243-prevent-event-propagation/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect, vi } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | describe("prevent-event-propagation", () => { 7 | it("should work", async() => { 8 | const result: string[] = [] 9 | console.log = vi.fn((log: string) => { 10 | result.push(log) 11 | }) 12 | 13 | const wrapper = mount(App) 14 | 15 | await wrapper.findAll("div")[1].trigger("click") 16 | expect(JSON.stringify(result)).toBe("[\"click2\"]") 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /questions/243-prevent-event-propagation/info.yml: -------------------------------------------------------------------------------- 1 | difficulty: easy 2 | title: prevent event propagation 3 | tags: Event Handling 4 | author: 5 | github: murongg 6 | name: 木荣 7 | 8 | -------------------------------------------------------------------------------- /questions/243-prevent-event-propagation/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | difficulty: easy 2 | title: 阻止事件冒泡 3 | tags: Event Handling 4 | author: 5 | github: murongg 6 | name: 木荣 7 | 8 | -------------------------------------------------------------------------------- /questions/25-useMouse/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /questions/25-useMouse/README.md: -------------------------------------------------------------------------------- 1 |

useMouse medium #Composable Function

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | 4 | We should focus on reusability when using `Vue.js`. Composables are a great way to ensure this. Let's go 👇: 5 | 6 | 7 | ```vue 8 | 21 | 22 | 23 | 24 | 25 | ``` 26 | 27 |
Back Share your Solutions Check out Solutions 28 | -------------------------------------------------------------------------------- /questions/25-useMouse/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

鼠标坐标 中等 #Composable Function

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 在使用`Vue.js`时,我们应该关注可复用性,可组合函数是一个很好的方式,让我们开始吧 👇: 5 | 6 | 7 | ```vue 8 | 21 | 22 | 23 | 24 | 25 | ``` 26 |
返回首页 分享你的解答 查看解答 27 | -------------------------------------------------------------------------------- /questions/25-useMouse/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | import { nextTick } from "vue" 4 | 5 | import App from "./App.vue" 6 | 7 | describe("useMouse", () => { 8 | it("should work", async() => { 9 | const wrapper = mount(App) 10 | expect(wrapper.html()).toBe("Mouse position is at: 0, 0") 11 | 12 | const mousemove = new MouseEvent("mousemove", { 13 | screenX: 10, 14 | screenY: 20, 15 | clientX: 10, 16 | clientY: 20, 17 | }) 18 | window.dispatchEvent(mousemove) 19 | 20 | await nextTick() 21 | expect(wrapper.html()).toBe("Mouse position is at: 10, 20") 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /questions/25-useMouse/info.yml: -------------------------------------------------------------------------------- 1 | title: useMouse 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: medium 8 | 9 | tags: Composable Function 10 | -------------------------------------------------------------------------------- /questions/25-useMouse/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 鼠标坐标 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/26-v-model/App.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 22 | -------------------------------------------------------------------------------- /questions/26-v-model/README.md: -------------------------------------------------------------------------------- 1 |

v-model hard #Directives

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | 4 | For this challenge, we're going to implement a simple `v-model` directive. Lets go 👇: 5 | 6 | ```vue 7 | 23 | 24 | 27 | 28 | ``` 29 | 30 |
Back Share your Solutions Check out Solutions 31 | -------------------------------------------------------------------------------- /questions/26-v-model/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

实现简易版`v-model`指令 困难 #Directives

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 在这个挑战中,我们将尝试实现一个简单的`v-model`指令,让我们开始吧 👇: 5 | 6 | ```vue 7 | 23 | 24 | 27 | 28 | ``` 29 |
返回首页 分享你的解答 查看解答 30 | -------------------------------------------------------------------------------- /questions/26-v-model/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | describe("v-model", () => { 7 | it("should work", async() => { 8 | const wrapper = mount(App) 9 | const input = wrapper.find("input") 10 | const p = wrapper.find("p") 11 | 12 | expect(input.element.value).toBe("Hello Vue.js") 13 | expect(p.text()).toBe(input.element.value) 14 | 15 | await input.setValue("Hello World") 16 | expect(input.element.value).toBe("Hello World") 17 | expect(p.text()).toBe(input.element.value) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /questions/26-v-model/info.yml: -------------------------------------------------------------------------------- 1 | title: v-model 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: hard 8 | 9 | tags: Directives 10 | -------------------------------------------------------------------------------- /questions/26-v-model/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 实现简易版`v-model`指令 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/27-global-css/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /questions/27-global-css/README.md: -------------------------------------------------------------------------------- 1 |

Global CSS medium #CSS Features

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | 4 | Sometimes, we may want to set global CSS in the scoped component. Do you know how to solve it?. Lets go 👇: 5 | 6 | ```css 7 | 10 | 11 | 27 | ``` 28 | 29 |
Back Share your Solutions Check out Solutions 30 | -------------------------------------------------------------------------------- /questions/27-global-css/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

全局CSS 中等 #CSS Features

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 有些时候,我们想在具有CSS作用域的`Vue`单文件组件设置全局CSS样式, 该怎么设置呢 ? 让我们开始吧 👇: 5 | 6 | ```css 7 | 10 | 11 | 27 | ``` 28 |
返回首页 分享你的解答 查看解答 29 | -------------------------------------------------------------------------------- /questions/27-global-css/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest" 2 | 3 | import AppRaw from "./App.vue?raw" 4 | 5 | describe("DomPortal", () => { 6 | it("render to body", () => { 7 | expect(AppRaw).toContain(atob("Omdsb2JhbChib2R5KQ==")) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /questions/27-global-css/info.yml: -------------------------------------------------------------------------------- 1 | title: Global CSS 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: medium 8 | 9 | tags: CSS Features 10 | -------------------------------------------------------------------------------- /questions/27-global-css/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 全局CSS 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/3-losing-reactivity/App.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 33 | -------------------------------------------------------------------------------- /questions/3-losing-reactivity/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | import type { Ref } from "vue" 4 | import { isRef } from "vue" 5 | 6 | import LosingReactivity from "./App.vue" 7 | 8 | interface LosingReactivityType { 9 | useCount: () => { 10 | state: {count: Ref} 11 | update: (value: number) => void 12 | } 13 | } 14 | 15 | describe("LosingReactivity", () => { 16 | it("count is Ref", () => { 17 | const wrapper = mount(LosingReactivity) 18 | const { state: { count } } = (wrapper.vm as unknown as LosingReactivityType).useCount() 19 | expect(isRef(count)).toBe(true) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /questions/3-losing-reactivity/info.yml: -------------------------------------------------------------------------------- 1 | title: losing-reactivity 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: easy 8 | 9 | tags: Composition API,Reactivity:Utilities 10 | -------------------------------------------------------------------------------- /questions/3-losing-reactivity/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 响应性丟失 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/305-capitalize/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | -------------------------------------------------------------------------------- /questions/305-capitalize/README.md: -------------------------------------------------------------------------------- 1 |

Capitalize easy #Directives

By Lov`u`e @heappynd

Take the Challenge    简体中文

2 | 3 | Create a custom modifier for the 'v-model' directive that changes the first letter of the 'v-model' binding value to uppercase. 4 | 5 | ```vue 6 | 8 | 9 | 12 | ``` 13 | 14 |
Back Share your Solutions Check out Solutions 15 | -------------------------------------------------------------------------------- /questions/305-capitalize/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

大写 简单 #Directives

By Lov`u`e @heappynd

接受挑战    English

2 | 3 | 请创建一个自定义的修饰符 `capitalize`,它会自动将 `v-model` 绑定输入的字符串值首字母转为大写: 4 | ```vue 5 | 7 | 8 | 11 | ``` 12 | 13 |
返回首页 分享你的解答 查看解答 14 | -------------------------------------------------------------------------------- /questions/305-capitalize/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | describe("capitalize", () => { 7 | it("should work", async() => { 8 | const wrapper = mount(App) 9 | await wrapper.find('input').setValue("hello") 10 | expect(wrapper.find('input').element.value).toBe("Hello") 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /questions/305-capitalize/info.yml: -------------------------------------------------------------------------------- 1 | difficulty: easy 2 | title: Capitalize 3 | tags: Directives 4 | author: 5 | github: heappynd 6 | name: Lov`u`e 7 | 8 | -------------------------------------------------------------------------------- /questions/305-capitalize/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | difficulty: easy 2 | title: 大写 3 | tags: Directives 4 | author: 5 | github: heappynd 6 | name: Lov`u`e 7 | 8 | -------------------------------------------------------------------------------- /questions/323-prop-validation/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /questions/323-prop-validation/Button.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /questions/323-prop-validation/README.md: -------------------------------------------------------------------------------- 1 |

Prop Validation easy #Components

By Lov`u`e @heappynd

Take the Challenge    简体中文

2 | 3 | 4 | Please validate the `type` prop of the `Button` component. it accepts the following strings `primary | ghost | dashed | link | text | default` only and the default value is `default`. 5 | 6 | ```vue 7 | 12 | 13 | 16 | ``` 17 | 18 |
Back Share your Solutions Check out Solutions 19 | -------------------------------------------------------------------------------- /questions/323-prop-validation/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

Prop验证 简单 #Components

By Lov`u`e @heappynd

接受挑战    English

2 | 3 | 4 | 请验证`Button`组件的`Prop`类型 ,使它只接收: `primary | ghost | dashed | link | text | default` ,且默认值为`default`。 5 | 6 | ```vue 7 | 12 | 13 | 16 | ``` 17 | 18 |
返回首页 分享你的解答 查看解答 19 | -------------------------------------------------------------------------------- /questions/323-prop-validation/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import Button from "./Button.vue" 5 | 6 | describe("prop-validation", () => { 7 | it("should have a default type prop of 'default'", () => { 8 | const wrapper = mount(Button) 9 | expect(wrapper.vm.type).toBe("default") 10 | }) 11 | it("should only accept specific values for the type prop", () => { 12 | const wrapper = mount(Button, { 13 | }) 14 | expect(wrapper.vm.$options.props.type.validator("invalid")).toBe(false) 15 | const validTypes = ["primary", "ghost", "dashed", "link", "text", "default"] 16 | const randomType = validTypes[Math.floor(Math.random() * validTypes.length)] 17 | expect(wrapper.vm.$options.props.type.validator(randomType)).toBe(true) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /questions/323-prop-validation/info.yml: -------------------------------------------------------------------------------- 1 | difficulty: easy 2 | title: Prop Validation 3 | tags: Components 4 | author: 5 | github: heappynd 6 | name: Lov`u`e 7 | 8 | -------------------------------------------------------------------------------- /questions/323-prop-validation/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | difficulty: easy 2 | title: Prop验证 3 | tags: Components 4 | author: 5 | github: heappynd 6 | name: Lov`u`e 7 | 8 | -------------------------------------------------------------------------------- /questions/4-writable-computed/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /questions/4-writable-computed/README.md: -------------------------------------------------------------------------------- 1 |

writable-computed easy #Composition API #Reactivity:Core

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | 4 | For this challenge, you will need to create a writable computed ref : 5 | 6 | ```vue 7 | 21 | 22 | 28 | 29 | ``` 30 | 31 |
Back Share your Solutions Check out Solutions 32 | -------------------------------------------------------------------------------- /questions/4-writable-computed/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

可写的计算属性 简单 #Composition API #Reactivity:Core

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 在这个挑战中,你需要创建一个可写的计算属性 : 5 | 6 | ```vue 7 | 21 | 22 | 28 | 29 | ``` 30 |
返回首页 分享你的解答 查看解答 31 | -------------------------------------------------------------------------------- /questions/4-writable-computed/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import Writable from "./App.vue" 5 | 6 | interface WritableType { 7 | plusOne: number 8 | count: number 9 | } 10 | 11 | describe("Writable", () => { 12 | it("Make the `plusOne` writable", () => { 13 | const wrapper = mount(Writable) 14 | expect((wrapper.vm as unknown as WritableType).plusOne).toBe(3) 15 | expect((wrapper.vm as unknown as WritableType).count).toBe(2) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /questions/4-writable-computed/info.yml: -------------------------------------------------------------------------------- 1 | title: writable-computed 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: easy 8 | 9 | tags: Composition API,Reactivity:Core 10 | -------------------------------------------------------------------------------- /questions/4-writable-computed/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 可写的计算属性 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/5-watch-family/App.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 55 | -------------------------------------------------------------------------------- /questions/5-watch-family/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect, vi } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | function delay(timeout: number) { 7 | return new Promise((resolve) => { 8 | setTimeout(resolve, timeout) 9 | }) 10 | } 11 | 12 | describe("Watch Faimily", () => { 13 | it("should work", async() => { 14 | const result: string[] = [] 15 | console.log = vi.fn((log: string) => { 16 | result.push(log?.toString()?.trim()) 17 | }) 18 | mount(App) 19 | await delay(1000) 20 | expect(JSON.stringify(result)).toBe(JSON.stringify([ 21 | "Only triggered once", 22 | "The state.count updated", 23 | "[object HTMLParagraphElement]", 24 | ])) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /questions/5-watch-family/info.yml: -------------------------------------------------------------------------------- 1 | title: watch family 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: easy 8 | 9 | tags: Composition API,Reactivity:Core 10 | 11 | -------------------------------------------------------------------------------- /questions/5-watch-family/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: watch 全家桶 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/6-shallow-ref/App.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /questions/6-shallow-ref/README.md: -------------------------------------------------------------------------------- 1 |

shallow ref easy #Composition API #Reactivity:Advanced

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | 4 | For this challenge, you'll use `Reactivity API: shallowRef` to complete the challenge. 5 | Here's what you need to implement 👇: 6 | 7 | ```vue 8 | 24 | 25 | 32 | 33 | ``` 34 | 35 |
Back Share your Solutions Check out Solutions 36 | -------------------------------------------------------------------------------- /questions/6-shallow-ref/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

浅层 ref 简单 #Composition API #Reactivity:Advanced

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 在这个挑战中,你将使用 `响应式 API: shallowRef` 来完成它。 5 | 以下是你要实现的内容 👇: 6 | 7 | ```vue 8 | 25 | 26 | 33 | 34 | ``` 35 |
返回首页 分享你的解答 查看解答 36 | -------------------------------------------------------------------------------- /questions/6-shallow-ref/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect, vi } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | describe("shallowRef", () => { 7 | it("should work", () => { 8 | const spy = vi.spyOn(console, "log") 9 | mount(App) 10 | expect(spy).toHaveBeenCalled() 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /questions/6-shallow-ref/info.yml: -------------------------------------------------------------------------------- 1 | title: shallow ref 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: easy 8 | 9 | tags: Composition API,Reactivity:Advanced 10 | -------------------------------------------------------------------------------- /questions/6-shallow-ref/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 浅层 ref 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/7-raw-api/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 29 | -------------------------------------------------------------------------------- /questions/7-raw-api/README.md: -------------------------------------------------------------------------------- 1 |

Raw API medium #Reactivity:Advanced

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | 4 | For this challenge, you'll use `Reactivity API: [xx]Raw` to complete the challenge. 5 | Here's what you need to implement 👇: 6 | 7 | ```vue 8 | 28 | 29 | 36 | 37 | 38 | ``` 39 | 40 |
Back Share your Solutions Check out Solutions 41 | -------------------------------------------------------------------------------- /questions/7-raw-api/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

原始值 API 中等 #Reactivity:Advanced

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 在这个挑战中,你将使用 `响应式 API: [xx]Raw` 来完成它。 5 | 以下是你要实现的内容 👇: 6 | 7 | ```vue 8 | 28 | 29 | 36 | 37 | 38 | ``` 39 |
返回首页 分享你的解答 查看解答 40 | -------------------------------------------------------------------------------- /questions/7-raw-api/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect, vi } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | describe("raw API", () => { 7 | it("should work", async() => { 8 | const result: string[] = [] 9 | console.log = vi.fn((log: string) => { 10 | result.push(log) 11 | }) 12 | mount(App) 13 | expect(JSON.stringify(result)).toBe(JSON.stringify([ 14 | true, 15 | false, 16 | ])) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /questions/7-raw-api/info.yml: -------------------------------------------------------------------------------- 1 | title: Raw API 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: medium 8 | 9 | tags: Reactivity:Advanced 10 | -------------------------------------------------------------------------------- /questions/7-raw-api/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 原始值 API 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/8-effect-scope/App.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /questions/8-effect-scope/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect, vi } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | function delay(timeout: number) { 7 | return new Promise((resolve) => { 8 | setTimeout(resolve, timeout) 9 | }) 10 | } 11 | 12 | describe("EffectScope", () => { 13 | it("should work", async() => { 14 | const result: string[] = [] 15 | console.log = vi.fn((log: string) => { 16 | result.push(log) 17 | }) 18 | mount(App) 19 | await delay(1000) 20 | expect(JSON.stringify(result)).toBe(JSON.stringify([ 21 | "Count: 2", 4, "Count: 4", 22 | ])) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /questions/8-effect-scope/info.yml: -------------------------------------------------------------------------------- 1 | title: effectScope API 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: medium 8 | 9 | tags: Composition API,Reactivity:Advanced 10 | -------------------------------------------------------------------------------- /questions/8-effect-scope/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: Effect作用域 API 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /questions/9-dependency-injection/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /questions/9-dependency-injection/Child.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /questions/9-dependency-injection/README.md: -------------------------------------------------------------------------------- 1 |

Dependency Injection easy #Composition API

By webfansplz @webfansplz

Take the Challenge    简体中文

2 | 3 | 4 | For this challenge, you'll use the `Composition API: Dependency Injection` to complete the challenge. 5 | Here's what you need to implement 👇: 6 | 7 | ```vue 8 | // Child.vue 9 | 10 | 13 | 14 | 17 | ``` 18 | 19 |
Back Share your Solutions Check out Solutions 20 | -------------------------------------------------------------------------------- /questions/9-dependency-injection/README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

依赖注入 简单 #Composition API

By webfansplz @webfansplz

接受挑战    English

2 | 3 | 4 | 在这个挑战中,你将使用 `组合式 API: 依赖注入` 来完成它。 5 | 以下是你要实现的内容 👇: 6 | 7 | ```vue 8 | // Child.vue 9 | 10 | 13 | 14 | 17 | ``` 18 | 19 |
返回首页 分享你的解答 查看解答 20 | -------------------------------------------------------------------------------- /questions/9-dependency-injection/index.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils" 2 | import { describe, it, expect } from "vitest" 3 | 4 | import App from "./App.vue" 5 | 6 | describe("Dependency Injection", () => { 7 | it("should work'", () => { 8 | const wrapper = mount(App) 9 | expect(wrapper.vm.$el.textContent).toBe("1") 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /questions/9-dependency-injection/info.yml: -------------------------------------------------------------------------------- 1 | title: Dependency Injection 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | 7 | difficulty: easy 8 | 9 | tags: Composition API 10 | -------------------------------------------------------------------------------- /questions/9-dependency-injection/info.zh-CN.yml: -------------------------------------------------------------------------------- 1 | title: 依赖注入 2 | 3 | author: 4 | name: webfansplz 5 | github: webfansplz 6 | -------------------------------------------------------------------------------- /scripts/actions/labeling.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "../types" 2 | 3 | const action: Action = async(github, context, core) => { 4 | const payload = context.payload 5 | const issue = payload.issue 6 | 7 | if (!issue) 8 | return 9 | 10 | const labels: string[] = (issue.labels || []) 11 | .map((i: any) => i && i.name) 12 | .filter(Boolean) 13 | 14 | if (labels.includes("answer")) { 15 | const match = issue.title.match(/^(\d+) - /) 16 | if (match && match[1]) { 17 | const no = Number(match[1]) 18 | if (isNaN(no)) 19 | return 20 | 21 | const name = no.toString() 22 | 23 | if (labels.includes("trigger-bot")) { 24 | await github.issues.removeLabel({ 25 | issue_number: context.issue.number, 26 | owner: context.repo.owner, 27 | repo: context.repo.repo, 28 | name: "trigger-bot", 29 | }) 30 | } 31 | 32 | if (labels.includes(name)) 33 | return 34 | 35 | try { 36 | await github.issues.getLabel({ 37 | owner: context.repo.owner, 38 | repo: context.repo.repo, 39 | name, 40 | }) 41 | } 42 | catch { 43 | await github.issues.createLabel({ 44 | owner: context.repo.owner, 45 | repo: context.repo.repo, 46 | name, 47 | color: "ffffff", 48 | }) 49 | } 50 | 51 | await github.issues.addLabels({ 52 | issue_number: context.issue.number, 53 | owner: context.repo.owner, 54 | repo: context.repo.repo, 55 | labels: [name], 56 | }) 57 | } 58 | } 59 | else { 60 | core.info("No matched labels, skipped") 61 | } 62 | } 63 | 64 | export default action 65 | -------------------------------------------------------------------------------- /scripts/actions/loader.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core" 2 | import { context, getOctokit } from "@actions/github" 3 | import * as io from "@actions/io" 4 | 5 | process.on("unhandledRejection", handleError) 6 | main().catch(handleError) 7 | 8 | async function main(): Promise { 9 | const token = process.argv[2] 10 | const fnName = process.argv[3] 11 | const github = getOctokit(token) 12 | 13 | const fn = require(`./${fnName}.ts`) 14 | fn.default(github, context, core, io) 15 | } 16 | 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | function handleError(err: any): void { 19 | console.error(err) 20 | core.setFailed(`Unhandled error: ${err}`) 21 | process.exit(1) 22 | } 23 | -------------------------------------------------------------------------------- /scripts/actions/toggle-pr-with-issue.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "../types" 2 | 3 | const action: Action = async(github, context, core) => { 4 | const payload = context.payload || {} 5 | const issue = payload.issue 6 | 7 | if (!issue) 8 | return 9 | 10 | const labels: string[] = (issue.labels || []) 11 | .map((i: any) => i && i.name) 12 | .filter(Boolean) 13 | 14 | if (!labels.includes("new-challenge")) 15 | return 16 | 17 | // close pull request 18 | // Leave a message: close by issue 19 | const no = issue.number 20 | const action = payload.action 21 | 22 | core.info(`action: ${action}`) 23 | 24 | // action: reopened 25 | // action: closed 26 | 27 | // find pull request 28 | const { data: pulls } = await github.pulls.list({ 29 | owner: context.repo.owner, 30 | repo: context.repo.repo, 31 | state: action === "closed" ? "open" : "closed", 32 | }) 33 | 34 | core.info(`pulls.length ${pulls.length}`) 35 | core.info(JSON.stringify(pulls)) 36 | 37 | const existing_pull = pulls.find(i => 38 | i.user.login === "github-actions[bot]" 39 | && i.title.startsWith(`#${no} `), 40 | ) 41 | 42 | if (!existing_pull) { 43 | core.info("existing_pull not exist") 44 | return 45 | } 46 | 47 | core.info(JSON.stringify(context)) 48 | 49 | if (context.payload.action === "reopened") { 50 | await github.pulls.update({ 51 | ...context.repo, 52 | pull_number: existing_pull.number, 53 | state: "open", 54 | }) 55 | } 56 | else { 57 | // close 58 | await github.pulls.update({ 59 | ...context.repo, 60 | pull_number: existing_pull.number, 61 | state: "closed", 62 | }) 63 | } 64 | } 65 | 66 | export default action 67 | -------------------------------------------------------------------------------- /scripts/badge.ts: -------------------------------------------------------------------------------- 1 | import { SupportedLocale, t } from "./locales" 2 | import { DIFFICULTY_COLORS } from "./configs" 3 | export function generateBadgeURL(label: string, text: string, color: string, args = "") { 4 | return `https://img.shields.io/badge/${encodeURIComponent(label.replace(/-/g, "--"))}-${encodeURIComponent(text.replace(/-/g, "--"))}-${color}${args}` 5 | } 6 | 7 | export function generateBadge(label: string, text: string, color: string, args = "") { 8 | return `${text}` 9 | } 10 | 11 | export function generateBadgeLink(url: string, label: string, text: string, color: string, args = "") { 12 | return `${generateBadge(label, text, color, args)} ` 13 | } 14 | 15 | export function generateDifficultyBadge(difficulty: string, locale: SupportedLocale) { 16 | return generateBadge("", t(locale, `difficulty.${difficulty}`), DIFFICULTY_COLORS[difficulty]) 17 | } 18 | 19 | export function generateDifficultyBadgeInverted(difficulty: string, locale: SupportedLocale, count: number) { 20 | return generateBadge(t(locale, `difficulty.${difficulty}`), count.toString(), DIFFICULTY_COLORS[difficulty]) 21 | } 22 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | 2 | import { loadQuizes } from "./loader" 3 | import { updateQuizREADME, updateIndexREADME } from "./readme" 4 | 5 | export async function build() { 6 | const quizes = await loadQuizes() 7 | quizes.sort((a, b) => a.no - b.no) 8 | await updateQuizREADME(quizes) 9 | await updateIndexREADME(quizes) 10 | } 11 | 12 | build() 13 | -------------------------------------------------------------------------------- /scripts/configs.ts: -------------------------------------------------------------------------------- 1 | 2 | import path from "path" 3 | export const QUIZ_ROOT = path.resolve(__dirname, "../questions") 4 | export const VUE_SFC_PLAYGROUND_URL = "https://sfc.vuejs.org/" 5 | export const STACKBLITZ_PLAYGROUND_URL = "https://vuejs-challenges-stackblitz.netlify.app/" 6 | export const REPO = "https://github.com/webfansplz/vuejs-challenges" 7 | 8 | export const DIFFICULTY_COLORS: Record = { 9 | warm: "teal", 10 | easy: "7aad0c", 11 | medium: "d9901a", 12 | hard: "de3d37", 13 | extreme: "b11b8d", 14 | } 15 | 16 | export const DIFFICULTY_RANK = [ 17 | "warm", 18 | "easy", 19 | "medium", 20 | "hard", 21 | "extreme", 22 | ] 23 | -------------------------------------------------------------------------------- /scripts/locales.ts: -------------------------------------------------------------------------------- 1 | import en from "./locales/en.json" 2 | import zhCN from "./locales/zh-CN.json" 3 | 4 | export const defaultLocale = "en" 5 | 6 | export const supportedLocales = ["en", "zh-CN"] as const 7 | 8 | export const messages = { 9 | "en": en, 10 | "zh-CN": zhCN, 11 | } 12 | 13 | export type SupportedLocale = keyof typeof messages 14 | 15 | export function t(locale: SupportedLocale, key: string): string { 16 | return (messages[locale] && messages[locale][key]) || messages[defaultLocale][key] 17 | } 18 | 19 | export function f(name: string, locale: string, ext: string) { 20 | if (locale === defaultLocale) 21 | return `${name}.${ext}` 22 | return `${name}.${locale}.${ext}` 23 | } 24 | -------------------------------------------------------------------------------- /scripts/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "badge.back": "Back", 3 | "badge.checkout-solutions": "Check out Solutions", 4 | "badge.preview-playground": "Preview in Playground", 5 | "badge.share-your-solutions": "Share your Solutions", 6 | "badge.take-the-challenge": "Take the Challenge", 7 | "badge.take-the-challenge-unit-test": "Take the Challenge(Passed unit tests)", 8 | "difficulty.easy": "easy", 9 | "difficulty.extreme": "extreme", 10 | "difficulty.hard": "hard", 11 | "difficulty.medium": "medium", 12 | "difficulty.warm": "warm-up", 13 | "display": "English" 14 | } 15 | -------------------------------------------------------------------------------- /scripts/locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "badge.back": "返回首页", 3 | "badge.checkout-solutions": "查看解答", 4 | "badge.preview-playground": "在 Playground 中预览", 5 | "badge.share-your-solutions": "分享你的解答", 6 | "badge.take-the-challenge": "接受挑战", 7 | "badge.take-the-challenge-unit-test": "接受挑战(通过单元测试)", 8 | "difficulty.easy": "简单", 9 | "difficulty.extreme": "地狱", 10 | "difficulty.hard": "困难", 11 | "difficulty.medium": "中等", 12 | "difficulty.warm": "热身", 13 | "display": "简体中文" 14 | } 15 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vuejs-challenges/scripts", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "@actions/core": "^1.2.7", 6 | "@actions/github": "^4.0.0", 7 | "@actions/io": "^1.1.0", 8 | "@type-challenges/octokit-create-pull-request": "^0.1.8", 9 | "fast-glob": "^3.2.5", 10 | "fflate": "^0.7.3", 11 | "fs-extra": "^9.1.0", 12 | "google-translate-open-api": "^1.3.7", 13 | "js-yaml": "^4.1.0", 14 | "limax": "^2.1.0", 15 | "tsx": "^3.6.0" 16 | }, 17 | "private": true 18 | } 19 | -------------------------------------------------------------------------------- /scripts/stackblitz/index.ts: -------------------------------------------------------------------------------- 1 | import { STACKBLITZ_PLAYGROUND_URL } from "../configs" 2 | import template from "./template" 3 | export interface StackBlitzPayloadOptions { 4 | title: string 5 | files: Record 6 | openFile: string 7 | } 8 | 9 | export function normalizePayload(payload: StackBlitzPayloadOptions) { 10 | return { 11 | openFile: payload.openFile, 12 | files: { 13 | "package.json": template.packageJSONContent, 14 | "package-lock.json": template.lock, 15 | "vite.config.ts": template.viteConfigContenet, 16 | "main.ts": template.mainTsContent, 17 | "index.html": template.indexHtmlContent, 18 | "env.d.ts": template.envTsContent, 19 | ...payload.files, 20 | }, 21 | title: payload.title, 22 | description: payload.title, 23 | template: "node", 24 | } 25 | } 26 | 27 | function serialize(data: string): string { 28 | return btoa(unescape(encodeURIComponent(data))) 29 | } 30 | 31 | export function normalizeStackBlitzLink(payload: StackBlitzPayloadOptions) { 32 | return `${STACKBLITZ_PLAYGROUND_URL}#${serialize(JSON.stringify(normalizePayload(payload)))}` 33 | } 34 | -------------------------------------------------------------------------------- /scripts/stackblitz/template.ts: -------------------------------------------------------------------------------- 1 | 2 | import { lock } from "./package-lock" 3 | 4 | const packageJSONContent = JSON.stringify({ 5 | scripts: { 6 | "test": "vitest --environment jsdom", 7 | "test:ui": "vitest --environment jsdom --ui", 8 | "dev": "vite", 9 | "init": "concurrently \"npm run dev\" \"npm run test\" ", 10 | }, 11 | stackblitz: { 12 | startCommand: "npm run init", 13 | }, 14 | dependencies: { 15 | vue: "^3.2.37", 16 | }, 17 | devDependencies: { 18 | "@types/jsdom": "^16.2.14", 19 | "@vitejs/plugin-vue": "^2.3.3", 20 | "@vitest/ui": "^0.17.0", 21 | "@vue/test-utils": "^2.0.2", 22 | "jsdom": "^20.0.0", 23 | "vite": "^2.9.13", 24 | "typescript": "^4.7.4", 25 | "vite-plugin-vue-inspector": "1.0.1", 26 | "vitest": "^0.17.0", 27 | "concurrently": "^7.2.2", 28 | }, 29 | }) 30 | 31 | const viteConfigContenet = ` 32 | import { defineConfig } from 'vite'; 33 | import vue from '@vitejs/plugin-vue'; 34 | import Inspector from 'vite-plugin-vue-inspector'; 35 | 36 | export default defineConfig({ 37 | plugins: [ 38 | vue(), 39 | Inspector({ 40 | enabled: false, 41 | toggleButtonVisibility: 'never' 42 | }), 43 | ], 44 | }); 45 | ` 46 | 47 | const mainTsContent = ` 48 | import { createApp } from 'vue' 49 | import App from './App.vue' 50 | 51 | createApp(App).mount('#app') 52 | ` 53 | 54 | const indexHtmlContent = ` 55 | 56 | 57 | 58 | 59 | 60 | 61 | Vite App 62 | 63 | 64 |
65 | 66 | 67 | 68 | ` 69 | 70 | const envTsContent = ` 71 | /// 72 | 73 | declare module '*.vue' { 74 | import type { DefineComponent } from 'vue'; 75 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 76 | const component: DefineComponent<{}, {}, any>; 77 | export default component; 78 | } 79 | ` 80 | export default { 81 | packageJSONContent, 82 | viteConfigContenet, 83 | mainTsContent, 84 | indexHtmlContent, 85 | envTsContent, 86 | lock, 87 | } 88 | -------------------------------------------------------------------------------- /scripts/types.ts: -------------------------------------------------------------------------------- 1 | import type { getOctokit, context } from "@actions/github" 2 | import type Core from "@actions/core" 3 | import type IO from "@actions/io" 4 | 5 | export interface QuizMetaInfo { 6 | title: string 7 | author: { 8 | name: string 9 | github: string 10 | } 11 | difficulty: string 12 | tags: [] 13 | } 14 | 15 | export interface Quiz { 16 | no: number 17 | quizLink: string 18 | stackblitzLink: Record 19 | path: string 20 | readme: Record 21 | info: Partial | undefined 22 | } 23 | 24 | export type Github = ReturnType 25 | export type Context = typeof context 26 | export type Action = (github: Github, context: Context, core: typeof Core, io: typeof IO) => Promise 27 | -------------------------------------------------------------------------------- /scripts/utils.ts: -------------------------------------------------------------------------------- 1 | import { zlibSync, strToU8, strFromU8 } from "fflate" 2 | import { defaultLocale } from "./locales" 3 | 4 | export function utoa(data: string): string { 5 | const buffer = strToU8(data) 6 | const zipped = zlibSync(buffer, { level: 9 }) 7 | const binary = strFromU8(zipped, true) 8 | return btoa(binary) 9 | } 10 | 11 | export function serialize(files) { 12 | return `#${utoa(JSON.stringify(files))}` 13 | } 14 | 15 | export function escapeHtml(unsafe: string) { 16 | return unsafe 17 | .replace(/&/g, "&") 18 | .replace(//g, ">") 20 | .replace(/"/g, """) 21 | .replace(/'/g, "'") 22 | } 23 | 24 | export function resolveFilePath(dir: string, name: string, ext: string, locale: string) { 25 | if (locale === defaultLocale) 26 | return `${dir}/${name}.${ext}` 27 | else 28 | return `${dir}/${name}.${locale}.${ext}` 29 | } 30 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | import vue from "@vitejs/plugin-vue" 3 | 4 | export default defineConfig({ 5 | plugins: [vue()], 6 | }) 7 | --------------------------------------------------------------------------------