The first primitive in the MDV design is a Model abstraction which creates a mechanism through which mutations to objects can be expressed directly, but allows observers to be notified when mutations occur.
The Model abstraction allows for two types of observation:
Further, it guarantees that
<script>
var data = {person: { name: 'Jim', age: 32 }};
var m = Model.get(data);
Model.observeObject(m, function(change) {
// Handle property add/update/deletes
});
Model.observe(m, "person.age", function(value, oldValue) {
// Handle value change of m.person.age
});
m.person.age = 33; // Second observer fires: (value = 33, oldValue=32)
m.parent = { name: 'Sam', age: 72 }; // First observer fires (property 'parent' added)
data.person.name = 'James'; // No observers are fired. Only model objects are observable.
</script>
Note: the code samples in this section are pseudo-code.
A Path is generally a JSON-style path. It is modelled after the
member lookup syntax in ECMAScript. Dot (.) and bracket
([]) notation are supported.
{"a": {"b": {"c": 42, "d": [97, 13]}}}
a.b.c => 42
a.b.d[1] => 13
A path can also contain parent (../), this
(./) and root (/). The syntax for these where
inspired by the file system path syntax. It is important to point out that even
though a path can reach above itself, whenever it is used it will never be able
to reach above the reference object.
The parent path name (../) allows you to select a
value above the current reference point. In general this is not that useful
since you cannot reach above the current reference object. However, having a
parent selector is useful when concatenating paths. When concatenating multiple
paths the parent strips the previous part.
path-a = a.b.c
path-b = ../d
concat(path-a, path-b) => a.b.d
The this path name (./) allows you to select the value
at the current reference point.
path-a = a.b.c
path-b = ./
concat(path-a, path-b) => a.b.c
The root path name (/) selects the root of the
reference. It's value is that, when concatenating multiple path it strips
everything to the left of the root part.
path-a = a.b.c
path-b = /d.e
concat(path-a, path-b) => /d.e
A Path Value is the value of a property at a path from a given reference object. If the path is unreachable, the value is considered undefined.
Needs to be formalized.
A Model object is a proxy for an underlying "raw" data object. The model can be read and written to as if it were the underlying object, but writes will cause observers to be notified of mutations. The object which the model is proxing is the proxied object. An object has at most one model and a model has at most one proxied object. Models have the following behaviors.
this inside the function body is the proxied object, not the model.Further:
Array.isArray(Model.get([])) === false. This is likely to be surprising to application code, and also causes Array.prototype.concat to not work as expected on models. ECMA TC-39 is aware of the problem and looking for a fix.
Clarify. The point here is that writes to models will fire notifications to observers. Further that the model maintain a wet/dry boundary for the models/proxied objects. The wet side is the "outside", i.e. traversing from a model will always return objects wrapped as models. The dry side is the "inside", i.e. functions of proxied objects (including setters) should never see model objects, only proxied "raw" objects.
Arrays objects behave like other objects with respect to models, except that Array fire "splice" mutation events when index properties are affected.
Model.get(object, [opt_path])
Create or fetch the existing model for a the value at a opt_path from object.
The Model maintains an internal weak mapping between raw objects and model objects. At most, one model object will exist for a given raw object.
The call behaves as follows:
Model.observeObject(object, callback)
Register a callback to be invoked every time object is mutated.
The call behaves as follows:
Model.get to retrieve the appropriate model object.object is not observable (not an object), the call silently returns.callback is guaranteed to be invoked eventually for each mutation.callback is provided a single change argument. Note that change is a model object which means that object-valued references from it are also models.change encodes only one type of mutation:
{
mutation: 'splice',
index: [UInt32]
added: [Array of added values]
removed: [Array of removed values]
}
change argument encodes all mutations as adds, updates or deletes of a named object property.
{
mutation: ['add' | 'update' | 'delete' ],
propertyName: [String],
value: [current value],
oldValue: [previous value - if 'update' or 'delete']
}
mutation is considered an 'update' and its oldValue is the value from the prototype chain. Likewise, if a property is deleted but the property is still available via the prototype chain, the mutation is considered an 'update' and its value is the value from the prototype chain.Model.observe(object, path, callback)
Register a callback to be invoked when the value at path from the object changes.
The call behaves as follows:
Model.get to retrieve the appropriate model object.object is not observable (not an object), the call silently returns.callback will not be invoked if, by the time it is to be called, the value has been returned to the callback's lastObservedValue.path is unreachable from object, it is considered to be undefined.lastObservedValue for each registered callback which is initially the value at the time of registration and updated to be the value after each invocation of callback.callback is invoked with two arguments:
callback(value, lastObservedValue)
Model.observe returns a PathValue objectObservers affected by an object mutation are notified in an order which reflects decreasing proximity to the mutation. Specifically:
Model.observeObject observers of the object that was mutatedModel.observe observers, breadth-first, starting at one property away from the object and expanded until all are reached.