Skip to content

Commit 2ad9ebe

Browse files
feat(overmind): handle StateModel in devtools with getters
1 parent 5ac8d34 commit 2ad9ebe

File tree

14 files changed

+680
-867
lines changed

14 files changed

+680
-867
lines changed

package-lock.json

Lines changed: 529 additions & 837 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"url": "git+https://github.com/cerebral/overmind.git"
2525
},
2626
"dependencies": {
27+
"@babel/plugin-proposal-class-properties": "^7.8.0",
2728
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
2829
"@babel/plugin-transform-runtime": "^7.2.0",
2930
"@babel/preset-env": "^7.2.0",
@@ -42,7 +43,7 @@
4243
"is-plain-obj": "1.1.0",
4344
"lodash.throttle": "4.1.1",
4445
"marksy": "6.1.0",
45-
"npm": "6.13.4",
46+
"npm": "6.3.0",
4647
"page": "1.8.6",
4748
"petit-dom": "0.2.2",
4849
"preact": "8.3.1",

packages/node_modules/overmind/src/Devtools.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import isPlainObject from 'is-plain-obj'
2-
import { IS_PROXY } from 'proxy-state-tree'
2+
import { IS_PROXY, StateModel } from 'proxy-state-tree'
33

44
export type Message = {
55
type: string
@@ -12,6 +12,27 @@ export type DevtoolsMessage = {
1212
data: any
1313
}
1414

15+
export function safeTree(tree) {
16+
return Object.keys(tree).reduce((aggr, key) => {
17+
if (typeof tree[key] === 'function') {
18+
aggr[key] = '[Function]'
19+
} else if (isPlainObject(tree[key])) {
20+
aggr[key] = safeTree(tree[key])
21+
} else if (
22+
typeof tree[key] === 'object' &&
23+
!Array.isArray(tree[key]) &&
24+
tree[key] !== null &&
25+
!(tree[key] instanceof StateModel)
26+
) {
27+
aggr[key] = `[${tree[key].constructor.name || 'NOT SERIALIZABLE'}]`
28+
} else {
29+
aggr[key] = tree[key]
30+
}
31+
32+
return aggr
33+
}, {})
34+
}
35+
1536
export function safeValue(value) {
1637
if (typeof value === 'function') {
1738
return '[Function]'

packages/node_modules/overmind/src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
VALUE,
1414
} from 'proxy-state-tree'
1515

16-
import { Devtools, DevtoolsMessage, safeValue, safeValues } from './Devtools'
16+
import { Devtools, DevtoolsMessage, safeTree, safeValue, safeValues } from './Devtools'
1717
import {
1818
DefaultMode,
1919
EventType,
@@ -64,6 +64,9 @@ import {
6464
rehydrateState
6565
} from './utils'
6666

67+
export { StateModel } from 'proxy-state-tree'
68+
69+
6770
export * from './types'
6871

6972
export { createOperator, createMutationOperator }
@@ -824,7 +827,7 @@ export class Overmind<ThisConfig extends IConfiguration>
824827
devtools.send({
825828
type: 'init',
826829
data: {
827-
state: this.proxyStateTree.state,
830+
state: safeTree(this.proxyStateTree.state[VALUE]),
828831
actions: getActionPaths(actions),
829832
},
830833
})

packages/node_modules/proxy-state-tree/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"bundlesize": [
4848
{
4949
"path": "./dist/proxy-state-tree.min.js",
50-
"maxSize": "3 kB"
50+
"maxSize": "3.1 kB"
5151
}
5252
]
5353
}

packages/node_modules/proxy-state-tree/src/Proxyfier.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export const PATH = Symbol('PATH')
77
export const VALUE = Symbol('VALUE')
88
export const PROXY_TREE = Symbol('PROXY_TREE')
99

10+
export class StateModel {}
11+
1012
const arrayMutations = new Set([
1113
'push',
1214
'shift',
@@ -272,10 +274,10 @@ export class Proxifier {
272274
if (typeof prop === 'symbol' || prop in Object.prototype)
273275
return target[prop]
274276

275-
const descriptor = Object.getOwnPropertyDescriptor(target, prop)
277+
const descriptor = Object.getOwnPropertyDescriptor(target, prop) || Object.getOwnPropertyDescriptor(Object.getPrototypeOf(target), prop)
276278

277279
if (descriptor && 'get' in descriptor) {
278-
const value = descriptor.get.call(object[proxifier.CACHED_PROXY])
280+
const value = descriptor.get.call(proxy)
279281

280282
if (
281283
proxifier.tree.master.options.devmode &&
@@ -295,16 +297,16 @@ export class Proxifier {
295297
const nestedPath = proxifier.concat(path, prop)
296298
const currentTree = trackingTree || proxifier.tree
297299

298-
if (typeof targetValue === 'function' && target.constructor.name === 'Object') {
300+
if (typeof targetValue === 'function' && target instanceof StateModel) {
301+
return (...args) => targetValue.call(proxy, ...args)
302+
} else if (typeof targetValue === 'function') {
299303
return proxifier.tree.master.options.dynamicWrapper
300304
? proxifier.tree.master.options.dynamicWrapper(
301305
trackingTree || proxifier.tree,
302306
nestedPath,
303307
targetValue
304308
)
305309
: targetValue.call(target, proxifier.tree, nestedPath)
306-
} else if (targetValue === 'function') {
307-
return (...args) => targetValue.call(target, ...args)
308310
} else {
309311
currentTree.trackPathListeners.forEach((cb) => cb(nestedPath))
310312
trackingTree && trackingTree.proxifier.trackPath(nestedPath)
@@ -406,10 +408,10 @@ export class Proxifier {
406408
return this.proxify(value[VALUE], path)
407409
} else if (value[IS_PROXY]) {
408410
return value
411+
} else if (isPlainObject(value) || value instanceof StateModel) {
412+
return this.createObjectProxy(value, path)
409413
} else if (Array.isArray(value)) {
410414
return this.createArrayProxy(value, path)
411-
} else if (typeof value === 'object' && value !== null) {
412-
return this.createObjectProxy(value, path)
413415
}
414416
}
415417

packages/node_modules/proxy-state-tree/src/index.test.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IS_PROXY, ProxyStateTree } from './'
1+
import { IS_PROXY, ProxyStateTree, StateModel } from './'
22

33
describe('CREATION', () => {
44
test('should create a ProxyStateTree instance', () => {
@@ -126,7 +126,7 @@ describe('TrackMutationTree', () => {})
126126
describe('CLASSES', () => {
127127
describe('ACCESS', () => {
128128
test('should create proxy when accessed', () => {
129-
class User {
129+
class User extends StateModel {
130130
name = 'Bob'
131131
}
132132
const state = {
@@ -139,7 +139,7 @@ describe('CLASSES', () => {
139139
})
140140

141141
test('should access properties', () => {
142-
class User {
142+
class User extends StateModel {
143143
name = 'Bob'
144144
}
145145
const state = {
@@ -150,7 +150,7 @@ describe('CLASSES', () => {
150150
})
151151

152152
test('should track access properties', () => {
153-
class User {
153+
class User extends StateModel {
154154
name = 'Bob'
155155
}
156156
const state = {
@@ -166,10 +166,33 @@ describe('CLASSES', () => {
166166
'user.name',
167167
])
168168
})
169+
170+
test('should track getters', () => {
171+
class User extends StateModel {
172+
private firstName = 'Bob'
173+
private lastName = 'Saget'
174+
get name() {
175+
return this.firstName + ' ' + this.lastName
176+
}
177+
}
178+
const state = {
179+
user: new User()
180+
}
181+
const tree = new ProxyStateTree(state)
182+
const trackStateTree = tree.getTrackStateTree().track(() => {})
183+
184+
expect(trackStateTree.state.user.name).toBe('Bob Saget')
185+
186+
expect(Object.keys((tree as any).pathDependencies)).toEqual([
187+
'user',
188+
'user.firstName',
189+
'user.lastName',
190+
])
191+
})
169192
})
170193
describe('MUTATIONS', () => {
171194
test('should throw when mutating without tracking', () => {
172-
class User {
195+
class User extends StateModel {
173196
name = 'Bob'
174197
}
175198
const state = {
@@ -182,7 +205,7 @@ describe('CLASSES', () => {
182205
})
183206

184207
test('should throw if parts of state are nested', () => {
185-
class User {
208+
class User extends StateModel {
186209
name = 'Bob'
187210
}
188211
const state = {
@@ -196,7 +219,7 @@ describe('CLASSES', () => {
196219
})
197220
test('should not notify changes to same value', () => {
198221
let renderCount = 0
199-
class User {
222+
class User extends StateModel {
200223
name = 'Bob'
201224
}
202225
const state = {
@@ -221,7 +244,7 @@ describe('CLASSES', () => {
221244
expect(renderCount).toBe(0)
222245
})
223246
test('should track SET mutations', () => {
224-
class User {
247+
class User extends StateModel {
225248
name = 'Bob'
226249
}
227250
const state = {
@@ -242,7 +265,7 @@ describe('CLASSES', () => {
242265
expect(mutationTree.state.user.name).toBe('bar2')
243266
})
244267
test('should track mutations through methods', () => {
245-
class User {
268+
class User extends StateModel {
246269
name = 'Bob'
247270
changeName() {
248271
this.name = 'Bob2'

packages/node_modules/proxy-state-tree/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export {
2929
MutationTree,
3030
}
3131

32+
export { StateModel } from './Proxyfier'
33+
3234
export * from './types'
3335

3436
export class ProxyStateTree<T extends object> implements IProxyStateTree<T> {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# StateModel
2+
3+
If you prefer to model your state as a class you can use the **StateModel** base class. This is a class that Overmind is able to recognize to give you the reactive functionality as expected. This class also ensures that the devtools is able to present it correctly.
4+
5+
```marksy
6+
h(Example, { name: "guide/definingstate/statemodel" })
7+
```
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
export default (ts) =>
2+
ts
3+
? [
4+
{
5+
fileName: 'overmind/models.ts',
6+
code: `
7+
import { StateModel } from 'overmind'
8+
9+
export class User extends StateModel {
10+
public id: string
11+
private firstName: string
12+
private lastName: string
13+
constructor(firstName: string, lastName: string) {
14+
super()
15+
this.firstName = firstName
16+
this.lastName = lastName
17+
}
18+
changeName(firstName: string, lastName: string) {
19+
this.firstName = firstName
20+
this.lastName = lastName
21+
}
22+
get name() {
23+
return this.firstName + ' ' + this.lastName
24+
}
25+
}
26+
`,
27+
},
28+
]
29+
: [
30+
{
31+
fileName: 'overmind/models.js',
32+
code: `
33+
import { StateModel } from 'overmind'
34+
35+
export class User extends StateModel {
36+
constructor(firstName, lastName) {
37+
super()
38+
this.firstName = firstName
39+
this.lastName = lastName
40+
}
41+
changeName(firstName, lastName) {
42+
this.firstName = firstName
43+
this.lastName = lastName
44+
}
45+
get name() {
46+
return this.firstName + ' ' + this.lastName
47+
}
48+
}
49+
`,
50+
},
51+
]

0 commit comments

Comments
 (0)