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"