From 433452a3e42127c75939579c8bbd4ae18f4dd259 Mon Sep 17 00:00:00 2001 From: Ramy Ben Aroya Date: Fri, 17 Aug 2018 11:55:28 +0300 Subject: [PATCH] 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 3c00f5f7c..554f1a4d4 100644 --- a/README.md +++ b/README.md @@ -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 Egghead.io lesson 3: computed values diff --git a/package.json b/package.json index d20c7d66e..80ae08262 100644 --- a/package.json +++ b/package.json @@ -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", @@ -116,4 +117,4 @@ "/node_modules/" ] } -} \ No newline at end of file +} diff --git a/src/api/decorate.ts b/src/api/decorate.ts index dcf649497..fad6e4705 100644 --- a/src/api/decorate.ts +++ b/src/api/decorate.ts @@ -2,25 +2,43 @@ import { invariant, isPlainObject } from "../internal" 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 695344fc2..848809d4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5225,6 +5225,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"