diff --git a/docs/best/pitfalls.md b/docs/best/pitfalls.md index a1b245800..c4a40f65f 100644 --- a/docs/best/pitfalls.md +++ b/docs/best/pitfalls.md @@ -20,8 +20,7 @@ MobX observable _objects_ do not detect or react to property assignments that we So MobX observable objects act as records with predefined keys. You can use `extendObservable(target, props)` to introduce new observable properties to an object. However object iterators like `for .. in` or `Object.keys()` won't react to this automatically. -If you need a dynamically keyed object, for example to store users by id, create observable _map_s using `asMap`. -More info on [asMap](https://github.com/mobxjs/mobx/issues/219#issuecomment-220224813). +If you need a dynamically keyed object, for example to store users by id, create observable _map_s using [`observable.map`](../refguide/map.md). For more info see [what will MobX react to?](react.md). ### Use `@observer` on all components that render `@observable`'s. diff --git a/docs/intro/overview.md b/docs/intro/overview.md index 101b17c52..754a4d106 100644 --- a/docs/intro/overview.md +++ b/docs/intro/overview.md @@ -70,6 +70,8 @@ setInterval(action(function tick() { }), 1000); ``` -The `action` wrapper is only needed when using MobX in strict mode (by default off), but will help you to better structure applications and expresses the intention of a function to modify state. +The `action` wrapper is only needed when using MobX in strict mode (by default off). +It is recommended to use action though as it will help you to better structure applications and expresses the intention of a function to modify state. +Also it automatically applies transactions for optimal performance. Feel free to try this example on [JSFiddle](http://jsfiddle.net/mweststrate/wgbe4guu/) or by cloning the [MobX boilerplate project](https://github.com/mobxjs/mobx-react-boilerplate) diff --git a/docs/refguide/action.md b/docs/refguide/action.md index 38ba8cdcb..9cd67ba84 100644 --- a/docs/refguide/action.md +++ b/docs/refguide/action.md @@ -3,10 +3,12 @@ Usage: * `action(fn)` * `action(name, fn)` -* `@action classMethod` -* `@action(name) classMethod` +* `@action classMethod() {}` +* `@action(name) classMethod () {}` * `@action boundClassMethod = (args) => { body }` * `@action(name) boundClassMethod = (args) => { body }` +* `@action.bound classMethod() {}` +* `@action.bound(function() {}) Any application has actions. Actions are anything that modify the state. With MobX you can make it explicit in your code where your actions live by marking them. @@ -68,3 +70,40 @@ Example: The usage of `runInAction` is: `runInAction(name?, fn, scope?)`. If you use babel, this plugin could help you to handle your async actions: [mobx-deep-action](https://github.com/mobxjs/babel-plugin-mobx-deep-action). + +## Bound actions + +The `action` decorator / function follows the normal rules for binding in javascript. +However, Mobx 3 introduces `action.bound` to automatically bind actions to the targeted object. +Note that `(@)action.bound` does, unlike `action`, not take a name parameter, the name will always be based on the property the action is bound to. + +Example: + +```javascript +class Ticker { + @observable this.tick = 0 + + @action.bound + increment() { + this.tick++ // 'this' will always be correct + } +} + +const ticker = new Ticker() +setInterval(ticker.increment, 1000) +``` + +Or + +```javascript +const ticker = observable({ + tick: 1, + increment: action.bound(function() { + this.tick++ // bound 'this' + }) +}) + +setInterval(ticker.increment, 1000) +``` + +_Note: don't use *action.bind* with arrow functions; arrow functions are already bound and cannot be rebound._ diff --git a/docs/refguide/api.md b/docs/refguide/api.md index ddbf5e9a2..0ecd48e1a 100644 --- a/docs/refguide/api.md +++ b/docs/refguide/api.md @@ -1,46 +1,129 @@ # MobX Api Reference +Applies to MobX 3 and higher. For MobX 2, the old documentation is still available on [githib](https://github.com/mobxjs/mobx/blob/7c9e7c86e0c6ead141bb0539d33143d0e1f576dd/docs/refguide/api.md) + # Core API _The most important MobX api's. Understanding `observable`, `computed`, `reactions` and `actions` is enough to master MobX and use it in your applications!_ ## Creating observables -### `observable` + +### `observable(value)` Usage: * `observable(value)` * `@observable classProperty = value` Observable values can be JS primitives, references, plain objects, class instances, arrays and maps. +`observable(value)` is a convenience overload, that always tries to create the best matching observable types. +You can also directly create the desired observable type, see below. + The following conversion rules are applied, but can be fine-tuned by using *modifiers*. See below. -1. If *value* is wrapped in the *modifier* `asMap`: a new [Observable Map](map.md) will be returned. Observable maps are very useful if you don't want to react just to the change of a specific entry, but also to the addition or removal of entries. +1. If *value* is an wrapped is an instance of an [ES6 Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map): a new [Observable Map](map.md) will be returned. Observable maps are very useful if you don't want to react just to the change of a specific entry, but also to the addition or removal of entries. 1. If *value* is an array, a new [Observable Array](array.md) will be returned. -1. If *value* is an object *without* prototype, all its current properties will be made observable. See [Observable Object](object.md) +1. If *value* is an object *without* prototype, the object will be cloned and all its current properties will be made observable. See [Observable Object](object.md) 1. If *value* is an object *with* a prototype, a JavaScript primitive or function, a [Boxed Observable](boxed.md) will be returned. MobX will not make objects with a prototype automatically observable; as that is the responsibility of it's constructor function. Use `extendObservable` in the constructor, or `@observable` in it's class definition instead. These rules might seem complicated at first sight, but you will notice that in practice they are very intuitive to work with. Some notes: -* To create dynamically keyed objects use the `asMap` modifier! Only initially existing properties on an object will be made observable, although new ones can be added using `extendObservable`. +* To create dynamically keyed objects always use maps! Only initially existing properties on an object will be made observable, although new ones can be added using `extendObservable`. * To use the `@observable` decorator, make sure that [decorators are enabled](http://mobxjs.github.io/mobx/refguide/observable-decorator.html) in your transpiler (babel or typescript). -* By default making a data structure observable is *infective*; that means that `observable` is applied automatically to any value that is contained by the data structure, or will be contained by the data structure in the future. This behavior can be changed by using *modifiers*. +* By default making a data structure observable is *infective*; that means that `observable` is applied automatically to any value that is contained by the data structure, or will be contained by the data structure in the future. This behavior can be changed by using *modifiers* or *shallow*. [«`observable`»](observable.md) — [«`@observable`»](observable-decorator.md) -### `extendObservable` -Usage: `extendObservable(target, propertyMap)`. For each key/value pair in `propertyMap` a (new) observable property will be introduced on the target object. +### `@observable property = value` + +`observable` can also be used as property decorator. It requires [decorators to be enabled](../best/decorators.md) and is syntactic +sugar for `extendObservable(this, { property: value })`. + +[«`details`»](observable-decorator.md) + +### `observable.box(value)` & `observable.shallowBox(value)` + +Creates an observable _box_ that stores an observable reference to a value. Use `get()` to get the current value of the box, and `set()` to update it. +This is the foundation on which all other observables are built, but in practice you will use it rarely. +Normal boxes will automatically try to turn any new value into an observable if it isn't already. Use `shallowBox` to disable this behavior. + +[«`details`»](boxed.md) + +### `observable.object(value)` & `observable.shallowObject(value)` + +Creates a clone of the provided object and makes all it's properties observable. +By default any values in those properties will be made observable as well, but when using `shallowObject` only the properties will be made into observable +references, but the values will be untouched. (This holds also for any values assigned in the future) + +[«`details`»](object.md) + +### `observable.array(value)` & `observable.shallowArray(value)` + +Creates a new observable array based on the provided value. Use `shallowArray` if the values in the array should not be turned into observables. + +[«`details`»](array.md) + +### `observable.map(value)` & `observable.shallowMap(value)` + +Creates a new observable map based on the provided value. Use `shallowMap` if the values in the array should not be turned into observables. +Use `map` whenever you want to create a dynamically keyed collections and the addition / removal of keys needs to be observed. +Note that only string keys are supported. + +[«`details`»](map.md) + +### `extendObservable` & `extendShallowObservable` +Usage: `extendObservable(target, ...propertyMaps)`. For each key/value pair in each `propertyMap` a (new) observable property will be introduced on the target object. This can be used in constructor functions to introduce observable properties without using decorators. -If a value of the `propertyMap` is an argumentless function, a *computed* property will be introduced. +If a value of the `propertyMap` is a getter function, a *computed* property will be introduced. + +Use `extendShallowObservable` if the new properties should not be infective (that is; newly assigned values should not be turned into observables automatically). +Note that `extendObservable` enhances existing objects, unlike `observable.object` which creates a new object. + [«details»](extend-observable.md) +### Modifiers + +Modifiers can be used decorator or in combination with `extendObservable` and `observable.object` to change the autoconversion rules for specific properties. + +The following modifiers are available: + +* `observable.deep`: This is the default modifier, used by any observable. It converts any assigned, non-primitive value into an observable if it isn't one yet. +* `observable.ref`: Disables automatic observable conversion, just creates an observable reference instead. +* `observable.shallow`: Can only used in combination with collections. Turns any assigned collection into an collection, which is shallowly observable (instead of deep). In other words; the values inside the collection won't become observables automatically. +* `computed`: Creates a derived property, see [`computed`](computed-decorator.md) +* `action`: Creates an action, see [`action`](action.md) + +Modifiers can be used as decorator: + +```javascript +class TaskStore { + @observable.shallow tasks = [] +} +``` + +Or as property modifier in combination with `observable.object` / `observable.extendObservable`. +Note that modifiers always 'stick' to the property. So they will remain in effect even if a new value is assigned. + +```javascript +const taskStore = observable({ + tasks: observable.shallow([]) +}) +``` + +[«details»](modifiers.md) + + ## Computed values Usage: * `computed(() => expression)` +* `computed(() => expression, (newValue) => void)` +* `computed(() => expression, options)` * `@computed get classProperty() { return expression; }` +* `@computed.struct get classProperty() { return expression; }` Creates a computed property. The `expression` should not have side effects but return a value. The expression will automatically be re-evaluated if any observables it uses changes, but only if it is in use by some *reaction*. + [«details»](computed-decorator.md) ## Actions @@ -64,7 +147,8 @@ Usage: For one-time-actions `runInAction(name?, fn, scope?)` can be used, which is sugar for `action(name, fn, scope)()`. -## Reactions +## Reactions & Derivations + *Computed values* are **values** that react automatically to state changes. *Reactions* are **side effects** that react automatically to state changes. Reactions _can_ be used to ensure that a certain side effect (mainly I/O) is automatically executed when relevant state changes, like logging, network requests etc. @@ -105,16 +189,11 @@ It takes two function, the first one is tracked and returns data that is used as Unlike `autorun` the side effect won't be run initially, and any observables that are accessed while executing the side effect will not be tracked. The side effect can be debounced, just like `autorunAsync`. [«details»](reaction.md) -## Modifiers for `observable` - -By default `observable` is applied recursively and to values that are assigned in the future as well. -Modifiers can be used to influence how `observable` treats specific values. -* `asMap`: This is the most important modifier. Instead of creating an object with observable properties, an *Observable Map* is created instead. The main difference with observable objects is that the addition and removal of properties can be easily observed. Use `asMap` if you want a map like data structure where the keys will change over time. -* `asFlat`: Will not apply `observable` recursively. The passed object / collection itself will be observable, but the values in it won't. This disables the possibility to deeply observe objects. -* `asReference`: Use the passed in value verbatim, just create an observable reference to the object. -* `asStructure`: When new values are assigned, ignore the new value if it structurally equal to the previous value. - -[«details»](modifiers.md) +### `expr` +Usage: `expr(() => someExpression)`. Just a shorthand for `computed(() => someExpression).get()`. +`expr` is useful in some rare cases to optimize another computed function or reaction. +In general it is simpler and better to just split the function in multiple smaller computed's to achieve the same effect. +[«details»](expr.md) ------ @@ -281,21 +360,6 @@ Resets MobX internal global state. MobX by defaults fails fast if an exception o This function resets MobX to the zero state. Existing `spy` listeners and the current value of strictMode will be preserved though. -# Functions that might get deprecated - -### `map` -*Will probably by deprecated, use `observable(asMap())` instead*. Usage: `map()`, `map(keyValueObject)`, `map(entries)`. -Returns an observable, largely ES6 compliant [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) data structure. -This is useful if you want to store data based on string keys. -For the full api of the returned `ObservableMap` see *Observable maps*. -[«details»](map.md) - -### `expr` -Usage: `expr(() => someExpression)`. Just a shorthand for `computed(() => someExpression).get()`. -`expr` is useful in some rare cases to optimize another computed function or reaction. -In general it is simpler and better to just split the function in multiple smaller computed's to achieve the same effect. -[«details»](expr.md) - diff --git a/docs/refguide/array.md b/docs/refguide/array.md index 87867e2e7..10aed031d 100644 --- a/docs/refguide/array.md +++ b/docs/refguide/array.md @@ -1,6 +1,6 @@ ## Observable Arrays -Similar to objects, arrays can be made observable using `observable`. +Similar to objects, arrays can be made observable using `observable.array(values?)` or by passing an array to `observable`. This works recursively as well, so all (future) values of the array will also be observable. ```javascript @@ -30,8 +30,7 @@ todos.shift(); // Prints: 'Remaining: Make coffee, Take a nap' ``` -Due to limitations of native arrays in ES5 (`array.observe` is only available in ES7, and arrays cannot be extend), -`observable` will instrument a clone of the provided array instead of the original one. +Due to limitations of native arrays in ES5 `observable.array` will create a faux-array (array-like object) instead of a real array. In practice, these arrays work just as fine as native arrays and all native methods are supported, including index assignments, up-to and including the length of the array. Bear in mind however that `Array.isArray(observable([]))` will yield `false`, so whenever you need to pass an observable array to an external library, @@ -42,13 +41,22 @@ Unlike the built-in implementation of the functions `sort` and `reverse`, observ Besides all built-in functions, the following goodies are available as well on observable arrays: -* `intercept(interceptor)`. Can be used to intercept any change before it is applied to the array. See [observe & intercept](observe.md) +* `intercept(interceptor)`. Can be used to intercept any change before it is applied to the array. See [observe & intercept](observe.md) * `observe(listener, fireImmediately? = false)` Listen to changes in this array. The callback will receive arguments that express an array splice or array change, conforming to [ES7 proposal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/observe). It returns a disposer function to stop the listener. * `clear()` Remove all current entries from the array. * `replace(newItems)` Replaces all existing entries in the array with new ones. * `find(predicate: (item, index, array) => boolean, thisArg?, fromIndex?)` Basically the same as the ES7 `Array.find` proposal, except for the additional `fromIndex` parameter. * `remove(value)` Remove a single item by value from the array. Returns `true` if the item was found and removed. * `peek()` Returns an array with all the values which can safely be passed to other libraries, similar to `slice()`. + In contrast to `slice`, `peek` doesn't create a defensive copy. Use this in performance critical applications if you know for sure that you use the array in a read-only manner. In performance critical sections it is recommended to use a flat observable array as well. +## `observable.shallowArray(values)` + +Any values assigned to an observable array will be default passed through [`observable`](observable.md) to make them observable. +Create a shallow array to disable this behavior and store are values as-is. See also [modifiers](modifiers.md) for more details on this mechanism. + +## Name argument + +Both `observable.array` and `observable.shallowArray` take a second parameter which is used as debug name in for example `spy` or the MobX dev tools. \ No newline at end of file diff --git a/docs/refguide/boxed.md b/docs/refguide/boxed.md index de726ada0..ac2ea949a 100644 --- a/docs/refguide/boxed.md +++ b/docs/refguide/boxed.md @@ -6,17 +6,30 @@ See also [observable objects](object.md). In rare cases it can be convenient to have an observable "primitive" that is not owned by an object. For these cases it is possible to create an observable box that manages such a primitive. -So `observable` accepts scalar values as well and returns an object with a getter / setter function that holds this value. +### `observable.box(value)` + +So `observable.box(value)` accepts any value and stores it inside a box. +The current value can be accessed through `.get()` and updated using `.set(newValue)`. + Furthermore you can register a callback using its `.observe` method to listen to changes on the stored value. -But in most cases it is better to use [`mobx.autorun`](autorun.md) instead. +But since MobX tracks changes to boxes automatically, in most cases it is better to use a reaction like [`mobx.autorun`](autorun.md) instead. -So the signature of object returned by `observable(scalar)` is: +So the signature of object returned by `observable.box(scalar)` is: * `.get()` Returns the current value. * `.set(value)` Replaces the currently stored value. Notifies all observers. * `intercept(interceptor)`. Can be used to intercept changes before they are applied. See [observe & intercept](observe.md) * `.observe(callback: (newValue, previousValue) => void, fireImmediately = false): disposerFunction`. Registers an observer function that will fire each time the stored value is replaced. Returns a function to cancel the observer. See [observe & intercept](observe.md) -Example: +### `observable.shallowBox(value)` + +`shallowBox` creates a box based on the [`ref`](modifiers.md) modifier. This means that any (future) value of box wouldn't be converted into an observable automatically. + + +### `observable(primitiveValue)` + +When using the generic `observable(value)` method, MobX will create an observable box for any value that could not be turned into an observable automatically.. + +### Example ```javascript import {observable} from "mobx"; @@ -68,4 +81,6 @@ cityName.splice(0, 1); // prints 'Amsterdam removed' ``` -Similar to `observable`, `computed(function)` creates a boxed computed value. See [`computed`](http://mobxjs.github.io/mobx/refguide/computed-decorator.html) +## Name argument + +Both `observable.box` and `observable.shallowBox` take a second parameter which is used as debug name in for example `spy` or the MobX dev tools. diff --git a/docs/refguide/computed-decorator.md b/docs/refguide/computed-decorator.md index 300d1cabc..2a888d204 100644 --- a/docs/refguide/computed-decorator.md +++ b/docs/refguide/computed-decorator.md @@ -1,17 +1,31 @@ -# @computed +# (@)computed -Decorator that can be used on ES6 or TypeScript derivable class properties to make them observable. -The `@computed` can only be used on `get` functions for instance properties. +Computed values are values that can be derived from the existing state or other computed values. +Conceptually, they are very similar to formulas in spreadsheets. +Computed values can't be underestimated, as they help you to make your actual modifyable state as small as possible. +Besides that they are higly optimized, so use them whereever possible. -Use `@computed` if you have a value that can be derived in a pure manner from other observables. +Don't confuse `computed` with `autorun`. They are both reactively invoked expressions, +but use `@computed` if you want to reactively produce a *value* that can be used by other observers and +`autorun` if you don't want to produce a new value but rather want ot achieve an *effect*. +For example imperative side effects like logging, making network requests etc. -Don't confuse `@computed` with `autorun`. They are both reactively invoked expressions, -but use `@computed` if you want to reactively produce a new value that can be used by other observers and -`autorun` if you don't want to produce a new value but rather invoke some imperative code like logging, network requests etc. +Computed values are automatically derived from your state if any value that affects them changes. +Computed values can be optimized away in many cases by MobX as they are assumed to be pure. +For example, a computed property won't re-run if none of the data used in the previous computation did change. +Nor will a computed property re-run if is not in use by some other computed property or reaction. +In such cases it will be suspended. -Computed properties can be optimized away in many cases by MobX as they are assumed to be pure. -So they will not be invoked when their input parameters didn't modifiy or if they are not observed by some other computed value or autorun. +This automatic suspension is very convenient; it means that a computed value that is not observed anymore (for example because the UI in which it was used has disappeared) +can automatically be garbabged collected by MobX, and unlike `autorun`'s you don't have to dispose them yourself. +This sometimes confuses people new to MobX; if you create a computed property but don't use it anywhere in a reaction, it will not cache it's value and recompute more often than seems necesarray. +However, in real life situations this by far the best default, and you can always forcefully keep a computed value awake if you need to by using either [`observe`](observe.md) or [`keepAlive`](https://github.com/mobxjs/mobx-utils#keepalive). +Note that `computed` properties are not enumerable. Nor can they be overwritten in an inheritance chain. + +## `@computed` + +If you have [decorators enabled](../best/decorators.md) you can use the `@computed` decorator on any getter of a class property to declaratively created computed properties. ```javascript import {observable, computed} from "mobx"; @@ -30,33 +44,34 @@ class OrderLine { } ``` -If your environment doesn't support decorators or field initializers, -`@computed get funcName() { }` is sugar for [`extendObservable(this, { funcName: func })`](extend-observable.md) - +## `computed` modifier -`@computed` can be parameterized. `@computed({asStructure: true})` makes sure that the result of a derivation is compared structurally instead of referentially with its preview value. This makes sure that observers of the computation don't re-evaluate if new structures are returned that are structurally equal to the original ones. This is very useful when working with point, vector or color structures for example. It behaves the same as the `asStructure` modifier for observable values. +If your environment doesn't support decorators, use the `computed(expression)` modifier incombination with `extendObservable` / `observable` to introduce new computed properties. -`@computed` properties are not enumerable. Nor can they be overwritten in an inheritance chain. - -# Creating computed values with `observable` or `extendObservable`. - -The functions `observable(object)` or `extendObservable(target, properties)` can be used to introduce computed properties as well, -as alternative to using the decorator. For this ES5 getters can be used, so the above example can also be written as: +`@computed get propertyName() { }` is basically sugar for [`extendObservable(this, { propertyName: get func() { } })`](extend-observable.md) in the constructor call. ```javascript -var orderLine = observable({ - price: 0, - amount: 1, - get total() { - return this.price * this.amount +import {extendObservable, computed} from "mobx"; + +class OrderLine { + constructor(price) { + extendObservable(this, { + price: price, + amount: 1, + // valid: + get total() { + return this.price * this.amount + }, + // also valid: + total: computed(function() { + return this.price * this.amount + }) + }) } -}); +} ``` -_Note: The support for getters was introduced in MobX 2.5.1. MobX will automatically convert any argumentless function that is passed as property value to `observable` / `extendObservable` to a computed property as well, -but that form will disappear in the next major version_. - -# Setters for computed values +## Setters for computed values It is possible to define a setter for computed values as well. Note that these setters cannot be used to alter the value of the computed property directly, but they can be used as 'inverse' of the derivation. For example: @@ -73,29 +88,33 @@ const box = observable({ }); ``` -This also works: +And similarly + ```javascript class Foo { @observable length: 2, @computed get squared() { return this.length * this.length; - }, + } set squared(value) { //this is automatically an action, no annotation necessary this.length = Math.sqrt(value); } } ``` +_Note: always define the setter *after* the getter, some TypeScript versions are known to declare two properties with the same name otherwise._ + _Note: setters require MobX 2.5.1 or higher_ -# `computed(expression)` +## `computed(expression)` as function `computed` can also be invoked directly as function. -Just like `observable(primitive value)` it will create a stand-alone observable. +Just like `observable.box(primitive value)` it will create a stand-alone observable. Use `.get()` on the returned object to get the current value of the computation, or `.observe(callback)` to observe its changes. This form of `computed` is not used very often, but in some cases where you need to pass a "boxed" computed value around it might prove useful. Example: + ```javascript import {observable, computed} from "mobx"; var name = observable("John"); @@ -109,3 +128,16 @@ var disposer = upperCaseName.observe(name => console.log(name)); name.set("Dave"); // prints: 'DAVE' ``` + +## Options for `computed` + +When using `computed` as modifier or as box, it accepts a second options argument with the following optional arguments: + +* `name`: String, the debug name used in spy and the MobX devtools +* `context`: The `this` that should be used in the provided expression +* `setter`: The setter function to be used. Without setter it is not possible to assign new values to a computed value. If the second argument passed to `computed` is a function, this is assumed to be a setter. +* `compareStructural`: By default `false`. If this value, the output of the expression is structurally compared with the previous value before any observer is notified about a change. This makes sure that observers of the computation don't re-evaluate if new structures are returned that are structurally equal to the original ones. This is very useful when working with point, vector or color structures for example. + +## `@computed.struct` for structural comparison + +The `@computed` decorator does not take arguments. If you want to to create a computed property which does structural comparison, use `@computed.struct`. \ No newline at end of file diff --git a/docs/refguide/extend-observable.md b/docs/refguide/extend-observable.md index f1040fb00..10eeec921 100644 --- a/docs/refguide/extend-observable.md +++ b/docs/refguide/extend-observable.md @@ -3,8 +3,6 @@ Quite similar to `Object.assign`, `extendObservable` takes two or more arguments, a `target` object and one or more `properties` maps. It adds all key-value pairs from the properties to the `target` as observable properties. -If an argumentless function is passed as value of a property, `extendObservable` will introduce a [`computed`](./computed-decorator.md) property instead of an observable property. - ```javascript var Person = function(firstName, lastName) { // initialize observable properties on a new instance @@ -22,4 +20,57 @@ extendObservable(matthew, { }); ``` -(N.b: `observable(object)` is actually an alias for `extendObservable(object, object)`) +N.b: `observable.object(object)` is actually an alias for `extendObservable({}, object)`. + +Note that the property maps are not always copied literally onto the target, but they are considered property descriptor. +Most values are copied as-is, but values wrapped in a modifier as treated specially. And so are properties that have a getter. + +## Modifiers + +[Modifiers](modifiers.md) can be used to define special behavior for certain properties. +For example `observable.ref` creates an observable reference which doesn't automatically convert it's values into observables, and `computed` introduces a derived property: + +```javascript +var Person = function(firstName, lastName) { + // initialize observable properties on a new instance + extendObservable(this, { + firstName: observable.ref(firstName), + lastName: observable.ref(lastName), + fullName: computed(function() { + return this.firstName + " " + this.lastName + }) + }); +} +``` + +An overview of all available modifiers can be found in the [modifiers](modifiers.md) section. + +## Computed properties + +Computed properties can also be written by using a *getter* function. Optionally accompanied with a setter: + +```javascript +var Person = function(firstName, lastName) { + // initialize observable properties on a new instance + extendObservable(this, { + firstName: firstName, + lastName: lastName, + get fullName() { + return this.firstName + " " + this.lastName + }, + set fullName(newValue) { + var parts = newValue.split(" ") + this.firstName = parts[0] + this.lastName = parts[1] + } + }); +} +``` + +_Note: getter / setter is valid ES5 syntax and doesn't require a transpiler!_ + +## `extendShallowObservable` + +`extendShallowObservable` is like `extendObservable`, except that by default the properties will by default *not* automatically convert their values into observables. +So it is similar to calling `extendObservable` with `observable.ref` modifier for each property. +Note that `observable.deep` can be used to get the automatic conversion back for a specific property. diff --git a/docs/refguide/map.md b/docs/refguide/map.md index 2887572a1..8f2e5751b 100644 --- a/docs/refguide/map.md +++ b/docs/refguide/map.md @@ -1,17 +1,18 @@ # Observable Maps -`observable(asMap(values?, modifier?))` (and `map(values?, modifier?)`) creates a dynamic keyed observable map. +## `observable.map(values)` + +`observable.map(values?)` creates a dynamic keyed observable map. Observable maps are very useful if you don't want to react just to the change of a specific entry, but also to the addition or removal of entries. -Optionally takes an object or entries array with initial values. +Optionally takes an object, entries array or string keyed [ES6 map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) with initial values. Unlike ES6 maps, only strings are accepted as keys. -The modifier param can be one of the MobX [modifiers](modifiers.md), such as `asReference`. The following methods are exposed according to the [ES6 Map spec](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map): * `has(key)` Returns whether this map has an entry the provided key. Note that the presence of a key is an observable fact in itself. * `set(key, value)`. Sets the given `key` to `value`. The provided key will be added to the map if it didn't exist yet. * `delete(key)`. Deletes the given key and its value from the map. -* `get(key)`. Returns the value at the given key (or `undefined`). +* `get(key)`. Returns the value at the given key (or `undefined`). * `keys()`. Returns all keys present in this map. The insertion order is preserved. * `values()`. Returns all values present in this map. Insertion order is preserved. * `entries()`. Returns an (insertion ordered) array that for each key/value pair in the map contains an array `[key, value]`. @@ -22,6 +23,16 @@ The following methods are exposed according to the [ES6 Map spec](https://develo The following functions are not in the ES6 spec but are available in MobX: * `toJS()`. Returns a shallow plain object representation of this map. (For a deep copy use `mobx.toJS(map)`). -* `intercept(interceptor)`. Registers an interceptor that will be triggered before any changes are applied to the map. See [observe & intercept](observe.md). +* `intercept(interceptor)`. Registers an interceptor that will be triggered before any changes are applied to the map. See [observe & intercept](observe.md). * `observe(listener, fireImmediately?)`. Registers a listener that fires upon each change in this map, similarly to the events that are emitted for [Object.observe](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe). See [observe & intercept](observe.md) for more details. -* `merge(object | map)`. Copies all entries from the provided object into this map. +* `merge(values)`. Copies all entries from the provided object into this map. `values` can be a plain object, array of entries or string-keyed ES6 Map. +* `replace(values)`. Replaces the entire contents of this map with the provided values. Short hand for `.clear().merge(values)` + +## `observable.shallowMap(values)` + +Any values assigned to an observable map will be default passed through [`observable`](observable.md) to make them observable. +Create a shallow map to disable this behavior and store are values as-is. See also [modifiers](modifiers.md) for more details on this mechanism. + +## Name argument + +Both `observable.map` and `observable.shallowMap` take a second parameter which is used as debug name in for example `spy` or the MobX dev tools. \ No newline at end of file diff --git a/docs/refguide/modifiers.md b/docs/refguide/modifiers.md index 1aaddbbea..7de3fb43b 100644 --- a/docs/refguide/modifiers.md +++ b/docs/refguide/modifiers.md @@ -1,132 +1,115 @@ # Modifiers for observable -By default, `observable` recursively makes all the values of _plain_ objects and arrays recursively observable. -Besides that, it automatically converts functions without arguments into reactive views or derived properties. -For all other types of values just a reference is stored. -In general, this should just do what you need, but if you want you can override the default behavior using _modifiers_. -Note that modifiers are 'sticky', they are interpreted as being annotations. -They do not only apply to the current value, but also to all values that are assigned in the future to the same attribute. +Modifiers can be used decorator or in combination with `extendObservable` and `observable.object` to change the autoconversion rules for specific properties. -Note that the attributes class instances (all objects that have a prototype) will not be made observable automatically by `observable`. -It is considered to be the responsibility of the class definition / constructor function to mark the necessary attributes of a class instance observable / computed. +* `observable.deep`: This is the default modifier, used by any observable. It converts any assigned, non-primitive value into an observable value if it isn't one yet. +* `observable.ref`: Disables automatic observable conversion, just creates an observable reference instead. +* `observable.shallow`: Can only used in combination with collections. Turns any assigned collection into an collection, which is shallowly observable (instead of deep) +* `computed`: Creates a derived property, see [`computed`](computed-decorator.md) +* `action`: Creates an action, see [`action`](action.md) -## computed +## Deep observability -Introduces a computed property, see [`computed`](http://mobxjs.github.io/mobx/refguide/computed-decorator.html) +When MobX creates an observable object, (using `observable`, `observable.object`, or `extendObservable`), it introduces observable properties which +by default use the `deep` modifier. The deep modifier basically recursively calls `observable(newValue)` for any newly assigned value. +Which in turns uses the `deep` modifier... you get the idea. -## action +This is a very convenient default. Without any additional effort all values assigned to an observable will themselves be made observable too (unless they already are), so no additional +effort is required to make objects deep observable. -Marks a function as action, see [`action`](http://mobxjs.github.io/mobx/refguide/action.html) +## Reference observability -## asMap +In some cases however, objects don't need to be converted into observables. +Typical cases are immutable objects, or objects that are not managed by you but by an external library. +Examples are JSX elements, DOM elements, native objects like History, window or etc. +To those kind of objects, you just want to store a reference without turning them into an observable. -Creates a new Observable Map instead of an Observable Object. See [`asMap`](map.md) +For these situations there is the `ref` modifier. It makes sure that an observable property is created, which only tracks the reference but doesn't try to convert it's value. +For example: -## asReference +```javascript +class Message { + @observable message = "Hello world" -The most common modifier is `asReference`. -If this modifier is used, `observable` will not attempt to make the value observable. -Use this for example if you want to store a reference to a function, instead of creating a view based on that function. -You can also use it to prevent that plain objects or arrays are made observable automatically. + // ficitonal example, if author is immutable, we just need to store a reference and shouldn't turn it into an mutable, observable object + @observable.ref author = null +} +``` -```javascript +Or with just ES5 syntax: -var test = observable({ - x : 3, - doubler: function() { - return this.x*2; - }, - someFunc: asReference(function() { - return this.x; - }) -}); - -console.log(test.doubler); // === 6 -console.log(test.someFunc); // still a function +```javascript +function Message() { + extendObservable({ + message: "Hello world", + author: observable.ref(null) + }) +} ``` -## asStructure +Note that an observable, boxed reference can be created by using `const box = observable.shallowBox(value)` + +## Shallow observability -Can be used on non-cyclic, plain JavaScript values. -Instead of comparing old values with new values based on whether the reference has changed, values are compared using deep equality before notifying any observers. -This is useful if you are working with 'struct' like objects like colors or coordinates and each time return fresh objects with possibly the same values. -`asStructure` can be used on reactive functions, plain objects and arrays. +The `observable.shallow` modifier applies observability 'one-level-deep'. You need those if you want to create a _collection_ of observable references. +If a new collection is assigned to a property with this modifier, it will be made observable, but it's values will be left as is, so unlike `deep`, it won't recurse. +Example: ```javascript -var ViewPort = mobxReact.observer(React.createClass({ - displayName: 'ViewPort', - - componentWillMount: function() { - mobx.extendObservable(this, { - screenSize: { - width: 0, - height: 0 - }, - minSize: { - width: 400, - height: 300 - }, - viewPortSize: mobx.asStructure(function() { - return { - width: Math.max(this.screenSize.width, this.minSize.width), - height: Math.max(this.screenSize.height, this.minSize.height) - } - } - }); - - window.onresize = function() { - mobx.transaction(function() { - this.screenSize.width = window.innerWidth; - this.screenSize.height = window.innerHeight; - }); - }.bind(this); - }, - - render: function() { - return ( -