From 44597e5900b0e3566bcdc6683557007cb65bfbf8 Mon Sep 17 00:00:00 2001 From: Ramy Ben Aroya Date: Fri, 17 Aug 2018 11:55:28 +0300 Subject: [PATCH 1/2] decorate - compose decorators for a single prop (#1652) * 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 https://github.com/mobxjs/mobx/pull/1652#discussion_r209952686 - Add decorators application order * Update README.md --- README.md | 18 ++++++++++ package.json | 3 +- src/api/decorate.ts | 30 ++++++++++++---- test/base/decorate.js | 81 ++++++++++++++++++++++++++++++++++++++++++- yarn.lock | 4 +++ 5 files changed, 128 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8adfef431..e4cbf5b83 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,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 Egghead.io lesson 3: computed values diff --git a/package.json b/package.json index e4c197042..5d6077a36 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,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", @@ -115,4 +116,4 @@ "/node_modules/" ] } -} \ No newline at end of file +} diff --git a/src/api/decorate.ts b/src/api/decorate.ts index 9c3b42fc8..1e687be61 100644 --- a/src/api/decorate.ts +++ b/src/api/decorate.ts @@ -2,25 +2,43 @@ import { invariant, isPlainObject } from "../utils/utils" export function decorate( clazz: new (...args: any[]) => T, - decorators: { [P in keyof T]?: MethodDecorator | PropertyDecorator } + decorators: { + [P in keyof T]?: + | MethodDecorator + | PropertyDecorator + | Array + | Array + } ): void export function decorate( object: T, - decorators: { [P in keyof T]?: MethodDecorator | PropertyDecorator } + decorators: { + [P in keyof T]?: + | MethodDecorator + | PropertyDecorator + | Array + | Array + } ): 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 diff --git a/test/base/decorate.js b/test/base/decorate.js index 7b7f3f199..8fec69602 100644 --- a/test/base/decorate.js +++ b/test/base/decorate.js @@ -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 @@ -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() +}) diff --git a/yarn.lock b/yarn.lock index 2ee906868..2bb2a3def 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4982,6 +4982,10 @@ send@0.15.3: 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" + serve-static@1.12.3: version "1.12.3" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2" From a703d0c6e99a5d38125488788923279563d58b1e Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Fri, 17 Aug 2018 10:57:58 +0200 Subject: [PATCH 2/2] Removed multiple decorators from readme and moved to gh-pages --- README.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/README.md b/README.md index e4cbf5b83..8adfef431 100644 --- a/README.md +++ b/README.md @@ -100,24 +100,6 @@ 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 Egghead.io lesson 3: computed values