decorative.js is an ES7 micro-framework promoting @decorator pattern-driven app development.
npm install decorativedecorative.js
=============
...is an ES7 micro-framework promoting @decorator pattern-driven app development.
Using decorators means to write:
- less lines
- less complex
- better maintainable
- better readable
- more loosely coupled
code.
It's really "decorative" code you will be writing using ``@decorators` ;)
Decorators were introduced to Babel.js in spring 2015 by Yehuda Katz. Since then, only
a few decorators were implemented (see `core-decorators` by `jayphelps`). But the world didn't care much,
no matter if they are amazing or not.
When I discovered the es7.decorators I realized the power they could bring
to my web apps and I began to implement decorative.js.
decorative.js is currently under development but already used to develop
a web app which I will release soon. I rely on this framework and it's toolchain (Babel.js)
and it works stable for my use-cases. I can't guarantee that it fully works for you but I think
it's worth a try or at least a quick look.
decorative.js heavily relies on ES6 and 7. If you aren't already familiar
with the great language features introduced in 6 right now, I kindly recommend you to read on here:
https://github.com/lukehoban/es6features
Well, decorators are part of the ES7 proposals. Let's see, how it looks like to use them.
First, we `import` decorative and the decorators we are planning to use.`
Afterwards we decorate classes` and `class members` like this:
import { Singleton } from 'decorative';
@Singleton()
class MyFirstDecoratedClass {
hello = "Hi, ";
sayHello() {
console.log(this.hello + "I'm a singleton!");
}
}
Guess what? It's really a singleton now!
MyFirstDecoratedClass.sayHello();
Well, now that you got picture: Lets look what we need to run this code
in today's JS engines.
Just install this package like this:
npm install decorative --save
If you also plan to use it with React, you may want to install `decorative-react`:
npm install decorative-react --save
To use decorative you need to cross-compile the code from tomorrows ECMAScript 7
code back to today's ECMAScript 5 / 6 code. Well, this is quite easy. We use Babel.js
and grunt for this. (while gulp is compatible too, I don't have a configuration example yet; feel free to provide one :)
So lets write a Grunt file and include webpack like this:
grunt.initConfig({
webpack: {
app: {
entry: "$yourAppEntry",
output: {
path: '$yourGeneratedOutputDirectory',
filename: "$nameOfTheSingleOutputFileBabelJSGeneratesForYou.js"
},
devtool: 'source-map', // <- so you can debug ECMAScript 7 code in devTools today
module: {
loaders: [
{
test: /\.js$/,
loader: "babel-loader"
}
]
},
watch: true, // <- re-compile on JS source/app code change
keepalive: true, // <- start the webpack dev-server
failOnError: false // <- prevents stopping the dev-server on error
}
}
});
A working example of a Gruntfile can be found in this repositories "Gruntfile.js".
Additionally create a `.babelrc` file in your projects root directory (next to Gruntfile.js)
and put it like:
{
"presets": ["es2015", "stage-0"],
"plugins": ["transform-decorators", "syntax-class-properties"]
}
This configures Babel.js to parse/transform the full spectrum of ES6+ features.
Now that we set up Babel.js and grunt, we can run the transpiling process continuously:
grunt webpack
(Or just "grunt" when you set "webpack" as a default task)
Now that the cross-compilation works and you're able to use decorative and it's decorators,
lets take a look which decorators have been implemented and are ready to use:
Creates an instance of the decorated class by calling it's constructor.
import { Singleton } from 'decorative';
@Singleton()
class MyFirstDecoratedClass {
hello = "Hi";
sayHello() {
console.log("${this.hello}, I'm a singleton!");
}
}
Assigns any object to the decorated class property.
May use/create a singleton instance of a class.
import { Singleton, Inject } from 'decorative';
@Singleton()
class UsingInject {
@Inject('MyFirstDecoratedClass')
myFirstDecoratedClass = null;
}
UsingInject.myFirstDecoratedClass.sayHello();
Injects the provided i18n translation object into the target class prototype.
If a locale is given, it maps like `translations[locale]`:
import { Translate, Singleton } from 'decorative';
import i18n from 'i18n/login';
@Translate(i18n, 'de_DE')
@Singleton
class LoginForm {
get loginButtonLabel() {
return this.i18n.loginButtonLabel;
}
getUserWelcomeMsg(name) {
return this.i18n.userWelcomeMsg(name);
}
}
You can use it like:
// if you like getters :)
LoginForm.loginButtonLabel() // "Einloggen"
LoginForm.getUserWelcomeMsg('John Doe') // "Herzlich Willkommen, John Doe!"
// without getters
LoginForm.i18n.loginButtonLabel // "Einloggen"
LoginForm.i18n.userWelcomeMsg('John Doe') // "Herzlich Willkommen, John Doe!"
Well, the `i18n/login.js` should look like this:
export default {
de_DE: {
loginButtonLabel: 'Einloggen',
userWelcomeMsg: (name) => return "Herzlich Willkommen, ${name}!"
},
en_GB: {
loginButtonLabel: 'Login',
userWelcomeMsg: (name) => return "Welcome, ${name}!"
}
}
$3
Creates an application instance (e.g. delayed, see below) and emits `appStarted` on the internal event bus. `
Modules will create their instances afterwards. (see @AppModule`).
An optional `onStart()` lifecycle method gets called (if impl.) on the app class right before the modules get started.`
The startConfig`-object of `@App` supports the following properties:
`delayed:Function` `
This function receives a callback function as it's first argument.
Call it at any time to create the App instance right at the time you call that start` function.`
This also delays the startup of the @AppModules` for sure.
import { App } from 'decorative';
// module imports should happen before App declaration
import './modules/Login';
import './modules/Dashboard';
@App({
delayed: (start:Function) => {
$(document).ready(() => {
start();
});
}
})
class MyApp {
}
Limitation: Module imports should happen before App declaration.
Automatically creates an instance of the decorated class and adds it to the application instance.
Afterwards it emits `moduleStarted` in the internal event bus so that `@Route()` decorators get activated in time.
import { AppModule } from 'decorative';
@AppModule()
class Login {
constructor() {
console.log("I'm a module of ${this.app}!");
}
}
$3
Watches for the module classes to be started and calls the decorated method
as soon a matching document.location route gets detected.
import { AppModule, Route } from 'decorative';
@AppModule()
class Login {
@Route('/')
showLoginView() {
// show the login view here (e.g. using decorative-react ;)
}
}
As you can see, the @Route() decorator may be called with a second argument `authCb`.
This enabled flexible, (even asynchronous) authentication checks:
@Route('/', isAllowed => {
// check if "/" is allowed, then call:
isAllowed(true); // or false, to deny
})
The method gets called only if `isAllowed` gets called.
Limitation:
Can be applied to methods in @AppModule decorated classes only yet. (see roadmap)
$3
Wraps a decorated method inside a Promise.
@Promised()
findMyPhone(howItLooksLike, foundCb, imSureSomeoneHasStolenItCb) {
$.ajax({
url: '/whereIsMyPhone',
data: howItLooksLike
})
.then(found);
.catch(imSureSomeoneHasStolenIt);
}
The magic here is, that the method's return value is a promise auto-magically:
.findMySocks({color: 'white', type: 'android'})
.then(myPhone => {
// alright
})
.catch(err => {
this.comingLateToday();
});
Although a lot has been implemented, there is still a lot to do and you are very welcome to push :)
Features planned to be introduced in the near future:
- `@Singleton` should support constructor arguments`
- @Inject` should be able to consume and resolve a full-featured DI config object as first argument`
- @Route`` should support to be applied on non-@StartModule classes (and therefore auto-detect if the class it belongs to is a module)
- Please create an issue regarding your own feature requests