reactive-di
Advanced tools
Comparing version 2.3.0 to 2.3.1
@@ -44,4 +44,4 @@ 'use strict'; | ||
function factory(target) { | ||
dm(_common.subtypeKey, 'func', target); | ||
function factory(target, isJsx) { | ||
dm(_common.subtypeKey, isJsx ? 'jsx' : 'func', target); | ||
return target; | ||
@@ -53,2 +53,3 @@ } | ||
dm(_common.metaKey, new _common.ComponentMeta(rec || {}), target); | ||
dm(_common.subtypeKey, 'jsx', target); | ||
return target; | ||
@@ -55,0 +56,0 @@ }; |
@@ -156,3 +156,3 @@ 'use strict'; | ||
function isComponent(target) { | ||
return typeof target === 'function' && gm(metaKey, target); | ||
return typeof target === 'function' && gm(subtypeKey, target); | ||
} | ||
@@ -159,0 +159,0 @@ |
@@ -80,10 +80,29 @@ 'use strict'; | ||
var createElement = this._componentFactory.createElement; | ||
var ce = this._componentFactory.createElement; | ||
var createWrappedElement = function createWrappedElement(tag, props) { | ||
for (var _len = arguments.length, children = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { | ||
children[_key - 2] = arguments[_key]; | ||
for (var _len = arguments.length, ch = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { | ||
ch[_key - 2] = arguments[_key]; | ||
} | ||
return createElement(_this.wrapComponent(tag), props, children.length ? children : null); | ||
switch (ch.length) { | ||
case 0: | ||
return ce(_this.wrapComponent(tag), props); | ||
case 1: | ||
return ce(_this.wrapComponent(tag), props, ch[0]); | ||
case 2: | ||
return ce(_this.wrapComponent(tag), props, ch[0], ch[1]); | ||
case 3: | ||
return ce(_this.wrapComponent(tag), props, ch[0], ch[1], ch[2]); | ||
case 4: | ||
return ce(_this.wrapComponent(tag), props, ch[0], ch[1], ch[2], ch[3]); | ||
case 5: | ||
return ce(_this.wrapComponent(tag), props, ch[0], ch[1], ch[2], ch[3], ch[4]); | ||
case 6: | ||
return ce(_this.wrapComponent(tag), props, ch[0], ch[1], ch[2], ch[3], ch[4], ch[5]); | ||
case 7: | ||
return ce(_this.wrapComponent(tag), props, ch[0], ch[1], ch[2], ch[3], ch[4], ch[5], ch[6]); | ||
default: | ||
return ce.apply(undefined, [_this.wrapComponent(tag), props].concat(ch)); | ||
} | ||
}; | ||
@@ -90,0 +109,0 @@ |
{ | ||
"name": "reactive-di", | ||
"version": "2.3.0", | ||
"version": "2.3.1", | ||
"description": "Reactive dependency injection", | ||
@@ -5,0 +5,0 @@ "publishConfig": { |
547
README.md
# reactive-di | ||
Definitely complete solution for dependency injection, state-to-css, state-to-dom rendering, data loading, optimistic updates and rollbacks. | ||
Solution for dependency injection and state-management, state-to-css, state-to-dom rendering, data loading, optimistic updates and rollbacks. | ||
[Dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) with [flowtype](http://flowtype.org/) support, based on [ds300 derivablejs](https://ds300.github.io/derivablejs/). For old browsers needs Map, Observable, Promise and optionally Reflect, Symbol polyfills. | ||
## About | ||
No statics, no singletones, abstract everything, configure everything. | ||
Hierarchical scope, state management IoC container uses a class constructor or function signature to identify and inject its dependencies. | ||
Features: | ||
- Annotation based and highly flow-compatible | ||
- Can resolve dependencies from [flowtype](http://flowtype.org/) interfaces, types, classes via [babel metadata plugin](https://github.com/zerkalica/babel-plugin-transform-metadata). | ||
- Each dependency is [Derivable](http://ds300.github.io/derivablejs/#derivable-Derivable) or [Atom](http://ds300.github.io/derivablejs/#derivable-Atom) | ||
- Can easily provide abstraction level on top of any state-to-dom manipulating library: [react](https://facebook.github.io/react/), [preact](https://preactjs.com/), [bel](https://github.com/shama/bel), [mercury](https://github.com/Raynos/mercury/), etc. | ||
- Provide themes support via state-to-css library, like [jss](https://github.com/jsstyles/jss) | ||
- Mimic to react: flow-compatible widgets with props autocomplete support, that looks like react, but without react dependencies | ||
- Hierarchical - can create local state per widget, like in [angular2 di](https://angular.io/docs/ts/latest/guide/hierarchical-dependency-injection.html) | ||
- Type based selectors | ||
- Data loading via promises or observables | ||
- Optimistic updates with rollbacks | ||
- About 2500 SLOC with tests, 1000 without | ||
- Suitable for both node and the browser | ||
- Middlewares for functions and class methods | ||
- Lifehooks onUpdate, onMount, onUnmount supported for any dependencies | ||
There are many IoC containers for javascript, for example [inversify](http://inversify.io/), but reactive-di works without registering dependencies in container and has some state management features, like [mobx](http://mobxjs.github.io/mobx/). | ||
## Flow | ||
All dependencies presented as [atoms](http://ds300.github.io/derivablejs/#derivable-atom) or [derivables](http://ds300.github.io/derivablejs/#derivable-Derivable). | ||
<img src="docs/flow.png" alt="reactive-di flow diagram" /> | ||
## Motivation | ||
## Basic entities | ||
We need good OO design with [Composition reuse](https://en.wikipedia.org/wiki/Composition_over_inheritance) and [SOLID](https://en.wikipedia.org/wiki/SOLID_(object-oriented_design) in complex javascript applications on sever and client. | ||
- source({key, construct}) - atom source: model with data or service, can be injected from outside and changed in runtime. | ||
- factory - mark function factory, if not used [babel metadata plugin](https://github.com/zerkalica/babel-plugin-transform-metadata) | ||
- deps(...deps: mixed[]) - declare dependencies, if not used [babel metadata plugin](https://github.com/zerkalica/babel-plugin-transform-metadata) | ||
- component({register: Function[]}(target) - any visual component | ||
- theme - jss-like style | ||
- updater(...updaters: Updater[]) - create loading status from updater services | ||
- service - for optimizations, do not recalculate, if dependencies changed, only call constructor with new deps | ||
Any stream is wrapper on top of domain data. We need to automate and move most of all reactive-data stream manipulations behind the scene. For example, [mobx](http://mobxjs.github.io/mobx/) is good there. | ||
We need to reduce boilerplate code, by maximally using flow-types. Many decorators are unnecessary: use reflection metadata for classes, functions and components. | ||
We need to keep all components clean and usable without di-framework: decorators must be used only for additional metadata, not as wrappers. | ||
We need to remove dependencies at react-like frameworks from compiletime to runtime. It give posibility to create unified jsx-based zero-dependency component, which can be used in any jsx-compatible render-to-dom library. | ||
We need to provide unified cssx-based component which can be used in any jss-compatible render-to-css library. | ||
## Install | ||
``` | ||
npm install reactive-di babel-plugin-transform-metadata | ||
``` | ||
For using unified components, we need to define jsx pragma in transform-metadata: | ||
.babelrc: | ||
```json | ||
{ | ||
"plugins": [ | ||
["transform-metadata", { | ||
"jsxPragma": "__h" | ||
}], | ||
["transform-react-jsx", { | ||
"pragma": "__h" | ||
}] | ||
] | ||
} | ||
``` | ||
reactive-di requires some polyfills: Promise, Observable (only if observables used in application code), Map, Proxy (only for middlewares). | ||
## Basics | ||
Reactive di container use classes or functions as unique identifiers of dependency. | ||
```js | ||
// @flow | ||
import {Di} from 'reactive-di' | ||
import type {Derivable} from 'reactive-di' | ||
class Logger { | ||
log(message: string): void { | ||
console.log(message) | ||
} | ||
} | ||
class TestClass { | ||
constructor(logger: Logger) { | ||
this._logger = logger | ||
} | ||
add(a: number): number { | ||
this._logger.log(`calling add ${a} + 1`) | ||
return a + 1 | ||
} | ||
} | ||
const di = new Di() | ||
const testClass: Derivable<TestClass> = di.val(TestClass) | ||
testClass.get().add(1) | ||
``` | ||
## Architecture overview | ||
<img src="https://rawgithub.com/zerkalica/reactive-di/master/docs/workflow-state.svg" alt="reactive-di flow diagram" /> | ||
## Sources | ||
Source is [atom](http://ds300.github.io/derivablejs/#derivable-atom) with data object. | ||
Source looks like pure data class with initial state. Source decorator can give some options: key: string - unique name of model class, this keys helps to associate models with data in json-object from prerender server. | ||
```js | ||
// @flow | ||
import {source} from 'reactive-di/annotations' | ||
interface UserRec { | ||
id?: string; | ||
name?: string; | ||
} | ||
@source({key: 'user'}) | ||
class User { | ||
id: string | ||
name: string | ||
constructor(rec?: UserRec = {}) { | ||
this.id = rec.id || '' | ||
this.name = rec.name || '' | ||
} | ||
} | ||
``` | ||
Or using reactive-di helper: | ||
```js | ||
//... | ||
@source({key: 'user'}) | ||
class User extends BaseModel<UserRec>{ | ||
id: string | ||
name: string | ||
static defaults: UserRec = { | ||
id: '', | ||
name: '' | ||
} | ||
} | ||
``` | ||
Source is updateable: | ||
```js | ||
// @flow | ||
import {Di} from 'reactive-di' | ||
// Updating source manually: | ||
const userAtom = (new Di()).val(User) | ||
userAtom.get() // User object | ||
userAtom.set(new User(...)) | ||
``` | ||
## Service | ||
Service is regular class or factory-functon with some actions: source manipulations. | ||
```js | ||
// @flow | ||
import {Di} from 'reactive-di' | ||
import {source} from 'reactive-di/annotations' | ||
@source({key: 'user'}) | ||
class User { | ||
id: string | ||
name: string | ||
} | ||
class UserService { | ||
_user: User | ||
constructor(user: User) { | ||
this._user = user | ||
} | ||
submit(): void { | ||
} | ||
} | ||
// or as factory-function: | ||
function createUserSubmit(user: User) { | ||
return function userSubmit() { | ||
// submit user | ||
} | ||
} | ||
const userServiceAtom = (new Di()).val(UserService) | ||
userServiceAtom.get().submit() | ||
userAtom.set(new User(...)) | ||
// User changed --> UserService changed, get new service | ||
userServiceAtom.get().submit() | ||
``` | ||
Usually you don't need to listen Service changes in component, use service decorator to detach service from atom updates: | ||
```js | ||
// @flow | ||
import {Di} from 'reactive-di' | ||
import {service} from 'reactive-di/annotations' | ||
@service | ||
class UserService { | ||
} | ||
``` | ||
## Components | ||
Component is function, where first argument is properties, second - is internal component state (dependencies), and third - is element factory: function(tag, props, children). In this form components does not depends on any react-like framework. | ||
Di container injects state into each component by wrapping creteElement method, passed to each component function. Di does not use [react context](https://facebook.github.io/react/docs/context.html), this is only react-feature. | ||
```js | ||
// @flow | ||
export type SrcComponent<Props, State> = (props: Props, state: State, h: ?((tag, props, children) => any)) => any | ||
``` | ||
[babel metadata plugin](https://github.com/zerkalica/babel-plugin-transform-metadata) autodetects functions with jsx and places h argument automatically. | ||
Example: | ||
```js | ||
// @flow | ||
import React from 'react' | ||
import {Di, ReactComponentFactory} from 'reactive-di' | ||
interface UserProps { | ||
id: string; | ||
name: string; | ||
} | ||
interface UserState { | ||
service: UserService; | ||
} | ||
export function User({id, name}: UserProps, {service}: UserState): mixed { | ||
return <div> | ||
User: {name}#{id} | ||
<a href="#" onClick={service.edit}>[change]</a> | ||
</div> | ||
} | ||
const di = new Di(new ReactComponentFactory(React)) | ||
const UserWithState: typeof User = di.wrapComponent(User) | ||
React.render(<UserWithState id="1", name="2" />, document.body) | ||
``` | ||
## Themes | ||
Theme is dependency with [jss](https://github.com/cssinjs/jss) object and css class names. On first component mount - theme invokes factory, which passed to di options at entry point and attach css to dom. On last component unmount css part will be removed. | ||
```js | ||
// @flow | ||
import {theme} from 'reactive-di/annotations' | ||
@theme | ||
class UserTheme { | ||
wrapper: string | ||
name: string | ||
__css: mixed | ||
constructor(deps: SomeDeps) { | ||
this.__css { | ||
wrapper: { | ||
backgroundColor: 'white' | ||
}, | ||
name: { | ||
backgroundColor: 'red' | ||
} | ||
} | ||
} | ||
} | ||
interface UserProps { | ||
id: string; | ||
name: string; | ||
} | ||
interface UserState { | ||
service: UserService; | ||
theme: UserTheme; | ||
} | ||
export function User({id, name}: UserProps, {theme, service}: UserState): mixed { | ||
return <div className={theme.wrapper}> | ||
User: <span className={theme.name}>{name}#{id}</span> | ||
<a href="#" onClick={service.edit}>[change]</a> | ||
</div> | ||
} | ||
``` | ||
## Lifecycles | ||
Hooks used for handling component mount/unmount cycles and target updates. Where target - is any dependency, which hook belongs to. Hooks can be attached to any dependency, not only component. Components, which use this dependency, automatically update hook on first component mount and last component unmount. | ||
```js | ||
//@flow | ||
export interface LifeCycle<Dep> { | ||
/** | ||
* Called on first mount of any component, which uses description | ||
*/ | ||
onMount?: (dep: Dep) => void; | ||
/** | ||
* Called on last unmount of any component, which uses description | ||
*/ | ||
onUnmount?: (dep: Dep) => void; | ||
/** | ||
* Called on Dep dependencies changes | ||
*/ | ||
onUpdate?: (oldDep: Dep, newDep: Dep) => void; | ||
} | ||
``` | ||
Example: | ||
```js | ||
//@flow | ||
import {hooks} | ||
class UserService { | ||
start(): void { | ||
// subscribe to some observable | ||
} | ||
stop(): void { | ||
// unsubscribe from observable | ||
} | ||
} | ||
@hooks(UserService) | ||
class UserServiceHooks { | ||
/** | ||
* Hooks is regular dependency: we can use injection in constructor | ||
*/ | ||
constructor(deps: SomeDeps) {} | ||
/** | ||
* Called on first mount of any component, which use UserService | ||
*/ | ||
onMount(userService: UserService): void { | ||
userServiuce.start() | ||
} | ||
/** | ||
* Called on last unmount of any component, which use UserService | ||
*/ | ||
onUnmount(userService: UserService): void { | ||
userService.stop() | ||
} | ||
/** | ||
* Called on UserService constructor dependencies updates | ||
*/ | ||
onUpdate(oldUserService: UserService, newUserService: UserService): void { | ||
oldUserService.stop() | ||
newUserService.start() | ||
} | ||
} | ||
``` | ||
## Middlewares | ||
Middlewares used in development for logging method calls and property get/set. | ||
```js | ||
// @flow | ||
export interface ArgsInfo { | ||
id: string; | ||
type: string; | ||
className: ?string; | ||
propName: string; | ||
} | ||
export interface Middleware { | ||
get?: <R>(value: R, info: ArgsInfo) => R; | ||
set?: <R>(oldValue: R, newValue: R, info: ArgsInfo) => R; | ||
exec?: <R>(resolve: (...args: any[]) => R, args: any[], info: ArgsInfo) => R; | ||
} | ||
``` | ||
```js | ||
// @flow | ||
import type {ArgsInfo, Middleware} from 'reactive-di' | ||
class Mdl1 { | ||
exec<R>(fn: (args: any[]) => R, args: any[], info: ArgsInfo): R { | ||
console.log(`begin ${info.className ? 'method' : 'function'} ${info.id}`) | ||
const result: R = fn(args) | ||
console.log(`end ${info.id}`) | ||
return result | ||
} | ||
get<R>(result: R, info: ArgsInfo): R { | ||
console.log(`get ${info.id}: ${result}`) | ||
return result | ||
} | ||
set<R>(oldValue: R, newValue: R, info: ArgsInfo): R { | ||
console.log(`${info.id} changed from ${oldValue} to ${newValue}`) | ||
return newValue | ||
} | ||
} | ||
function createAdd(): (a: string) => { | ||
return function add(a: string): string { | ||
return a + 'b' | ||
} | ||
} | ||
class Service { | ||
add(a: string): string { | ||
return a + 'b' | ||
} | ||
} | ||
const di = (new Di()).middlewares([Mdl1]) | ||
// Function factories calls: | ||
di.val(createAdd).get()('a') | ||
// begin function add | ||
// end add | ||
// Class method calls | ||
di.val(Service).get().add('a') | ||
// begin method Service.add | ||
// end Service.add | ||
class TestClass { | ||
a: string = '1' | ||
} | ||
const tc: TestClass = di.val(TestClass).get() | ||
// Propery set/get: | ||
tc.a | ||
// get TestClass.a: 1 | ||
tc.a = '123' | ||
// TestClass.a changed from 1 to 213 | ||
``` | ||
## Complete example | ||
@@ -191,5 +575,4 @@ | ||
{children}: UserComponentProps, | ||
{theme, user, loading, saving, service}: UserComponentState, | ||
h | ||
): mixed { | ||
{theme, user, loading, saving, service}: UserComponentState | ||
) { | ||
if (loading.pending) { | ||
@@ -229,104 +612,2 @@ return <div class={theme.wrapper}>Loading...</div> | ||
## Middlewares | ||
Middlewares used for development for logging method calls and property get/set. | ||
```js | ||
// @flow | ||
export interface ArgsInfo { | ||
id: string; | ||
type: string; | ||
className: ?string; | ||
propName: string; | ||
} | ||
export interface Middleware { | ||
types?: string[]; | ||
get?: <R>(value: R, info: ArgsInfo) => R; | ||
set?: <R>(oldValue: R, newValue: R, info: ArgsInfo) => R; | ||
exec?: <R>(resolve: (...args: any[]) => R, args: any[], info: ArgsInfo) => R; | ||
} | ||
``` | ||
Function factories calls: | ||
```js | ||
// @flow | ||
import type {ArgsInfo, Middleware} from 'reactive-di' | ||
class Mdl1 { | ||
exec<R>(fn: (args: any[]) => R, args: any[], info: ArgsInfo): R { | ||
console.log(`begin ${info.className ? 'method' : 'function'} ${info.id}`) | ||
const result: R = fn(args) | ||
console.log(`end ${info.id}`) | ||
return result | ||
} | ||
} | ||
function createAdd(): (a: string) => { | ||
return function add(a: string): string { | ||
return a + 'b' | ||
} | ||
} | ||
const di = (new Di()).middlewares([Mdl1]) | ||
di.val(createAdd).get()('a') | ||
// begin function add | ||
// end add | ||
``` | ||
Class method calls: | ||
```js | ||
// @flow | ||
import type {ArgsInfo} from 'reactive-di' | ||
class Mdl1 { | ||
exec<R>(fn: (args: any[]) => R, args: any[], info: ArgsInfo): R { | ||
console.log(`begin ${info.className ? 'method' : 'function'} ${info.id}`) | ||
const result: R = fn(args) | ||
console.log(`end ${info.id}`) | ||
return result | ||
} | ||
} | ||
class Service { | ||
add(a: string): string { | ||
return a + 'b' | ||
} | ||
} | ||
const di = (new Di()).middlewares([Mdl1]) | ||
di.val(Service).get().add('a') | ||
// begin method Service.add | ||
// end Service.add | ||
``` | ||
Propery set/get: | ||
```js | ||
// @flow | ||
import type {ArgsInfo} from 'reactive-di' | ||
class Mdl1 { | ||
get<R>(result: R, info: ArgsInfo): R { | ||
console.log(`get ${info.id}: ${result}`) | ||
return result | ||
} | ||
set<R>(oldValue: R, newValue: R, info: ArgsInfo): R { | ||
console.log(`${info.id} changed from ${oldValue} to ${newValue}`) | ||
return newValue | ||
} | ||
} | ||
const wrapper = new MiddlewareFactory([new Mdl1()]) | ||
class TestClass { | ||
a: string = '1' | ||
} | ||
const tc: TestClass = di.val(TestClass).get() | ||
tc.a | ||
// get TestClass.a: 1 | ||
tc.a = '123' | ||
// TestClass.a changed from 1 to 213 | ||
``` | ||
## Manifest | ||
@@ -333,0 +614,0 @@ |
@@ -32,4 +32,4 @@ // @flow | ||
export function factory<V: Function>(target: V): V { | ||
dm(subtypeKey, 'func', target) | ||
export function factory<V: Function>(target: V, isJsx?: boolean): V { | ||
dm(subtypeKey, isJsx ? 'jsx' : 'func', target) | ||
return target | ||
@@ -41,2 +41,3 @@ } | ||
dm(metaKey, new ComponentMeta(rec || {}), target) | ||
dm(subtypeKey, 'jsx', target) | ||
return target | ||
@@ -43,0 +44,0 @@ } |
@@ -130,3 +130,3 @@ // @flow | ||
export function isComponent(target: Function): boolean { | ||
return typeof target === 'function' && gm(metaKey, target) | ||
return typeof target === 'function' && gm(subtypeKey, target) | ||
} | ||
@@ -133,0 +133,0 @@ |
@@ -74,9 +74,30 @@ // @flow | ||
_getCreateElement(): CreateElement<*, *> { | ||
const createElement = this._componentFactory.createElement | ||
const ce = this._componentFactory.createElement | ||
const createWrappedElement = (tag: Function, props?: ?{[id: string]: mixed}, ...children: any) => createElement( | ||
this.wrapComponent(tag), | ||
props, | ||
children.length ? children : null | ||
) | ||
const createWrappedElement = ( | ||
tag: Function, | ||
props?: ?{[id: string]: mixed}, | ||
...ch: any | ||
) => { | ||
switch (ch.length) { | ||
case 0: | ||
return ce(this.wrapComponent(tag), props) | ||
case 1: | ||
return ce(this.wrapComponent(tag), props, ch[0]) | ||
case 2: | ||
return ce(this.wrapComponent(tag), props, ch[0], ch[1]) | ||
case 3: | ||
return ce(this.wrapComponent(tag), props, ch[0], ch[1], ch[2]) | ||
case 4: | ||
return ce(this.wrapComponent(tag), props, ch[0], ch[1], ch[2], ch[3]) | ||
case 5: | ||
return ce(this.wrapComponent(tag), props, ch[0], ch[1], ch[2], ch[3], ch[4]) | ||
case 6: | ||
return ce(this.wrapComponent(tag), props, ch[0], ch[1], ch[2], ch[3], ch[4], ch[5]) | ||
case 7: | ||
return ce(this.wrapComponent(tag), props, ch[0], ch[1], ch[2], ch[3], ch[4], ch[5], ch[6]) | ||
default: | ||
return ce(this.wrapComponent(tag), props, ...ch) | ||
} | ||
} | ||
@@ -83,0 +104,0 @@ return createWrappedElement |
@@ -15,6 +15,16 @@ // @flow | ||
export interface LifeCycle<Dep> { | ||
/** | ||
* Called on first mount of any component, which uses description | ||
*/ | ||
onMount?: (dep: Dep) => void; | ||
/** | ||
* Called on last unmount of any component, which uses description | ||
*/ | ||
onUnmount?: (dep: Dep) => void; | ||
onAfterUpdate?: (dep: Dep) => void; | ||
/** | ||
* Called on Dep dependencies changes | ||
*/ | ||
onUpdate?: (oldDep: Dep, newDep: Dep) => void; | ||
} |
@@ -13,3 +13,2 @@ // @flow | ||
export interface Middleware { | ||
types?: string[]; | ||
get?: <R>(value: R, info: ArgsInfo) => R; | ||
@@ -16,0 +15,0 @@ set?: <R>(oldValue: R, newValue: R, info: ArgsInfo) => R; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
312813
4872
637