├── .editorconfig ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── rollup.config.js ├── src ├── decorators │ ├── Emit.ts │ ├── Inject.ts │ ├── InjectReactive.ts │ ├── Model.ts │ ├── ModelSync.ts │ ├── Prop.ts │ ├── PropSync.ts │ ├── Provide.ts │ ├── ProvideReactive.ts │ ├── Ref.ts │ ├── VModel.ts │ └── Watch.ts ├── helpers │ ├── metadata.ts │ └── provideInject.ts ├── index.ts └── tsconfig.json ├── tests └── decorators │ ├── Emit.spec.ts │ ├── Inject.spec.ts │ ├── InjectReactive.spec.ts │ ├── Model.spec.ts │ ├── ModelSync.spec.ts │ ├── Prop.spec.ts │ ├── PropSync.spec.ts │ ├── Provide.spec.ts │ ├── ProvideReactive.spec.ts │ ├── Ref.spec.ts │ ├── VModel.spec.ts │ └── Watch.spec.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | npm-debug.log 4 | src/*.js 5 | tests/*.spec.js 6 | package-lock.json 7 | .vscode/setting.json 8 | .idea 9 | yarn-error.log 10 | coverage 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'node' 4 | cache: npm 5 | script: npm test -- --coverage 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | 3 | # v9.1.2 4 | 5 | ## Bug fixes 6 | 7 | - Fix `typings` field in `package.json` (#356) 8 | 9 | # v9.1.1 10 | 11 | ## Bug fixes 12 | 13 | - Fix `main` and `module` field in `package.json` 14 | 15 | # v9.1.0 16 | 17 | ## New features 18 | 19 | - Add `@ModelSync` decorator (#254) 20 | - Add `@VModel` decorator (#276) 21 | 22 | ## Bug fixes 23 | 24 | - Make reactive provided values configureable (#330) 25 | 26 | ## Refactoring / others 27 | 28 | - **Breaking change** Rename `vue-property-decorator.ts` to `index.ts` (c8c88642f589c8cb1a2f3a09034a01b17152bae7) 29 | - Exported files are also renamed from `vue-property-decorator.*` to `index.*` 30 | - Split source code into separate files (d7954f8ca1a729a53da207317139fc76cefe98b2) 31 | - Bump dependency versions 32 | 33 | # v9.0.2 34 | 35 | # v9.0.1 (Failed to publish to npm) 36 | 37 | - Fix ProvideReactive (#328) 38 | - Fix README.md (#329) 39 | 40 | # v9.0.0 41 | 42 | - Move `vue-class-component` to `peerDependencies` 43 | 44 | # v8.5.1 45 | 46 | - Move `vue-class-component` to `dependencies` 47 | 48 | # v8.5.0 49 | 50 | - Revert #299 51 | - Add CHANGELOG.md 52 | - Fix README.md (#319) 53 | - Move `vue-class-component` to `peerDependencies` 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 kaorun343 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 | 2 | # [DEPRECATED] Vue Property Decorator 3 | ## ⚠️ Notice 4 | This library is no longer actively maintained. If you still want to use classes, check out the community-maintained project [`vue-facing-decorator`](https://facing-dev.github.io/vue-facing-decorator/#/). 5 | 6 | --- 7 | 8 | [![npm](https://img.shields.io/npm/v/vue-property-decorator.svg)](https://www.npmjs.com/package/vue-property-decorator) 9 | [![Build Status](https://travis-ci.org/kaorun343/vue-property-decorator.svg?branch=master)](https://travis-ci.org/kaorun343/vue-property-decorator) 10 | 11 | This library fully depends on [vue-class-component](https://github.com/vuejs/vue-class-component), so please read its README before using this library. 12 | 13 | ## License 14 | 15 | MIT License 16 | 17 | ## Install 18 | 19 | ```bash 20 | npm i -S vue-property-decorator 21 | ``` 22 | 23 | ## Usage 24 | 25 | There are several decorators and 1 function (Mixin): 26 | 27 | - [`@Prop`](#Prop) 28 | - [`@PropSync`](#PropSync) 29 | - [`@Model`](#Model) 30 | - [`@ModelSync`](#ModelSync) 31 | - [`@Watch`](#Watch) 32 | - [`@Provide`](#Provide) 33 | - [`@Inject`](#Provide) 34 | - [`@ProvideReactive`](#ProvideReactive) 35 | - [`@InjectReactive`](#ProvideReactive) 36 | - [`@Emit`](#Emit) 37 | - [`@Ref`](#Ref) 38 | - [`@VModel`](#VModel) 39 | - `@Component` (**provided by** [vue-class-component](https://github.com/vuejs/vue-class-component)) 40 | - `Mixins` (the helper function named `mixins` **provided by** [vue-class-component](https://github.com/vuejs/vue-class-component)) 41 | 42 | ## See also 43 | 44 | [vuex-class](https://github.com/ktsn/vuex-class/) 45 | 46 | ### `@Prop(options: (PropOptions | Constructor[] | Constructor) = {})` decorator 47 | 48 | ```ts 49 | import { Vue, Component, Prop } from 'vue-property-decorator' 50 | 51 | @Component 52 | export default class YourComponent extends Vue { 53 | @Prop(Number) readonly propA: number | undefined 54 | @Prop({ default: 'default value' }) readonly propB!: string 55 | @Prop([String, Boolean]) readonly propC: string | boolean | undefined 56 | } 57 | ``` 58 | 59 | is equivalent to 60 | 61 | ```js 62 | export default { 63 | props: { 64 | propA: { 65 | type: Number, 66 | }, 67 | propB: { 68 | default: 'default value', 69 | }, 70 | propC: { 71 | type: [String, Boolean], 72 | }, 73 | }, 74 | } 75 | ``` 76 | 77 | #### If you'd like to set `type` property of each prop value from its type definition, you can use [reflect-metadata](https://github.com/rbuckton/reflect-metadata). 78 | 79 | 1. Set `emitDecoratorMetadata` to `true`. 80 | 2. Import `reflect-metadata` **before** importing `vue-property-decorator` (importing `reflect-metadata` is needed just once.) 81 | 82 | ```ts 83 | import 'reflect-metadata' 84 | import { Vue, Component, Prop } from 'vue-property-decorator' 85 | 86 | @Component 87 | export default class MyComponent extends Vue { 88 | @Prop() age!: number 89 | } 90 | ``` 91 | 92 | #### Each prop's default value need to be defined as same as the example code shown in above. 93 | 94 | It's **not** supported to define each `default` property like `@Prop() prop = 'default value'` . 95 | 96 | ### `@PropSync(propName: string, options: (PropOptions | Constructor[] | Constructor) = {})` decorator 97 | 98 | ```ts 99 | import { Vue, Component, PropSync } from 'vue-property-decorator' 100 | 101 | @Component 102 | export default class YourComponent extends Vue { 103 | @PropSync('name', { type: String }) syncedName!: string 104 | } 105 | ``` 106 | 107 | is equivalent to 108 | 109 | ```js 110 | export default { 111 | props: { 112 | name: { 113 | type: String, 114 | }, 115 | }, 116 | computed: { 117 | syncedName: { 118 | get() { 119 | return this.name 120 | }, 121 | set(value) { 122 | this.$emit('update:name', value) 123 | }, 124 | }, 125 | }, 126 | } 127 | ``` 128 | 129 | [`@PropSync`](#PropSync) works like [`@Prop`](#Prop) besides the fact that it takes the propName as an argument of the decorator, and also creates a computed getter and setter behind the scenes. This way you can interface with the property as if it was a regular data property whilst making it as easy as appending the `.sync` modifier in the parent component. 130 | 131 | ### `@Model(event?: string, options: (PropOptions | Constructor[] | Constructor) = {})` decorator 132 | 133 | ```ts 134 | import { Vue, Component, Model } from 'vue-property-decorator' 135 | 136 | @Component 137 | export default class YourComponent extends Vue { 138 | @Model('change', { type: Boolean }) readonly checked!: boolean 139 | } 140 | ``` 141 | 142 | is equivalent to 143 | 144 | ```js 145 | export default { 146 | model: { 147 | prop: 'checked', 148 | event: 'change', 149 | }, 150 | props: { 151 | checked: { 152 | type: Boolean, 153 | }, 154 | }, 155 | } 156 | ``` 157 | 158 | `@Model` property can also set `type` property from its type definition via `reflect-metadata` . 159 | 160 | ### `@ModelSync(propName: string, event?: string, options: (PropOptions | Constructor[] | Constructor) = {})` decorator 161 | 162 | ```ts 163 | import { Vue, Component, ModelSync } from 'vue-property-decorator' 164 | 165 | @Component 166 | export default class YourComponent extends Vue { 167 | @ModelSync('checked', 'change', { type: Boolean }) 168 | readonly checkedValue!: boolean 169 | } 170 | ``` 171 | 172 | is equivalent to 173 | 174 | ```js 175 | export default { 176 | model: { 177 | prop: 'checked', 178 | event: 'change', 179 | }, 180 | props: { 181 | checked: { 182 | type: Boolean, 183 | }, 184 | }, 185 | computed: { 186 | checkedValue: { 187 | get() { 188 | return this.checked 189 | }, 190 | set(value) { 191 | this.$emit('change', value) 192 | }, 193 | }, 194 | }, 195 | } 196 | ``` 197 | 198 | `@ModelSync` property can also set `type` property from its type definition via `reflect-metadata` . 199 | 200 | ### `@Watch(path: string, options: WatchOptions = {})` decorator 201 | 202 | ```ts 203 | import { Vue, Component, Watch } from 'vue-property-decorator' 204 | 205 | @Component 206 | export default class YourComponent extends Vue { 207 | @Watch('child') 208 | onChildChanged(val: string, oldVal: string) {} 209 | 210 | @Watch('person', { immediate: true, deep: true }) 211 | onPersonChanged1(val: Person, oldVal: Person) {} 212 | 213 | @Watch('person') 214 | onPersonChanged2(val: Person, oldVal: Person) {} 215 | 216 | @Watch('person') 217 | @Watch('child') 218 | onPersonAndChildChanged() {} 219 | } 220 | ``` 221 | 222 | is equivalent to 223 | 224 | ```js 225 | export default { 226 | watch: { 227 | child: [ 228 | { 229 | handler: 'onChildChanged', 230 | immediate: false, 231 | deep: false, 232 | }, 233 | { 234 | handler: 'onPersonAndChildChanged', 235 | immediate: false, 236 | deep: false, 237 | }, 238 | ], 239 | person: [ 240 | { 241 | handler: 'onPersonChanged1', 242 | immediate: true, 243 | deep: true, 244 | }, 245 | { 246 | handler: 'onPersonChanged2', 247 | immediate: false, 248 | deep: false, 249 | }, 250 | { 251 | handler: 'onPersonAndChildChanged', 252 | immediate: false, 253 | deep: false, 254 | }, 255 | ], 256 | }, 257 | methods: { 258 | onChildChanged(val, oldVal) {}, 259 | onPersonChanged1(val, oldVal) {}, 260 | onPersonChanged2(val, oldVal) {}, 261 | onPersonAndChildChanged() {}, 262 | }, 263 | } 264 | ``` 265 | 266 | ### `@Provide(key?: string | symbol)` / `@Inject(options?: { from?: InjectKey, default?: any } | InjectKey)` decorator 267 | 268 | ```ts 269 | import { Component, Inject, Provide, Vue } from 'vue-property-decorator' 270 | 271 | const symbol = Symbol('baz') 272 | 273 | @Component 274 | export class MyComponent extends Vue { 275 | @Inject() readonly foo!: string 276 | @Inject('bar') readonly bar!: string 277 | @Inject({ from: 'optional', default: 'default' }) readonly optional!: string 278 | @Inject(symbol) readonly baz!: string 279 | 280 | @Provide() foo = 'foo' 281 | @Provide('bar') baz = 'bar' 282 | } 283 | ``` 284 | 285 | is equivalent to 286 | 287 | ```js 288 | const symbol = Symbol('baz') 289 | 290 | export const MyComponent = Vue.extend({ 291 | inject: { 292 | foo: 'foo', 293 | bar: 'bar', 294 | optional: { from: 'optional', default: 'default' }, 295 | baz: symbol, 296 | }, 297 | data() { 298 | return { 299 | foo: 'foo', 300 | baz: 'bar', 301 | } 302 | }, 303 | provide() { 304 | return { 305 | foo: this.foo, 306 | bar: this.baz, 307 | } 308 | }, 309 | }) 310 | ``` 311 | 312 | ### `@ProvideReactive(key?: string | symbol)` / `@InjectReactive(options?: { from?: InjectKey, default?: any } | InjectKey)` decorator 313 | 314 | These decorators are reactive version of `@Provide` and `@Inject`. If a provided value is modified by parent component, then the child component can catch this modification. 315 | 316 | ```ts 317 | const key = Symbol() 318 | @Component 319 | class ParentComponent extends Vue { 320 | @ProvideReactive() one = 'value' 321 | @ProvideReactive(key) two = 'value' 322 | } 323 | 324 | @Component 325 | class ChildComponent extends Vue { 326 | @InjectReactive() one!: string 327 | @InjectReactive(key) two!: string 328 | } 329 | ``` 330 | 331 | ### `@Emit(event?: string)` decorator 332 | 333 | The functions decorated by `@Emit` `$emit` their return value followed by their original arguments. If the return value is a promise, it is resolved before being emitted. 334 | 335 | If the name of the event is not supplied via the `event` argument, the function name is used instead. In that case, the camelCase name will be converted to kebab-case. 336 | 337 | ```ts 338 | import { Vue, Component, Emit } from 'vue-property-decorator' 339 | 340 | @Component 341 | export default class YourComponent extends Vue { 342 | count = 0 343 | 344 | @Emit() 345 | addToCount(n: number) { 346 | this.count += n 347 | } 348 | 349 | @Emit('reset') 350 | resetCount() { 351 | this.count = 0 352 | } 353 | 354 | @Emit() 355 | returnValue() { 356 | return 10 357 | } 358 | 359 | @Emit() 360 | onInputChange(e) { 361 | return e.target.value 362 | } 363 | 364 | @Emit() 365 | promise() { 366 | return new Promise((resolve) => { 367 | setTimeout(() => { 368 | resolve(20) 369 | }, 0) 370 | }) 371 | } 372 | } 373 | ``` 374 | 375 | is equivalent to 376 | 377 | ```js 378 | export default { 379 | data() { 380 | return { 381 | count: 0, 382 | } 383 | }, 384 | methods: { 385 | addToCount(n) { 386 | this.count += n 387 | this.$emit('add-to-count', n) 388 | }, 389 | resetCount() { 390 | this.count = 0 391 | this.$emit('reset') 392 | }, 393 | returnValue() { 394 | this.$emit('return-value', 10) 395 | }, 396 | onInputChange(e) { 397 | this.$emit('on-input-change', e.target.value, e) 398 | }, 399 | promise() { 400 | const promise = new Promise((resolve) => { 401 | setTimeout(() => { 402 | resolve(20) 403 | }, 0) 404 | }) 405 | 406 | promise.then((value) => { 407 | this.$emit('promise', value) 408 | }) 409 | }, 410 | }, 411 | } 412 | ``` 413 | 414 | ### `@Ref(refKey?: string)` decorator 415 | 416 | ```ts 417 | import { Vue, Component, Ref } from 'vue-property-decorator' 418 | 419 | import AnotherComponent from '@/path/to/another-component.vue' 420 | 421 | @Component 422 | export default class YourComponent extends Vue { 423 | @Ref() readonly anotherComponent!: AnotherComponent 424 | @Ref('aButton') readonly button!: HTMLButtonElement 425 | } 426 | ``` 427 | 428 | is equivalent to 429 | 430 | ```js 431 | export default { 432 | computed() { 433 | anotherComponent: { 434 | cache: false, 435 | get() { 436 | return this.$refs.anotherComponent as AnotherComponent 437 | } 438 | }, 439 | button: { 440 | cache: false, 441 | get() { 442 | return this.$refs.aButton as HTMLButtonElement 443 | } 444 | } 445 | } 446 | } 447 | ``` 448 | 449 | ### `@VModel(propsArgs?: PropOptions)` decorator 450 | 451 | ```ts 452 | import { Vue, Component, VModel } from 'vue-property-decorator' 453 | 454 | @Component 455 | export default class YourComponent extends Vue { 456 | @VModel({ type: String }) name!: string 457 | } 458 | ``` 459 | 460 | is equivalent to 461 | 462 | ```js 463 | export default { 464 | props: { 465 | value: { 466 | type: String, 467 | }, 468 | }, 469 | computed: { 470 | name: { 471 | get() { 472 | return this.value 473 | }, 474 | set(value) { 475 | this.$emit('input', value) 476 | }, 477 | }, 478 | }, 479 | } 480 | ``` 481 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/v9/t67k5f6x7plgsjrgm38_94nw0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | // clearMocks: false, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: 'coverage', 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // An array of directory names to be searched recursively up from the requiring module's location 64 | // moduleDirectories: [ 65 | // "node_modules" 66 | // ], 67 | 68 | // An array of file extensions your modules use 69 | // moduleFileExtensions: [ 70 | // "js", 71 | // "json", 72 | // "jsx", 73 | // "ts", 74 | // "tsx", 75 | // "node" 76 | // ], 77 | 78 | // A map from regular expressions to module names that allow to stub out resources with a single module 79 | // moduleNameMapper: {}, 80 | 81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 82 | // modulePathIgnorePatterns: [], 83 | 84 | // Activates notifications for test results 85 | // notify: false, 86 | 87 | // An enum that specifies notification mode. Requires { notify: true } 88 | // notifyMode: "failure-change", 89 | 90 | // A preset that is used as a base for Jest's configuration 91 | // preset: null, 92 | 93 | // Run tests from one or more projects 94 | // projects: null, 95 | 96 | // Use this configuration option to add custom reporters to Jest 97 | // reporters: undefined, 98 | 99 | // Automatically reset mock state between every test 100 | resetMocks: true, 101 | 102 | // Reset the module registry before running each individual test 103 | // resetModules: false, 104 | 105 | // A path to a custom resolver 106 | // resolver: null, 107 | 108 | // Automatically restore mock state between every test 109 | // restoreMocks: false, 110 | 111 | // The root directory that Jest should scan for tests and modules within 112 | // rootDir: null, 113 | 114 | // A list of paths to directories that Jest should use to search for files in 115 | // roots: [ 116 | // "" 117 | // ], 118 | 119 | // Allows you to use a custom runner instead of Jest's default test runner 120 | // runner: "jest-runner", 121 | 122 | // The paths to modules that run some code to configure or set up the testing environment before each test 123 | // setupFiles: [], 124 | 125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 126 | // setupFilesAfterEnv: [], 127 | 128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 129 | snapshotSerializers: ['jest-serializer-vue'], 130 | 131 | // The test environment that will be used for testing 132 | testEnvironment: 'jsdom', 133 | 134 | // Options that will be passed to the testEnvironment 135 | // testEnvironmentOptions: {}, 136 | 137 | // Adds a location field to test results 138 | // testLocationInResults: false, 139 | 140 | // The glob patterns Jest uses to detect test files 141 | // testMatch: [ 142 | // "**/__tests__/**/*.[jt]s?(x)", 143 | // "**/?(*.)+(spec|test).[tj]s?(x)" 144 | // ], 145 | 146 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 147 | // testPathIgnorePatterns: [ 148 | // "/node_modules/" 149 | // ], 150 | 151 | // The regexp pattern or array of patterns that Jest uses to detect test files 152 | // testRegex: [], 153 | 154 | // This option allows the use of a custom results processor 155 | // testResultsProcessor: null, 156 | 157 | // This option allows use of a custom test runner 158 | // testRunner: "jasmine2", 159 | 160 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 161 | // testURL: "http://localhost", 162 | 163 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 164 | // timers: "real", 165 | 166 | // A map from regular expressions to paths to transformers 167 | transform: { 168 | '^.+\\.(ts|tsx)$': 'ts-jest', 169 | }, 170 | 171 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 172 | // transformIgnorePatterns: [ 173 | // "/node_modules/" 174 | // ], 175 | 176 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 177 | // unmockedModulePathPatterns: undefined, 178 | 179 | // Indicates whether each individual test should be reported during the run 180 | // verbose: null, 181 | 182 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 183 | // watchPathIgnorePatterns: [], 184 | 185 | // Whether to use watchman for file crawling 186 | // watchman: true, 187 | } 188 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-property-decorator", 3 | "version": "9.1.2", 4 | "description": "property decorators for Vue Component", 5 | "main": "lib/index.umd.js", 6 | "module": "lib/index.js", 7 | "keywords": [ 8 | "vue", 9 | "typescript", 10 | "decorator" 11 | ], 12 | "author": "kaorun343", 13 | "license": "MIT", 14 | "directories": { 15 | "tests": "tests" 16 | }, 17 | "scripts": { 18 | "build": "tsc -p ./src/tsconfig.json && rollup -c", 19 | "test": "jest" 20 | }, 21 | "files": [ 22 | "lib" 23 | ], 24 | "devDependencies": { 25 | "@types/jest": "^26.0.15", 26 | "@types/node": "^14.14.9", 27 | "@vue/test-utils": "^1.1.3", 28 | "jest": "^26.6.3", 29 | "jest-serializer-vue": "^2.0.2", 30 | "prettier": "^2.2.1", 31 | "reflect-metadata": "^0.1.13", 32 | "rollup": "^2.33.3", 33 | "ts-jest": "^26.4.4", 34 | "typescript": "^4.1.2", 35 | "vue": "^2.6.12", 36 | "vue-class-component": "^7.2.3", 37 | "vue-template-compiler": "^2.6.12" 38 | }, 39 | "typings": "./lib/index.d.ts", 40 | "dependencies": {}, 41 | "peerDependencies": { 42 | "vue": "*", 43 | "vue-class-component": "*" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "git+https://github.com/kaorun343/vue-property-decorator.git" 48 | }, 49 | "bugs": { 50 | "url": "https://github.com/kaorun343/vue-property-decorator/issues" 51 | }, 52 | "homepage": "https://github.com/kaorun343/vue-property-decorator#readme" 53 | } 54 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'lib/index.js', 3 | output: { 4 | file: 'lib/index.umd.js', 5 | format: 'umd', 6 | name: 'VuePropertyDecorator', 7 | globals: { 8 | vue: 'Vue', 9 | 'vue-class-component': 'VueClassComponent', 10 | }, 11 | exports: 'named', 12 | }, 13 | external: ['vue', 'vue-class-component', 'reflect-metadata'], 14 | } 15 | -------------------------------------------------------------------------------- /src/decorators/Emit.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | // Code copied from Vue/src/shared/util.js 4 | const hyphenateRE = /\B([A-Z])/g 5 | const hyphenate = (str: string) => str.replace(hyphenateRE, '-$1').toLowerCase() 6 | 7 | /** 8 | * decorator of an event-emitter function 9 | * @param event The name of the event 10 | * @return MethodDecorator 11 | */ 12 | export function Emit(event?: string) { 13 | return function (_target: Vue, propertyKey: string, descriptor: any) { 14 | const key = hyphenate(propertyKey) 15 | const original = descriptor.value 16 | descriptor.value = function emitter(...args: any[]) { 17 | const emit = (returnValue: any) => { 18 | const emitName = event || key 19 | 20 | if (returnValue === undefined) { 21 | if (args.length === 0) { 22 | this.$emit(emitName) 23 | } else if (args.length === 1) { 24 | this.$emit(emitName, args[0]) 25 | } else { 26 | this.$emit(emitName, ...args) 27 | } 28 | } else { 29 | args.unshift(returnValue) 30 | this.$emit(emitName, ...args) 31 | } 32 | } 33 | 34 | const returnValue: any = original.apply(this, args) 35 | 36 | if (isPromise(returnValue)) { 37 | returnValue.then(emit) 38 | } else { 39 | emit(returnValue) 40 | } 41 | 42 | return returnValue 43 | } 44 | } 45 | } 46 | 47 | function isPromise(obj: any): obj is Promise { 48 | return obj instanceof Promise || (obj && typeof obj.then === 'function') 49 | } 50 | -------------------------------------------------------------------------------- /src/decorators/Inject.ts: -------------------------------------------------------------------------------- 1 | import { createDecorator } from 'vue-class-component' 2 | import { InjectKey } from 'vue/types/options' 3 | 4 | export type InjectOptions = { from?: InjectKey; default?: any } 5 | /** 6 | * decorator of an inject 7 | * @param from key 8 | * @return PropertyDecorator 9 | */ 10 | 11 | export function Inject(options?: InjectOptions | InjectKey) { 12 | return createDecorator((componentOptions, key) => { 13 | if (typeof componentOptions.inject === 'undefined') { 14 | componentOptions.inject = {} 15 | } 16 | if (!Array.isArray(componentOptions.inject)) { 17 | componentOptions.inject[key] = options || key 18 | } 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/decorators/InjectReactive.ts: -------------------------------------------------------------------------------- 1 | import { createDecorator } from 'vue-class-component' 2 | import { InjectKey } from 'vue/types/options' 3 | import { reactiveInjectKey } from '../helpers/provideInject' 4 | import { InjectOptions } from './Inject' 5 | 6 | /** 7 | * decorator of a reactive inject 8 | * @param from key 9 | * @return PropertyDecorator 10 | */ 11 | 12 | export function InjectReactive(options?: InjectOptions | InjectKey) { 13 | return createDecorator((componentOptions, key) => { 14 | if (typeof componentOptions.inject === 'undefined') { 15 | componentOptions.inject = {} 16 | } 17 | if (!Array.isArray(componentOptions.inject)) { 18 | const fromKey = !!options ? (options as any).from || options : key 19 | const defaultVal = (!!options && (options as any).default) || undefined 20 | if (!componentOptions.computed) componentOptions.computed = {} 21 | componentOptions.computed![key] = function () { 22 | const obj = (this as any)[reactiveInjectKey] 23 | return obj ? obj[fromKey] : defaultVal 24 | } 25 | componentOptions.inject[reactiveInjectKey] = reactiveInjectKey 26 | } 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/decorators/Model.ts: -------------------------------------------------------------------------------- 1 | import Vue, { PropOptions } from 'vue' 2 | import { createDecorator } from 'vue-class-component' 3 | import { Constructor } from 'vue/types/options' 4 | import { applyMetadata } from '../helpers/metadata' 5 | 6 | /** 7 | * decorator of model 8 | * @param event event name 9 | * @param options options 10 | * @return PropertyDecorator 11 | */ 12 | export function Model( 13 | event?: string, 14 | options: PropOptions | Constructor[] | Constructor = {}, 15 | ) { 16 | return (target: Vue, key: string) => { 17 | applyMetadata(options, target, key) 18 | createDecorator((componentOptions, k) => { 19 | ;(componentOptions.props || ((componentOptions.props = {}) as any))[ 20 | k 21 | ] = options 22 | componentOptions.model = { prop: k, event: event || k } 23 | })(target, key) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/decorators/ModelSync.ts: -------------------------------------------------------------------------------- 1 | import Vue, { PropOptions } from 'vue' 2 | import { createDecorator } from 'vue-class-component' 3 | import { Constructor } from 'vue/types/options' 4 | import { applyMetadata } from '../helpers/metadata' 5 | 6 | /** 7 | * decorator of synced model and prop 8 | * @param propName the name to interface with from outside, must be different from decorated property 9 | * @param event event name 10 | * @param options options 11 | * @return PropertyDecorator 12 | */ 13 | export function ModelSync( 14 | propName: string, 15 | event?: string, 16 | options: PropOptions | Constructor[] | Constructor = {}, 17 | ) { 18 | return (target: Vue, key: string) => { 19 | applyMetadata(options, target, key) 20 | createDecorator((componentOptions, k) => { 21 | ;(componentOptions.props || ((componentOptions.props = {}) as any))[ 22 | propName 23 | ] = options 24 | componentOptions.model = { prop: propName, event: event || k } 25 | ;(componentOptions.computed || (componentOptions.computed = {}))[k] = { 26 | get() { 27 | return (this as any)[propName] 28 | }, 29 | set(value) { 30 | // @ts-ignore 31 | this.$emit(event, value) 32 | }, 33 | } 34 | })(target, key) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/decorators/Prop.ts: -------------------------------------------------------------------------------- 1 | import Vue, { PropOptions } from 'vue' 2 | import { createDecorator } from 'vue-class-component' 3 | import { Constructor } from 'vue/types/options' 4 | import { applyMetadata } from '../helpers/metadata' 5 | 6 | /** 7 | * decorator of a prop 8 | * @param options the options for the prop 9 | * @return PropertyDecorator | void 10 | */ 11 | export function Prop(options: PropOptions | Constructor[] | Constructor = {}) { 12 | return (target: Vue, key: string) => { 13 | applyMetadata(options, target, key) 14 | createDecorator((componentOptions, k) => { 15 | ;(componentOptions.props || ((componentOptions.props = {}) as any))[ 16 | k 17 | ] = options 18 | })(target, key) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/decorators/PropSync.ts: -------------------------------------------------------------------------------- 1 | import Vue, { PropOptions } from 'vue' 2 | import { createDecorator } from 'vue-class-component' 3 | import { Constructor } from 'vue/types/options' 4 | import { applyMetadata } from '../helpers/metadata' 5 | 6 | /** 7 | * decorator of a synced prop 8 | * @param propName the name to interface with from outside, must be different from decorated property 9 | * @param options the options for the synced prop 10 | * @return PropertyDecorator | void 11 | */ 12 | export function PropSync( 13 | propName: string, 14 | options: PropOptions | Constructor[] | Constructor = {}, 15 | ) { 16 | return (target: Vue, key: string) => { 17 | applyMetadata(options, target, key) 18 | createDecorator((componentOptions, k) => { 19 | ;(componentOptions.props || (componentOptions.props = {} as any))[ 20 | propName 21 | ] = options 22 | ;(componentOptions.computed || (componentOptions.computed = {}))[k] = { 23 | get() { 24 | return (this as any)[propName] 25 | }, 26 | set(this: Vue, value) { 27 | this.$emit(`update:${propName}`, value) 28 | }, 29 | } 30 | })(target, key) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/decorators/Provide.ts: -------------------------------------------------------------------------------- 1 | import { createDecorator } from 'vue-class-component' 2 | import { 3 | inheritInjected, 4 | needToProduceProvide, 5 | produceProvide, 6 | } from '../helpers/provideInject' 7 | 8 | /** 9 | * decorator of a provide 10 | * @param key key 11 | * @return PropertyDecorator | void 12 | */ 13 | 14 | export function Provide(key?: string | symbol) { 15 | return createDecorator((componentOptions, k) => { 16 | let provide: any = componentOptions.provide 17 | inheritInjected(componentOptions) 18 | if (needToProduceProvide(provide)) { 19 | provide = componentOptions.provide = produceProvide(provide) 20 | } 21 | provide.managed[k] = key || k 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/decorators/ProvideReactive.ts: -------------------------------------------------------------------------------- 1 | import { createDecorator } from 'vue-class-component' 2 | import { 3 | inheritInjected, 4 | needToProduceProvide, 5 | produceProvide, 6 | } from '../helpers/provideInject' 7 | 8 | /** 9 | * decorator of a reactive provide 10 | * @param key key 11 | * @return PropertyDecorator | void 12 | */ 13 | export function ProvideReactive(key?: string | symbol) { 14 | return createDecorator((componentOptions, k) => { 15 | let provide: any = componentOptions.provide 16 | inheritInjected(componentOptions) 17 | if (needToProduceProvide(provide)) { 18 | provide = componentOptions.provide = produceProvide(provide) 19 | } 20 | provide.managedReactive[k] = key || k 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/decorators/Ref.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { createDecorator } from 'vue-class-component' 3 | 4 | /** 5 | * decorator of a ref prop 6 | * @param refKey the ref key defined in template 7 | */ 8 | export function Ref(refKey?: string) { 9 | return createDecorator((options, key) => { 10 | options.computed = options.computed || {} 11 | options.computed[key] = { 12 | cache: false, 13 | get(this: Vue) { 14 | return this.$refs[refKey || key] 15 | }, 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/decorators/VModel.ts: -------------------------------------------------------------------------------- 1 | import Vue, { PropOptions } from 'vue' 2 | import { createDecorator } from 'vue-class-component' 3 | 4 | /** 5 | * decorator for capturings v-model binding to component 6 | * @param options the options for the prop 7 | */ 8 | export function VModel(options: PropOptions = {}) { 9 | const valueKey: string = 'value' 10 | return createDecorator((componentOptions, key) => { 11 | ;(componentOptions.props || ((componentOptions.props = {}) as any))[ 12 | valueKey 13 | ] = options 14 | ;(componentOptions.computed || (componentOptions.computed = {}))[key] = { 15 | get() { 16 | return (this as any)[valueKey] 17 | }, 18 | set(this: Vue, value: any) { 19 | this.$emit('input', value) 20 | }, 21 | } 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/decorators/Watch.ts: -------------------------------------------------------------------------------- 1 | import { WatchOptions } from 'vue' 2 | import { createDecorator } from 'vue-class-component' 3 | 4 | /** 5 | * decorator of a watch function 6 | * @param path the path or the expression to observe 7 | * @param watchOptions 8 | */ 9 | export function Watch(path: string, watchOptions: WatchOptions = {}) { 10 | return createDecorator((componentOptions, handler) => { 11 | componentOptions.watch ||= Object.create(null) 12 | const watch: any = componentOptions.watch 13 | if (typeof watch[path] === 'object' && !Array.isArray(watch[path])) { 14 | watch[path] = [watch[path]] 15 | } else if (typeof watch[path] === 'undefined') { 16 | watch[path] = [] 17 | } 18 | 19 | watch[path].push({ handler, ...watchOptions }) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/helpers/metadata.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import Vue, { PropOptions } from 'vue' 3 | import { Constructor } from 'vue/types/options' 4 | 5 | /** @see {@link https://github.com/vuejs/vue-class-component/blob/master/src/reflect.ts} */ 6 | const reflectMetadataIsSupported = 7 | typeof Reflect !== 'undefined' && typeof Reflect.getMetadata !== 'undefined' 8 | 9 | export function applyMetadata( 10 | options: PropOptions | Constructor[] | Constructor, 11 | target: Vue, 12 | key: string, 13 | ) { 14 | if (reflectMetadataIsSupported) { 15 | if ( 16 | !Array.isArray(options) && 17 | typeof options !== 'function' && 18 | !options.hasOwnProperty('type') && 19 | typeof options.type === 'undefined' 20 | ) { 21 | const type = Reflect.getMetadata('design:type', target, key) 22 | if (type !== Object) { 23 | options.type = type 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/helpers/provideInject.ts: -------------------------------------------------------------------------------- 1 | import Vue, { ComponentOptions } from 'vue' 2 | 3 | export function needToProduceProvide(original: any) { 4 | return ( 5 | typeof original !== 'function' || 6 | (!original.managed && !original.managedReactive) 7 | ) 8 | } 9 | 10 | interface ProvideObj { 11 | managed?: { [k: string]: any } 12 | managedReactive?: { [k: string]: any } 13 | } 14 | 15 | type ProvideFunc = ((this: any) => Object) & ProvideObj 16 | 17 | export function produceProvide(original: any) { 18 | let provide: ProvideFunc = function (this: any) { 19 | let rv = typeof original === 'function' ? original.call(this) : original 20 | rv = Object.create(rv || null) 21 | // set reactive services (propagates previous services if necessary) 22 | rv[reactiveInjectKey] = Object.create(this[reactiveInjectKey] || {}) 23 | for (let i in provide.managed) { 24 | rv[provide.managed[i]] = this[i] 25 | } 26 | for (let i in provide.managedReactive) { 27 | rv[provide.managedReactive[i]] = this[i] // Duplicates the behavior of `@Provide` 28 | Object.defineProperty(rv[reactiveInjectKey], provide.managedReactive[i], { 29 | enumerable: true, 30 | configurable: true, 31 | get: () => this[i], 32 | }) 33 | } 34 | return rv 35 | } 36 | provide.managed = {} 37 | provide.managedReactive = {} 38 | return provide 39 | } 40 | 41 | /** Used for keying reactive provide/inject properties */ 42 | export const reactiveInjectKey = '__reactiveInject__' 43 | 44 | export function inheritInjected(componentOptions: ComponentOptions) { 45 | // inject parent reactive services (if any) 46 | if (!Array.isArray(componentOptions.inject)) { 47 | componentOptions.inject = componentOptions.inject || {} 48 | componentOptions.inject[reactiveInjectKey] = { 49 | from: reactiveInjectKey, 50 | default: {}, 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** vue-property-decorator verson 9.1.2 MIT LICENSE copyright 2020 kaorun343 */ 2 | /// 3 | import Vue from 'vue' 4 | import Component, { mixins } from 'vue-class-component' 5 | 6 | export { Component, Vue, mixins as Mixins } 7 | 8 | export { Emit } from './decorators/Emit' 9 | export { Inject } from './decorators/Inject' 10 | export { InjectReactive } from './decorators/InjectReactive' 11 | export { Model } from './decorators/Model' 12 | export { ModelSync } from './decorators/ModelSync' 13 | export { Prop } from './decorators/Prop' 14 | export { PropSync } from './decorators/PropSync' 15 | export { Provide } from './decorators/Provide' 16 | export { ProvideReactive } from './decorators/ProvideReactive' 17 | export { Ref } from './decorators/Ref' 18 | export { VModel } from './decorators/VModel' 19 | export { Watch } from './decorators/Watch' 20 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true /* Generates corresponding '.d.ts' file. */, 5 | "outDir": "../lib" /* Redirect output structure to the directory. */, 6 | "rootDir": "./" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 7 | // "typeRoots": [], /* List of folders to include type definitions from. */ 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/decorators/Emit.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount, Wrapper } from '@vue/test-utils' 2 | import Vue, { CreateElement } from 'vue' 3 | import Component from 'vue-class-component' 4 | import { Emit } from '../../src/decorators/Emit' 5 | 6 | const mockFn = jest.fn() 7 | 8 | describe(Emit, () => { 9 | describe('when event name is given', () => { 10 | @Component 11 | class ChildComponent extends Vue { 12 | count = 0 13 | 14 | @Emit('reset') resetCount() { 15 | this.count = 0 16 | } 17 | 18 | render(h: CreateElement) { 19 | return h('div') 20 | } 21 | } 22 | 23 | @Component 24 | class ParentComponent extends Vue { 25 | $refs!: { child: ChildComponent } 26 | render(h: CreateElement) { 27 | return h(ChildComponent, { on: { reset: mockFn }, ref: 'child' }) 28 | } 29 | } 30 | 31 | let wrapper: Wrapper 32 | 33 | beforeEach(() => { 34 | wrapper = mount(ParentComponent) 35 | wrapper.vm.$refs.child.resetCount() 36 | }) 37 | 38 | test('call $emit method', () => { 39 | expect(mockFn).toHaveBeenCalled() 40 | }) 41 | 42 | test('emit event with given name', () => { 43 | expect(mockFn).toHaveBeenCalledWith() 44 | }) 45 | }) 46 | 47 | describe('when arguments are given', () => { 48 | @Component 49 | class ChildComponent extends Vue { 50 | count = 0 51 | 52 | @Emit() increment(n1: number, n2: number) { 53 | this.count += n1 + n2 54 | } 55 | 56 | render(h: CreateElement) { 57 | return h('div') 58 | } 59 | } 60 | 61 | @Component 62 | class ParentComponent extends Vue { 63 | $refs!: { child: ChildComponent } 64 | render(h: CreateElement) { 65 | return h(ChildComponent, { on: { increment: mockFn }, ref: 'child' }) 66 | } 67 | } 68 | 69 | let wrapper: Wrapper 70 | 71 | const NEW_VALUE1 = 30 72 | const NEW_VALUE2 = 40 73 | 74 | beforeEach(() => { 75 | wrapper = mount(ParentComponent) 76 | wrapper.vm.$refs.child.increment(NEW_VALUE1, NEW_VALUE2) 77 | }) 78 | 79 | test('emit event with multiple arguments', () => { 80 | expect(mockFn).toHaveBeenCalledWith(NEW_VALUE1, NEW_VALUE2) 81 | }) 82 | }) 83 | 84 | describe('when the value is returned and multiple arguments is given', () => { 85 | @Component 86 | class ChildComponent extends Vue { 87 | count = 0 88 | 89 | @Emit() increment(n1: number, n2: number) { 90 | return n1 + n2 91 | } 92 | 93 | render(h: CreateElement) { 94 | return h('div') 95 | } 96 | } 97 | 98 | @Component 99 | class ParentComponent extends Vue { 100 | $refs!: { child: ChildComponent } 101 | render(h: CreateElement) { 102 | return h(ChildComponent, { on: { increment: mockFn }, ref: 'child' }) 103 | } 104 | } 105 | 106 | let wrapper: Wrapper 107 | 108 | const NEW_VALUE1 = 30 109 | const NEW_VALUE2 = 40 110 | 111 | beforeEach(() => { 112 | wrapper = mount(ParentComponent) 113 | wrapper.vm.$refs.child.increment(NEW_VALUE1, NEW_VALUE2) 114 | }) 115 | 116 | test('emit event with multiple arguments', () => { 117 | expect(mockFn).toHaveBeenCalledWith( 118 | NEW_VALUE1 + NEW_VALUE2, 119 | NEW_VALUE1, 120 | NEW_VALUE2, 121 | ) 122 | }) 123 | }) 124 | 125 | describe('when promise has been returned', () => { 126 | const VALUE = 10 127 | 128 | @Component 129 | class ChildComponent extends Vue { 130 | @Emit() promise() { 131 | return Promise.resolve(VALUE) 132 | } 133 | 134 | render(h: CreateElement) { 135 | return h('div') 136 | } 137 | } 138 | 139 | @Component 140 | class ParentComponent extends Vue { 141 | $refs!: { child: ChildComponent } 142 | render(h: CreateElement) { 143 | return h(ChildComponent, { on: { promise: mockFn }, ref: 'child' }) 144 | } 145 | } 146 | 147 | let wrapper: Wrapper 148 | 149 | beforeEach(async () => { 150 | wrapper = mount(ParentComponent) 151 | await wrapper.vm.$refs.child.promise() 152 | }) 153 | 154 | test('call $emit method', () => { 155 | expect(mockFn).toHaveBeenCalled() 156 | }) 157 | 158 | test('emit even with resolved value', () => { 159 | expect(mockFn).toHaveBeenCalledWith(VALUE) 160 | }) 161 | }) 162 | }) 163 | -------------------------------------------------------------------------------- /tests/decorators/Inject.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Component from 'vue-class-component' 3 | import { Inject } from '../../src/decorators/Inject' 4 | 5 | describe(Inject, () => { 6 | describe('when inject key is given', () => { 7 | const injectKey = Symbol() 8 | const value = 'PROVIDED_VALUE' 9 | 10 | @Component({ 11 | provide() { 12 | return { 13 | [injectKey]: value, 14 | } 15 | }, 16 | }) 17 | class ParentComponent extends Vue {} 18 | 19 | @Component 20 | class ChildComponent extends Vue { 21 | @Inject(injectKey) foo!: string 22 | } 23 | 24 | const component = new ChildComponent({ parent: new ParentComponent() }) 25 | 26 | test('injects provided value', () => { 27 | expect(component.foo).toBe(value) 28 | }) 29 | }) 30 | 31 | describe('when inject key is not given', () => { 32 | const propertyName = 'PROPERTY_NAME' 33 | const value = 'PROVIDED_VALUE' 34 | 35 | @Component({ 36 | provide() { 37 | return { 38 | [propertyName]: value, 39 | } 40 | }, 41 | }) 42 | class ParentComponent extends Vue {} 43 | 44 | @Component 45 | class ChildComponent extends Vue { 46 | @Inject() [propertyName]!: string 47 | } 48 | 49 | const component = new ChildComponent({ parent: new ParentComponent() }) 50 | 51 | test('injects provided value', () => { 52 | expect(component[propertyName]).toBe(value) 53 | }) 54 | }) 55 | 56 | describe('when default value is given', () => { 57 | const value = 'DEFAULT_VALUE' 58 | 59 | @Component 60 | class ChildComponent extends Vue { 61 | @Inject({ from: 'notFound', default: value }) optional!: string 62 | } 63 | 64 | const component = new ChildComponent() 65 | 66 | test('injects default value', () => { 67 | expect(component.optional).toBe(value) 68 | }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /tests/decorators/InjectReactive.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Component from 'vue-class-component' 3 | import { InjectReactive } from '../../src/decorators/InjectReactive' 4 | import { ProvideReactive } from '../../src/decorators/ProvideReactive' 5 | 6 | describe(InjectReactive, () => { 7 | describe('when inject key is given', () => { 8 | const injectKey = Symbol() 9 | const value = 'PROVIDED_VALUE' 10 | 11 | @Component 12 | class ParentComponent extends Vue { 13 | @ProvideReactive(injectKey) baz = value 14 | } 15 | 16 | const parent = new ParentComponent() 17 | 18 | @Component 19 | class ChildComponent extends Vue { 20 | @InjectReactive(injectKey) foo!: string 21 | } 22 | 23 | @Component 24 | class GrandChildComponent extends Vue { 25 | @InjectReactive(injectKey) foo!: string 26 | } 27 | 28 | const child = new ChildComponent({ parent }) 29 | const grandChild = new GrandChildComponent({ parent: child }) 30 | 31 | test('injects provided value', () => { 32 | expect(child.foo).toBe(value) 33 | expect(grandChild.foo).toBe(value) 34 | }) 35 | 36 | describe('when injected value is changed', () => { 37 | const updatedValue = 'UPDATED_PROVIDED_VALUE' 38 | 39 | beforeAll(() => { 40 | parent.baz = updatedValue 41 | }) 42 | 43 | test('reflects updated value', () => { 44 | expect(child.foo).toBe(updatedValue) 45 | expect(grandChild.foo).toBe(updatedValue) 46 | }) 47 | }) 48 | }) 49 | 50 | describe('when inject key is not given', () => { 51 | const propertyName = 'PROPERTY_NAME' 52 | const value = 'PROVIDED_VALUE' 53 | 54 | @Component 55 | class ParentComponent extends Vue { 56 | @ProvideReactive() [propertyName] = value 57 | } 58 | 59 | const parent = new ParentComponent() 60 | 61 | @Component 62 | class ChildComponent extends Vue { 63 | @InjectReactive() [propertyName]: string 64 | } 65 | 66 | @Component 67 | class GrandChildComponent extends Vue { 68 | @InjectReactive() [propertyName]!: string 69 | } 70 | 71 | const child = new ChildComponent({ parent }) 72 | const grandChild = new GrandChildComponent({ parent: child }) 73 | 74 | test('injects provided value', () => { 75 | expect(child[propertyName]).toBe(value) 76 | expect(grandChild[propertyName]).toBe(value) 77 | }) 78 | }) 79 | 80 | describe('when default value is given', () => { 81 | const value = 'DEFAULT_VALUE' 82 | 83 | @Component 84 | class ChildComponent extends Vue { 85 | @InjectReactive({ from: 'notFound', default: value }) optional!: string 86 | } 87 | 88 | @Component 89 | class GrandChildComponent extends Vue { 90 | @InjectReactive({ from: 'notFound', default: value }) optional!: string 91 | } 92 | 93 | const child = new ChildComponent({ parent: new Vue() }) 94 | const grandChild = new GrandChildComponent({ parent: child }) 95 | 96 | test('injects default value', () => { 97 | expect(child.optional).toBe(value) 98 | expect(grandChild.optional).toBe(value) 99 | }) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /tests/decorators/Model.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Component from 'vue-class-component' 3 | import { Model } from '../../src/decorators/Model' 4 | 5 | describe(Model, () => { 6 | @Component 7 | class TestComponent extends Vue { 8 | @Model('change', Boolean) checked!: boolean 9 | } 10 | 11 | const { $options } = new TestComponent() 12 | 13 | test('define model option correctly', () => { 14 | expect($options.model).toEqual({ prop: 'checked', event: 'change' }) 15 | }) 16 | 17 | test('define props option correctly', () => { 18 | const props = ($options.props as any) as Record 19 | expect(props!['checked']).toEqual({ type: Boolean }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /tests/decorators/ModelSync.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import Vue from 'vue' 3 | import Component from 'vue-class-component' 4 | import { ModelSync } from '../../src/decorators/ModelSync' 5 | 6 | const mockFunction = jest.fn() 7 | 8 | describe(ModelSync, () => { 9 | const eventName = 'change' 10 | const propertyName = 'checked' 11 | const accessorName = 'chackedValue' 12 | @Component 13 | class TestComponent extends Vue { 14 | @ModelSync(propertyName, eventName, Boolean) [accessorName]!: boolean 15 | 16 | changeChecked(newValue: boolean) { 17 | this[accessorName] = newValue 18 | } 19 | } 20 | 21 | const initialValue = false 22 | let component: TestComponent 23 | 24 | beforeEach(() => { 25 | component = new TestComponent({ 26 | propsData: { [propertyName]: initialValue }, 27 | }) 28 | component.$emit = mockFunction 29 | }) 30 | 31 | test('define model option correctly', () => { 32 | expect(component.$options.model).toEqual({ 33 | prop: propertyName, 34 | event: eventName, 35 | }) 36 | }) 37 | 38 | test('define props option correctly', () => { 39 | const props = (component.$options.props as any) as Record 40 | expect(props![propertyName]).toEqual({ type: Boolean }) 41 | }) 42 | 43 | test('component recieves prop', () => { 44 | expect(component[accessorName]).toBe(initialValue) 45 | }) 46 | 47 | describe('when props has been changed', () => { 48 | const newValue = true 49 | 50 | beforeEach(() => { 51 | component.changeChecked(newValue) 52 | }) 53 | 54 | test('calls $emit method', () => { 55 | expect(mockFunction).toHaveBeenCalled() 56 | }) 57 | 58 | test('emits event with event name', () => { 59 | expect(mockFunction.mock.calls[0][0]).toBe(eventName) 60 | }) 61 | 62 | test('emits event with new value', () => { 63 | expect(mockFunction.mock.calls[0][1]).toBe(newValue) 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /tests/decorators/Prop.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import Vue from 'vue' 3 | import Component from 'vue-class-component' 4 | import { Prop } from '../../src/decorators/Prop' 5 | 6 | describe(Prop, () => { 7 | describe('when constructor is given', () => { 8 | const propertyName = 'PROPERTY_NAME' 9 | 10 | @Component 11 | class Test extends Vue { 12 | @Prop(Number) [propertyName]!: number 13 | } 14 | 15 | const value = 10 16 | const component = new Test({ propsData: { [propertyName]: value } }) 17 | 18 | test('defines prop option', () => { 19 | const props = component.$options.props as any 20 | expect(props[propertyName]).toEqual({ type: Number }) 21 | }) 22 | 23 | test('component recieves prop', () => { 24 | expect(component[propertyName]).toBe(value) 25 | }) 26 | }) 27 | 28 | describe('when default value is given', () => { 29 | const propertyName = 'PROPERTY_NAME' 30 | const value = 'DEFAULT_VALUE' 31 | 32 | @Component 33 | class Test extends Vue { 34 | @Prop({ default: value }) [propertyName]!: string 35 | } 36 | 37 | const component = new Test() 38 | 39 | test('defines prop option', () => { 40 | const props = component.$options.props as any 41 | expect(props[propertyName]).toEqual({ type: String, default: value }) 42 | }) 43 | 44 | test('component uses default value', () => { 45 | expect(component[propertyName]).toBe(value) 46 | }) 47 | }) 48 | 49 | describe('when no value is given', () => { 50 | const propertyName = 'PROPERTY_NAME' 51 | 52 | @Component 53 | class Test extends Vue { 54 | @Prop() [propertyName]!: boolean 55 | } 56 | 57 | const component = new Test() 58 | 59 | test('defines prop option', () => { 60 | const props = component.$options.props as any 61 | expect(props[propertyName]).toEqual({ type: Boolean }) 62 | }) 63 | }) 64 | 65 | describe('Boolean type', () => { 66 | describe('when type is not given', () => { 67 | @Component 68 | class Test extends Vue { 69 | @Prop() target!: boolean 70 | } 71 | 72 | describe('when prop is given', () => { 73 | const component = new Test({ propsData: { target: true } }) 74 | 75 | it('returns true', () => { 76 | expect(component.target).toBe(true) 77 | }) 78 | }) 79 | 80 | describe('when prop is not given', () => { 81 | const component = new Test() 82 | 83 | it('returns false', () => { 84 | expect(component.target).toBe(false) 85 | }) 86 | }) 87 | }) 88 | 89 | describe('when type is undefined', () => { 90 | @Component 91 | class Test extends Vue { 92 | @Prop({ type: undefined }) target!: boolean 93 | } 94 | 95 | describe('when prop is given', () => { 96 | const component = new Test({ propsData: { target: true } }) 97 | 98 | it('returns true', () => { 99 | expect(component.target).toBe(true) 100 | }) 101 | }) 102 | 103 | describe('when prop is not given', () => { 104 | const component = new Test() 105 | 106 | it('returns undefined', () => { 107 | expect(component.target).toBe(undefined) 108 | }) 109 | }) 110 | }) 111 | 112 | describe('when type is Boolean', () => { 113 | @Component 114 | class Test extends Vue { 115 | @Prop({ type: Boolean }) target!: boolean 116 | } 117 | 118 | describe('when prop is given', () => { 119 | const component = new Test({ propsData: { target: true } }) 120 | 121 | it('returns true', () => { 122 | expect(component.target).toBe(true) 123 | }) 124 | }) 125 | 126 | describe('when prop is not given', () => { 127 | const component = new Test() 128 | 129 | it('returns false', () => { 130 | expect(component.target).toBe(false) 131 | }) 132 | }) 133 | }) 134 | }) 135 | }) 136 | -------------------------------------------------------------------------------- /tests/decorators/PropSync.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import Vue from 'vue' 3 | import Component from 'vue-class-component' 4 | import { PropSync } from '../../src/decorators/PropSync' 5 | 6 | describe(PropSync, () => { 7 | const propertyName = 'PROPERTY_NAME' 8 | const accessorName = 'GETTER_NAME' 9 | 10 | @Component 11 | class Test extends Vue { 12 | @PropSync(propertyName) [accessorName]!: string 13 | 14 | changeName(newName: string) { 15 | this[accessorName] = newName 16 | } 17 | } 18 | 19 | const value = 'John' 20 | let component: Test 21 | const mockFn = jest.fn() 22 | 23 | beforeEach(() => { 24 | component = new Test({ propsData: { [propertyName]: value } }) 25 | component.$emit = mockFn 26 | }) 27 | 28 | test('defines prop option', () => { 29 | const props = component.$options.props as any 30 | expect(props[propertyName]).toEqual({ type: String }) 31 | }) 32 | 33 | test('component recieves prop', () => { 34 | expect(component[accessorName]).toBe(value) 35 | }) 36 | 37 | describe('when prop has been changed', () => { 38 | const newValue = 'Ola' 39 | 40 | beforeEach(() => { 41 | component.changeName(newValue) 42 | }) 43 | 44 | test('calls $emit method', () => { 45 | expect(mockFn).toHaveBeenCalled() 46 | }) 47 | 48 | test('emits event with event name', () => { 49 | expect(mockFn.mock.calls[0][0]).toBe(`update:${propertyName}`) 50 | }) 51 | 52 | test('emits event with new value', () => { 53 | expect(mockFn.mock.calls[0][1]).toBe(newValue) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /tests/decorators/Provide.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Component from 'vue-class-component' 3 | import { Inject } from '../../src/decorators/Inject' 4 | import { Provide } from '../../src/decorators/Provide' 5 | 6 | describe(Provide, () => { 7 | describe('when key is not given', () => { 8 | const value = 'VALUE' 9 | 10 | @Component 11 | class ParentComponent extends Vue { 12 | @Provide() one = value 13 | } 14 | 15 | @Component 16 | class ChildComponent extends Vue { 17 | @Inject() one!: string 18 | } 19 | 20 | const component = new ChildComponent({ parent: new ParentComponent() }) 21 | 22 | test('provides value', () => { 23 | expect(component.one).toBe(value) 24 | }) 25 | }) 26 | 27 | describe('does not override parent dependencies', () => { 28 | @Component 29 | class ParentComponent extends Vue { 30 | @Provide() root = 'root' 31 | } 32 | @Component 33 | class NodeComponent extends Vue { 34 | @Provide() node = 'node' 35 | } 36 | @Component 37 | class ChildComponent extends Vue { 38 | @Inject() root!: string 39 | @Inject() node!: string 40 | } 41 | 42 | const parent = new ParentComponent() 43 | const node = new NodeComponent({ parent }) 44 | const component = new ChildComponent({ parent: node }) 45 | 46 | test('provides value', () => { 47 | expect(component.node).toBe('node') 48 | expect(component.root).toBe('root') 49 | }) 50 | }) 51 | 52 | describe('when key is given', () => { 53 | const key = 'KEY' 54 | const value = 'VALUE' 55 | 56 | @Component 57 | class ParentComponent extends Vue { 58 | @Provide(key) eleven = value 59 | } 60 | 61 | @Component 62 | class ChildComponent extends Vue { 63 | @Inject(key) one!: string 64 | } 65 | 66 | const component = new ChildComponent({ parent: new ParentComponent() }) 67 | 68 | test('provides value', () => { 69 | expect(component.one).toBe(value) 70 | }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /tests/decorators/ProvideReactive.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Component from 'vue-class-component' 3 | import { Inject } from '../../src/decorators/Inject' 4 | import { InjectReactive } from '../../src/decorators/InjectReactive' 5 | import { Provide } from '../../src/decorators/Provide' 6 | import { ProvideReactive } from '../../src/decorators/ProvideReactive' 7 | 8 | describe(ProvideReactive, () => { 9 | describe('when key is not given', () => { 10 | const value = 'VALUE' 11 | 12 | @Component 13 | class ParentComponent extends Vue { 14 | @ProvideReactive() one = value 15 | } 16 | 17 | @Component 18 | class ChildComponent extends Vue { 19 | @InjectReactive() one!: string 20 | } 21 | 22 | const parent = new ParentComponent() 23 | const component = new ChildComponent({ parent }) 24 | 25 | test('provides value', () => { 26 | expect(component.one).toBe(value) 27 | }) 28 | 29 | describe('when changed', () => { 30 | const newValue = 'NEW VALUE' 31 | 32 | beforeAll(() => { 33 | parent.one = newValue 34 | }) 35 | 36 | test('reflects updates', () => { 37 | expect(component.one).toBe(newValue) 38 | }) 39 | }) 40 | }) 41 | 42 | describe('is compatible with @Provide()', () => { 43 | @Component 44 | class ParentComponent extends Vue { 45 | @Provide() first = 'whatever' 46 | @ProvideReactive() one = 'one' 47 | } 48 | @Component 49 | class ChildComponent extends Vue { 50 | @InjectReactive() one!: string 51 | } 52 | 53 | const parent = new ParentComponent() 54 | const component = new ChildComponent({ parent }) 55 | 56 | test('provides value', () => { 57 | expect(component.one).toBe('one') 58 | }) 59 | }) 60 | 61 | describe('can @Inject() dependency provided using @ProvideReactive()', () => { 62 | @Component 63 | class ParentComponent extends Vue { 64 | @ProvideReactive() one = 'one' 65 | } 66 | @Component 67 | class ChildComponent extends Vue { 68 | @Inject() one!: string 69 | } 70 | 71 | const parent = new ParentComponent() 72 | const component = new ChildComponent({ parent }) 73 | 74 | test('provides value', () => { 75 | expect(component.one).toBe('one') 76 | }) 77 | }) 78 | 79 | describe('does not override parent reactive dependencies', () => { 80 | @Component 81 | class ParentComponent extends Vue { 82 | @ProvideReactive() root = 'root' 83 | } 84 | @Component 85 | class NodeComponent extends Vue { 86 | @ProvideReactive() node = 'node' 87 | } 88 | @Component 89 | class ChildComponent extends Vue { 90 | @InjectReactive() root!: string 91 | @InjectReactive() node!: string 92 | } 93 | 94 | const parent = new ParentComponent() 95 | const node = new NodeComponent({ parent }) 96 | const component = new ChildComponent({ parent: node }) 97 | 98 | test('provides value', () => { 99 | expect(component.node).toBe('node') 100 | expect(component.root).toBe('root') // <== this one used to throw 101 | 102 | // check that they update correctly 103 | parent.root = 'new root' 104 | node.node = 'new node' 105 | expect(component.root).toBe('new root') 106 | expect(component.node).toBe('new node') 107 | }) 108 | }) 109 | 110 | describe('when key is given', () => { 111 | const key = 'KEY' 112 | const value = 'VALUE' 113 | 114 | @Component 115 | class ParentComponent extends Vue { 116 | @ProvideReactive(key) eleven = value 117 | } 118 | 119 | @Component 120 | class ChildComponent extends Vue { 121 | @InjectReactive(key) one!: string 122 | } 123 | 124 | const parent = new ParentComponent() 125 | const component = new ChildComponent({ parent }) 126 | 127 | test('provides value', () => { 128 | expect(component.one).toBe(value) 129 | }) 130 | 131 | describe('when changed', () => { 132 | const newValue = 'NEW VALUE' 133 | 134 | beforeAll(() => { 135 | parent.eleven = newValue 136 | }) 137 | 138 | test('reflects updates', () => { 139 | expect(component.one).toBe(newValue) 140 | }) 141 | }) 142 | 143 | describe('multiple provider chains', () => { 144 | const key = 'KEY' 145 | const value1 = 'VALUE_1' 146 | const value2 = 'VALUE_2' 147 | 148 | @Component 149 | class ParentChain1 extends Vue { 150 | @ProvideReactive(key) provided = value1 151 | } 152 | 153 | @Component 154 | class ChildChain1 extends Vue { 155 | @InjectReactive(key) injected!: string 156 | } 157 | 158 | @Component 159 | class ParentChain2 extends Vue { 160 | @ProvideReactive(key) provided = value2 161 | } 162 | 163 | @Component 164 | class ChildChain2 extends Vue { 165 | @InjectReactive(key) injected!: string 166 | } 167 | const parent1 = new ParentChain1() 168 | const child1 = new ChildChain1({ parent: parent1 }) 169 | const parent2 = new ParentChain2() 170 | const child2 = new ChildChain2({ parent: parent2 }) 171 | 172 | test('respect values in chains', () => { 173 | expect(child1.injected).toBe(value1) 174 | expect(child2.injected).toBe(value2) 175 | }) 176 | }) 177 | 178 | describe('middle component participating in provider chain', () => { 179 | const rootKey = Symbol() 180 | const middleKey = Symbol() 181 | const rootValue = 'ROOT_VALUE' 182 | const middleValue = 'MIDDLE_VALUE' 183 | 184 | @Component 185 | class RootComponent extends Vue { 186 | @ProvideReactive(rootKey) baz = rootValue 187 | } 188 | 189 | const root = new RootComponent() 190 | 191 | @Component 192 | class MiddleComponent extends Vue { 193 | @ProvideReactive(middleKey) foo = middleValue 194 | } 195 | 196 | @Component 197 | class ChildComponent extends Vue { 198 | @InjectReactive(rootKey) baz!: string 199 | @InjectReactive(middleKey) foo!: string 200 | } 201 | 202 | const middle = new MiddleComponent({ parent: root }) 203 | const child = new ChildComponent({ parent: middle }) 204 | 205 | test('provided values from the chain', () => { 206 | expect(child.baz).toBe(rootValue) 207 | expect(child.foo).toBe(middleValue) 208 | }) 209 | }) 210 | }) 211 | }) 212 | -------------------------------------------------------------------------------- /tests/decorators/Ref.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Component from 'vue-class-component' 3 | import { Ref } from '../../src/decorators/Ref' 4 | 5 | describe(Ref, () => { 6 | describe('when key is not given', () => { 7 | const propertyName = 'PROPERTY_NAME' 8 | 9 | @Component 10 | class Test extends Vue { 11 | @Ref() [propertyName]: any 12 | } 13 | 14 | const component = new Test() 15 | const ref = 'REFERENCE' as any 16 | component.$refs[propertyName] = ref 17 | 18 | test('defines computed option', () => { 19 | const computed = component.$options.computed as any 20 | expect(computed[propertyName].cache).toBe(false) 21 | expect(computed[propertyName].get).toBeInstanceOf(Function) 22 | }) 23 | 24 | test('computed property returns ref object', () => { 25 | expect(component[propertyName]).toBe(ref) 26 | }) 27 | }) 28 | 29 | describe('when key is given', () => { 30 | const referenceName = 'REFERENCE_NAME' 31 | const propertyName = 'PROPERTY_NAME' 32 | 33 | @Component 34 | class Test extends Vue { 35 | @Ref(referenceName) [propertyName]: any 36 | } 37 | 38 | const component = new Test() 39 | const ref = 'REFERENCE' as any 40 | component.$refs[referenceName] = ref 41 | 42 | test('defines computed option', () => { 43 | const computed = component.$options.computed as any 44 | expect(computed[propertyName].cache).toBe(false) 45 | expect(computed[propertyName].get).toBeInstanceOf(Function) 46 | }) 47 | 48 | test('computed property returns ref object', () => { 49 | expect(component[propertyName]).toBe(ref) 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /tests/decorators/VModel.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Component from 'vue-class-component' 3 | import { VModel } from '../../src/decorators/VModel' 4 | 5 | describe(VModel, () => { 6 | @Component 7 | class Test extends Vue { 8 | @VModel({ type: String }) name!: string 9 | } 10 | 11 | it('returns prop value', () => { 12 | const value = 'NAME' 13 | const component = new Test({ propsData: { value } }) 14 | expect(component.name).toBe(value) 15 | }) 16 | 17 | it('emits input event', () => { 18 | const component = new Test() 19 | jest.spyOn(component, '$emit') 20 | const name = 'NEW NAME' 21 | component.name = name 22 | expect(component.$emit).toHaveBeenCalledWith('input', name) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /tests/decorators/Watch.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount, Wrapper } from '@vue/test-utils' 2 | import Vue, { CreateElement } from 'vue' 3 | import Component from 'vue-class-component' 4 | import { Watch } from '../../src/decorators/Watch' 5 | 6 | describe(Watch, () => { 7 | const onChange = jest.fn() 8 | const WATCH_PATH = 'WATCH_PATH' 9 | const INITIAL_VALUE = 'INITIAL_VALUE' 10 | const UPDATED_VALUE = 'UPDATED_VALUE' 11 | 12 | describe('without watch options', () => { 13 | @Component 14 | class MyComponent extends Vue { 15 | [WATCH_PATH] = INITIAL_VALUE 16 | 17 | @Watch(WATCH_PATH) 18 | onChange(value: string, oldValue: string) { 19 | onChange(value, oldValue) 20 | } 21 | 22 | @Watch(WATCH_PATH) 23 | onChangeAnother() {} 24 | 25 | render(h: CreateElement) { 26 | return h('div') 27 | } 28 | } 29 | 30 | let wrapper: Wrapper 31 | 32 | beforeEach(() => { 33 | wrapper = mount(MyComponent) 34 | }) 35 | 36 | it('sets options correctly', () => { 37 | expect(wrapper.vm.$options.watch?.[WATCH_PATH]).toMatchInlineSnapshot(` 38 | Array [ 39 | Object { 40 | "handler": "onChange", 41 | "user": true, 42 | }, 43 | Object { 44 | "handler": "onChangeAnother", 45 | "user": true, 46 | }, 47 | ] 48 | `) 49 | }) 50 | 51 | it('does not call on mounted', () => { 52 | expect(onChange).toHaveBeenCalledTimes(0) 53 | }) 54 | 55 | it('calls onChange after value is changed', async () => { 56 | wrapper.vm[WATCH_PATH] = UPDATED_VALUE 57 | await wrapper.vm.$nextTick() 58 | expect(onChange).toHaveBeenCalledTimes(1) 59 | }) 60 | 61 | it('calls onChange with new value and old value', async () => { 62 | wrapper.vm[WATCH_PATH] = UPDATED_VALUE 63 | await wrapper.vm.$nextTick() 64 | expect(onChange).toHaveBeenCalledWith(UPDATED_VALUE, INITIAL_VALUE) 65 | }) 66 | }) 67 | 68 | describe('with watch options', () => { 69 | @Component 70 | class MyComponent extends Vue { 71 | [WATCH_PATH] = INITIAL_VALUE 72 | 73 | @Watch(WATCH_PATH, { deep: true, immediate: true }) 74 | onChange(value: string, oldValue: string) { 75 | onChange(value, oldValue) 76 | } 77 | 78 | render(h: CreateElement) { 79 | return h('div') 80 | } 81 | } 82 | 83 | let wrapper: Wrapper 84 | 85 | beforeEach(() => { 86 | wrapper = mount(MyComponent) 87 | }) 88 | 89 | it('sets options correctly', () => { 90 | expect(wrapper.vm.$options.watch?.[WATCH_PATH]).toMatchInlineSnapshot(` 91 | Array [ 92 | Object { 93 | "deep": true, 94 | "handler": "onChange", 95 | "immediate": true, 96 | "user": true, 97 | }, 98 | ] 99 | `) 100 | }) 101 | }) 102 | 103 | describe('when multiple child components have the same watch event name', () => { 104 | const watcher1 = jest.fn() 105 | const watcher2 = jest.fn() 106 | const watcher3 = jest.fn() 107 | 108 | @Component 109 | class ChildComponent1 extends Vue { 110 | target = 30 111 | 112 | @Watch('target') 113 | handle(...args: any[]) { 114 | watcher1(...args) 115 | } 116 | 117 | render(h: CreateElement) { 118 | return h('div') 119 | } 120 | } 121 | 122 | @Component 123 | class ChildComponent2 extends Vue { 124 | target = 30 125 | 126 | @Watch('target') 127 | handle(...args: any[]) { 128 | watcher2(...args) 129 | } 130 | 131 | render(h: CreateElement) { 132 | return h('div') 133 | } 134 | } 135 | 136 | @Component 137 | class ChildComponent3 extends Vue { 138 | target = 30 139 | 140 | @Watch('target') 141 | handle(...args: any[]) { 142 | watcher3(...args) 143 | } 144 | 145 | render(h: CreateElement) { 146 | return h('div') 147 | } 148 | } 149 | 150 | @Component 151 | class ParentComponent extends Vue { 152 | $refs!: { 153 | child1: ChildComponent1 154 | child2: ChildComponent2 155 | child3: ChildComponent3 156 | } 157 | 158 | render(h: CreateElement) { 159 | return h('div', undefined, [ 160 | h(ChildComponent1, { ref: 'child1' }), 161 | h(ChildComponent2, { ref: 'child2' }), 162 | h(ChildComponent3, { ref: 'child3' }), 163 | ]) 164 | } 165 | } 166 | 167 | let wrapper: Wrapper 168 | 169 | beforeEach(() => { 170 | wrapper = mount(ParentComponent) 171 | }) 172 | 173 | describe('child component 1', () => { 174 | it('triggers watch event once at each child component', async () => { 175 | wrapper.vm.$refs.child1.target += 10 176 | await Vue.nextTick() 177 | expect(watcher1).toHaveBeenCalledTimes(1) 178 | expect(watcher2).toHaveBeenCalledTimes(0) 179 | expect(watcher3).toHaveBeenCalledTimes(0) 180 | }) 181 | }) 182 | 183 | describe('child component 2', () => { 184 | it('triggers watch event once at each child component', async () => { 185 | wrapper.vm.$refs.child2.target += 10 186 | await Vue.nextTick() 187 | expect(watcher1).toHaveBeenCalledTimes(0) 188 | expect(watcher2).toHaveBeenCalledTimes(1) 189 | expect(watcher3).toHaveBeenCalledTimes(0) 190 | }) 191 | }) 192 | 193 | describe('child component 3', () => { 194 | it('triggers watch event once at each child component', async () => { 195 | wrapper.vm.$refs.child3.target += 10 196 | await Vue.nextTick() 197 | expect(watcher1).toHaveBeenCalledTimes(0) 198 | expect(watcher2).toHaveBeenCalledTimes(0) 199 | expect(watcher3).toHaveBeenCalledTimes(1) 200 | }) 201 | }) 202 | }) 203 | }) 204 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "ES5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "ES2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | "lib": [ 8 | "dom", 9 | "es2015", 10 | "es2016", 11 | "es2017" 12 | ] /* Specify library files to be included in the compilation. */, 13 | // "allowJs": true, /* Allow javascript files to be compiled. */ 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 16 | // "declaration": true /* Generates corresponding '.d.ts' file. */, 17 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 18 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | // "outDir": "./", /* Redirect output structure to the directory. */ 21 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 22 | // "composite": true, /* Enable project compilation */ 23 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 24 | // "removeComments": true, /* Do not emit comments to output. */ 25 | // "noEmit": true, /* Do not emit outputs. */ 26 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 27 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 28 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 29 | /* Strict Type-Checking Options */ 30 | "strict": true /* Enable all strict type-checking options. */, 31 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 32 | // "strictNullChecks": true, /* Enable strict null checks. */ 33 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 34 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 35 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 36 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 37 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 38 | /* Additional Checks */ 39 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 40 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 41 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 42 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 43 | /* Module Resolution Options */ 44 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | /* Source Map Options */ 55 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 59 | /* Experimental Options */ 60 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 61 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | --------------------------------------------------------------------------------