Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add @WatchSub decorator #28

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
node_modules
dist
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ npm run dev

4. `render` and life cycle handler like `created` is declared by decorated methods. They are declared in class body because these handlers use `this`. But you cannot invoke them on the instance itself. So they are decorated to remind users. When declaring custom methods, you should avoid these reserved names.

5. `watch` handlers are declared by `@Watch(propName)` decorator, handler type is checked by `keyof` lookup type.
5. `watch` handlers are declared by `@Watch(propName)` decorator, handler type is checked by `keyof` lookup type. Non-strict watches must use `@WatchSub(propPath)` decorator.

6. All other options are considered as component's meta info. So users should declare them in the `@Component` decorator function.

Expand Down Expand Up @@ -266,6 +266,36 @@ watch: {
}
```

### `WatchSub`
-----

Same as `@Watch`, `@WatchSub` is applied to a **watched handler**.
`Watch` takes expression name as the first argument, and an optional config object as the second one.


```typescript
// watch handler is declared by decorator
properyBeingWatched = { key1: 0 }
@Watch('properyBeingWatched.key1', {deep: true})
handler(newVal, oldVal) {
console.log('the delta is ' + (newVal - oldVal))
})
// ....
```

is equivalent to

```typescript
watch: {
'properyBeingWatched.key1' {
handler: function(newVal, oldVal) {
console.log('the delta is ' + (newVal - oldVal))
},
deep: true
}
}
```

### `Lifecycle` and `Render`
-----

Expand Down
12 changes: 12 additions & 0 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Vue = require('vue');
export { Component } from './src/core';
export { Lifecycle } from './src/lifecycle';
export { Prop, p } from './src/prop';
export { Render } from './src/render';
export { Transition } from './src/transition';
export { Watch } from './src/watch';
export { WatchSub } from './src/watch_sub';
export { Data } from './src/data';
export * from './src/functions';
export declare type CreateElement = typeof Vue.prototype.$createElement;
export { Vue };
24 changes: 24 additions & 0 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
var Vue = require("vue");
exports.Vue = Vue;
var core_1 = require("./src/core");
exports.Component = core_1.Component;
var lifecycle_1 = require("./src/lifecycle");
exports.Lifecycle = lifecycle_1.Lifecycle;
var prop_1 = require("./src/prop");
exports.Prop = prop_1.Prop;
exports.p = prop_1.p;
var render_1 = require("./src/render");
exports.Render = render_1.Render;
var transition_1 = require("./src/transition");
exports.Transition = transition_1.Transition;
var watch_1 = require("./src/watch");
exports.Watch = watch_1.Watch;
var watch_sub_1 = require("./src/watch_sub");
exports.WatchSub = watch_sub_1.WatchSub;
var data_1 = require("./src/data");
exports.Data = data_1.Data;
__export(require("./src/functions"));
22 changes: 22 additions & 0 deletions dist/src/core.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* The basic idea behind Component is marking on prototype
* and then process these marks to collect options and modify class/instance.
*
* A decorator will mark `internalKey` on prototypes, storgin meta information
* Then register `DecoratorProcessor` on Component, which will be called in `Component` decorator
* `DecoratorProcessor` can execute custom logic based on meta information stored before
*
* For non-annotated fields, `Component` will treat them as `methods` and `computed` in `option`
* instance variable is treated as the return value of `data()` in `option`
*
* So a `DecoratorProcessor` may delete fields on prototype and instance,
* preventing meta properties like lifecycle and prop to pollute `method` and `data`
*/
import Vue = require('vue');
import { VClass, DecoratorProcessor, ComponentOptions, $$Prop } from './interface';
export declare function Component<T extends VClass<Vue>>(ctor: T): T;
export declare function Component(config?: ComponentOptions<Vue>): <T extends VClass<Vue>>(ctor: T) => T;
export declare namespace Component {
function register(key: $$Prop, logic: DecoratorProcessor): void;
let inDefinition: boolean;
}
175 changes: 175 additions & 0 deletions dist/src/core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/**
* The basic idea behind Component is marking on prototype
* and then process these marks to collect options and modify class/instance.
*
* A decorator will mark `internalKey` on prototypes, storgin meta information
* Then register `DecoratorProcessor` on Component, which will be called in `Component` decorator
* `DecoratorProcessor` can execute custom logic based on meta information stored before
*
* For non-annotated fields, `Component` will treat them as `methods` and `computed` in `option`
* instance variable is treated as the return value of `data()` in `option`
*
* So a `DecoratorProcessor` may delete fields on prototype and instance,
* preventing meta properties like lifecycle and prop to pollute `method` and `data`
*/
"use strict";
var Vue = require("vue");
var util_1 = require("./util");
// option is a full-blown Vue compatible option
// meta is vue.ts specific type for annotation, a subset of option
function makeOptionsFromMeta(meta, name) {
meta.name = name;
for (var _i = 0, _a = ['props', 'computed', 'watch', 'methods']; _i < _a.length; _i++) {
var key = _a[_i];
if (!util_1.hasOwn(meta, key)) {
meta[key] = {};
}
}
return meta;
}
// given a vue class' prototype, return its internalKeys and normalKeys
// internalKeys are for decorators' use, like $$Prop, $$Lifecycle
// normalKeys are for methods / computed property
function getKeys(proto) {
var protoKeys = Object.getOwnPropertyNames(proto);
var internalKeys = [];
var normalKeys = [];
for (var _i = 0, protoKeys_1 = protoKeys; _i < protoKeys_1.length; _i++) {
var key = protoKeys_1[_i];
if (key === 'constructor') {
continue;
}
else if (key.substr(0, 2) === '$$') {
internalKeys.push(key);
}
else {
normalKeys.push(key);
}
}
return {
internalKeys: internalKeys, normalKeys: normalKeys
};
}
var registeredProcessors = util_1.createMap();
// delegate to processor
function collectInternalProp(propKey, proto, instance, optionsToWrite) {
var processor = registeredProcessors[propKey];
if (!processor) {
return;
}
processor(proto, instance, optionsToWrite);
}
// un-annotated and undeleted methods/getters are handled as `methods` and `computed`
function collectMethodsAndComputed(propKey, proto, optionsToWrite) {
var descriptor = Object.getOwnPropertyDescriptor(proto, propKey);
if (!descriptor) {
return;
}
if (typeof descriptor.value === 'function') {
optionsToWrite.methods[propKey] = descriptor.value;
}
else if (descriptor.get || descriptor.set) {
optionsToWrite.computed[propKey] = {
get: descriptor.get,
set: descriptor.set,
};
}
}
var VUE_KEYS = Object.keys(new Vue);
// find all undeleted instance property as the return value of data()
// need to remove Vue keys to avoid cyclic references
function collectData(cls, keys, optionsToWrite) {
// already implemented by @Data
if (optionsToWrite.data)
return;
// what a closure! :(
optionsToWrite.data = function () {
var selfData = {};
var vm = this;
// _init is the only method required for `cls` call
// for not data property, set as a readonly prop
// so @Prop does not rewrite it to undefined
cls.prototype._init = function () {
var _loop_1 = function (key) {
if (keys.indexOf(key) >= 0)
return "continue";
Object.defineProperty(this_1, key, {
get: function () { return vm[key]; },
set: util_1.NOOP
});
};
var this_1 = this;
for (var _i = 0, _a = Object.keys(vm); _i < _a.length; _i++) {
var key = _a[_i];
_loop_1(key);
}
};
var proxy = new cls();
for (var _i = 0, keys_1 = keys; _i < keys_1.length; _i++) {
var key = keys_1[_i];
if (VUE_KEYS.indexOf(key) === -1) {
selfData[key] = proxy[key];
}
}
return selfData;
};
}
// find proto's superclass' constructor to correctly extend
function findSuper(proto) {
// prototype: {} -> VueInst -> ParentInst, aka. proto
// constructor: Vue -> Parent -> Child
var superProto = Object.getPrototypeOf(proto);
var Super = superProto instanceof Vue
? superProto.constructor // TS does not setup constructor :(
: Vue;
return Super;
}
function Component_(meta) {
if (meta === void 0) { meta = {}; }
function decorate(cls) {
Component.inDefinition = true;
// let instance = Object.create(cls.prototype)
// Object.defineProperty(instance, '_init', {
// value: NOOP, enumerable: false
// })
cls.prototype._init = util_1.NOOP;
var instance = null;
try {
instance = new cls();
}
finally {
Component.inDefinition = false;
}
delete cls.prototype._init;
var proto = cls.prototype;
var options = makeOptionsFromMeta(meta, cls['name']);
var _a = getKeys(proto), internalKeys = _a.internalKeys, normalKeys = _a.normalKeys;
for (var _i = 0, internalKeys_1 = internalKeys; _i < internalKeys_1.length; _i++) {
var protoKey = internalKeys_1[_i];
collectInternalProp(protoKey, proto, instance, options);
}
for (var _b = 0, normalKeys_1 = normalKeys; _b < normalKeys_1.length; _b++) {
var protoKey = normalKeys_1[_b];
collectMethodsAndComputed(protoKey, proto, options);
}
// everything on instance is packed into data
collectData(cls, Object.keys(instance), options);
var Super = findSuper(proto);
return Super.extend(options);
}
return decorate;
}
function Component(target) {
if (typeof target === 'function') {
return Component_()(target);
}
return Component_(target);
}
exports.Component = Component;
(function (Component) {
function register(key, logic) {
registeredProcessors[key] = logic;
}
Component.register = register;
Component.inDefinition = false;
})(Component = exports.Component || (exports.Component = {}));
5 changes: 5 additions & 0 deletions dist/src/data.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Vue = require('vue');
export declare type Dict = {
[k: string]: any;
};
export declare function Data(target: Vue, key: 'data', _: TypedPropertyDescriptor<() => Dict>): void;
14 changes: 14 additions & 0 deletions dist/src/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use strict";
var core_1 = require("./core");
var DATA_KEY = '$$data';
function Data(target, key, _) {
target[DATA_KEY] = target[key];
}
exports.Data = Data;
core_1.Component.register(DATA_KEY, function (proto, instance, options) {
var dataFunc = proto['data'];
options.data = function () {
return dataFunc.call(this);
};
delete proto['data'];
});
4 changes: 4 additions & 0 deletions dist/src/functions.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { VClass } from './interface';
import Vue = require('vue');
export declare function Mixin<T extends Vue>(parent: typeof Vue, ...traits: (typeof Vue)[]): VClass<T>;
export { Component as Trait } from './core';
11 changes: 11 additions & 0 deletions dist/src/functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use strict";
function Mixin(parent) {
var traits = [];
for (var _i = 1; _i < arguments.length; _i++) {
traits[_i - 1] = arguments[_i];
}
return parent.extend({ mixins: traits });
}
exports.Mixin = Mixin;
var core_1 = require("./core");
exports.Trait = core_1.Component;
29 changes: 29 additions & 0 deletions dist/src/interface.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Vue } from 'vue/types/vue';
import { VNode, VNodeData } from 'vue/types/vnode';
export { VNode } from 'vue/types/vnode';
export { PropOptions } from 'vue/types/options';
import { ComponentOptions, FunctionalComponentOptions } from 'vue/types/options';
export { ComponentOptions } from 'vue/types/options';
export declare type Hash<V> = {
[k: string]: V;
};
export declare type VClass<T extends Vue> = {
new (): T;
extend(option: ComponentOptions<Vue> | FunctionalComponentOptions): typeof Vue;
};
export interface DecoratorProcessor {
(proto: Vue, instance: Vue, options: ComponentOptions<Vue>): void;
}
export declare type $$Prop = string & {
'$$Prop Brand': never;
};
export interface ContextObject<T> {
readonly props: T;
readonly children: VNode[];
readonly slots: Hash<VNode>;
readonly data: VNodeData;
readonly parent: VNode;
}
export declare type Class = {
new (...args: {}[]): {};
};
1 change: 1 addition & 0 deletions dist/src/interface.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"use strict";
3 changes: 3 additions & 0 deletions dist/src/lifecycle.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Vue = require('vue');
export declare type Lifecycles = 'beforeCreate' | 'created' | 'beforeDestroy' | 'destroyed' | 'beforeMount' | 'mounted' | 'beforeUpdate' | 'updated' | 'activated' | 'deactivated';
export declare function Lifecycle(target: Vue, life: Lifecycles, _: TypedPropertyDescriptor<() => void>): void;
18 changes: 18 additions & 0 deletions dist/src/lifecycle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use strict";
var core_1 = require("./core");
var util_1 = require("./util");
var LIFECYCLE_KEY = '$$Lifecycle';
function Lifecycle(target, life, _) {
var lifecycles = target[LIFECYCLE_KEY] = target[LIFECYCLE_KEY] || util_1.createMap();
lifecycles[life] = true;
}
exports.Lifecycle = Lifecycle;
core_1.Component.register(LIFECYCLE_KEY, function (proto, instance, options) {
var lifecycles = proto[LIFECYCLE_KEY];
for (var lifecycle in lifecycles) {
// lifecycles must be on proto because internalKeys is processed before method
var handler = proto[lifecycle];
delete proto[lifecycle];
options[lifecycle] = handler;
}
});
Loading