toxic-decorators
Inspired by core-decorators written by jayphelps. I think decorators will one powerful util for developer. So I create some function I want to use.
Library of JavaScript stage-0 decorators (aka ES2016/ES7 decorators but that's not accurate) include methods like @autobind, @waituntil, @alias etc. It's mainly focus on some useful methods to help us create javascript application.
If you have any idea of function you want. Please tell me or help me.
state of decoratos
Most of the paragraph below is mainly quoted from core-decorators written by jayhelps.
These are stage-0 decorators because while the decorators spec has changed and is now stage-2, no transpiler has yet to implement these changes and until they do, this library won't either. Although the TypeScript documentationuses the phrase "Decorators are a stage 2 proposal for JavaScript" this is misleading because TypeScript still only implements the stage-0 version of the spec, which is very incompatible with stage-2 (as of this writing). Though we have babel-plugin-transform-decorators-stage-2-initial to translate stage-2 version of decorators. But the author do not encourage us to use it.
So I think we will support stage-2 when we have mature compiler.
Get start
npm
A version compiled to ES5 in CJS format is published to npm as toxic-decorators.
If you want to use it in Node.js.
npm install --save toxic-decorators
If you want to use it in the front-end project, I encourage you to use:
npm install --save-dev toxic-decorators
just get the code
You can get the compiled code in the lib
file
lib/toxic-decorators.js
cjs version, require babel-runtimelib/toxic-decorators.mjs
es version, which face to js:next require babel-runtimelib/toxic-decorators.browser.js
umd version, which you can use in the browser, but maybe you will need to add babel-polyfill in some situation.lib/toxic-decorators.min.js
minify version based on umd version.
Decorators
For Properties a Methods
For Properties
For Methods
For Classes
TODO
Helpers
Docs
@accessor
Set getter/setter hook on any properties or methods. In fact, it will change all kind of descriptors into an accessor descriptor.
import {accessor} from 'toxic-decorators';
class Foo {
@accessor({
get (value) {
return ++value;
},
set (value) {
return ++value
}
})
bar = 1;
}
console.log(foo.bar);
foo.bar = 3;
console.log(foo.bar);
@alias
Help you to set alias for properties on any instance or for methods on any class.
import {alias} from 'toxic-decorators';
class Cat {};
const cat = new Cat();
class Dog {
@alias('run')
@alias('run', Cat.prototype)
@alias('move', cat)
move() {
console.log('it moved');
}
@alias('old')
@alias('age', cat)
age = 1;
}
const dog = new Dog();
const antoherCat = new Cat();
dog.move();
dog.run();
cat.run();
anotherCat.run();
cat.move();
console.log(anotherCat.move === undefined);
console.log(cat.age);
console.log(dog.old);
You can also set alias on getter/setter too.
But there's one problem is we will set the alias until the origin one has been initialized.
It means that you must get access to your origin property before you get access to your alias property, otherwise the alias one will be undefined.
@nonconfigurable
Makes a porperty or method so that they cannot be deleted. Also accroding to the specification, it can prevent them from editing via Object.defineProperty
. But it doesn't work quiet well. In that situation, @readonly may be a better choice.
import {nonconfigurable} from 'toxic-decorators';
class Foo {
@nonconfigurable
bar = 1;
}
delete foo.bar;
@enumerable
Marks a property or method as being enumerable. As we know, property is enumerable by default.
import {enumerable} from 'toxic-decoarators';
class Foo {
@enumerable
bar () {}
car () {}
}
const foo = new Foo();
for (const key in foo) console.log(key);
@nonenumerable
Marks a property as not being enumerable. Note that methods aren't enumerable by default.
import {nonenumerable} from 'toxic-decorators';
class Foo {
@nonenumerable
a = 1;
b = 2;
}
const foo = new Foo();
for (const key in foo) console.log(key);
@initialize
Help you to do something when you initialize your property or function.
import {initialize} from 'toxic-decorators';
class Foo {
@initialize(function (value) {
return ++value;
})
bar = 1;
};
const foo = new Foo();
console.log(foo.bar);
foo.bar = 3;
console.log(foo.bar);
You can use this on getter/setter, too. Once you use that, we will always run the initialze function that until you set the value again.
@readonly
You cannot write the porperty again.
import { readonly } from 'toxic-decorators';
class Meal {
@readonly
entree = 'steak';
}
const dinner = new Meal();
dinner.entree = 'salmon';
You can also use readonly on getter/setter, but there is something you should pay attention.
We have just remove the setter here. But you getter stillreturn the origin value. You can change the origin value.
import { readonly } from 'toxic-decorators';
let dish = 'steak'
class Meal {
@readonly
get entree () {return dish};
set entree (value) {
dish = value;
return dish
}
}
const dinner = new Meal();
dinner.entree = 'salmon';
dish = 'salmon';
console.log(dinner.entree);
@frozen
We will totally freeze the property. It can not be rewrite, delete or iterate.
import { frozen } from 'toxic-decorators';
class Meal {
@frozen
entree = 'steak';
}
const dinner = new Meal();
dinner.entree = 'salmon';
delete dinner.entree;
You can also set the getter/setter property frozen. In this way, it's value could change once it's settle down.
import { frozen } from 'toxic-decorators';
let dish = 'steak'
class Meal {
@frozen
get entree () {return dish};
set entree (value) {
dish = value;
return dish
}
}
const dinner = new Meal();
dinner.entree = 'salmon';
dish = 'salmon';
console.log(dinner.entree);
Note: Escpecially on property, Once you set frozen, it can't be change, even with decorators. So you may better put it on the top.
@initString
Ensure a property's initial value must be string. You can also pass another function as you want. It's just a grammar sugar for @initialize.
import {initString} from 'toxic-decorators';
const info = {
name: 'Kobe Bryant',
champions: 5
};
class Intro {
@initString(value => value.toLowerCase())
name = info.name
@initString(value => value.toLowerCase())
champions = info.champions
}
const intro = new Intro();
console.log(intro.name);
console.log(intro.champions);
@initNumber
Ensure a property's initial value must be number. You can see the detial in @intiString
@initBoolean
Ensure a property's initial value must be boolean. You can see the detial in @intiString
@initArray
Ensure a property's initial value must be Array. You can see the detial in @intiString.
@alwaysString
Ensure the property's value always be string. We change the property into getter/setter to implement this. It's a grammar sugar for @accessor
import {initString} from 'toxic-decorators';
class Intro {
@initString(value => value.toLowerCase())
name = 'BEN'
}
const intro = new Intro();
console.log(intro.name);
intro.name = 'JONES';
console.log(intro.name);
@alwaysNumber
Ensure the property's value always be number. You can see the detail in @alwaysString
@alwaysBoolean
Ensure the property's value always be boolean. You can see the detail in @alwaysString
@alwaysArray
Ensure the property's value always be Array. You can see the detail in @alwaysString
@before
You can add your preprocessor here on your methods.Mostly, we will use this to do some arguments check.
import {before} from 'toxic-decorators';
class Foo {
@before(function (a, b) {
if(typeof a !== 'number' || typeof b !== 'number') {
throw new Error('only accept number');
}
return [a, b];
})
sum (a, b) {
return a + b;
}
}
const foo = new Foo();
foo.sum(1, 3);
foo.sum('1', 3);
@waituntil
In some situation, our application is not ready. But others can call our function. We hope that they can wait for us. Most of time, we offer them a Promise to wait.
However, can we just offer them a method, and run it until we are ready.
@waituntil will help us do this stuff.
We set a method to waituntil a flag, you can pass in a promise or a boolean.
If you pass in a promise or a function return promise
- When the promise is pending, we will wait.
- When the promise resolve, we run all the function
- When the promise reject, we do not run at all.
But in this way, your method will become async.
If you pass in a function return boolean
- when the function return
true
, we will call the method. - when the function return
false
, we do nothing.
This way your method will only be called when you are ready.
But in this way, we can't delay the call that when you're not ready.
So the better way is, pass in a string represent the property's name, alse you can pass another instance.
- if the property is
false
, we will put the call into waiting queue - once the property get
true
, we will run all the call in the waiting queue. - if the property is
true
, you call will be run.
import {waituntil} from 'toxic-decorators';
let promiseResolve;
class Bar {
flag = false;
}
const bar = new Bar();
class Foo {
ready = new Promise(resolve => {promiseResolve = resolve});
@waituntil(function () {return this.ready})
runUntilPromise () {
console.log('Promise is resolve!');
}
@waituntil(function () {return bar.flag});
runUntilTrue () {
console.log('flag is true!');
}
@waituntil('flag', bar);
runUntilReady () {
console.log('bar.flag is true!');
}
}
const foo = new Foo();
foo.runUntilPromise();
foo.runUntilTrue();
foo.runUntilReady();
bar.flag = true;
foo.runUntilTrue();
promiseResolve();
setTimeout(async () => {
foo.runUntilPromise();
await foo.ready;
}, 0)
applyDecorators()
If you want to use decorators, you may need to use babel-plugin-transform-decorators-legacy to compile. What if you don't want to use that. You can use applyDecorators
.
import {applyDecorators, before} from 'toxic-decorators';
class Person {
run () {
console.log('i am running');
}
walk () {
console.log('i am walking');
}
}
applyDecorators(Foo, {
walk: before(() => console.log('go')),
run: [before(() => console.log('ready')), before(() => console.log('go'))]
});
const foo = new Foo();
foo.walk();
foo.run();
In the way above, we can apply decorators on function's prototype. That's enough for methods. But what if we want to apply some property decorators.
You can act like above, but it will modify portotype's property. They make take effect on multiple instance, and it's works bad on some situation.
So, if you want to apply decorators on property, I advice you to pass in an instance in self mode.
import {initialize, applyDecorators} from 'toxic-decorators';
class Foo {
a = 1;
b = 2;
};
const foo = new Foo();
console.log(foo.a);
console.log(foo.b);
applyDecorators(foo, {
a: initialize(function () {return 2;}),
b: initialize(function () {return 3;})
}, {self: true});
console.log(foo.a);
console.log(foo.b);
Need lodash utilities as decorators?
We have mostly the same idea with core-decorators. So I just quote this from it's README.
toxic-decorators aims to provide decorators that are fundamental to JavaScript itself--mostly things you could do with normal Object.defineProperty
but not as easily when using ES2015 classes. Things like debouncing, throttling, and other more opinionated decorators are being phased out in favor of lodash-decorators which wraps applicable lodash utilities as decorators. We don't want to duplicate the effort of lodash, which has years and years of robust testing and bugfixes.