Skip to content

Commit

Permalink
decorate - compose decorators for a single prop (#1652)
Browse files Browse the repository at this point in the history
* decorate - compose decorators for a single prop

- Reduce multiple decorators to a single decorator
- Added a test for multiple decorators (@action + custom) on a function property
- Added a test for multiple decorators (@observable + @serializable) on
  a regular property
- Added a usage example in readme under `decorate` section

* delete package-lock

* doc: update README

- Change caveat note about docorators composition as suggested in
  #1652 (comment)
- Add decorators application order

* Update README.md
  • Loading branch information
Ramy Ben Aroya authored and mweststrate committed Aug 17, 2018
1 parent a967356 commit 433452a
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 8 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,24 @@ decorate(Todo, {
})
```

For applying multiple decorators on a single property, you can pass an array of decorators. The decorators application order is from right to left.
```javascript
import { decorate, observable } from "mobx"
import { serializable, primitive } from "serializr"
import persist from "mobx-persist";

class Todo {
id = Math.random();
title = "";
finished = false;
}
decorate(Todo, {
title: [serializable(primitive), persist("object"), observable],
finished: [serializable(primitive), observable]
})
```
Note: Not all decorators can be composed together, and this functionality is just best-effort. Some decorators affect the instance directly and can 'hide' the effect of other decorators that only change the prototype.

### Computed values

<i><a style="color: white; background:green;padding:5px;margin:5px;border-radius:2px" href="https://egghead.io/lessons/javascript-derive-computed-values-and-manage-side-effects-with-mobx-reactions">Egghead.io lesson 3: computed values</a></i>
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"rollup": "^0.41.6",
"rollup-plugin-filesize": "^1.3.2",
"rollup-plugin-node-resolve": "^3.0.0",
"serializr": "^1.3.0",
"size-limit": "^0.2.0",
"tape": "^4.2.2",
"ts-jest": "^22.0.0",
Expand Down Expand Up @@ -116,4 +117,4 @@
"<rootDir>/node_modules/"
]
}
}
}
30 changes: 24 additions & 6 deletions src/api/decorate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,43 @@ import { invariant, isPlainObject } from "../internal"

export function decorate<T>(
clazz: new (...args: any[]) => T,
decorators: { [P in keyof T]?: MethodDecorator | PropertyDecorator }
decorators: {
[P in keyof T]?:
| MethodDecorator
| PropertyDecorator
| Array<MethodDecorator>
| Array<PropertyDecorator>
}
): void
export function decorate<T>(
object: T,
decorators: { [P in keyof T]?: MethodDecorator | PropertyDecorator }
decorators: {
[P in keyof T]?:
| MethodDecorator
| PropertyDecorator
| Array<MethodDecorator>
| Array<PropertyDecorator>
}
): T
export function decorate(thing: any, decorators: any) {
process.env.NODE_ENV !== "production" &&
invariant(isPlainObject(decorators), "Decorators should be a key value map")
const target = typeof thing === "function" ? thing.prototype : thing
for (let prop in decorators) {
const decorator = decorators[prop]
let propertyDecorators = decorators[prop]
if (!Array.isArray(propertyDecorators)) {
propertyDecorators = [propertyDecorators]
}
process.env.NODE_ENV !== "production" &&
invariant(
typeof decorator === "function",
`Decorate: expected a decorator function for '${prop}'`
propertyDecorators.every(decorator => typeof decorator === "function"),
`Decorate: expected a decorator function or array of decorator functions for '${prop}'`
)
const descriptor = Object.getOwnPropertyDescriptor(target, prop)
const newDescriptor = decorator(target, prop, descriptor)
const newDescriptor = propertyDecorators.reduce(
(accDescriptor, decorator) => decorator(target, prop, accDescriptor),
descriptor
)
if (newDescriptor) Object.defineProperty(target, prop, newDescriptor)
}
return thing
Expand Down
81 changes: 80 additions & 1 deletion test/base/decorate.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import {
isComputedProp,
spy,
isAction,
decorate
decorate,
reaction
} from "../../src/mobx"

import { serializable, primitive, serialize, deserialize } from "serializr"

test("decorate should work", function() {
class Box {
// @ts-ignore
Expand Down Expand Up @@ -368,3 +371,79 @@ test("decorate should not allow @observable on getter", function() {
expect(() => obj.x).toThrow(/"y"/)
expect(() => obj.y).toThrow()
})

test("decorate a function property with two decorators", function() {
let callsCount = 0
let spyCount = 0
const spyDisposer = spy(ev => {
if (ev.type === "action" && ev.name === "fn") spyCount++
})

const countFunctionCallsDecorator = (target, key, descriptor) => {
const func = descriptor.value
descriptor.value = function wrapper(...args) {
const result = func.call(this, ...args)
callsCount++
return result
}
for (const key in func) {
descriptor.value[key] = func[key]
}
return descriptor
}

class Obj {
fn() {}
}

decorate(Obj, {
fn: [action("fn"), countFunctionCallsDecorator]
})

const obj = new Obj()

expect(isAction(obj.fn)).toBe(true)

obj.fn()

expect(callsCount).toEqual(1)
expect(spyCount).toEqual(1)

obj.fn()

expect(callsCount).toEqual(2)
expect(spyCount).toEqual(2)

spyDisposer()
})

test("decorate a property with two decorators", function() {
let updatedByAutorun

class Obj {
x = null
}

decorate(Obj, {
x: [serializable(primitive()), observable]
})

const obj = deserialize(Obj, {
x: 0
})

const d = autorun(() => {
updatedByAutorun = obj.x
})

expect(isObservableProp(obj, "x")).toBe(true)
expect(updatedByAutorun).toEqual(0)

obj.x++

expect(obj.x).toEqual(1)
expect(updatedByAutorun).toEqual(1)
expect(serialize(obj).x).toEqual(1)

d()
})
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5225,6 +5225,10 @@ [email protected]:
range-parser "~1.2.0"
statuses "~1.3.1"

serializr@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/serializr/-/serializr-1.3.0.tgz#6c7f977461d54a24bb1f17a03ed0ce61d239b010"

[email protected]:
version "1.12.3"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2"
Expand Down

0 comments on commit 433452a

Please sign in to comment.