Skip to content

Commit

Permalink
Refactor toJS for mobx4 (#1733)
Browse files Browse the repository at this point in the history
* Recursively convert mobx observable to plain object with an optional recurseEverything flag

* Sync up changes with refactor-toJS
  • Loading branch information
wangyiz4262 authored and mweststrate committed Sep 28, 2018
1 parent 57ae8d2 commit 952e4a8
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 27 deletions.
58 changes: 31 additions & 27 deletions src/api/tojs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { isObservableArray } from "../types/observablearray"
import { isObservableObject } from "../types/observableobject"
import { isObservableMap } from "../types/observablemap"
import { isObservableValue } from "../types/observablevalue"
import { isObservable } from "./isobservable"
Expand All @@ -8,11 +7,13 @@ import { keys } from "./object-api"
export type ToJSOptions = {
detectCycles?: boolean
exportMapsAsObjects?: boolean
recurseEverything?: boolean
}

const defaultOptions: ToJSOptions = {
detectCycles: true,
exportMapsAsObjects: true
exportMapsAsObjects: true,
recurseEverything: false
}

function cache<K, V>(map: Map<any, any>, key: K, value: V, options: ToJSOptions): V {
Expand All @@ -21,37 +22,35 @@ function cache<K, V>(map: Map<any, any>, key: K, value: V, options: ToJSOptions)
}

function toJSHelper(source, options: ToJSOptions, __alreadySeen: Map<any, any>) {
if (!isObservable(source)) 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

if (
detectCycles &&
source !== null &&
typeof source === "object" &&
__alreadySeen.has(source)
) {
if (detectCycles && source !== null && __alreadySeen.has(source)) {
return __alreadySeen.get(source)
}

if (isObservableArray(source)) {
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
for (let i = 0, l = toAdd.length; i < l; i++) res[i] = toAdd[i]
return res
}

if (isObservableObject(source)) {
const res = cache(__alreadySeen, source, {}, options)
keys(source) // make sure we track the keys of the object
for (let key in source) {
res[key] = toJSHelper(source[key], options!, __alreadySeen)
}
return res
}

if (isObservableMap(source)) {
if (isObservableMap(source) || Object.getPrototypeOf(source) === Map.prototype) {
if (options.exportMapsAsObjects === false) {
const res = cache(__alreadySeen, source, new Map(), options)
source.forEach((value, key) => {
Expand All @@ -67,9 +66,13 @@ function toJSHelper(source, options: ToJSOptions, __alreadySeen: Map<any, any>)
}
}

if (isObservableValue(source)) return toJSHelper(source.get(), options!, __alreadySeen)
// Fallback to the situation that source is an ObservableObject or a plain object
const res = cache(__alreadySeen, source, {}, options)
for (let key in source) {
res[key] = toJSHelper(source[key], options!, __alreadySeen)
}

return source
return res
}

/**
Expand All @@ -78,16 +81,17 @@ function toJSHelper(source, options: ToJSOptions, __alreadySeen: Map<any, any>)
export function toJS<T>(source: T, options?: ToJSOptions): T
export function toJS(source: any, options?: ToJSOptions): any
export function toJS(source, options: ToJSOptions) // internal overload
export function toJS(source, options: ToJSOptions) {
if (!isObservable(source)) return source

export function toJS(source, options?: ToJSOptions) {
// backward compatibility
if (typeof options === "boolean") options = { detectCycles: options }
if (!options) options = defaultOptions
const detectCycles = options.detectCycles === true
options.detectCycles =
options.detectCycles === undefined
? options.recurseEverything === true
: options.detectCycles === true

let __alreadySeen
if (detectCycles) __alreadySeen = new Map()
if (options.detectCycles) __alreadySeen = new Map()

return toJSHelper(source, options, __alreadySeen)
}
63 changes: 63 additions & 0 deletions test/base/tojs.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,66 @@ 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.a).toBe("a")
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)
})

describe("observable array", function() {
test("observable array should be converted to a plain array", function() {
const arr = [1, 2, 3]
expect(mobx.toJS(mobx.observable.array(arr), { recurseEverything: true })).toEqual(arr)
expect(mobx.toJS(arr, { recurseEverything: true })).toEqual(arr)
})

test("observable array inside an array will be converted with recurseEverything flag", function() {
const obj = { arr: mobx.observable.array([1, 2, 3]) }
expect(mobx.isObservable(mobx.toJS(obj).arr)).toBeTruthy()
expect(mobx.isObservable(mobx.toJS(obj, { recurseEverything: true }).arr)).toBeFalsy()
expect(mobx.toJS(obj, { recurseEverything: true }).arr).toEqual([1, 2, 3])
})
})

test("detectCycles should forcibly be set to true if recurseEverything is true", function() {
const cycledObj = {}
cycledObj.cycle = cycledObj
const convertedObj = mobx.toJS({ key: cycledObj }, { recurseEverything: true })
expect(convertedObj.key).toBe(convertedObj.key.cycle)
})
})

0 comments on commit 952e4a8

Please sign in to comment.