Skip to content

Commit 4e6ca67

Browse files
Built in support for arrays, inheritance, removed Collection
1 parent d74ff7f commit 4e6ca67

10 files changed

+598
-212
lines changed

.vscode/launch.json

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
// Use IntelliSense to learn about possible Node.js debug attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"type": "node",
9+
"request": "launch",
10+
"name": "Launch Program",
11+
"program": "${workspaceRoot}/built/test/arrays.js",
12+
"cwd": "${workspaceRoot}",
13+
"outFiles": []
14+
},
15+
{
16+
"type": "node",
17+
"request": "attach",
18+
"name": "Attach to Process",
19+
"port": 5858,
20+
"outFiles": []
21+
}
22+
]
23+
}

README.md

+39-37
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
# json-mobx
22
*Simple undo/redo and persistence for MobX*
33

4+
npm install --save json-mobx
5+
46
Based on a single trivial concept: an object must have a mutable property called `json` that holds its JSON representation (and by JSON we mean a plain object tree that can be round-tripped via JSON).
57

6-
As the `json` property is mutable, it means you can restore the object to a prior state by assigning to its `json` property. This is in contrast to most serialization systems which deserialize by creating a brand new tree of objects. Here we tend towards *minimally updating* an existing tree to bring it into line with the provided JSON.
8+
As the `json` property is mutable, it means you can restore the object to a prior state by assigning to its `json` property. This is in contrast to most serialization systems which deserialize by creating a brand new tree of objects. Here we tend towards *reconciling* or *minimally updating* an existing tree to bring it into line with the provided JSON.
79

810
This is particularly suited to situations where an object is not pure data but is also dependent on (or depended on by) the "environment". This is closely related to the way React components can use `componentDidMount` and `componentWillUnmount` to wire themselves into environmental dependencies. Or to put it another way, objects have a life-cycle.
911

10-
npm install --save json-mobx
12+
It also means that, thanks to MobX, implementing Undo/Redo is very easy. Prior states can be captured efficiently, and can be "loaded into" a live object hierarchy with minimal impact on unchanged objects.
1113

12-
## The `json` decorator
14+
In these notes we will use the term *live object* to refer to objects that have a `json` computed property.
1315

14-
For many simple types of object, which just have a set of properties that need to be stored, it is a pain to write the `json` property by hand. So we provide a `json` decorator:
16+
## The `@json` decorator
17+
18+
For typical live objects it is a pain to write the `json` computed property by hand. So we provide a `json` decorator:
1519

1620
```ts
1721
export class Widget {
@@ -27,24 +31,9 @@ export class Widget {
2731

2832
This class automagically gains a hidden `json` property. It is defined by a MobX `computed` so it only regenerates the JSON representation if anything changes.
2933

30-
There are also helper functions `json.save` and `json.load`. These are trivial and just deal with missing objects and checking that the `json` property exists before accessing or updating it:
34+
## Saving and Loading
3135

32-
```ts
33-
function load(obj: any, data: any) {
34-
if (data) {
35-
checkJsonProperty(obj);
36-
obj.json = data;
37-
}
38-
}
39-
40-
function save(obj: any) {
41-
if (!obj) {
42-
return undefined;
43-
}
44-
checkJsonProperty(obj);
45-
return obj.json;
46-
}
47-
```
36+
There are also helper functions `json.save` and `json.load`. These select the correct way to serialize based on the kind of object. Note that `json.load` does not return a new de-serialized object. Rather, it updates the object passed to it.
4837

4938
So:
5039

@@ -59,38 +48,52 @@ json.load(w2, j);
5948

6049
If you use the `@json` decorator on a property that refers to an object with its own `json` implementation, that implementation will be used to persist that object, so ultimately you have control over what is saved/loaded and what side-effects this can have (this is important when objects may be "wired up" to external dependencies).
6150

62-
This means that stateful objects form a tree in which each object has a single owner.
51+
This means that stateful objects form a tree in which each object has a single owner. Where a `@json` property refers to another live object, consider marking it `readonly`; instead of allocating a new object, you will be reconfiguring the existing one. (This is not an absolute rule, but usually makes sense.)
6352

64-
Everything else is just extending or consuming this idea.
53+
## Arrays
54+
There is built-in support for arrays. The `save` and `load` functions descend recursively into the items of arrays. Arrays of plain objects or primitives are not treated any differently to primitives.
6555

66-
## Built-in classes
56+
More interestingly, you can construct an array property using `json.arrayOf(SomeClass)` instead of plain `[]`:
6757

68-
Building on this idea, we define two built-in classes, but note that these are just objects with their own `json` property implementation and are very simple, so you can see them as examples for rolling your own varieties:
58+
```ts
59+
class FancyItem {
60+
@json firstName = "Homer";
61+
@json lastName = "Simpson";
62+
}
63+
64+
class HasFancyArray {
65+
@json readonly fancyArray = json.arrayOf(FancyItem);
66+
67+
// instead of:
68+
// @json fancyArray = [];
69+
}
70+
```
6971

70-
* `Collection` (an array of objects)
71-
* `Polymorph` (a reference to a child object that can be of a set of types)
72+
Again, note the use of `readonly` on the `fancyArray` property. This is a good idea because if you accidentally assigned a new (ordinary) array to `fancyArray` it would lose the ability to perform reconciliation.
7273

73-
In addition we provide `Undo`, an automatic undo/redo system suitable for editors. You construct it by passing an object with a `json` property, and it does the rest.
74+
Behind the scenes, the items of the array will be stamped with unique IDs, which are later used for reconciliation. When a prior state of `HasFancyArray` is restored by `json.load`, it will match up the data with the right objects by ID. It will also use the `FancyItem` constructor to default-construct any additional objects specified in the saved state, so it can load into them.
7475

75-
## Collection
76+
The type parameter of `json.arrayOf` is constrained so it must be a `new`-able class constructor that takes no arguments. If it has a method `dispose`, that must require no arguments (it will be automatically called whenever `json.load` discards an existing item from the array).
7677

77-
The `Collection` class holds a readonly observable array of `items`. Each item is an object with a `json` property, and the collection's own `json` format is an array.
78+
This reconciliation process is closely analogous to React's treatement of the virtual DOM. The optional `dispose` method works like `componentWillUnmount`, providing objects with an opportunity to detach themselves from any environmental dependencies before they are abandoned.
7879

79-
Furthermore `Collection` mandates that each item must have a property called `id`, which is either a number or a string. This is very closely analogous to React's `key` prop. When the `json` is assigned to, the `Collection` compares its array of items with the provided array and makes minimal updates, matching items by `id`.
80+
Also the auto-generated ID stamped onto each array item plays a similar role to React's `key` prop. The major difference is that you don't need to specify the ID manually.
8081

81-
Optionally, items can have a `dispose` method. This is called when the diffing process discards an item. This is again closely analogous to `componentWillUnmount`, providing objects with an opportunity to detach themselves from any environmental dependencies.
82+
If you want to use the ID as a React `key`, you can get it with `json.idOf(item)`. But note that it will return the value 0 until its containing array has been saved. (If you attach an `Undo` system to your root object, this will happen automatically).
8283

8384
## Polymorph
8485

85-
A polymorph is a container that holds exactly one object, of a type that may change. It doesn't just refer to the object; it *owns* it (just as `Collection` owns all its items), so it can optionally `dispose` it when necessary.
86+
As you can create a class with a `json` computed property to define a custom serialization technique, it is easy to extend this library. One very common requirement is to refer to an object whose precise type may vary. This is supported by the built-in `Polymorph` class, though in reality it is just a (very simple) example of a class with a custom `json` computed property.
87+
88+
A polymorph is a container that holds exactly one object, of a type that may change. It doesn't just refer to the object; it *owns* it, so it can optionally `dispose` it when necessary.
8689

8790
To construct it, you specify the string that names the initial type and a `factory` function that constructs an instance of the type (the constructor immediately calls it to get the initial instance).
8891

8992
```ts
9093
constructor(type: string, private factory: (type: string) => T) ...
9194
```
9295

93-
It has a `readonly` property `target` which is the current owned instance.
96+
It has a `readonly` property `target` which is the current owned instance (note: this property's value may change. It is `readonly`, not `const`!)
9497

9598
It has `get` and `set` methods that operate on the object type. So `p.set("MisterTickle")` will assign a new instance of `MisterTickle` (that is, whatever is returned when the factory is called with `"MisterTickle"`). If you're familiar with [bidi-mobx](https://github.com/danielearwicker/bidi-mobx) you'll have noticed that this makes it a `BoxedValue` holding the type of the object, so it can be bound to a `SelectString`.
9699

@@ -103,11 +106,10 @@ It implements the `json` property so that the format is
103106
}
104107
```
105108

106-
The `settings` part depends on the type.
109+
The `settings` part depends on the type: `Polymorph` simply uses `json.load` and `json.save` to take care of it.
107110

108111
When the type changes, the previous instance has its `dispose` method called, if any. Also `Polymorph` itself implements `dispose` by calling on to the current instance's `dispose`, if any.
109112

110113
## Undo
111114

112-
When you construct an `Undo` object you pass it the root object-with-a-`json`-property and it immediately captures the current state. It does this inside `autorun`, so if the state changes it will be recaptured. The second time this happens, the previous state is pushed onto the undo stack. `Undo` has public properties `canUndo` and `canRedo`, and methods `undo` and `redo`, so you can link those up to a couple of toolbar buttons in an editor.
113-
115+
When you construct an `Undo` object you pass it the root live object and it immediately captures the current state. It does this inside `autorun`, so if the state changes it will be recaptured. The second time this happens, the previous state is pushed onto the undo stack. `Undo` has public properties `canUndo` and `canRedo`, and methods `undo` and `redo`, so you can link those up to a couple of toolbar buttons in an editor.

index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export * from "./src/Disposable";
22
export * from "./src/json";
33
export * from "./src/Polymorph";
4-
export * from "./src/Collection";
54
export * from "./src/Undo";

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-mobx",
3-
"version": "0.4.0",
3+
"version": "0.5.3",
44
"description": "Simple undo/redo and persistence for MobX",
55
"main": "built/index.js",
66
"types": "built/index.d.ts",

src/Collection.ts

-61
This file was deleted.

0 commit comments

Comments
 (0)