From 56e2ac201bfb929ee1f16ff1ef19c771f69a4a7d Mon Sep 17 00:00:00 2001 From: Yizhe Wang Date: Sun, 26 Aug 2018 21:57:56 -0400 Subject: [PATCH] Added option recurseEverything to toJS --- src/api/tojs.ts | 41 +++++++++++++++++++------------------- test/base/tojs.js | 50 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 65 insertions(+), 26 deletions(-) diff --git a/src/api/tojs.ts b/src/api/tojs.ts index 6b9b991226..eea14103c3 100644 --- a/src/api/tojs.ts +++ b/src/api/tojs.ts @@ -1,15 +1,13 @@ -import { - isObservable, - isObservableArray, - isObservableMap, - isObservableObject, - isObservableValue, - keys -} from "../internal" +import { keys } from "./object-api" +import { isObservable } from "./isobservable" +import { isObservableArray } from "../types/observablearray" +import { isObservableValue } from "../types/observablevalue" +import { isObservableMap } from "../types/observablemap" export type ToJSOptions = { detectCycles?: boolean exportMapsAsObjects?: boolean + recurseEverything?: boolean } const defaultOptions: ToJSOptions = { @@ -23,8 +21,18 @@ function cache(map: Map, key: K, value: V, options: ToJSOptions) } function toJSHelper(source, options: ToJSOptions, __alreadySeen: Map) { - if (typeof source !== "object") { - return source + if (!options.recurseEverything && !isObservable(source)) return source + + if (typeof source !== "object") return source + + // Directly return the Date object itself if contained in the observable + if (source instanceof Date) return source + + if (isObservableValue(source)) return toJSHelper(source.get(), options!, __alreadySeen) + + // make sure we track the keys of the object + if (isObservable(source)) { + keys(source) } const detectCycles = options.detectCycles === true @@ -33,7 +41,7 @@ function toJSHelper(source, options: ToJSOptions, __alreadySeen: Map) return __alreadySeen.get(source) } - if (isObservableArray(source) || Object.getPrototypeOf(source) === Array.prototype) { + if (isObservableArray(source) || Array.isArray(source)) { const res = cache(__alreadySeen, source, [] as any, options) const toAdd = source.map(value => toJSHelper(value, options!, __alreadySeen)) res.length = toAdd.length @@ -57,20 +65,13 @@ function toJSHelper(source, options: ToJSOptions, __alreadySeen: Map) } } - if (isObservableValue(source)) return toJSHelper(source.get(), options!, __alreadySeen) - - // Directly return the Date object itself if contained in the observable - if (source instanceof Date) return source - - // Fallback to situation if source is an ObservableObject or a plain object + // Fallback to the situation that source is an ObservableObject or a plain object const res = cache(__alreadySeen, source, {}, options) - Object.setPrototypeOf(res, Object.getPrototypeOf(source)) for (let key in source) { res[key] = toJSHelper(source[key], options!, __alreadySeen) } - return res - return source + return res } /** diff --git a/test/base/tojs.js b/test/base/tojs.js index 50c30d1523..36d1102904 100644 --- a/test/base/tojs.js +++ b/test/base/tojs.js @@ -155,7 +155,7 @@ test("json2", function() { url: "booking.com" } }) - expect(mobx.toJS(o)).toEqual({ + expect(JSON.parse(JSON.stringify(o))).toEqual({ todos: [ { title: "write blog", @@ -288,7 +288,7 @@ test("#285 non-mobx class instances with toJS", () => { // check before lazy initialization expect(mobx.toJS(p1)).toEqual({ firstName: "michel", - lastName: "weststrate" // toJS will recurse into any object that may contain observable value + lastName: nameObservable // toJS doesn't recurse into non observable objects! }) }) @@ -298,11 +298,9 @@ test("verify #566 solution", () => { const b = mobx.observable({ x: 3 }) const c = mobx.observable({ a: a, b: b }) - expect(mobx.toJS(c).a).toEqual(a) - expect(mobx.toJS(c).a).toBeInstanceOf(MyClass) - expect(mobx.isObservableObject(c.b)).toBeTruthy() - expect(mobx.isObservableObject(mobx.toJS(c).b)).toBeFalsy() + expect(mobx.toJS(c).a).toBe(a) expect(mobx.toJS(c).b).toEqual(mobx.toJS(c.b)) + expect(mobx.toJS(c).b.x).toEqual(b.x) }) test("verify already seen", () => { @@ -343,3 +341,43 @@ test("json cycles when exporting maps as maps", function() { expect(cloneD.get("c")).toBe(cloneC) expect(cloneA.e).toBe(cloneA) }) + +describe("recurseEverything set to true", function() { + test("prototype chain will be removed even if the object is not observable", function() { + function Person() { + this.firstname = "michel" + this.lastname = "weststrate" + } + const p = new Person() + + expect(mobx.toJS(p)).toBeInstanceOf(Person) + expect(mobx.toJS(p, { recurseEverything: true })).not.toBeInstanceOf(Person) + expect(mobx.toJS(p)).toEqual({ firstname: "michel", lastname: "weststrate" }) + expect(mobx.toJS(p)).toEqual(mobx.toJS(p, { recurseEverything: true })) + }) + + test("properties on prototype should be flattened to plain object", function() { + const observableValue = mobx.observable.box("b") + const Base = function() { + this.a = "a" + } + const derived = Object.create(new Base(), { + b: { value: observableValue, enumerable: true } + }) + + const simpleCopy = mobx.toJS(derived) + const deepCopy = mobx.toJS(derived, { recurseEverything: true }) + expect(simpleCopy).toBeInstanceOf(Base) + expect(simpleCopy).toEqual({ b: observableValue }) + expect(simpleCopy.hasOwnProperty("a")).toBeFalsy() + + expect(deepCopy).not.toBeInstanceOf(Base) + expect(deepCopy).toEqual({ a: "a", b: "b" }) + expect(deepCopy.hasOwnProperty("a")).toBeTruthy() + }) + + test("Date type should not be converted", function() { + const date = new Date() + expect(mobx.toJS(mobx.observable.box(date), { recurseEverything: true })).toBe(date) + }) +})