From bbbdb4ec834e171c69e5c80231ec20be20b458e3 Mon Sep 17 00:00:00 2001 From: budiadiono Date: Tue, 15 Nov 2016 21:37:41 +0700 Subject: [PATCH 01/26] Should be able to declare props in options while using @Prop() --- index.js | 7 ++++++- src/component.ts | 8 +++++++- test/common/specs/props.ts | 28 ++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 test/common/specs/props.ts diff --git a/index.js b/index.js index bc088de..04fe0c8 100644 --- a/index.js +++ b/index.js @@ -63,7 +63,12 @@ function Component(options) { } props[prop] = propVal; } - options['props'] = props; + if (!options.props) { + options.props = {}; + } + for (var p in props) { + options.props[p] = props[p]; + } } Object.getOwnPropertyNames(proto).forEach(function (key) { if (key === 'constructor') { diff --git a/src/component.ts b/src/component.ts index 76cf332..fc1de7a 100644 --- a/src/component.ts +++ b/src/component.ts @@ -97,8 +97,14 @@ export function Component(options? : ComponentOptions): ClassDecorator { props[prop] = propVal; } + if (!options.props) { + options.props = {} + } + + for(let p in props) { + options.props[p] = props[p] + } - options['props'] = props; } diff --git a/test/common/specs/props.ts b/test/common/specs/props.ts new file mode 100644 index 0000000..e747ca9 --- /dev/null +++ b/test/common/specs/props.ts @@ -0,0 +1,28 @@ +import { Component, Prop } from '../../../index' +import { expect } from 'chai' + + +describe('props extending tests', () => { + + it('should be able to declare props in options while using @Prop()', () => { + @Component({ + props: { + message: { + type: String + } + } + }) + class App { + + @Prop({ type: Number }) + age + } + + var app = new App() + + expect(app['$options']['props'], 'declared prop in options should be there').to.have.property('message').to.have.property('type').that.equals(String); + expect(app['$options']['props'], 'declared prop by @Prop() should be there').to.have.property('age').to.have.property('type').that.equals(Number); + }) + + +}) \ No newline at end of file From a8141fd7d30d655332fe607e1f6a0bda4d0595b6 Mon Sep 17 00:00:00 2001 From: budiadiono Date: Tue, 15 Nov 2016 21:43:08 +0700 Subject: [PATCH 02/26] v2.0.3 --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 04fe0c8..6c11848 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ /** - * vue-typed 2.0.2 + * vue-typed 2.0.3 * Sets of ECMAScript / Typescript decorators that helps you write Vue component easily. * http://vue-typed.github.io/vue-typed/ From 5a90274958678564dab340dd983cc909328def15 Mon Sep 17 00:00:00 2001 From: budiadiono Date: Tue, 15 Nov 2016 21:43:10 +0700 Subject: [PATCH 03/26] Release version 2.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6547606..eb03da4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-typed", - "version": "2.0.2", + "version": "2.0.3", "description": "Sets of ECMAScript / Typescript decorators that helps you write Vue component easily.", "keywords": [ "vue", From 451e68fa84c8d47a1e171a0cf384540c6311d0aa Mon Sep 17 00:00:00 2001 From: budiadiono Date: Wed, 11 Jan 2017 15:14:30 +0700 Subject: [PATCH 04/26] Fix failing tests - close #5 --- .vscode/settings.json | 3 ++- package.json | 1 + tsconfig.json | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6d53924..514178e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,6 @@ "esversion": "6" } , -"typescript.tsdk": "./node_modules/typescript/lib" +"typescript.tsdk": "./node_modules/typescript/lib", +"tslint.enable": false } \ No newline at end of file diff --git a/package.json b/package.json index 6547606..5bf2240 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "ts-loader": "^0.8.2", "ts-node": "^1.6.1", "typescript": "^2.0.3", + "vue": "^2.1.8", "webpack": "^1.13.1", "webpack-merge": "^0.13.0" }, diff --git a/tsconfig.json b/tsconfig.json index e7b1345..9dff48b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,9 @@ "preserveConstEnums": true, "suppressImplicitAnyIndexErrors": true, "experimentalDecorators": true, + "typeRoots": [ + "node_modules/@types" + ], "types": [ "node", "mocha", From c855233c60c9b6080a662e7f361c64d709fff9cd Mon Sep 17 00:00:00 2001 From: budiadiono Date: Mon, 23 Jan 2017 11:22:23 +0700 Subject: [PATCH 05/26] avoid init super class while building component --- index.js | 1 + src/component.ts | 5 +++++ test/common/specs/basic.ts | 5 ++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 04fe0c8..e0fd705 100644 --- a/index.js +++ b/index.js @@ -23,6 +23,7 @@ function Component(options) { } options.name = options.name || Component.name; var proto = Component.prototype; + if (Object.getPrototypeOf(proto) instanceof Vue) Object.setPrototypeOf(proto.constructor, function () {}); var constructor = new proto.constructor(); var vueKeys = []; if (proto.vuex) { diff --git a/src/component.ts b/src/component.ts index fc1de7a..3c2615a 100644 --- a/src/component.ts +++ b/src/component.ts @@ -37,6 +37,11 @@ export function Component(options? : ComponentOptions): ClassDecorator { // class prototype. var proto = Component.prototype + + // avoid parent component initialization while building component + if (Object.getPrototypeOf(proto) instanceof Vue) + Object.setPrototypeOf(proto.constructor, function() {}) + var constructor = new proto.constructor(); diff --git a/test/common/specs/basic.ts b/test/common/specs/basic.ts index a8c531a..ed317a4 100644 --- a/test/common/specs/basic.ts +++ b/test/common/specs/basic.ts @@ -200,7 +200,10 @@ describe('vue-class-component based test (ts)', () => { b: number constructor() { - super(); + super(); + } + + created() { this.b = this.a + 1 } } From 00eae4887377521adfd907b863e6317a070282cd Mon Sep 17 00:00:00 2001 From: budiadiono Date: Mon, 23 Jan 2017 16:17:24 +0700 Subject: [PATCH 06/26] mixins, options & virtual. close #8 --- index.d.ts | 13 +- index.js | 249 ++++++++++++++++++----------- src/component.ts | 193 +--------------------- src/index.ts | 4 + src/mixins-global.ts | 19 +++ src/mixins.ts | 18 +++ src/options.ts | 34 ++++ src/utils.ts | 196 +++++++++++++++++++++++ test/common/specs/mixins-global.ts | 51 ++++++ test/common/specs/mixins.ts | 220 +++++++++++++++++++++++++ test/common/specs/options.ts | 126 +++++++++++++++ 11 files changed, 836 insertions(+), 287 deletions(-) create mode 100644 src/mixins-global.ts create mode 100644 src/mixins.ts create mode 100644 src/options.ts create mode 100644 src/utils.ts create mode 100644 test/common/specs/mixins-global.ts create mode 100644 test/common/specs/mixins.ts create mode 100644 test/common/specs/options.ts diff --git a/index.d.ts b/index.d.ts index 7ddcf12..8ddce48 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,5 +2,16 @@ import { Vue } from 'vue/types/vue'; import { ComponentOptions, PropOptions } from 'vue/types/options'; export function Component(options?: ComponentOptions): ClassDecorator +export function Options(options?: ComponentOptions): ClassDecorator export function Prop(options?: PropOptions): PropertyDecorator -export function Watch(property: string, deep?:boolean): MethodDecorator \ No newline at end of file +export function Watch(property: string, deep?:boolean): MethodDecorator +export function Mixin(component: { new () : T; }): VirtualClass +export function Mixins(...components: { new (); }[]): VirtualClass +export function GlobalMixin(options?: ComponentOptions): ClassDecorator + + +// helpers +export type VirtualClass = { + new (): T +} +export function Virtual(): VirtualClass \ No newline at end of file diff --git a/index.js b/index.js index e0fd705..012fc22 100644 --- a/index.js +++ b/index.js @@ -16,111 +16,118 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol var vueInternalPropNames = Object.getOwnPropertyNames(new Vue()); var vueInternalHooks = ['data', 'props', 'watch', 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated', 'deactivated', 'beforeDestroy', 'destroyed', 'render']; -function Component(options) { - var factory = function factory(Component, options) { - if (!options) { - options = {}; +function BuildOptions(Component, options) { + if (!options) { + options = {}; + } + options.name = options.name || Component.name; + var proto = Component.prototype; + if (Object.getPrototypeOf(proto) instanceof Vue) Object.setPrototypeOf(proto.constructor, function () {}); + var constructor = new proto.constructor(); + var vueKeys = []; + if (proto.vuex) { + var protoVue = proto.vuex; + if (protoVue['getters']) { + Object.getOwnPropertyNames(protoVue['getters']).forEach(function (k) { + vueKeys.push(k); + }); } - options.name = options.name || Component.name; - var proto = Component.prototype; - if (Object.getPrototypeOf(proto) instanceof Vue) Object.setPrototypeOf(proto.constructor, function () {}); - var constructor = new proto.constructor(); - var vueKeys = []; - if (proto.vuex) { - var protoVue = proto.vuex; - if (protoVue['getters']) { - Object.getOwnPropertyNames(protoVue['getters']).forEach(function (k) { - vueKeys.push(k); - }); - } - if (protoVue['actions']) { - Object.getOwnPropertyNames(protoVue['actions']).forEach(function (k) { - vueKeys.push(k); - }); - } + if (protoVue['actions']) { + Object.getOwnPropertyNames(protoVue['actions']).forEach(function (k) { + vueKeys.push(k); + }); } - var propAttrs = proto['$_vt_props']; - var propNames = undefined; - delete proto['$_vt_props']; - if (propAttrs) { - var key = 'props'; - var props = {}; - propNames = Object.getOwnPropertyNames(propAttrs); - for (var i = 0; i < propNames.length; i++) { - var prop = propNames[i]; - var propVal = undefined; - var descriptor = Object.getOwnPropertyDescriptor(propAttrs, prop); - var constructorDefault = constructor[prop]; - if (_typeof(descriptor.value) === 'object') { - propVal = descriptor.value; - if (!propVal.default) propVal.default = constructorDefault; - } else if (constructorDefault) { - propVal = { - default: constructorDefault - }; - } - if (typeof propVal === 'undefined') { - propVal = true; - } - props[prop] = propVal; - } - if (!options.props) { - options.props = {}; - } - for (var p in props) { - options.props[p] = props[p]; - } - } - Object.getOwnPropertyNames(proto).forEach(function (key) { - if (key === 'constructor') { - return; - } - if (vueInternalHooks.indexOf(key) > -1) { - options[key] = proto[key]; - return; - } - var descriptor = Object.getOwnPropertyDescriptor(proto, key); - if (typeof descriptor.value === 'function') { - if (vueKeys.indexOf(key) > -1) return; - (options.methods || (options.methods = {}))[key] = descriptor.value; - } else if (descriptor.get || descriptor.set) { - (options.computed || (options.computed = {}))[key] = { - get: descriptor.get, - set: descriptor.set + } + var propAttrs = proto['$_vt_props']; + var propNames = undefined; + delete proto['$_vt_props']; + if (propAttrs) { + var key = 'props'; + var props = {}; + propNames = Object.getOwnPropertyNames(propAttrs); + for (var i = 0; i < propNames.length; i++) { + var prop = propNames[i]; + var propVal = undefined; + var descriptor = Object.getOwnPropertyDescriptor(propAttrs, prop); + var constructorDefault = constructor[prop]; + if (_typeof(descriptor.value) === 'object') { + propVal = descriptor.value; + if (!propVal.default) propVal.default = constructorDefault; + } else if (constructorDefault) { + propVal = { + default: constructorDefault }; } - }); - var dataNames = []; - var restrictedNames = vueInternalPropNames; - if (propNames) restrictedNames = restrictedNames.concat(propNames); - if (vueKeys && vueKeys.length) restrictedNames = restrictedNames.concat(vueKeys); - Object.getOwnPropertyNames(constructor).forEach(function (key) { - if (restrictedNames.indexOf(key) === -1) { - dataNames.push(key); - } - }); - if (dataNames.length > 0) { - var parentData = undefined; - var parentDataType = _typeof(options['data']); - if (parentDataType === 'function') { - parentData = options['data'](); - } else if (parentDataType === 'object') { - parentData = options['data']; + if (typeof propVal === 'undefined') { + propVal = true; } - options['data'] = function () { - var data_obj = parentData || {}; - dataNames.forEach(function (prop) { - var descriptor = Object.getOwnPropertyDescriptor(constructor, prop); - if (!descriptor.get && !descriptor.set && typeof descriptor.value !== 'function') { - data_obj[prop] = constructor[prop]; - } - }); - return data_obj; + props[prop] = propVal; + } + if (!options.props) { + options.props = {}; + } + for (var p in props) { + options.props[p] = props[p]; + } + } + Object.getOwnPropertyNames(proto).forEach(function (key) { + if (key === 'constructor') { + return; + } + if (vueInternalHooks.indexOf(key) > -1) { + options[key] = proto[key]; + return; + } + var descriptor = Object.getOwnPropertyDescriptor(proto, key); + if (typeof descriptor.value === 'function') { + if (vueKeys.indexOf(key) > -1) return; + (options.methods || (options.methods = {}))[key] = descriptor.value; + } else if (descriptor.get || descriptor.set) { + (options.computed || (options.computed = {}))[key] = { + get: descriptor.get, + set: descriptor.set }; } - var superProto = Object.getPrototypeOf(proto); + }); + var dataNames = []; + var restrictedNames = vueInternalPropNames; + if (propNames) restrictedNames = restrictedNames.concat(propNames); + if (vueKeys && vueKeys.length) restrictedNames = restrictedNames.concat(vueKeys); + Object.getOwnPropertyNames(constructor).forEach(function (key) { + if (restrictedNames.indexOf(key) === -1) { + dataNames.push(key); + } + }); + if (dataNames.length > 0) { + var parentData = undefined; + var parentDataType = _typeof(options['data']); + if (parentDataType === 'function') { + parentData = options['data'](); + } else if (parentDataType === 'object') { + parentData = options['data']; + } + options['data'] = function () { + var data_obj = parentData || {}; + dataNames.forEach(function (prop) { + var descriptor = Object.getOwnPropertyDescriptor(constructor, prop); + if (!descriptor.get && !descriptor.set && typeof descriptor.value !== 'function') { + data_obj[prop] = constructor[prop]; + } + }); + return data_obj; + }; + } + return options; +} +function Virtual() { + return function () {}; +} + +function Component(options) { + var factory = function factory(Component, options) { + var superProto = Object.getPrototypeOf(Component.prototype); var Super = superProto instanceof Vue ? superProto.constructor : Vue; - return Super['extend'](options); + return Super['extend'](BuildOptions(Component, options)); }; if (options instanceof Function) { return factory(options); @@ -130,6 +137,25 @@ function Component(options) { }; } +function Options(options) { + return function (Component) { + function chainUp(component) { + var Super = Object.getPrototypeOf(component); + if (Super instanceof Object && Super.prototype) { + var keys = Object.getOwnPropertyNames(Super.prototype); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var descriptor = Object.getOwnPropertyDescriptor(Super.prototype, key); + if (!Component.prototype.hasOwnProperty(key)) Object.defineProperty(Component.prototype, key, descriptor); + } + chainUp(Super); + } + } + chainUp(Component); + return BuildOptions(Component, options); + }; +} + function Prop(options) { return function (target, key) { var id = '$_vt_props'; @@ -166,9 +192,38 @@ function Watch(property, deep) { }; } +function Mixin(component) { + return Vue.extend({ + mixins: [component] + }); +} +function Mixins() { + for (var _len = arguments.length, components = Array(_len), _key = 0; _key < _len; _key++) { + components[_key] = arguments[_key]; + } + + return Vue.extend({ + mixins: components + }); +} + +function GlobalMixin(options) { + var factory = function factory(Component, options) { + Vue.mixin(BuildOptions(Component, options)); + }; + return function (Component) { + return factory(Component, options); + }; +} + exports.Component = Component; +exports.Options = Options; exports.Prop = Prop; exports.Watch = Watch; +exports.Mixin = Mixin; +exports.Mixins = Mixins; +exports.GlobalMixin = GlobalMixin; +exports.Virtual = Virtual; Object.defineProperty(exports, '__esModule', { value: true }); diff --git a/src/component.ts b/src/component.ts index 3c2615a..5f6f60a 100644 --- a/src/component.ts +++ b/src/component.ts @@ -5,202 +5,17 @@ import * as Vue from 'vue' import { ComponentOptions } from 'vue/types/options'; +import { BuildOptions } from './utils'; -var vueInternalPropNames = Object.getOwnPropertyNames(new Vue()); -var vueInternalHooks = [ - 'data', - 'props', - 'watch', - 'beforeCreate', - 'created', - 'beforeMount', - 'mounted', - 'beforeUpdate', - 'updated', - 'activated', - 'deactivated', - 'beforeDestroy', - 'destroyed', - 'render' -] - - -export function Component(options? : ComponentOptions): ClassDecorator { +export function Component(options?: ComponentOptions): ClassDecorator { var factory = function (Component: Function, options?: any): (target: any) => Function | void { - - if (!options) { - options = {} - } - options.name = options.name || Component.name - - - // class prototype. - var proto = Component.prototype - - // avoid parent component initialization while building component - if (Object.getPrototypeOf(proto) instanceof Vue) - Object.setPrototypeOf(proto.constructor, function() {}) - - var constructor = new proto.constructor(); - - - // has vuex? - var vueKeys = []; - if (proto.vuex) { - var protoVue = proto.vuex; - if (protoVue['getters']) { - Object.getOwnPropertyNames(protoVue['getters']).forEach((k) => { - vueKeys.push(k); - }); - } - - if (protoVue['actions']) { - Object.getOwnPropertyNames(protoVue['actions']).forEach((k) => { - vueKeys.push(k); - }); - } - } - - var propAttrs = proto['$_vt_props']; - var propNames = undefined; - - delete proto['$_vt_props']; - if (propAttrs) { - var key = 'props'; - var props = {} - - propNames = Object.getOwnPropertyNames(propAttrs); - for (var i = 0; i < propNames.length; i++) { - var prop = propNames[i]; - var propVal = undefined; - var descriptor = Object.getOwnPropertyDescriptor(propAttrs, prop) - var constructorDefault = constructor[prop]; - - if (typeof (descriptor.value) === 'object') { - - // prop options defined - propVal = descriptor.value - - // try to assign default value - if (!propVal.default) - propVal.default = constructorDefault; - - } else if (constructorDefault) { - - // create prop object with default value from constructor - propVal = { - default: constructorDefault - } - } - - // just define empty prop - if (typeof (propVal) === 'undefined') { - propVal = true; - } - - props[prop] = propVal; - } - - if (!options.props) { - options.props = {} - } - - for(let p in props) { - options.props[p] = props[p] - } - - } - - - - Object.getOwnPropertyNames(proto).forEach(function (key) { - - // skip constructor - if (key === 'constructor') { - return - } - - - // hooks - if (vueInternalHooks.indexOf(key) > -1) { - options[key] = proto[key] - return - } - - - var descriptor = Object.getOwnPropertyDescriptor(proto, key) - if (typeof descriptor.value === 'function') { - - if (vueKeys.indexOf(key) > -1) - return - - // methods - (options.methods || (options.methods = {}))[key] = descriptor.value - - - } else if (descriptor.get || descriptor.set) { - // computed properties - (options.computed || (options.computed = {}))[key] = { - get: descriptor.get, - set: descriptor.set - } - } - }) - - - // Processing data attributes - - var dataNames = [] - var restrictedNames = vueInternalPropNames; - if (propNames) restrictedNames = restrictedNames.concat(propNames); - if (vueKeys && vueKeys.length) restrictedNames = restrictedNames.concat(vueKeys); - - Object.getOwnPropertyNames(constructor).forEach(function (key) { - - if (restrictedNames.indexOf(key) === -1) { - dataNames.push(key); - } - - }); - - - if (dataNames.length > 0) { - - // evaluate parent data - var parentData = undefined - var parentDataType = typeof options['data']; - if (parentDataType === 'function') { - parentData = options['data'](); - } else if (parentDataType === 'object') { - parentData = options['data']; - } - - options['data'] = () => { - - // define new data object - var data_obj = parentData || {} - - // set data default values initialized from constructor - dataNames.forEach(function (prop) { - var descriptor = Object.getOwnPropertyDescriptor(constructor, prop) - if (!descriptor.get && !descriptor.set && typeof descriptor.value !== 'function') { - data_obj[prop] = constructor[prop] - } - }) - - return data_obj - } - } - - - // Build vue component - var superProto = Object.getPrototypeOf(proto) + var superProto = Object.getPrototypeOf(Component.prototype) var Super = superProto instanceof Vue ? superProto.constructor : Vue - return Super['extend'](options) + return Super['extend'](BuildOptions(Component, options)) } diff --git a/src/index.ts b/src/index.ts index 1d89499..c1262d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,5 +3,9 @@ */ export { Component } from './component' +export { Options } from './options' export { Prop } from './prop'; export { Watch } from './watch'; +export { Mixin, Mixins } from './mixins' +export { GlobalMixin } from './mixins-global'; +export { Virtual } from './utils' \ No newline at end of file diff --git a/src/mixins-global.ts b/src/mixins-global.ts new file mode 100644 index 0000000..7203ac7 --- /dev/null +++ b/src/mixins-global.ts @@ -0,0 +1,19 @@ +/** + * Vue Global Mixins + */ + +import * as Vue from 'vue' +import { ComponentOptions } from 'vue/types/options'; +import { BuildOptions } from './utils'; + +export function GlobalMixin(options?: ComponentOptions): ClassDecorator { + + var factory = function (Component: Function, options?: any): void { + Vue.mixin(BuildOptions(Component, options)) + } + + return function (Component) { + return factory(Component, options) + } + +} \ No newline at end of file diff --git a/src/mixins.ts b/src/mixins.ts new file mode 100644 index 0000000..fceba70 --- /dev/null +++ b/src/mixins.ts @@ -0,0 +1,18 @@ +/** + * Vue Mixins + */ + +import * as Vue from 'vue' +import { VirtualClass } from '../' + +export function Mixin(component: { new () : T; }): VirtualClass { + return Vue.extend({ + mixins: [component] + }) as any; +} + +export function Mixins(...components: { new (); }[]): VirtualClass { + return Vue.extend({ + mixins: components + }) as any +} \ No newline at end of file diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 0000000..8f7a9c3 --- /dev/null +++ b/src/options.ts @@ -0,0 +1,34 @@ +/** + * Vue Options + */ + +import * as Vue from 'vue' +import { ComponentOptions } from 'vue/types/options'; +import { BuildOptions } from './utils'; + +export function Options(options?: ComponentOptions): ClassDecorator { + + return function (Component) { + + function chainUp(component) { + var Super = Object.getPrototypeOf(component) + + if (Super instanceof Object && Super.prototype) { + var keys = Object.getOwnPropertyNames(Super.prototype) + for (var i = 0; i < keys.length; i++) { + var key = keys[i] + var descriptor = Object.getOwnPropertyDescriptor(Super.prototype, key); + if (!Component.prototype.hasOwnProperty(key)) + Object.defineProperty(Component.prototype, key, descriptor) + } + chainUp(Super) + } + } + + // normalize object + chainUp(Component) + + return BuildOptions(Component, options) + } + +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..07b0392 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,196 @@ +import { VirtualClass } from '../' +import * as Vue from 'vue' + +var vueInternalPropNames = Object.getOwnPropertyNames(new Vue()); +var vueInternalHooks = [ + 'data', + 'props', + 'watch', + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'activated', + 'deactivated', + 'beforeDestroy', + 'destroyed', + 'render' +] + +export function BuildOptions(Component: Function, options?: any): (target: any) => Function | void { + + + + if (!options) { + options = {} + } + options.name = options.name || Component.name + + + // class prototype. + var proto = Component.prototype + + // avoid parent component initialization while building component + if (Object.getPrototypeOf(proto) instanceof Vue) + Object.setPrototypeOf(proto.constructor, function () { }) + + var constructor = new proto.constructor(); + + + // has vuex? + var vueKeys = []; + if (proto.vuex) { + var protoVue = proto.vuex; + if (protoVue['getters']) { + Object.getOwnPropertyNames(protoVue['getters']).forEach((k) => { + vueKeys.push(k); + }); + } + + if (protoVue['actions']) { + Object.getOwnPropertyNames(protoVue['actions']).forEach((k) => { + vueKeys.push(k); + }); + } + } + + var propAttrs = proto['$_vt_props']; + var propNames = undefined; + + delete proto['$_vt_props']; + if (propAttrs) { + var key = 'props'; + var props = {} + + propNames = Object.getOwnPropertyNames(propAttrs); + for (var i = 0; i < propNames.length; i++) { + var prop = propNames[i]; + var propVal = undefined; + var descriptor = Object.getOwnPropertyDescriptor(propAttrs, prop) + var constructorDefault = constructor[prop]; + + if (typeof (descriptor.value) === 'object') { + + // prop options defined + propVal = descriptor.value + + // try to assign default value + if (!propVal.default) + propVal.default = constructorDefault; + + } else if (constructorDefault) { + + // create prop object with default value from constructor + propVal = { + default: constructorDefault + } + } + + // just define empty prop + if (typeof (propVal) === 'undefined') { + propVal = true; + } + + props[prop] = propVal; + } + + if (!options.props) { + options.props = {} + } + + for (let p in props) { + options.props[p] = props[p] + } + + } + + + + Object.getOwnPropertyNames(proto).forEach(function (key) { + + // skip constructor + if (key === 'constructor') { + return + } + + + // hooks + if (vueInternalHooks.indexOf(key) > -1) { + options[key] = proto[key] + return + } + + + var descriptor = Object.getOwnPropertyDescriptor(proto, key) + if (typeof descriptor.value === 'function') { + + if (vueKeys.indexOf(key) > -1) + return + + // methods + (options.methods || (options.methods = {}))[key] = descriptor.value + + + } else if (descriptor.get || descriptor.set) { + // computed properties + (options.computed || (options.computed = {}))[key] = { + get: descriptor.get, + set: descriptor.set + } + } + }) + + + // Processing data attributes + + var dataNames = [] + var restrictedNames = vueInternalPropNames; + if (propNames) restrictedNames = restrictedNames.concat(propNames); + if (vueKeys && vueKeys.length) restrictedNames = restrictedNames.concat(vueKeys); + + Object.getOwnPropertyNames(constructor).forEach(function (key) { + + if (restrictedNames.indexOf(key) === -1) { + dataNames.push(key); + } + + }); + + + if (dataNames.length > 0) { + + // evaluate parent data + var parentData = undefined + var parentDataType = typeof options['data']; + if (parentDataType === 'function') { + parentData = options['data'](); + } else if (parentDataType === 'object') { + parentData = options['data']; + } + + options['data'] = () => { + + // define new data object + var data_obj = parentData || {} + + // set data default values initialized from constructor + dataNames.forEach(function (prop) { + var descriptor = Object.getOwnPropertyDescriptor(constructor, prop) + if (!descriptor.get && !descriptor.set && typeof descriptor.value !== 'function') { + data_obj[prop] = constructor[prop] + } + }) + + return data_obj + } + } + + return options; + +} + +export function Virtual(): VirtualClass { + return function () { } as any; +} \ No newline at end of file diff --git a/test/common/specs/mixins-global.ts b/test/common/specs/mixins-global.ts new file mode 100644 index 0000000..05ed989 --- /dev/null +++ b/test/common/specs/mixins-global.ts @@ -0,0 +1,51 @@ +import { Component, Prop, GlobalMixin, Virtual } from '../../../index' +import { expect } from 'chai' +import * as Vue from 'vue' + + +describe('global mixins', () => { + + + it('should be ok', () => { + + let globalMixinCreated = false + let globalMethodInvoked = false + + interface IGlobal { + globalMethod() + } + + @GlobalMixin() + class Global implements IGlobal { + + created() { + globalMixinCreated = true + } + + globalMethod() { + globalMethodInvoked = true + } + + globalData = 'global data' + + @Prop() + globalProp = 'global prop' + } + + @Component() + class MyComp extends Virtual() { + created() { + this.globalMethod() + } + } + + const c = new MyComp() + + expect(globalMixinCreated).eq(true) + expect(globalMethodInvoked).eq(true) + expect(c['$options']['data']()).to.have.property('globalData').that.equals('global data') + expect(c['$options']['props']).to.have.property('globalProp').have.property('default').that.equals('global prop') + + }) + +}) \ No newline at end of file diff --git a/test/common/specs/mixins.ts b/test/common/specs/mixins.ts new file mode 100644 index 0000000..b54f25d --- /dev/null +++ b/test/common/specs/mixins.ts @@ -0,0 +1,220 @@ +import { Component, Prop, Mixin, Mixins, Options } from '../../../index' +import { expect } from 'chai' +import * as Vue from 'vue' + + +describe('mixins', () => { + + + it('single mixin', () => { + + let mixinCreatedCalls = 0 + let mixinCreated = false + let mixinMethodCalled = false + + @Component() + class MyMixin { + + mixinData = 'data from mixin' + + @Prop() + mixinProp = 'prop from mixin' + + created() { + mixinCreatedCalls++ + mixinCreated = true + } + mymix() { + mixinMethodCalled = true + } + } + + @Component() + class Container extends Mixin(MyMixin) { + created() { + this.mymix() + } + } + + var c = new Container() + + expect(mixinCreated).eq(true) + expect(mixinMethodCalled).eq(true) + expect(mixinCreatedCalls, 'mixin created method should be called once').eq(1) + expect(c['$options']['data']()).to.have.property('mixinData').that.equals('data from mixin') + expect(c['$options']['props']).to.have.property('mixinProp').have.property('default').that.equals('prop from mixin') + + + }) + + it('multiple mixins', () => { + + let mixinCreated1 = false + let mixinMethodCalled1 = false + let mixinCreated2 = false + let mixinMethodCalled2 = false + + + @Component() + class MyMixin1 { + + mixinData1 = 'data from mixin 1' + + @Prop() + mixinProp1 = 'prop from mixin 1' + + created() { + mixinCreated1 = true + } + + mymix1() { + mixinMethodCalled1 = true + } + } + + @Component() + class MyMixin2 { + mixinData2 = 'data from mixin 2' + + @Prop() + mixinProp2 = 'prop from mixin 2' + + created() { + mixinCreated2 = true + } + + mymix2() { + mixinMethodCalled2 = true + } + } + + interface AllMyMixins extends MyMixin1, MyMixin2 { } + + @Component() + class Container extends Mixins(MyMixin1, MyMixin2) { + created() { + this.mymix1() + this.mymix2() + } + } + + var c = new Container() + + expect(mixinCreated1).eq(true) + expect(mixinMethodCalled1).eq(true) + expect(mixinCreated2).eq(true) + expect(mixinMethodCalled2).eq(true) + expect(c['$options']['data']()).to.have.property('mixinData1').that.equals('data from mixin 1') + expect(c['$options']['props']).to.have.property('mixinProp1').have.property('default').that.equals('prop from mixin 1') + expect(c['$options']['data']()).to.have.property('mixinData2').that.equals('data from mixin 2') + expect(c['$options']['props']).to.have.property('mixinProp2').have.property('default').that.equals('prop from mixin 2') + + }) + + + it('single mixin using @Options', () => { + + let mixinCreatedCalls = 0 + let mixinCreated = false + let mixinMethodCalled = false + + @Options() + class MyMixin { + + mixinData = 'data from mixin' + + @Prop() + mixinProp = 'prop from mixin' + + created() { + mixinCreatedCalls++ + mixinCreated = true + } + mymix() { + mixinMethodCalled = true + } + } + + @Component() + class Container extends Mixin(MyMixin) { + created() { + this.mymix() + } + } + + var c = new Container() + + expect(mixinCreated).eq(true) + expect(mixinMethodCalled).eq(true) + expect(mixinCreatedCalls, 'mixin created method should be called once').eq(1) + expect(c['$options']['data']()).to.have.property('mixinData').that.equals('data from mixin') + expect(c['$options']['props']).to.have.property('mixinProp').have.property('default').that.equals('prop from mixin') + + + }) + + it('multiple mixins using @Options', () => { + + let mixinCreated1 = false + let mixinMethodCalled1 = false + let mixinCreated2 = false + let mixinMethodCalled2 = false + + + @Options() + class MyMixin1 { + + mixinData1 = 'data from mixin 1' + + @Prop() + mixinProp1 = 'prop from mixin 1' + + created() { + mixinCreated1 = true + } + + mymix1() { + mixinMethodCalled1 = true + } + } + + @Options() + class MyMixin2 { + mixinData2 = 'data from mixin 2' + + @Prop() + mixinProp2 = 'prop from mixin 2' + + created() { + mixinCreated2 = true + } + + mymix2() { + mixinMethodCalled2 = true + } + } + + interface AllMyMixins extends MyMixin1, MyMixin2 { } + + @Component() + class Container extends Mixins(MyMixin1, MyMixin2) { + created() { + this.mymix1() + this.mymix2() + } + } + + var c = new Container() + + expect(mixinCreated1).eq(true) + expect(mixinMethodCalled1).eq(true) + expect(mixinCreated2).eq(true) + expect(mixinMethodCalled2).eq(true) + expect(c['$options']['data']()).to.have.property('mixinData1').that.equals('data from mixin 1') + expect(c['$options']['props']).to.have.property('mixinProp1').have.property('default').that.equals('prop from mixin 1') + expect(c['$options']['data']()).to.have.property('mixinData2').that.equals('data from mixin 2') + expect(c['$options']['props']).to.have.property('mixinProp2').have.property('default').that.equals('prop from mixin 2') + + }) + +}) \ No newline at end of file diff --git a/test/common/specs/options.ts b/test/common/specs/options.ts new file mode 100644 index 0000000..9fc92a2 --- /dev/null +++ b/test/common/specs/options.ts @@ -0,0 +1,126 @@ +import { Prop, Mixin, Mixins, Options, Watch } from '../../../index' +import { expect } from 'chai' +import * as Vue from 'vue' + + +describe('options', () => { + + + it('simple options', () => { + + + @Options({ + props: { + 'prop-in-options': 'Foo' + } + }) + class Container { + + data1 = 'the data' + + @Prop() + prop1 = 'the prop' + + created() { + } + + method1() { + } + + @Watch('data1') + watcher(val) { + + } + + + get identity() { + return this.data1; + } + + set identity(val) { + this.data1 = val + } + + } + + + expect(Container['data'](), 'assert data').to.have.property('data1').eq('the data') + expect(Container, 'assert props').to.have.property('props').to.have.property('prop1').to.have.property('default').eq('the prop') + expect(Container, 'assert props in options').to.have.property('props').to.have.property('prop-in-options').eq('Foo') + expect(Container, 'assert methods').to.have.property('methods').to.have.property('method1').to.be.a('Function') + expect(Container, 'assert created').to.have.property('created').to.be.a('Function') + expect(Container, 'assert watch').to.have.property('watch').to.have.property('data1').to.be.a('Function') + expect(Container, 'assert computed').to.have.property('computed').to.have.property('identity').to.have.property('get').to.be.a('Function') + expect(Container, 'assert computed').to.have.property('computed').to.have.property('identity').to.have.property('set').to.be.a('Function') + + + }) + + + it('options inheritance', () => { + + class SuperParent { + _identity = '' + + get identity() { + return this._identity; + } + + set identity(val) { + this._identity = val + } + } + + + class Parent extends SuperParent { + dataParent = 'the parent data' + + @Prop() + propParent = 'the parent prop' + + created() { } + + method2() { } + } + + + @Options({ + props: { + 'prop-in-options': 'Foo' + } + }) + class Container extends Parent { + + data1 = 'the data' + + @Prop() + prop1 = 'the prop' + + method1() { + } + + @Watch('data1') + watcher(val) { + + } + + } + + + + + expect(Container['data'](), 'assert data').to.have.property('data1').eq('the data') + expect(Container['data'](), 'assert parent data').to.have.property('dataParent').eq('the parent data') + expect(Container['data'](), 'assert super parent data').to.have.property('_identity').eq('') + expect(Container, 'assert props').to.have.property('props').to.have.property('prop1').to.have.property('default').eq('the prop') + expect(Container, 'assert props in options').to.have.property('props').to.have.property('prop-in-options').eq('Foo') + expect(Container, 'assert parent props').to.have.property('props').to.have.property('propParent').to.have.property('default').eq('the parent prop') + expect(Container, 'assert created').to.have.property('created').to.be.a('Function') + expect(Container, 'assert methods').to.have.property('methods').to.have.property('method1').to.be.a('Function') + expect(Container, 'assert parent methods').to.have.property('methods').to.have.property('method2').to.be.a('Function') + expect(Container, 'assert watch').to.have.property('watch').to.have.property('data1').to.be.a('Function') + expect(Container, 'assert computed').to.have.property('computed').to.have.property('identity').to.have.property('get').to.be.a('Function') + expect(Container, 'assert computed').to.have.property('computed').to.have.property('identity').to.have.property('set').to.be.a('Function') + + }) +}) \ No newline at end of file From 7f61643e89a50630cc297a7d871223ae056043a2 Mon Sep 17 00:00:00 2001 From: budiadiono Date: Mon, 23 Jan 2017 18:44:53 +0700 Subject: [PATCH 07/26] update docs for mixins, options and components --- docs/SUMMARY.md | 7 ++- docs/usage/mixins/global.md | 95 +++++++++++++++++++++++++++++++++++ docs/usage/mixins/multiple.md | 71 ++++++++++++++++++++++++++ docs/usage/mixins/single.md | 53 +++++++++++++++++++ docs/usage/options.md | 80 +++++++++++++++++++++++++++++ 5 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 docs/usage/mixins/global.md create mode 100644 docs/usage/mixins/multiple.md create mode 100644 docs/usage/mixins/single.md create mode 100644 docs/usage/options.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index cf6315e..ed38164 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -4,9 +4,14 @@ * [Getting Started](getting-started.md) * Basic Usage * [Component](usage/component.md) + * [Options](usage/options.md) * [Methods and Computed Properties](usage/methods-and-computed-properties.md) * [Prop](usage/props.md) - * [Watch](usage/watch.md) + * [Watch](usage/watch.md) + * Mixins + * [Single Mixin](usage/mixins/single.md) + * [Multiple Mixins](usage/mixins/multiple.md) + * [Global Mixins](usage/mixins/global.md) * Advanced Usage * [VueRouter](advanced/vue-router.md) * [Class Inheritance](advanced/class-inheritance.md) diff --git a/docs/usage/mixins/global.md b/docs/usage/mixins/global.md new file mode 100644 index 0000000..ef966bc --- /dev/null +++ b/docs/usage/mixins/global.md @@ -0,0 +1,95 @@ +# Global Mixin + +To have a global mixin just simply decorate your class with `@GlobalMixin` decorator. + +### Example + +```typescript +import { GlobalMixin } from 'vue-typed' + +@GlobalMixin() +class Global { + created() { + console.log('created method called from global mixin'); + } + globalMethod() { + console.log('global method called'); + } +} +``` + +is equivalent to + +```javascript +Vue.mixin({ + created: function() { + console.log('created method called from global mixin'); + }, + methods: { + globalMethod: function() { + console.log('global method called'); + } + } +}) +``` + + +## Invoke global method + +From the example above you should be able to do this: + +```typescript +import { Component } from 'vue-typed' + +@Component({ + template: require('./container.html') +}) +export class Container { + created() { + // Raising error since globalMethod is not defined + this.globalMethod() + } +} +``` + +But unfortunatelly NOT, the `globalMethod()` will not recognized by `Container` class since it's not defined anywhere. Thus you have to extend this class from `Global` class. +But again it's not make sense since the `globalMethod()` is actually already exists magically be the global mixin. Then here's come `Virtual` function to help you: + +```typescript +import { Component, Virtual } from 'vue-typed' + +@Component({ + template: require('./container.html') +}) +export class Container extends Virtual() { + created() { + // It's available now + this.globalMethod() + } +} +``` + +`Virtual` function is just a helper to help us deal with typescript. It does not extend the class anywhere but an empty function. + +You will do same way to invoke both the global methods and mixin(s) methods together: + +```typescript +import { Component, Options, Mixin, Virtual } from 'vue-typed' + +@Options() +class MyMixin extends Virtual() { + mymix() { + console.log('my mix method called'); + } +} + +@Component({ + template: require('./container.html'), +}) +export class Container extends Mixin(MyMixin) { + created() { + this.mymix() + this.globalMethod() + } +} +``` \ No newline at end of file diff --git a/docs/usage/mixins/multiple.md b/docs/usage/mixins/multiple.md new file mode 100644 index 0000000..e5dc347 --- /dev/null +++ b/docs/usage/mixins/multiple.md @@ -0,0 +1,71 @@ +# Multiple Mixin + +You need `Mixins` function to extends your component with several mixins: + +```typescript +class MainClass extends Mixins(Mixin1, Mixin2, OtherMixins, ...) +``` + +`AllMixinsInterface` is an interface that holds the class members of `Mixin1`, `Mixin2` and all other mixins you want. To be clear see example bellow: + +### Example + +```typescript +import { Component, Options, Mixins } from 'vue-typed' + +@Options() +class MyMixin1 { + mymix1() { + console.log('my mix 1 method called'); + } +} + +@Options() +class MyMixin2 { + mymix2() { + console.log('my mix 2 method called'); + } +} + +interface AllMyMixins extends MyMixin1, MyMixin2 { } + +@Component() +class Container extends Mixins(MyMixin1, MyMixin2) { + created() { + this.mymix1() + this.mymix2() + } +} +``` + +That is equivalent to: + +```javascript +var AllMyMixins = Vue.extend({ + mixins: [ + { // MyMixin1 + methods: { + mymix1: function() { + console.log('my mix 1 method called'); + } + } + }, + { // MyMixin2 + methods: { + mymix2: function() { + console.log('my mix 2 method called'); + } + } + } + ] +}) + + +var Container = AllMyMixins.extend({ + created: function() { + this.mymix1() + this.mymix2() + } +}) + +``` \ No newline at end of file diff --git a/docs/usage/mixins/single.md b/docs/usage/mixins/single.md new file mode 100644 index 0000000..3c8b6a8 --- /dev/null +++ b/docs/usage/mixins/single.md @@ -0,0 +1,53 @@ +# Single Mixin + +For single mixin you can use `Mixin` function to extends your component: + +```typescript +class MainClass extends Mixin(ComponentOrOptionsYouWantToMixIn) +``` + +`ComponentOrOptionsYouWantToMixIn` is a class that you have to decorate it either with [`@Component`](../component.md) or [`@Options`](../options.md) decorator. + +### Example: + +```typescript +import { Component, Options, Mixin } from 'vue-typed' + +@Options() +class MyMixin { + mymix() { + console.log('my mix method called'); + } +} + +@Component({ + template: require('./container.html'), +}) +export class Container extends Mixin(MyMixin) { + created() { + this.mymix() + } +} +``` + +is equivalent to + +```javascript +var MyMixin = Vue.extend({ + mixins: [{ + methods: { + mymix: function () { + console.log('my mix method called'); + } + } + }] +}) + +export var Container = MyMixin.extend({ + template: require('./container.html'), + created: function () { + this.mymix() + } +}) + +``` diff --git a/docs/usage/options.md b/docs/usage/options.md new file mode 100644 index 0000000..ab27115 --- /dev/null +++ b/docs/usage/options.md @@ -0,0 +1,80 @@ +# Options + +Is a decorator that converts your `class` into **Vue** options object. + +```typescript +@Options({ + props: { + 'prop-in-options': 'Foo' + } +}) +class Container { + + data1 = 'the data' + + @Prop() + prop1 = 'the prop' + + created() { + // do something... + } + + method1() { + // do something... + } + + @Watch('data1') + watcher(val) { + // do something... + } + + get identity() { + return this.data1; + } + + set identity(val) { + this.data1 = val + } +} +``` + +is equivalent to + +```javascript +var Container = { + name: 'Container', + data: function() { + return { + data1: 'the data' + } + }, + props: { + 'prop-in-options': 'Foo', + prop1: { default: 'the prop' } + }, + created: function(){ + // do something... + }, + methods: { + method1: function() { + // do something... + } + }, + computed: { + identity: { + get: function(){ + return this.data1 + }, + set: function(val) { + this.data1 = val + } + } + }, + watch: { + data1: function(val){ + // do something... + } + }, + data: [Function] +} +``` \ No newline at end of file From 1bc17834fbcfb70cc9adc14881a570dd4ee587d0 Mon Sep 17 00:00:00 2001 From: budiadiono Date: Mon, 23 Jan 2017 18:48:16 +0700 Subject: [PATCH 08/26] v2.0.4 --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index fee7b0b..88121da 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ /** - * vue-typed 2.0.3 + * vue-typed 2.0.4 * Sets of ECMAScript / Typescript decorators that helps you write Vue component easily. * http://vue-typed.github.io/vue-typed/ From f3616114e5aa2c8995dd8913580b080c8984f607 Mon Sep 17 00:00:00 2001 From: budiadiono Date: Mon, 23 Jan 2017 18:48:18 +0700 Subject: [PATCH 09/26] Release version 2.0.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d8fbe24..f3045e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-typed", - "version": "2.0.3", + "version": "2.0.4", "description": "Sets of ECMAScript / Typescript decorators that helps you write Vue component easily.", "keywords": [ "vue", From bdd3bacefa8d157f462e24082d23ed2cb02db55b Mon Sep 17 00:00:00 2001 From: budiadiono Date: Sun, 29 Jan 2017 21:34:41 +0700 Subject: [PATCH 10/26] [doc] fix broken todo-mvc sample --- docs/examples/todo-mvc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/todo-mvc.md b/docs/examples/todo-mvc.md index 8367c29..3daebae 100644 --- a/docs/examples/todo-mvc.md +++ b/docs/examples/todo-mvc.md @@ -1,7 +1,7 @@ #TodoMVC Example