├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.js ├── .github ├── CONTRIBUTING.md └── ISSUE_TEMPLATE.md ├── .gitignore ├── .gitpod.yml ├── LICENSE ├── README.md ├── build ├── build.js ├── dev-test.js └── release.sh ├── dist ├── vue-class-component.cjs.js ├── vue-class-component.common.js ├── vue-class-component.d.ts ├── vue-class-component.esm-browser.js ├── vue-class-component.esm-browser.prod.js ├── vue-class-component.esm-bundler.js ├── vue-class-component.esm.browser.js ├── vue-class-component.esm.browser.min.js ├── vue-class-component.esm.js ├── vue-class-component.global.js ├── vue-class-component.global.prod.js ├── vue-class-component.js └── vue-class-component.min.js ├── docs ├── .vuepress │ └── config.js ├── README.md ├── api │ └── README.md ├── assets │ ├── vue-cli-1.png │ ├── vue-cli-2.png │ └── vue-cli-3.png └── guide │ ├── additional-hooks.md │ ├── annotate-component-type-in-decorator.md │ ├── caveats.md │ ├── class-component.md │ ├── custom-decorators.md │ ├── extend-and-mixins.md │ ├── hooks-auto-complete.md │ ├── installation.md │ ├── property-type-declaration.md │ ├── props-definition.md │ └── refs-type-extension.md ├── example ├── .babelrc ├── index.html ├── src │ ├── App.vue │ ├── components │ │ ├── Hello.vue │ │ └── World.tsx │ ├── main.ts │ ├── shims-tsx.d.ts │ ├── shims-vue.d.ts │ └── store.ts ├── tsconfig.json └── webpack.config.js ├── hooks.d.ts ├── hooks.js ├── package.json ├── src ├── component.ts ├── data.ts ├── declarations.ts ├── globals.d.ts ├── index.ts ├── reflect.ts └── util.ts ├── test ├── .babelrc ├── .eslintrc.js ├── test-babel.js ├── test.ts ├── tsconfig.json └── webpack.config.js ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | working_directory: ~/repo 5 | docker: 6 | - image: circleci/node:8-browsers 7 | 8 | jobs: 9 | install: 10 | <<: *defaults 11 | steps: 12 | - checkout 13 | - restore_cache: 14 | keys: 15 | - v1-vue-class-component-{{ .Branch }}-{{ checksum "yarn.lock" }} 16 | - v1-vue-class-component-{{ .Branch }}- 17 | - v1-vue-class-component- 18 | - run: yarn install 19 | - save_cache: 20 | paths: 21 | - node_modules 22 | key: v1-vue-class-component-{{ .Branch }}-{{ checksum "yarn.lock" }} 23 | - persist_to_workspace: 24 | root: ~/ 25 | paths: 26 | - repo 27 | 28 | build: 29 | <<: *defaults 30 | steps: 31 | - attach_workspace: 32 | at: ~/ 33 | - run: yarn build 34 | 35 | example-build: 36 | <<: *defaults 37 | steps: 38 | - attach_workspace: 39 | at: ~/ 40 | - run: yarn example 41 | 42 | lint: 43 | <<: *defaults 44 | steps: 45 | - attach_workspace: 46 | at: ~/ 47 | - run: yarn lint 48 | 49 | test: 50 | <<: *defaults 51 | steps: 52 | - attach_workspace: 53 | at: ~/ 54 | - run: yarn test 55 | 56 | workflows: 57 | version: 2 58 | build-and-test: 59 | jobs: 60 | - install 61 | - build: 62 | requires: 63 | - install 64 | - example-build: 65 | requires: 66 | - install 67 | - lint: 68 | requires: 69 | - install 70 | - test: 71 | requires: 72 | - install -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /lib/ 3 | node_modules/ 4 | 5 | /test/test.build.js 6 | /example/build.js 7 | 8 | # TOOD: remove after fixed decorator indent issue 9 | # https://github.com/eslint/typescript-eslint-parser/issues/438 10 | /example 11 | /test -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['plugin:vue-libs/recommended'], 4 | parserOptions: { 5 | parser: '@typescript-eslint/parser' 6 | }, 7 | rules: { 8 | 'no-unused-vars': 'off', 9 | 'no-undef': 'off' 10 | } 11 | } -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Vue Class Component 2 | 3 | Thank you for contributing to Vue Class Component! To manage the process smoothly, please take a look at the following points. 4 | 5 | ## Issue Reporting 6 | 7 | Please make sure that your issue is either a **bug report** or **feature request**. The other kinds of issue will be closed immediately. For usage questions, 8 | please use [Stack Overflow](https://stackoverflow.com/), [the official Vue.js forum](https://forum.vuejs.org), [the official Discord server](https://chat.vuejs.org), etc. 9 | 10 | Also, please try searching existing issue before creating a new one. Your issue may exist already in an old thread. 11 | 12 | ### Bug Report 13 | 14 | Please **make sure to provide [minimal and self-contained reproduction](https://new-issue.vuejs.org/?repo=vuejs/vue#why-repro)**. 15 | A bug report without a proper reproduction may be closed immediately. 16 | 17 | To create a reproduction, you can use on-browser playground such as JSFiddle ([template for Vue Class Component](https://jsfiddle.net/ktsn/nm55jnjk/)) 18 | or create a GitHub repository and share its link. 19 | 20 | Please also clarify the expected behavior, actual behavior and steps to reproduce the bug to confirm it precisely. 21 | 22 | ### Feature Request 23 | 24 | For feature request, please make sure to clarify **the use case** that you want to solve. 25 | Explaining it with actual code example or pseudo code would be useful to share your idea. 26 | 27 | ## Pull Request 28 | 29 | Please do not commit the files under `dist/`. They are supporsed to be generated during the releasing process. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example/build.js 3 | example/build.js.map 4 | docs/.vuepress/dist 5 | test/test.build.js 6 | lib 7 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - name: Compile 3 | init: yarn install && yarn build && gp sync-done install 4 | command: yarn dev 5 | - name: Docs 6 | init: gp sync-await install 7 | command: yarn docs:dev 8 | 9 | ports: 10 | - port: 8080 11 | onOpen: open-preview 12 | 13 | vscode: 14 | extensions: 15 | - octref.vetur@0.23.0:TEzauMObB6f3i2JqlvrOpA== 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Evan You 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [DEPRECATED] Vue Class Component 2 | 3 | 4 | ## ⚠️ Notice 5 | 6 | This library is no longer actively maintained. It is no longer recommend to use Class-based components in Vue 3. The recommended way to use Vue 3 in large applications is Single-File Components, Composition API, and ` 41 | ``` 42 | 43 | As the example shows, you can define component data and methods in the intuitive and standard class syntax by annotating the class with the `@Component` decorator. You can simply replace your component definition with a class-style component as it is equivalent with the ordinary options object style of component definition. 44 | 45 | By defining your component in class-style, you not only change the syntax but also can utilize some ECMAScript language features such as class inheritance and decorators. Vue Class Component also provides a [`mixins` helper](guide/extend-and-mixins.md#Mixins) for mixin inheritance, and a [`createDecorator` function](guide/custom-decorators.md) to create your own decorators easily. 46 | 47 | You may also want to check out the `@Prop` and `@Watch` decorators provided by [Vue Property Decorator](https://github.com/kaorun343/vue-property-decorator). 48 | -------------------------------------------------------------------------------- /docs/api/README.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ## `@Component([options])` 4 | 5 | - **Arguments** 6 | - `{Object} [options]` 7 | 8 | A decorator to define class style components. You can pass [Vue component options](https://vuejs.org/v2/api/#Options-Data) via the optional 1st argument. 9 | 10 | See also: [Class Component](../guide/class-component.md) 11 | 12 | ## `Component.registerHooks(hooks)` 13 | 14 | - **Arguments** 15 | - `{Array} hooks` 16 | 17 | Registers method names that class components handles them as hooks. 18 | 19 | See [Additional Hooks](../guide/additional-hooks.md) for more details. 20 | 21 | ## `createDecorator(callback)` 22 | 23 | - **Arguments** 24 | - `{Function} callback` 25 | - **Return** 26 | - `{Function}` 27 | 28 | Creates a new decorator which class components process. 29 | 30 | See [Custom Decorators](../guide/custom-decorators.md) for more details. 31 | 32 | ## Built-in Hook Methods 33 | 34 | The followings are built-in hook names that class components treat as special methods. 35 | 36 | - data 37 | - beforeCreate 38 | - created 39 | - beforeMount 40 | - mounted 41 | - beforeDestroy 42 | - destroyed 43 | - beforeUpdate 44 | - updated 45 | - activated 46 | - deactivated 47 | - render 48 | - errorCaptured 49 | - serverPrefetch 50 | 51 | They will not be registered as component methods but (lifecycle) hooks. You should avoid these reserved names when your properties or methods are not supposed to be such hooks. 52 | 53 | See also: [Hooks](../guide/class-component.md#Hooks) 54 | 55 | ## Built-in Hook Method Types 56 | 57 | Only available in TypeScript. It enables built-in hooks methods auto-complete once you import it: 58 | 59 | ```ts 60 | import 'vue-class-component/hooks' 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/assets/vue-cli-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/vue-class-component/b866fde8ba1ef8cf29fa44f26d19b3167ecae1dd/docs/assets/vue-cli-1.png -------------------------------------------------------------------------------- /docs/assets/vue-cli-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/vue-class-component/b866fde8ba1ef8cf29fa44f26d19b3167ecae1dd/docs/assets/vue-cli-2.png -------------------------------------------------------------------------------- /docs/assets/vue-cli-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/vue-class-component/b866fde8ba1ef8cf29fa44f26d19b3167ecae1dd/docs/assets/vue-cli-3.png -------------------------------------------------------------------------------- /docs/guide/additional-hooks.md: -------------------------------------------------------------------------------- 1 | # Additional Hooks 2 | 3 | If you use some Vue plugins like [Vue Router](https://router.vuejs.org/), you may want class components to resolve hooks that they provide. In that case, `Component.registerHooks` allows you to register such hooks: 4 | 5 | ```js 6 | // class-component-hooks.js 7 | import Component from 'vue-class-component' 8 | 9 | // Register the router hooks with their names 10 | Component.registerHooks([ 11 | 'beforeRouteEnter', 12 | 'beforeRouteLeave', 13 | 'beforeRouteUpdate' 14 | ]) 15 | ``` 16 | 17 | After registering the hooks, class component realizes them as class prototype methods: 18 | 19 | ```js 20 | import Vue from 'vue' 21 | import Component from 'vue-class-component' 22 | 23 | @Component 24 | export default class HelloWorld extends Vue { 25 | // The class component now treats beforeRouteEnter, 26 | // beforeRouteUpdate and beforeRouteLeave as Vue Router hooks 27 | beforeRouteEnter(to, from, next) { 28 | console.log('beforeRouteEnter') 29 | next() 30 | } 31 | 32 | beforeRouteUpdate(to, from, next) { 33 | console.log('beforeRouteUpdate') 34 | next() 35 | } 36 | 37 | beforeRouteLeave(to, from, next) { 38 | console.log('beforeRouteLeave') 39 | next() 40 | } 41 | } 42 | ``` 43 | 44 | It is recommended to write this registration code in a separated file because you have to register them before any component definitions. You can make sure the execution order by putting `import` statement for the hooks registration on the top of the main file: 45 | 46 | ```js 47 | // main.js 48 | 49 | // Make sure to register before importing any components 50 | import './class-component-hooks' 51 | 52 | import Vue from 'vue' 53 | import App from './App' 54 | 55 | new Vue({ 56 | el: '#app', 57 | render: h => h(App) 58 | }) 59 | ``` -------------------------------------------------------------------------------- /docs/guide/annotate-component-type-in-decorator.md: -------------------------------------------------------------------------------- 1 | # Annotate Component Type in Decorator 2 | 3 | There are cases that you want to use your component type on a function in `@Component` decorator argument. 4 | For example, to access component methods in a watch handler: 5 | 6 | ```ts 7 | @Component({ 8 | watch: { 9 | postId(id: string) { 10 | // To fetch post data when the id is changed. 11 | this.fetchPost(id) // -> Property 'fetchPost' does not exist on type 'Vue'. 12 | } 13 | } 14 | }) 15 | class Post extends Vue { 16 | postId: string 17 | 18 | fetchPost(postId: string): Promise { 19 | // ... 20 | } 21 | } 22 | ``` 23 | 24 | The above code produces a type error that indicates `fetchPost` does not exist on `this` in the watch handler. This happens because `this` type in `@Component` decorator argument is the base `Vue` type. 25 | 26 | To use your own component type (in this case `Post`), you can annotate the decorator through its type parameter. 27 | 28 | ```ts 29 | // Annotate the decorator with the component type 'Post' so that `this` type in 30 | // the decorator argument becomes 'Post'. 31 | @Component({ 32 | watch: { 33 | postId(id: string) { 34 | this.fetchPost(id) // -> No errors 35 | } 36 | } 37 | }) 38 | class Post extends Vue { 39 | postId: string 40 | 41 | fetchPost(postId: string): Promise { 42 | // ... 43 | } 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/guide/caveats.md: -------------------------------------------------------------------------------- 1 | # Caveats of Class Component 2 | 3 | Vue Class Component collects class properties as Vue instance data by instantiating the original constructor under the hood. While we can define instance data like native class manner, we sometimes need to know how it works. 4 | 5 | ## `this` value in property initializer 6 | 7 | If you define an arrow function as a class property and access `this` in it, it will not work. This is because `this` is just a proxy object to the Vue instance when initializing class properties: 8 | 9 | ```js 10 | import Vue from 'vue' 11 | import Component from 'vue-class-component' 12 | 13 | @Component 14 | export default class MyComp extends Vue { 15 | foo = 123 16 | 17 | // DO NOT do this 18 | bar = () => { 19 | // Does not update the expected property. 20 | // `this` value is not a Vue instance in fact. 21 | this.foo = 456 22 | } 23 | } 24 | ``` 25 | 26 | You can simply define a method instead of a class property in that case because Vue will bind the instance automatically: 27 | 28 | ```js 29 | import Vue from 'vue' 30 | import Component from 'vue-class-component' 31 | 32 | @Component 33 | export default class MyComp extends Vue { 34 | foo = 123 35 | 36 | // DO this 37 | bar() { 38 | // Correctly update the expected property. 39 | this.foo = 456 40 | } 41 | } 42 | ``` 43 | 44 | ## Always use lifecycle hooks instead of `constructor` 45 | 46 | As the original constructor is invoked to collect initial component data, it is recommended against declaring `constructor` by yourself: 47 | 48 | ```js 49 | import Vue from 'vue' 50 | import Component from 'vue-class-component' 51 | 52 | @Component 53 | export default class Posts extends Vue { 54 | posts = [] 55 | 56 | // DO NOT do this 57 | constructor() { 58 | fetch('/posts.json') 59 | .then(res => res.json()) 60 | .then(posts => { 61 | this.posts = posts 62 | }) 63 | } 64 | } 65 | ``` 66 | 67 | The above code intends to fetch post list on component initialization but the fetch will be called twice unexpectedly because of how Vue Class Component works. 68 | 69 | It is recommended to write lifecycle hooks such as `created` instead of `constructor`: 70 | 71 | ```js 72 | import Vue from 'vue' 73 | import Component from 'vue-class-component' 74 | 75 | @Component 76 | export default class Posts extends Vue { 77 | posts = [] 78 | 79 | // DO this 80 | created() { 81 | fetch('/posts.json') 82 | .then(res => res.json()) 83 | .then(posts => { 84 | this.posts = posts 85 | }) 86 | } 87 | } 88 | ``` -------------------------------------------------------------------------------- /docs/guide/class-component.md: -------------------------------------------------------------------------------- 1 | # Class Component 2 | 3 | `@Component` decorator makes your class a Vue component: 4 | 5 | ```js 6 | import Vue from 'vue' 7 | import Component from 'vue-class-component' 8 | 9 | // HelloWorld class will be a Vue component 10 | @Component 11 | export default class HelloWorld extends Vue {} 12 | ``` 13 | 14 | ## Data 15 | 16 | Initial `data` can be declared as class properties: 17 | 18 | ```vue 19 | 22 | 23 | 33 | ``` 34 | 35 | The above component renders `Hello World!` in the `
` element as `message` is component data. 36 | 37 | Note that if the initial value is `undefined`, the class property will not be reactive which means the changes for the properties will not be detected: 38 | 39 | ```js 40 | import Vue from 'vue' 41 | import Component from 'vue-class-component' 42 | 43 | @Component 44 | export default class HelloWorld extends Vue { 45 | // `message` will not be reactive value 46 | message = undefined 47 | } 48 | ``` 49 | 50 | To avoid this, you can use `null` value or use `data` hook instead: 51 | 52 | ```js 53 | import Vue from 'vue' 54 | import Component from 'vue-class-component' 55 | 56 | @Component 57 | export default class HelloWorld extends Vue { 58 | // `message` will be reactive with `null` value 59 | message = null 60 | 61 | // See Hooks section for details about `data` hook inside class. 62 | data() { 63 | return { 64 | // `hello` will be reactive as it is declared via `data` hook. 65 | hello: undefined 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | ## Methods 72 | 73 | Components `methods` can be declared directly as class prototype methods: 74 | 75 | ```vue 76 | 79 | 80 | 92 | ``` 93 | 94 | ## Computed Properties 95 | 96 | Computed properties can be declared as class property getter / setter: 97 | 98 | ```vue 99 | 102 | 103 | 125 | ``` 126 | 127 | ## Hooks 128 | 129 | `data`, `render` and all Vue lifecycle hooks can be directly declared as class prototype methods as well, but you cannot invoke them on the instance itself. When declaring custom methods, you should avoid these reserved names. 130 | 131 | ```jsx 132 | import Vue from 'vue' 133 | import Component from 'vue-class-component' 134 | 135 | @Component 136 | export default class HelloWorld extends Vue { 137 | // Declare mounted lifecycle hook 138 | mounted() { 139 | console.log('mounted') 140 | } 141 | 142 | // Declare render function 143 | render() { 144 | return
Hello World!
145 | } 146 | } 147 | ``` 148 | 149 | ## Other Options 150 | 151 | For all other options, pass them to the decorator function: 152 | 153 | ```vue 154 | 157 | 158 | 173 | ``` -------------------------------------------------------------------------------- /docs/guide/custom-decorators.md: -------------------------------------------------------------------------------- 1 | # Custom Decorators 2 | 3 | You can extend the functionality of this library by creating your own decorators. Vue Class Component provides `createDecorator` helper to create custom decorators. `createDecorator` expects a callback function as the 1st argument and the callback will receive following arguments: 4 | 5 | - `options`: Vue component options object. Changes for this object will affect the provided component. 6 | - `key`: The property or method key that the decorator is applied. 7 | - `parameterIndex`: The index of a decorated argument if the custom decorator is used for an argument. 8 | 9 | Example of creating `Log` decorator which prints a log message with the method name and passed arguments when the decorated method is called: 10 | 11 | ```js 12 | // decorators.js 13 | import { createDecorator } from 'vue-class-component' 14 | 15 | // Declare Log decorator. 16 | export const Log = createDecorator((options, key) => { 17 | // Keep the original method for later. 18 | const originalMethod = options.methods[key] 19 | 20 | // Wrap the method with the logging logic. 21 | options.methods[key] = function wrapperMethod(...args) { 22 | // Print a log. 23 | console.log(`Invoked: ${key}(`, ...args, ')') 24 | 25 | // Invoke the original method. 26 | originalMethod.apply(this, args) 27 | } 28 | }) 29 | ``` 30 | 31 | Using it as a method decorator: 32 | 33 | ```js 34 | import Vue from 'vue' 35 | import Component from 'vue-class-component' 36 | import { Log } from './decorators' 37 | 38 | @Component 39 | class MyComp extends Vue { 40 | // It prints a log when `hello` method is invoked. 41 | @Log 42 | hello(value) { 43 | // ... 44 | } 45 | } 46 | ``` 47 | 48 | In the above code, when `hello` method is called with `42`, the following log will be printed: 49 | 50 | ``` 51 | Invoked: hello( 42 ) 52 | ``` -------------------------------------------------------------------------------- /docs/guide/extend-and-mixins.md: -------------------------------------------------------------------------------- 1 | # Extend and Mixins 2 | 3 | ## Extend 4 | 5 | You can extend an existing class component as native class inheritance. Imagine you have following super class component: 6 | 7 | ```js 8 | // super.js 9 | import Vue from 'vue' 10 | import Component from 'vue-class-component' 11 | 12 | // Define a super class component 13 | @Component 14 | export default class Super extends Vue { 15 | superValue = 'Hello' 16 | } 17 | ``` 18 | 19 | You can extend it by using native class inheritance syntax: 20 | 21 | ```js 22 | import Super from './super' 23 | import Component from 'vue-class-component' 24 | 25 | // Extending the Super class component 26 | @Component 27 | export default class HelloWorld extends Super { 28 | created() { 29 | console.log(this.superValue) // -> Hello 30 | } 31 | } 32 | ``` 33 | 34 | Note that every super class must be a class component. In other words, it needs to inherit `Vue` constructor as an ancestor and be decorated by `@Component` decorator. 35 | 36 | ## Mixins 37 | 38 | Vue Class Component provides `mixins` helper function to use [mixins](https://vuejs.org/v2/guide/mixins.html) in class style manner. By using `mixins` helper, TypeScript can infer mixin types and inherit them on the component type. 39 | 40 | Example of declaring mixins `Hello` and `World`: 41 | 42 | ```js 43 | // mixins.js 44 | import Vue from 'vue' 45 | import Component from 'vue-class-component' 46 | 47 | // You can declare mixins as the same style as components. 48 | @Component 49 | export class Hello extends Vue { 50 | hello = 'Hello' 51 | } 52 | 53 | @Component 54 | export class World extends Vue { 55 | world = 'World' 56 | } 57 | ``` 58 | 59 | Use them in a class style component: 60 | 61 | ```js 62 | import Component, { mixins } from 'vue-class-component' 63 | import { Hello, World } from './mixins' 64 | 65 | // Use `mixins` helper function instead of `Vue`. 66 | // `mixins` can receive any number of arguments. 67 | @Component 68 | export class HelloWorld extends mixins(Hello, World) { 69 | created () { 70 | console.log(this.hello + ' ' + this.world + '!') // -> Hello World! 71 | } 72 | } 73 | ``` 74 | 75 | As same as super class, all mixins must be declared as class components. 76 | -------------------------------------------------------------------------------- /docs/guide/hooks-auto-complete.md: -------------------------------------------------------------------------------- 1 | # Hooks Auto-complete 2 | 3 | Vue Class Component provides built-in hook types, which enables auto-complete for `data`, `render` and other lifecycle hooks in class component declarations, for TypeScript. To enable it, you need to import hooks type located at `vue-class-component/hooks`. 4 | 5 | ```ts 6 | // main.ts 7 | import 'vue-class-component/hooks' // import hooks type to enable auto-complete 8 | import Vue from 'vue' 9 | import App from './App.vue' 10 | 11 | new Vue({ 12 | render: h => h(App) 13 | }).$mount('#app') 14 | ``` 15 | 16 | If you want to make it work with custom hooks, you can manually add it by yourself: 17 | 18 | ```ts 19 | import Vue from 'vue' 20 | import { Route, RawLocation } from 'vue-router' 21 | 22 | declare module 'vue/types/vue' { 23 | // Augment component instance type 24 | interface Vue { 25 | beforeRouteEnter?( 26 | to: Route, 27 | from: Route, 28 | next: (to?: RawLocation | false | ((vm: Vue) => void)) => void 29 | ): void 30 | 31 | beforeRouteLeave?( 32 | to: Route, 33 | from: Route, 34 | next: (to?: RawLocation | false | ((vm: Vue) => void)) => void 35 | ): void 36 | 37 | beforeRouteUpdate?( 38 | to: Route, 39 | from: Route, 40 | next: (to?: RawLocation | false | ((vm: Vue) => void)) => void 41 | ): void 42 | } 43 | } 44 | ``` 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/guide/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Vue CLI Setup 4 | 5 | You can easily setup your Vue Class Component project by using [Vue CLI](https://cli.vuejs.org/). Run the following command to create a new project: 6 | 7 | ```sh 8 | $ vue create hello-world 9 | ``` 10 | 11 | You will be asked whether using preset or not. Select "Manually select features": 12 | 13 | ![](../assets/vue-cli-1.png) 14 | 15 | Check TypeScript feature to use Vue Class Component. You can add other features in addition if you need: 16 | 17 | ![](../assets/vue-cli-2.png) 18 | 19 | Press `y` for the question `Use class-style component syntax?`: 20 | 21 | ![](../assets/vue-cli-3.png) 22 | 23 | You can answer the remaining questions as your preferences. After finishing this setup process, Vue CLI creates a new project directory with Vue Class Component installed. 24 | 25 | ## Manual Setup 26 | 27 | If you prefer manual setup, install it via npm and configure your build tool. 28 | 29 | ### npm 30 | 31 | You can install Vue Class Component with `npm` command. Please make sure to also install Vue core library as Vue Class Component depends on it: 32 | 33 | ```sh 34 | $ npm install --save vue vue-class-component 35 | ``` 36 | 37 | You can use `yarn` command if you prefer: 38 | 39 | ```sh 40 | $ yarn add --save vue vue-class-component 41 | ``` 42 | 43 | ### Build Setup 44 | 45 | To use Vue Class Component, you need to configure [TypeScript](https://www.typescriptlang.org/) or [Babel](https://babeljs.io/) in your project as it relies on [ECMAScript stage 1 decorators](https://github.com/wycats/javascript-decorators/blob/master/README.md) which is needed to transpile to run on browsers. 46 | 47 | ::: warning 48 | It does not support the stage 2 decorators yet since TypeScript transpiler still only supports the old decorators spec. 49 | ::: 50 | 51 | #### TypeScript 52 | 53 | Create `tsconfig.json` on your project root and specify `experimentalDecorators` option so that it transpiles decorator syntax: 54 | 55 | ```json 56 | { 57 | "compilerOptions": { 58 | "target": "es5", 59 | "module": "es2015", 60 | "moduleResolution": "node", 61 | "strict": true, 62 | "experimentalDecorators": true 63 | } 64 | } 65 | ``` 66 | 67 | #### Babel 68 | 69 | Install `@babel/plugin-proposal-decorators` and `@babel/plugin-proposal-class-properties`: 70 | 71 | ```sh 72 | $ npm install --save-dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties 73 | ``` 74 | 75 | Then configure `.babelrc` on your project root: 76 | 77 | ```json 78 | { 79 | "plugins": [ 80 | ["@babel/proposal-decorators", { "legacy": true }], 81 | ["@babel/proposal-class-properties", { "loose": true }] 82 | ] 83 | } 84 | ``` 85 | 86 | Note that `legacy` and `loose` option are needed as Vue Class Component only supports stage 1 (legacy) decorator spec yet. 87 | 88 | ## CDN 89 | 90 | [unpkg.com](https://unpkg.com/) provides npm-based CDN links. You can choose specific version of Vue Class Component by replacing the `@latest` part in url (e.g. `https://unpkg.com/vue-class-component@7.2.2/dist/vue-class-component.js` to use version 7.2.2). 91 | 92 | ```html 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | ``` 105 | 106 | ## Different Builds 107 | 108 | Vue Class Component is provided as different builds for different environments and usages. 109 | 110 | - **For development** 111 | - `vue-class-component.js` (UMD) 112 | - `vue-class-component.common.js` (CommonJS) 113 | - `vue-class-component.esm.js` (ES Module for bundlers) 114 | - `vue-class-component.esm.browser.js` (ES Module for browsers) 115 | - **For production (minified)** 116 | - `vue-class-component.min.js` (UMD) 117 | - `vue-class-component.esm.browser.min.js` (ES Module for browsers) 118 | -------------------------------------------------------------------------------- /docs/guide/property-type-declaration.md: -------------------------------------------------------------------------------- 1 | # Property Type Declaration 2 | 3 | Sometimes, you have to define component properties and methods out of a class component. For example, [Vuex](https://github.com/vuejs/vuex), the official state management library for Vue, provides `mapGetters` and `mapActions` helpers to map a store to component properties and methods. These helpers need to be used in a component options object. 4 | 5 | Even in this case, you can pass component options to the `@Component` decorator's argument. However it does not automatically declare the properties and methods on type level while they work on runtime. 6 | 7 | You need to manually declare their types in the class component: 8 | 9 | ```ts 10 | import Vue from 'vue' 11 | import Component from 'vue-class-component' 12 | import { mapGetters, mapActions } from 'vuex' 13 | 14 | // Interface of post 15 | import { Post } from './post' 16 | 17 | @Component({ 18 | computed: mapGetters([ 19 | 'posts' 20 | ]), 21 | 22 | methods: mapActions([ 23 | 'fetchPosts' 24 | ]) 25 | }) 26 | export default class Posts extends Vue { 27 | // Declare mapped getters and actions on type level. 28 | // You may need to add `!` after the property name 29 | // to avoid compilation error (definite assignment assertion). 30 | 31 | // Type the mapped posts getter. 32 | posts!: Post[] 33 | 34 | // Type the mapped fetchPosts action. 35 | fetchPosts!: () => Promise 36 | 37 | mounted() { 38 | // Use the mapped getter and action. 39 | this.fetchPosts().then(() => { 40 | console.log(this.posts) 41 | }) 42 | } 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/guide/props-definition.md: -------------------------------------------------------------------------------- 1 | # Props Definition 2 | 3 | There is no dedicated API for props definition that Vue Class Component provides. You, however, can do that by using canonical `Vue.extend` API: 4 | 5 | ```vue 6 | 9 | 10 | 30 | ``` 31 | 32 | As `Vue.extend` infers defined prop types, it is possible to use them in your class component by extending it. 33 | 34 | If you have a super class component or mixins to extend, use `mixins` helper to combine defined props with them: 35 | 36 | ```vue 37 | 40 | 41 | 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/guide/refs-type-extension.md: -------------------------------------------------------------------------------- 1 | # `$refs` Type Extension 2 | 3 | `$refs` type of a component is declared as the broadest type to handle all possible type of ref. While it is theoretically correct, in most cases, each ref only has a specific element or a component in practice. 4 | 5 | You can specify a specific ref type by overriding `$refs` type in a class component: 6 | 7 | ```vue 8 | 11 | 12 | 31 | ``` 32 | 33 | You can access `input` type without type cast as `$refs.input` type is specified on the class component in the above example. 34 | 35 | Note that it should be a type annotation (using colon `:`) rather than value assignment (`=`). 36 | -------------------------------------------------------------------------------- /example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", { "modules": false }] 4 | ], 5 | "plugins": [ 6 | "@babel/syntax-jsx", 7 | "transform-vue-jsx" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue Class Component Example 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/src/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 90 | -------------------------------------------------------------------------------- /example/src/components/Hello.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /example/src/components/World.tsx: -------------------------------------------------------------------------------- 1 | import Vue, { CreateElement } from 'vue' 2 | import Component from '../../../lib/index' 3 | 4 | @Component 5 | export default class World extends Vue { 6 | render (h: CreateElement) { 7 | return

This is rendered via TSX

8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import store from './store' 4 | 5 | new Vue({ 6 | el: '#app', 7 | store, 8 | render: h => h(App, { 9 | props: { propMessage: 'World' } 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /example/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | interface Element extends VNode {} 6 | interface ElementClass extends Vue {} 7 | interface IntrinsicElements { 8 | [elem: string]: any 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /example/src/store.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | interface CounterState { 5 | count: number 6 | } 7 | 8 | Vue.use(Vuex) 9 | 10 | const state = { 11 | count: 0 12 | } 13 | 14 | const mutations = { 15 | increment (state: CounterState) { 16 | state.count++ 17 | } 18 | } 19 | 20 | export default new Vuex.Store({ 21 | state, 22 | mutations 23 | }) 24 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": [ 5 | "dom", 6 | "esnext" 7 | ], 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "jsx": "preserve", 15 | "jsxFactory": "h" 16 | }, 17 | "include": [ 18 | "./**/*.ts" 19 | ], 20 | "compileOnSave": false 21 | } 22 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 2 | 3 | module.exports = { 4 | mode: 'development', 5 | context: __dirname, 6 | entry: './src/main.ts', 7 | output: { 8 | path: __dirname, 9 | filename: 'build.js' 10 | }, 11 | resolve: { 12 | alias: { 13 | vue$: 'vue/dist/vue.esm.js' 14 | }, 15 | extensions: ['.ts', '.tsx', '.js'] 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.tsx?$/, 21 | exclude: /node_modules/, 22 | use: [ 23 | 'babel-loader', 24 | { 25 | loader: 'ts-loader', 26 | options: { 27 | appendTsSuffixTo: [/\.vue$/], 28 | appendTsxSuffixTo: [/\.vue$/] 29 | } 30 | } 31 | ] 32 | }, 33 | { 34 | test: /\.vue$/, 35 | use: ['vue-loader'] 36 | } 37 | ] 38 | }, 39 | devtool: 'source-map', 40 | plugins: [ 41 | new VueLoaderPlugin() 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /hooks.d.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from 'vue' 2 | 3 | declare module 'vue/types/vue' { 4 | interface Vue { 5 | data?(): object 6 | beforeCreate?(): void 7 | created?(): void 8 | beforeMount?(): void 9 | mounted?(): void 10 | beforeDestroy?(): void 11 | destroyed?(): void 12 | beforeUpdate?(): void 13 | updated?(): void 14 | activated?(): void 15 | deactivated?(): void 16 | render?(createElement: CreateElement): VNode 17 | errorCaptured?(err: Error, vm: Vue, info: string): boolean | undefined 18 | serverPrefetch?(): Promise 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /hooks.js: -------------------------------------------------------------------------------- 1 | // Dummy empty file to avoid import error when using hooks.d.ts 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-class-component", 3 | "version": "7.2.6", 4 | "description": "ES201X/TypeScript class decorator for Vue components", 5 | "main": "dist/vue-class-component.common.js", 6 | "module": "dist/vue-class-component.esm.js", 7 | "unpkg": "dist/vue-class-component.js", 8 | "typings": "lib/index.d.ts", 9 | "sideEffects": false, 10 | "files": [ 11 | "dist", 12 | "lib", 13 | "hooks.js", 14 | "hooks.d.ts" 15 | ], 16 | "scripts": { 17 | "build": "npm run build:ts && npm run build:main", 18 | "build:ts": "tsc -p .", 19 | "build:main": "node build/build.js", 20 | "clean": "rimraf ./lib", 21 | "example": "npm run build && webpack --config example/webpack.config.js", 22 | "dev": "webpack --config example/webpack.config.js --watch", 23 | "dev:test": "node build/dev-test.js", 24 | "lint": "eslint --ext js,jsx,ts,tsx,vue .", 25 | "test": "npm run build && webpack --config test/webpack.config.js && mocha test/test.build.js", 26 | "docs:dev": "vuepress dev docs", 27 | "docs:build": "vuepress build docs", 28 | "release": "bash build/release.sh" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/vuejs/vue-class-component.git" 33 | }, 34 | "keywords": [ 35 | "vue", 36 | "class", 37 | "babel", 38 | "typescript" 39 | ], 40 | "author": "Evan You", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/vuejs/vue-class-component/issues" 44 | }, 45 | "homepage": "https://github.com/vuejs/vue-class-component#readme", 46 | "peerDependencies": { 47 | "vue": "^2.0.0" 48 | }, 49 | "devDependencies": { 50 | "@babel/core": "^7.7.7", 51 | "@babel/plugin-proposal-class-properties": "^7.7.4", 52 | "@babel/plugin-proposal-decorators": "^7.7.4", 53 | "@babel/plugin-syntax-jsx": "^7.7.4", 54 | "@babel/preset-env": "^7.7.7", 55 | "@types/chai": "^4.2.7", 56 | "@types/mocha": "^5.2.7", 57 | "@types/node": "^13.1.6", 58 | "@typescript-eslint/parser": "^2.15.0", 59 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 60 | "babel-loader": "^8.0.6", 61 | "babel-plugin-transform-vue-jsx": "^4.0.1", 62 | "chai": "^4.2.0", 63 | "css-loader": "^3.4.2", 64 | "eslint": "^6.8.0", 65 | "eslint-plugin-vue-libs": "^4.0.0", 66 | "mocha": "^7.0.0", 67 | "reflect-metadata": "^0.1.13", 68 | "rimraf": "^3.0.0", 69 | "rollup": "^1.29.0", 70 | "rollup-plugin-babel": "^4.3.3", 71 | "rollup-plugin-replace": "^2.2.0", 72 | "testdouble": "^3.12.5", 73 | "ts-loader": "^6.2.1", 74 | "typescript": "^3.7.4", 75 | "uglify-es": "^3.3.9", 76 | "vue": "^2.6.11", 77 | "vue-loader": "^15.8.3", 78 | "vue-template-compiler": "^2.6.11", 79 | "vuepress": "^1.2.0", 80 | "vuex": "^3.1.2", 81 | "webpack": "^4.41.5", 82 | "webpack-cli": "^3.3.10" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/component.ts: -------------------------------------------------------------------------------- 1 | import Vue, { ComponentOptions } from 'vue' 2 | import { copyReflectionMetadata, reflectionIsSupported } from './reflect' 3 | import { VueClass, DecoratedClass } from './declarations' 4 | import { collectDataFromConstructor } from './data' 5 | import { hasProto, isPrimitive, warn } from './util' 6 | 7 | export const $internalHooks = [ 8 | 'data', 9 | 'beforeCreate', 10 | 'created', 11 | 'beforeMount', 12 | 'mounted', 13 | 'beforeDestroy', 14 | 'destroyed', 15 | 'beforeUpdate', 16 | 'updated', 17 | 'activated', 18 | 'deactivated', 19 | 'render', 20 | 'errorCaptured', // 2.5 21 | 'serverPrefetch' // 2.6 22 | ] 23 | 24 | export function componentFactory ( 25 | Component: VueClass, 26 | options: ComponentOptions = {} 27 | ): VueClass { 28 | options.name = options.name || (Component as any)._componentTag || (Component as any).name 29 | // prototype props. 30 | const proto = Component.prototype 31 | Object.getOwnPropertyNames(proto).forEach(function (key) { 32 | if (key === 'constructor') { 33 | return 34 | } 35 | 36 | // hooks 37 | if ($internalHooks.indexOf(key) > -1) { 38 | options[key] = proto[key] 39 | return 40 | } 41 | const descriptor = Object.getOwnPropertyDescriptor(proto, key)! 42 | if (descriptor.value !== void 0) { 43 | // methods 44 | if (typeof descriptor.value === 'function') { 45 | (options.methods || (options.methods = {}))[key] = descriptor.value 46 | } else { 47 | // typescript decorated data 48 | (options.mixins || (options.mixins = [])).push({ 49 | data (this: Vue) { 50 | return { [key]: descriptor.value } 51 | } 52 | }) 53 | } 54 | } else if (descriptor.get || descriptor.set) { 55 | // computed properties 56 | (options.computed || (options.computed = {}))[key] = { 57 | get: descriptor.get, 58 | set: descriptor.set 59 | } 60 | } 61 | }) 62 | 63 | // add data hook to collect class properties as Vue instance's data 64 | ;(options.mixins || (options.mixins = [])).push({ 65 | data (this: Vue) { 66 | return collectDataFromConstructor(this, Component) 67 | } 68 | }) 69 | 70 | // decorate options 71 | const decorators = (Component as DecoratedClass).__decorators__ 72 | if (decorators) { 73 | decorators.forEach(fn => fn(options)) 74 | delete (Component as DecoratedClass).__decorators__ 75 | } 76 | 77 | // find super 78 | const superProto = Object.getPrototypeOf(Component.prototype) 79 | const Super = superProto instanceof Vue 80 | ? superProto.constructor as VueClass 81 | : Vue 82 | const Extended = Super.extend(options) 83 | 84 | forwardStaticMembers(Extended, Component, Super) 85 | 86 | if (reflectionIsSupported()) { 87 | copyReflectionMetadata(Extended, Component) 88 | } 89 | 90 | return Extended 91 | } 92 | 93 | const reservedPropertyNames = [ 94 | // Unique id 95 | 'cid', 96 | 97 | // Super Vue constructor 98 | 'super', 99 | 100 | // Component options that will be used by the component 101 | 'options', 102 | 'superOptions', 103 | 'extendOptions', 104 | 'sealedOptions', 105 | 106 | // Private assets 107 | 'component', 108 | 'directive', 109 | 'filter' 110 | ] 111 | 112 | const shouldIgnore = { 113 | prototype: true, 114 | arguments: true, 115 | callee: true, 116 | caller: true 117 | } 118 | 119 | function forwardStaticMembers ( 120 | Extended: typeof Vue, 121 | Original: typeof Vue, 122 | Super: typeof Vue 123 | ): void { 124 | // We have to use getOwnPropertyNames since Babel registers methods as non-enumerable 125 | Object.getOwnPropertyNames(Original).forEach(key => { 126 | // Skip the properties that should not be overwritten 127 | if (shouldIgnore[key]) { 128 | return 129 | } 130 | 131 | // Some browsers does not allow reconfigure built-in properties 132 | const extendedDescriptor = Object.getOwnPropertyDescriptor(Extended, key) 133 | if (extendedDescriptor && !extendedDescriptor.configurable) { 134 | return 135 | } 136 | 137 | const descriptor = Object.getOwnPropertyDescriptor(Original, key)! 138 | 139 | // If the user agent does not support `__proto__` or its family (IE <= 10), 140 | // the sub class properties may be inherited properties from the super class in TypeScript. 141 | // We need to exclude such properties to prevent to overwrite 142 | // the component options object which stored on the extended constructor (See #192). 143 | // If the value is a referenced value (object or function), 144 | // we can check equality of them and exclude it if they have the same reference. 145 | // If it is a primitive value, it will be forwarded for safety. 146 | if (!hasProto) { 147 | // Only `cid` is explicitly exluded from property forwarding 148 | // because we cannot detect whether it is a inherited property or not 149 | // on the no `__proto__` environment even though the property is reserved. 150 | if (key === 'cid') { 151 | return 152 | } 153 | 154 | const superDescriptor = Object.getOwnPropertyDescriptor(Super, key) 155 | 156 | if ( 157 | !isPrimitive(descriptor.value) && 158 | superDescriptor && 159 | superDescriptor.value === descriptor.value 160 | ) { 161 | return 162 | } 163 | } 164 | 165 | // Warn if the users manually declare reserved properties 166 | if ( 167 | process.env.NODE_ENV !== 'production' && 168 | reservedPropertyNames.indexOf(key) >= 0 169 | ) { 170 | warn( 171 | `Static property name '${key}' declared on class '${Original.name}' ` + 172 | 'conflicts with reserved property name of Vue internal. ' + 173 | 'It may cause unexpected behavior of the component. Consider renaming the property.' 174 | ) 175 | } 176 | 177 | Object.defineProperty(Extended, key, descriptor) 178 | }) 179 | } 180 | -------------------------------------------------------------------------------- /src/data.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { VueClass } from './declarations' 3 | import { warn } from './util' 4 | 5 | export function collectDataFromConstructor (vm: Vue, Component: VueClass) { 6 | // override _init to prevent to init as Vue instance 7 | const originalInit = Component.prototype._init 8 | Component.prototype._init = function (this: Vue) { 9 | // proxy to actual vm 10 | const keys = Object.getOwnPropertyNames(vm) 11 | // 2.2.0 compat (props are no longer exposed as self properties) 12 | if (vm.$options.props) { 13 | for (const key in vm.$options.props) { 14 | if (!vm.hasOwnProperty(key)) { 15 | keys.push(key) 16 | } 17 | } 18 | } 19 | keys.forEach(key => { 20 | Object.defineProperty(this, key, { 21 | get: () => vm[key], 22 | set: value => { vm[key] = value }, 23 | configurable: true 24 | }) 25 | }) 26 | } 27 | 28 | // should be acquired class property values 29 | const data = new Component() 30 | 31 | // restore original _init to avoid memory leak (#209) 32 | Component.prototype._init = originalInit 33 | 34 | // create plain data object 35 | const plainData = {} 36 | Object.keys(data).forEach(key => { 37 | if (data[key] !== undefined) { 38 | plainData[key] = data[key] 39 | } 40 | }) 41 | 42 | if (process.env.NODE_ENV !== 'production') { 43 | if (!(Component.prototype instanceof Vue) && Object.keys(plainData).length > 0) { 44 | warn( 45 | 'Component class must inherit Vue or its descendant class ' + 46 | 'when class property is used.' 47 | ) 48 | } 49 | } 50 | 51 | return plainData 52 | } 53 | -------------------------------------------------------------------------------- /src/declarations.ts: -------------------------------------------------------------------------------- 1 | import Vue, { ComponentOptions } from 'vue' 2 | 3 | export type VueClass = { new (...args: any[]): V & Vue } & typeof Vue 4 | 5 | export type DecoratedClass = VueClass & { 6 | // Property, method and parameter decorators created by `createDecorator` helper 7 | // will enqueue functions that update component options for lazy processing. 8 | // They will be executed just before creating component constructor. 9 | __decorators__?: ((options: ComponentOptions) => void)[] 10 | } 11 | -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * global type declarations in this project 3 | * should not expose to userland 4 | */ 5 | import 'reflect-metadata' 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Vue, { ComponentOptions } from 'vue' 2 | import { VueClass } from './declarations' 3 | import { componentFactory, $internalHooks } from './component' 4 | 5 | export { createDecorator, VueDecorator, mixins } from './util' 6 | 7 | function Component (options: ComponentOptions & ThisType): >(target: VC) => VC 8 | function Component >(target: VC): VC 9 | function Component (options: ComponentOptions | VueClass): any { 10 | if (typeof options === 'function') { 11 | return componentFactory(options) 12 | } 13 | return function (Component: VueClass) { 14 | return componentFactory(Component, options) 15 | } 16 | } 17 | 18 | Component.registerHooks = function registerHooks (keys: string[]): void { 19 | $internalHooks.push(...keys) 20 | } 21 | 22 | export default Component 23 | -------------------------------------------------------------------------------- /src/reflect.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VueConstructor } from 'vue' 2 | import { VueClass } from './declarations' 3 | 4 | // The rational behind the verbose Reflect-feature check below is the fact that there are polyfills 5 | // which add an implementation for Reflect.defineMetadata but not for Reflect.getOwnMetadataKeys. 6 | // Without this check consumers will encounter hard to track down runtime errors. 7 | export function reflectionIsSupported () { 8 | return typeof Reflect !== 'undefined' && Reflect.defineMetadata && Reflect.getOwnMetadataKeys 9 | } 10 | 11 | export function copyReflectionMetadata ( 12 | to: VueConstructor, 13 | from: VueClass 14 | ) { 15 | forwardMetadata(to, from) 16 | 17 | Object.getOwnPropertyNames(from.prototype).forEach(key => { 18 | forwardMetadata(to.prototype, from.prototype, key) 19 | }) 20 | 21 | Object.getOwnPropertyNames(from).forEach(key => { 22 | forwardMetadata(to, from, key) 23 | }) 24 | } 25 | 26 | function forwardMetadata (to: object, from: object, propertyKey?: string): void { 27 | const metaKeys = propertyKey 28 | ? Reflect.getOwnMetadataKeys(from, propertyKey) 29 | : Reflect.getOwnMetadataKeys(from) 30 | 31 | metaKeys.forEach(metaKey => { 32 | const metadata = propertyKey 33 | ? Reflect.getOwnMetadata(metaKey, from, propertyKey) 34 | : Reflect.getOwnMetadata(metaKey, from) 35 | 36 | if (propertyKey) { 37 | Reflect.defineMetadata(metaKey, metadata, to, propertyKey) 38 | } else { 39 | Reflect.defineMetadata(metaKey, metadata, to) 40 | } 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import Vue, { ComponentOptions } from 'vue' 2 | import { VueClass, DecoratedClass } from './declarations' 3 | 4 | export const noop = () => {} 5 | 6 | const fakeArray = { __proto__: [] } 7 | export const hasProto = fakeArray instanceof Array 8 | 9 | export interface VueDecorator { 10 | // Class decorator 11 | (Ctor: typeof Vue): void 12 | 13 | // Property decorator 14 | (target: Vue, key: string): void 15 | 16 | // Parameter decorator 17 | (target: Vue, key: string, index: number): void 18 | } 19 | 20 | export function createDecorator (factory: (options: ComponentOptions, key: string, index: number) => void): VueDecorator { 21 | return (target: Vue | typeof Vue, key?: any, index?: any) => { 22 | const Ctor = typeof target === 'function' 23 | ? target as DecoratedClass 24 | : target.constructor as DecoratedClass 25 | if (!Ctor.__decorators__) { 26 | Ctor.__decorators__ = [] 27 | } 28 | if (typeof index !== 'number') { 29 | index = undefined 30 | } 31 | Ctor.__decorators__.push(options => factory(options, key, index)) 32 | } 33 | } 34 | 35 | export type UnionToIntersection = (U extends any 36 | ? (k: U) => void 37 | : never) extends (k: infer I) => void 38 | ? I 39 | : never 40 | 41 | export type ExtractInstance = T extends VueClass ? V : never 42 | 43 | export type MixedVueClass< 44 | Mixins extends VueClass[] 45 | > = Mixins extends (infer T)[] 46 | ? VueClass>> 47 | : never 48 | 49 | // Retain legacy declaration for backward compatibility 50 | export function mixins (CtorA: VueClass): VueClass 51 | export function mixins (CtorA: VueClass, CtorB: VueClass): VueClass 52 | export function mixins (CtorA: VueClass, CtorB: VueClass, CtorC: VueClass): VueClass 53 | export function mixins (CtorA: VueClass, CtorB: VueClass, CtorC: VueClass, CtorD: VueClass): VueClass 54 | export function mixins (CtorA: VueClass, CtorB: VueClass, CtorC: VueClass, CtorD: VueClass, CtorE: VueClass): VueClass 55 | export function mixins(...Ctors: VueClass[]): VueClass 56 | 57 | export function mixins[]>(...Ctors: T): MixedVueClass 58 | export function mixins (...Ctors: VueClass[]): VueClass { 59 | return Vue.extend({ mixins: Ctors }) 60 | } 61 | 62 | export function isPrimitive (value: any): boolean { 63 | const type = typeof value 64 | return value == null || (type !== 'object' && type !== 'function') 65 | } 66 | 67 | export function warn (message: string): void { 68 | if (typeof console !== 'undefined') { 69 | console.warn('[vue-class-component] ' + message) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", { "modules": false }] 4 | ], 5 | "plugins": [ 6 | ["@babel/proposal-decorators", { "legacy": true }], 7 | ["@babel/proposal-class-properties", { "loose": true }] 8 | ] 9 | } -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true 4 | } 5 | } -------------------------------------------------------------------------------- /test/test-babel.js: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import Component, { createDecorator, mixins } from '../lib' 3 | import { expect } from 'chai' 4 | import * as td from 'testdouble' 5 | import Vue from 'vue' 6 | 7 | describe('vue-class-component with Babel', () => { 8 | it('should be instantiated without any errors', () => { 9 | @Component 10 | class MyComp {} 11 | expect(() => new MyComp()).to.not.throw(Error) 12 | }) 13 | 14 | it('should collect class properties as data', () => { 15 | @Component({ 16 | props: ['propValue'] 17 | }) 18 | class MyComp extends Vue { 19 | foo = 'hello' 20 | bar = 1 + this.propValue 21 | } 22 | const c = new MyComp({ 23 | propsData: { 24 | propValue: 1 25 | } 26 | }) 27 | expect(c.foo).to.equal('hello') 28 | expect(c.propValue).to.equal(1) 29 | expect(c.bar).to.equal(2) 30 | }) 31 | 32 | it('should collect decorated class properties', () => { 33 | const valueDecorator = (value) => () => { 34 | return { 35 | enumerable: true, 36 | value: value 37 | } 38 | } 39 | 40 | const getterDecorator = (value) => () => { 41 | return { 42 | enumerable: true, 43 | get () { 44 | return value 45 | } 46 | } 47 | } 48 | 49 | @Component 50 | class MyComp extends Vue { 51 | @valueDecorator('field1') 52 | field1 53 | 54 | @getterDecorator('field2') 55 | field2 56 | } 57 | 58 | const c = new MyComp() 59 | expect(c.field1).to.equal('field1') 60 | expect(c.field2).to.equal('field2') 61 | }) 62 | 63 | it('should not collect uninitialized class properties', () => { 64 | const Prop = createDecorator((options, key) => { 65 | if (!options.props) { 66 | options.props = {} 67 | } 68 | options.props[key] = true 69 | }) 70 | 71 | @Component 72 | class MyComp { 73 | foo 74 | @Prop bar 75 | } 76 | const c = new MyComp() 77 | expect('foo' in c.$data).to.be.false 78 | expect('bar' in c.$data).to.be.false 79 | }) 80 | 81 | it('warn if class property is used without inheriting Vue class', () => { 82 | const originalWarn = console.warn 83 | console.warn = td.function('warn') 84 | 85 | @Component({ 86 | foo: Number 87 | }) 88 | class MyComp { 89 | bar = this.foo + 2 90 | } 91 | const c = new MyComp({ 92 | propsData: { 93 | foo: 1 94 | } 95 | }) 96 | 97 | const message = '[vue-class-component] ' + 98 | 'Component class must inherit Vue or its descendant class ' + 99 | 'when class property is used.' 100 | 101 | try { 102 | td.verify(console.warn(message)) 103 | } finally { 104 | console.warn = originalWarn 105 | } 106 | }) 107 | 108 | // #155 109 | it('createDecrator: create a class decorator', () => { 110 | const DataMixin = createDecorator(options => { 111 | options.data = function () { 112 | return { 113 | test: 'foo' 114 | } 115 | } 116 | }) 117 | 118 | @Component 119 | @DataMixin 120 | class MyComp extends Vue {} 121 | 122 | const vm = new MyComp() 123 | expect(vm.test).to.equal('foo') 124 | }) 125 | 126 | it('should not throw if property decorator declare some methods', () => { 127 | const Test = createDecorator((options, key) => { 128 | if (!options.methods) { 129 | options.methods = {} 130 | } 131 | options.methods[key] = () => 'test' 132 | }) 133 | 134 | @Component 135 | class MyComp extends Vue { 136 | @Test test 137 | } 138 | 139 | const vm = new MyComp() 140 | expect(vm.test()).to.equal('test') 141 | }) 142 | 143 | it('should forward static members', () => { 144 | @Component 145 | class MyComp extends Vue { 146 | static foo = 'foo' 147 | 148 | static bar () { 149 | return 'bar' 150 | } 151 | } 152 | 153 | expect(MyComp.foo).to.equal('foo') 154 | expect(MyComp.bar()).to.equal('bar') 155 | }) 156 | 157 | it('mixin helper', function () { 158 | @Component 159 | class MixinA extends Vue { 160 | valueA = 'hello' 161 | } 162 | 163 | @Component 164 | class MixinB extends Vue { 165 | valueB = 123 166 | } 167 | 168 | @Component 169 | class MyComp extends mixins(MixinA, MixinB) { 170 | test () { 171 | this.valueA = 'hi' 172 | this.valueB = 456 173 | } 174 | } 175 | 176 | const vm = new MyComp() 177 | expect(vm.valueA).to.equal('hello') 178 | expect(vm.valueB).to.equal(123) 179 | vm.test() 180 | expect(vm.valueA).to.equal('hi') 181 | expect(vm.valueB).to.equal(456) 182 | }) 183 | 184 | it('copies reflection metadata', function () { 185 | @Component 186 | @Reflect.metadata('worksConstructor', true) 187 | class Test extends Vue { 188 | @Reflect.metadata('worksStatic', true) 189 | static staticValue = 'staticValue' 190 | 191 | _test = false 192 | 193 | @Reflect.metadata('worksMethod', true) 194 | test () { 195 | void 0 196 | } 197 | 198 | @Reflect.metadata('worksAccessor', true) 199 | get testAccessor () { 200 | return this._test 201 | } 202 | } 203 | 204 | expect(Reflect.getOwnMetadata('worksConstructor', Test)).to.equal(true) 205 | expect(Reflect.getOwnMetadata('worksStatic', Test, 'staticValue')).to.equal(true) 206 | expect(Reflect.getOwnMetadata('worksMethod', Test.prototype, 'test')).to.equal(true) 207 | expect(Reflect.getOwnMetadata('worksAccessor', Test.prototype, 'testAccessor')).to.equal(true) 208 | }) 209 | }) 210 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import Component, { createDecorator, mixins } from '../lib' 3 | import { expect } from 'chai' 4 | import * as td from 'testdouble' 5 | import Vue, { ComputedOptions } from 'vue' 6 | 7 | describe('vue-class-component', () => { 8 | it('hooks', () => { 9 | let created = false 10 | let destroyed = false 11 | 12 | @Component 13 | class MyComp extends Vue { 14 | created () { 15 | created = true 16 | } 17 | destroyed () { 18 | destroyed = true 19 | } 20 | } 21 | 22 | const c = new MyComp() 23 | expect(created).to.be.true 24 | expect(destroyed).to.be.false 25 | c.$destroy() 26 | expect(destroyed).to.be.true 27 | }) 28 | 29 | it('hooks: adding custom hooks', () => { 30 | Component.registerHooks(['beforeRouteEnter']) 31 | 32 | @Component 33 | class MyComp extends Vue { 34 | static options: any 35 | 36 | beforeRouteEnter () { 37 | return 'beforeRouteEnter' 38 | } 39 | } 40 | 41 | expect(MyComp.options.beforeRouteEnter()).to.equal('beforeRouteEnter') 42 | }) 43 | 44 | it('data: should collect from class properties', () => { 45 | @Component({ 46 | props: ['foo'] 47 | }) 48 | class MyComp extends Vue { 49 | foo!: number 50 | a: string = 'hello' 51 | b: number = this.foo + 1 52 | } 53 | 54 | const c = new MyComp({ 55 | propsData: { 56 | foo: 1 57 | } 58 | }) 59 | expect(c.a).to.equal('hello') 60 | expect(c.b).to.equal(2) 61 | }) 62 | 63 | it('data: should collect from decorated class properties', () => { 64 | const valueDecorator = (value: any) => (_: any, __: any): any => { 65 | return { 66 | enumerable: true, 67 | value 68 | } 69 | } 70 | 71 | const getterDecorator = (value: any) => (_: any, __: any): any => { 72 | return { 73 | enumerable: true, 74 | get () { 75 | return value 76 | } 77 | } 78 | } 79 | 80 | @Component 81 | class MyComp extends Vue { 82 | @valueDecorator('field1') 83 | field1!: string 84 | 85 | @getterDecorator('field2') 86 | field2!: string 87 | } 88 | 89 | const c = new MyComp() 90 | expect(c.field1).to.equal('field1') 91 | expect(c.field2).to.equal('field2') 92 | }) 93 | 94 | it('data: should collect custom property defined on beforeCreate', () => { 95 | @Component 96 | class MyComp extends Vue { 97 | $store: any 98 | foo: string = 'Hello, ' + this.$store.state.msg 99 | 100 | beforeCreate () { 101 | this.$store = { 102 | state: { 103 | msg: 'world' 104 | } 105 | } 106 | } 107 | } 108 | 109 | const c = new MyComp() 110 | expect(c.foo).to.equal('Hello, world') 111 | }) 112 | 113 | it('methods', () => { 114 | let msg: string = '' 115 | 116 | @Component 117 | class MyComp extends Vue { 118 | hello () { 119 | msg = 'hi' 120 | } 121 | } 122 | 123 | const c = new MyComp() 124 | c.hello() 125 | expect(msg).to.equal('hi') 126 | }) 127 | 128 | it('computed', () => { 129 | @Component 130 | class MyComp extends Vue { 131 | a!: number 132 | data () { 133 | return { 134 | a: 1 135 | } 136 | } 137 | get b () { 138 | return this.a + 1 139 | } 140 | } 141 | 142 | const c = new MyComp() 143 | expect(c.a).to.equal(1) 144 | expect(c.b).to.equal(2) 145 | c.a = 2 146 | expect(c.b).to.equal(3) 147 | }) 148 | 149 | describe('name', () => { 150 | it('via name option', () => { 151 | @Component({ name: 'test' }) 152 | class MyComp extends Vue {} 153 | 154 | const c = new MyComp() 155 | expect(c.$options.name).to.equal('test') 156 | }) 157 | 158 | it('via _componentTag', () => { 159 | @Component 160 | class MyComp extends Vue { 161 | static _componentTag = 'test' 162 | } 163 | 164 | const c = new MyComp() 165 | expect(c.$options.name).to.equal('test') 166 | }) 167 | 168 | it('via class name', () => { 169 | @Component 170 | class MyComp extends Vue {} 171 | 172 | const c = new MyComp() 173 | expect(c.$options.name).to.equal('MyComp') 174 | }) 175 | }) 176 | 177 | it('other options', (done) => { 178 | let v: number 179 | 180 | @Component({ 181 | watch: { 182 | a: val => v = val 183 | } 184 | }) 185 | class MyComp extends Vue { 186 | a!: number 187 | data () { 188 | return { a: 1 } 189 | } 190 | } 191 | 192 | const c = new MyComp() 193 | c.a = 2 194 | Vue.nextTick(() => { 195 | expect(v).to.equal(2) 196 | done() 197 | }) 198 | }) 199 | 200 | it('extending', function () { 201 | @Component 202 | class Base extends Vue { 203 | a!: number 204 | data (): any { 205 | return { a: 1 } 206 | } 207 | } 208 | 209 | @Component 210 | class A extends Base { 211 | b!: number 212 | data (): any { 213 | return { b: 2 } 214 | } 215 | } 216 | 217 | const a = new A() 218 | expect(a.a).to.equal(1) 219 | expect(a.b).to.equal(2) 220 | }) 221 | 222 | // #199 223 | it('should not re-execute super class decortors', function (done) { 224 | const Watch = (valueKey: string) => createDecorator((options, key) => { 225 | if (!options.watch) { 226 | options.watch = {} 227 | } 228 | options.watch[valueKey] = key 229 | }) 230 | 231 | const spy = td.function() 232 | 233 | @Component 234 | class Base extends Vue { 235 | count = 0 236 | 237 | @Watch('count') 238 | notify () { 239 | spy() 240 | } 241 | } 242 | 243 | @Component 244 | class A extends Base {} 245 | 246 | const vm = new A() 247 | vm.count++ 248 | vm.$nextTick(() => { 249 | td.verify(spy(), { times: 1 }) 250 | done() 251 | }) 252 | }) 253 | 254 | it('createDecorator', function () { 255 | const Prop = createDecorator((options, key) => { 256 | // component options should be passed to the callback 257 | // and update for the options affect the component 258 | (options.props || (options.props = {}))[key] = true 259 | }) 260 | 261 | const NoCache = createDecorator((options, key) => { 262 | // options should have computed and methods etc. 263 | // that specified by class property accessors and methods 264 | const computedOption = options.computed![key] as ComputedOptions 265 | computedOption.cache = false 266 | }) 267 | 268 | @Component 269 | class MyComp extends Vue { 270 | @Prop foo!: string 271 | @NoCache get bar (): string { 272 | return 'world' 273 | } 274 | } 275 | 276 | const c = new MyComp({ 277 | propsData: { 278 | foo: 'hello' 279 | } 280 | }) 281 | expect(c.foo).to.equal('hello') 282 | expect(c.bar).to.equal('world') 283 | expect((MyComp as any).options.computed.bar.cache).to.be.false 284 | }) 285 | 286 | // #104 287 | it('createDecorator: decorate correctly even if a component is created in another @Component decorator', () => { 288 | // Just assigns the given value to the decorated property 289 | const Value = (value: any) => createDecorator((options, key) => { 290 | const data = options.data as Function || (() => ({})) 291 | options.data = function () { 292 | return { 293 | ...data.call(this), 294 | [key]: value 295 | } 296 | } 297 | }) 298 | 299 | const createChild = () => { 300 | @Component 301 | class Child extends Vue { 302 | @Value('child') 303 | value!: string 304 | } 305 | return Child 306 | } 307 | 308 | @Component({ 309 | components: { 310 | Child: createChild() 311 | } 312 | }) 313 | class Parent extends Vue { 314 | @Value('parent') 315 | value!: string 316 | } 317 | 318 | const parent = new Parent() 319 | const child = new (parent as any).$options.components.Child() 320 | expect(parent.value).to.equal('parent') 321 | expect(child.value).to.equal('child') 322 | }) 323 | 324 | // #155 325 | it('createDecrator: create a class decorator', () => { 326 | const DataMixin = createDecorator(options => { 327 | options.data = function () { 328 | return { 329 | test: 'foo' 330 | } 331 | } 332 | }) 333 | 334 | @Component 335 | @DataMixin 336 | class MyComp extends Vue {} 337 | 338 | const vm: any = new MyComp() 339 | expect(vm.test).to.equal('foo') 340 | }) 341 | 342 | it('forwardStatics', function () { 343 | @Component 344 | class MyComp extends Vue { 345 | static myValue = 52 346 | 347 | static myFunc () { 348 | return 42 349 | } 350 | } 351 | 352 | expect(MyComp.myValue).to.equal(52) 353 | expect(MyComp.myFunc()).to.equal(42) 354 | }) 355 | 356 | it('should warn if declared static property uses a reserved name but not prevent forwarding', function () { 357 | const originalWarn = console.warn 358 | console.warn = td.function('warn') as any 359 | 360 | @Component 361 | class MyComp extends Vue { 362 | static options = 'test' 363 | } 364 | 365 | const message = '[vue-class-component] ' + 366 | 'Static property name \'options\' declared on class \'MyComp\' conflicts with ' + 367 | 'reserved property name of Vue internal. It may cause unexpected behavior of the component. Consider renaming the property.' 368 | 369 | expect(MyComp.options).to.equal('test') 370 | try { 371 | td.verify(console.warn(message)) 372 | } finally { 373 | console.warn = originalWarn 374 | } 375 | }) 376 | 377 | it('mixin helper', function () { 378 | @Component 379 | class MixinA extends Vue { 380 | valueA = 'hello' 381 | } 382 | 383 | @Component 384 | class MixinB extends Vue { 385 | valueB = 123 386 | } 387 | 388 | @Component 389 | class MyComp extends mixins(MixinA, MixinB) { 390 | test () { 391 | this.valueA = 'hi' 392 | this.valueB = 456 393 | } 394 | } 395 | 396 | const vm = new MyComp() 397 | expect(vm.valueA).to.equal('hello') 398 | expect(vm.valueB).to.equal(123) 399 | vm.test() 400 | expect(vm.valueA).to.equal('hi') 401 | expect(vm.valueB).to.equal(456) 402 | }) 403 | 404 | it('copies reflection metadata', function () { 405 | @Component 406 | @Reflect.metadata('worksConstructor', true) 407 | class Test extends Vue { 408 | @Reflect.metadata('worksStatic', true) 409 | static staticValue: string = 'staticValue' 410 | 411 | private _test: boolean = false 412 | 413 | @Reflect.metadata('worksMethod', true) 414 | test (): void { 415 | void 0 416 | } 417 | 418 | @Reflect.metadata('worksAccessor', true) 419 | get testAccessor (): boolean { 420 | return this._test 421 | } 422 | } 423 | 424 | expect(Reflect.getOwnMetadata('worksConstructor', Test)).to.equal(true) 425 | expect(Reflect.getOwnMetadata('worksStatic', Test, 'staticValue')).to.equal(true) 426 | expect(Reflect.getOwnMetadata('worksMethod', Test.prototype, 'test')).to.equal(true) 427 | expect(Reflect.getOwnMetadata('worksAccessor', Test.prototype, 'testAccessor')).to.equal(true) 428 | }) 429 | }) 430 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "", 5 | "experimentalDecorators": true, 6 | "declaration": false 7 | }, 8 | "include": [ 9 | "./**/*.ts" 10 | ], 11 | "compileOnSave": false 12 | } 13 | -------------------------------------------------------------------------------- /test/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'development', 3 | entry: [ 4 | './test/test.ts', 5 | './test/test-babel.js' 6 | ], 7 | output: { 8 | path: __dirname, 9 | filename: 'test.build.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.ts$/, 15 | exclude: /node_modules|vue\/src/, 16 | loader: 'ts-loader' 17 | }, 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules|vue\/src/, 21 | loader: 'babel-loader' 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": [ 5 | "dom", 6 | "es2015" 7 | ], 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "outDir": "lib", 11 | "declaration": true, 12 | "strict": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "suppressImplicitAnyIndexErrors": true 16 | }, 17 | "include": [ 18 | "src/**/*.ts" 19 | ], 20 | "compileOnSave": false 21 | } 22 | --------------------------------------------------------------------------------