├── .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 | [](https://www.npmjs.com/package/vue-property-decorator)
9 | [](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 |
--------------------------------------------------------------------------------