diff --git a/README.md b/README.md index d04b67bc3..5a974c468 100644 --- a/README.md +++ b/README.md @@ -77,12 +77,12 @@ const Box = types.model("Box",{ x: 0, y: 0, - // computed prop + // computed prop / views get width() { return this.name.length * 15 - }, - - // action + } +}, { + // actions move(dx, dy) { this.x += dx this.y += dy @@ -91,7 +91,8 @@ const Box = types.model("Box",{ const BoxStore = types.model("BoxStore",{ boxes: types.map(Box), - selection: types.reference("boxes/name"), + selection: types.reference("boxes/name") +}, { addBox(name, x, y) { const box = Box.create({ id: uuid(), name, x, y }) this.boxes.put(box) @@ -158,6 +159,14 @@ Useful methods: It is not necessary to express all logic around models as actions. For example it is not possible to define constructors on models. Rather, it is recommended to create stateless utility methods that operate on your models. It is recommended to keep models self-contained and to do orchestration around models in utilities around it. +## Views + +TODO + +Views versus actions + +Exception: `"Invariant failed: Side effects like changing state are not allowed at this point."` indicates that a view function tries to modifies a model. This is only allowed in actions. + ## Protecting the state tree By default it is allowed to both directly modify a model or through an action. @@ -167,7 +176,8 @@ To disable modifying data in the tree without action, simple call `protect(model ```javascript const Todo = types.model({ - done: false, + done: false +}, { toggle() { this.done = !this.done } @@ -318,9 +328,9 @@ The result of this function is the return value of the callbacks, or the origina **Parameters** -- `value` -- `asNodeCb` -- `asPrimitiveCb` +- `value` +- `asNodeCb` +- `asPrimitiveCb` ## ComplexType @@ -396,7 +406,7 @@ Example of a logging middleware: **Parameters** - `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** model to intercept actions on -- `middleware` +- `middleware` Returns **IDisposer** function to remove the middleware @@ -411,7 +421,7 @@ Patches can be used to deep observe a model tree. **Parameters** - `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** the model instance from which to receive patches -- `callback` +- `callback` Returns **IDisposer** function to remove the listener @@ -423,8 +433,8 @@ Applies a JSON-patch to the given model instance or bails out if the patch could **Parameters** -- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** -- `patch` **IJsonPatch** +- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** +- `patch` **IJsonPatch** ## applyPatches @@ -434,8 +444,8 @@ Applies a number of JSON patches in a single MobX transaction **Parameters** -- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** -- `patches` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<IJsonPatch>** +- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** +- `patches` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<IJsonPatch>** ## applyActions @@ -447,9 +457,9 @@ Does not return any value **Parameters** -- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** -- `actions` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<IActionCall>** -- `options` **\[IActionCallOptions]** +- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** +- `actions` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<IActionCall>** +- `options` **\[IActionCallOptions]** ## protect @@ -462,7 +472,7 @@ To disable modifying data in the tree without action, simple call `protect(model **Parameters** -- `target` +- `target` **Examples** @@ -489,7 +499,7 @@ Returns true if the object is in protected mode, @see protect **Parameters** -- `target` +- `target` ## applySnapshot @@ -499,8 +509,8 @@ Applies a snapshot to a given model instances. Patch and snapshot listeners will **Parameters** -- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** -- `snapshot` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** +- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** +- `snapshot` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** ## hasParent @@ -510,10 +520,10 @@ Given a model instance, returns `true` if the object has a parent, that is, is p **Parameters** -- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** +- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** - `depth` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** = 1, how far should we look upward? -Returns **[boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** +Returns **[boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** ## getPath @@ -523,9 +533,9 @@ Returns the path of the given object in the model tree **Parameters** -- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** +- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** -Returns **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** +Returns **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** ## getPathParts @@ -535,9 +545,9 @@ Returns the path of the given object as unescaped string array **Parameters** -- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** +- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** -Returns **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>** +Returns **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>** ## isRoot @@ -547,9 +557,9 @@ Returns true if the given object is the root of a model tree **Parameters** -- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** +- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** -Returns **[boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** +Returns **[boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** ## resolve @@ -559,10 +569,10 @@ Resolves a path relatively to a given object. **Parameters** -- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** +- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** - `path` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** escaped json path -Returns **Any** +Returns **Any** ## tryResolve @@ -570,10 +580,10 @@ Returns **Any** **Parameters** -- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** -- `path` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** +- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** +- `path` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** -Returns **Any** +Returns **Any** ## clone @@ -581,10 +591,10 @@ Returns **Any** **Parameters** -- `source` **T** -- `keepEnvironment` +- `source` **T** +- `keepEnvironment` -Returns **T** +Returns **T** ## detach @@ -594,7 +604,7 @@ Removes a model element from the state tree, and let it live on as a new state t **Parameters** -- `thing` +- `thing` ## destroy @@ -604,7 +614,7 @@ Removes a model element from the state tree, and mark it as end-of-life; the ele **Parameters** -- `thing` +- `thing` ## applyAction @@ -615,9 +625,9 @@ Returns the value of the last actoin **Parameters** -- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** -- `action` **IActionCall** -- `options` **\[IActionCallOptions]** +- `target` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** +- `action` **IActionCall** +- `options` **\[IActionCallOptions]** ## escapeJsonPath @@ -628,7 +638,7 @@ escape slashes and backslashes **Parameters** -- `str` +- `str` ## unescapeJsonPath @@ -638,7 +648,7 @@ unescape slashes and backslashes **Parameters** -- `str` +- `str` # FAQ @@ -683,13 +693,17 @@ So far this might look a lot like an immutable state tree as found for example i ## TypeScript & MST +TypeScript support is best effort, as not all patterns can be expressed in TypeScript. But except for assigning snapshots to properties we got pretty close! As MST uses the latest fancy typescript features it is recommended to use TypeScript 2.3 or higher, with `noImplicitThis` and `strictNullChecks` enabled. + When using models, you write interface along with it's property types that will be used to perform type checks at runtime. What about compile time? You can use TypeScript interfaces indeed to perform those checks, but that would require writing again all the properties and their actions! + Good news? You don't need to write it twice! Using the `typeof` operator of TypeScript over the `.Type` property of a MST Type, will result in a valid TypeScript Type! ```typescript const Todo = types.model({ - title: types.string, + title: types.string +}, { setTitle(v: string) { this.title = v } diff --git a/changelog.md b/changelog.md index 14b245f4e..fdebfe939 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,34 @@ +# 0.4.0 + +**BREAKING** `types.model` no requires 2 parameters to define a model. The first parameter defines the properties, derived values and view functions. The second argment is used to define the actions. For example: + +```javascript +const Todo = types.model("Todo", { + done: types.boolean, + toggle() { + this.done = !this.done + } +}) +``` + +Now should be defined as: + +```javascript +const Todo = types.model( + "Todo", + { + done: types.boolean, + }, + { + toggle() { + this.done = !this.done + } + } +) +``` + +It is still possible to define functions on the first object. However, those functions are not considered to be actions, but views. They are not allowed to modify values, but instead should produce a new value themselves. + # 0.3.3 * Introduced lifecycle hooks `afterCreate`, `afterAttach`, `beforeDetach`, `beforeDestroy`, implements #76 diff --git a/package.json b/package.json index 36565fd9f..b280397dc 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "nyc": "^10.0.0", "tape": "^4.6.0", "tslint": "^3.15.1", - "typescript": "2.2.2", + "typescript": "next", "webpack": "^1.13.1", "webpack-fail-plugin": "^1.0.6" }, @@ -66,4 +66,4 @@ "lib/**/*.js" ] } -} \ No newline at end of file +} diff --git a/src/core/action.ts b/src/core/action.ts index 424623d10..4e70a48c7 100644 --- a/src/core/action.ts +++ b/src/core/action.ts @@ -48,7 +48,7 @@ function runMiddleWares(node: MSTAdministration, baseCall: IRawActionCall): any export function createActionInvoker(name: string, fn: Function) { const action = mobxAction(name, fn) - const actionInvoker = function () { + const actionInvoker = function (this: IMSTNode) { const adm = getMSTAdministration(this) adm.assertAlive() if (adm.isRunningAction()) { diff --git a/src/types/complex-types/object.ts b/src/types/complex-types/object.ts index 26eb0fbcf..829b56712 100644 --- a/src/types/complex-types/object.ts +++ b/src/types/complex-types/object.ts @@ -3,7 +3,6 @@ import { extendShallowObservable, IObjectChange, IObjectWillChange, - IAction, intercept, observe } from "mobx" @@ -29,6 +28,7 @@ import { ReferenceProperty } from "../property-types/reference-property" import { ComputedProperty } from "../property-types/computed-property" import { ValueProperty } from "../property-types/value-property" import { ActionProperty } from "../property-types/action-property" +import { ViewProperty } from "../property-types/view-property" export class ObjectType extends ComplexType { isObjectFactory = true @@ -37,6 +37,7 @@ export class ObjectType extends ComplexType { * The original object definition */ baseModel: any + baseActions: any modelConstructor: new () => any @@ -49,13 +50,15 @@ export class ObjectType extends ComplexType { identifierAttribute: string | null = null - constructor(name: string, baseModel: Object) { + constructor(name: string, baseModel: Object, baseActions: Object) { super(name) Object.freeze(baseModel) // make sure nobody messes with it + Object.freeze(baseActions) this.baseModel = baseModel + this.baseActions = baseActions invariant(/^\w[\w\d_]*$/.test(name), `Typename should be a valid identifier: ${name}`) this.modelConstructor = new Function(`return function ${name} (){}`)() // fancy trick to get a named function...., http://stackoverflow.com/questions/5905492/dynamic-function-name-in-javascript - this.modelConstructor.prototype.toString = function() { + this.modelConstructor.prototype.toString = function(this: any) { return `${name}${JSON.stringify(getSnapshot(this))}` } this.parseModelProps() @@ -86,8 +89,9 @@ export class ObjectType extends ComplexType { } parseModelProps() { - const baseModel = this.baseModel + const {baseModel, baseActions} = this for (let key in baseModel) if (hasOwnProperty(baseModel, key)) { + // TODO: check that hooks are not defined as part of baseModel const descriptor = Object.getOwnPropertyDescriptor(baseModel, key) if ("get" in descriptor) { this.props[key] = new ComputedProperty(key, descriptor.get!, descriptor.set) @@ -109,13 +113,24 @@ export class ObjectType extends ComplexType { } else if (isReferenceFactory(value)) { this.props[key] = new ReferenceProperty(key, value.targetType, value.basePath) } else if (typeof value === "function") { - this.props[key] = new ActionProperty(key, value) + this.props[key] = new ViewProperty(key, value) } else if (typeof value === "object") { fail(`In property '${key}': base model's should not contain complex values: '${value}'`) } else { fail(`Unexpected value for property '${key}'`) } } + + for (let key in baseActions) if (hasOwnProperty(baseActions, key)) { + const value = baseActions[key] + if (key in this.baseModel) + fail(`Property '${key}' was also defined as action. Actions and properties should not collide`) + if (typeof value === "function") { + this.props[key] = new ActionProperty(key, value) + } else { + fail(`Unexpected value for action '${key}'. Expected function, got ${typeof value}`) + } + } } getChildMSTs(node: MSTAdministration): [string, MSTAdministration][] { @@ -192,22 +207,25 @@ export class ObjectType extends ComplexType { } export type IBaseModelDefinition = { - [K in keyof T]: IType | T[K] & IAction | T[K] + [K in keyof T]: IType | T[K] } export type Snapshot = { [K in keyof T]?: Snapshot | any // Any because we cannot express conditional types yet, so this escape is needed for refs and such.... } -export interface IModelType extends IComplexType, T> { } +export interface IModelType extends IComplexType, T & A> { } -export function createModelFactory(baseModel: IBaseModelDefinition): IModelType -export function createModelFactory(name: string, baseModel: IBaseModelDefinition): IModelType -export function createModelFactory(arg1: any, arg2?: any) { +export function createModelFactory(baseModel: IBaseModelDefinition & ThisType): IModelType +export function createModelFactory(name: string, baseModel: IBaseModelDefinition & ThisType): IModelType +export function createModelFactory(baseModel: IBaseModelDefinition & ThisType, actions: A & ThisType): IModelType +export function createModelFactory(name: string, baseModel: IBaseModelDefinition & ThisType, actions: A & ThisType): IModelType +export function createModelFactory(arg1: any, arg2?: any, arg3?: any) { let name = typeof arg1 === "string" ? arg1 : "AnonymousModel" let baseModel: Object = typeof arg1 === "string" ? arg2 : arg1 + let actions: Object = typeof arg1 === "string" ? arg3 : arg2 - return new ObjectType(name, baseModel) + return new ObjectType(name, baseModel, actions || {}) } function getObjectFactoryBaseModel(item: any) { @@ -216,13 +234,12 @@ function getObjectFactoryBaseModel(item: any) { return isObjectFactory(type) ? (type as ObjectType).baseModel : {} } -export function extend(name: string, a: IModelType, b: IModelType): IModelType -export function extend(name: string, a: IModelType, b: IModelType, c: IModelType): IModelType -export function extend(name: string, a: IModelType, b: IModelType, c: IModelType, d: IModelType): IModelType -export function extend(a: IModelType, b: IModelType): IModelType -export function extend(a: IModelType, b: IModelType, c: IModelType): IModelType -export function extend(a: IModelType, b: IModelType, c: IModelType, d: IModelType): IModelType +export function extend(name: string, a: IModelType, b: IModelType): IModelType +export function extend(name: string, a: IModelType, b: IModelType, c: IModelType): IModelType +export function extend(a: IModelType, b: IModelType): IModelType +export function extend(a: IModelType, b: IModelType, c: IModelType): IModelType export function extend(...args: any[]) { + console.warn("[mobx-state-tree] `extend` is an experimental feature and it's behavior will probably change in the future") const baseFactories = typeof args[0] === "string" ? args.slice(1) : args const factoryName = typeof args[0] === "string" ? args[0] : baseFactories.map(f => f.name).join("_") diff --git a/src/types/property-types/view-property.ts b/src/types/property-types/view-property.ts new file mode 100644 index 000000000..26546ffac --- /dev/null +++ b/src/types/property-types/view-property.ts @@ -0,0 +1,34 @@ +import { extras } from "mobx" +import { addHiddenFinalProp, createNamedFunction } from "../../utils" +import { IMSTNode, getMSTAdministration } from "../../core" +import { Property } from "./property" + +export class ViewProperty extends Property { + invokeView: Function + + constructor(name: string, fn: Function) { + super(name) + this.invokeView = createViewInvoker(name, fn) + } + + initialize(target: any) { + addHiddenFinalProp(target, this.name, this.invokeView.bind(target)) + } + + isValidSnapshot(snapshot: any) { + return !(this.name in snapshot) + } +} + +export function createViewInvoker(name: string, fn: Function) { + const viewInvoker = function (this: IMSTNode) { + const args = arguments + const adm = getMSTAdministration(this) + adm.assertAlive() + return extras.allowStateChanges(false, () => fn.apply(this, args)) + } + + // This construction helps producing a better function name in the stack trace, but could be optimized + // away in prod builds, and `actionInvoker` be returned directly + return createNamedFunction(name, viewInvoker) +} diff --git a/test/action.ts b/test/action.ts index 96ba612b2..5b6aeb743 100644 --- a/test/action.ts +++ b/test/action.ts @@ -5,7 +5,8 @@ declare var Buffer /// Simple action replay and invocation const Task = types.model({ - done: false, + done: false +}, { toggle() { this.done = !this.done return this.done @@ -47,7 +48,8 @@ const Customer = types.model("Customer", { }) const Order = types.model("Order", { - customer: types.reference(Customer), + customer: types.reference(Customer) +}, { setCustomer(customer) { this.customer = customer } diff --git a/test/boxes-store.ts b/test/boxes-store.ts index 5b2f5eff2..fafc47387 100644 --- a/test/boxes-store.ts +++ b/test/boxes-store.ts @@ -1,24 +1,25 @@ /** * Based on examples/boxes/domain-state.js */ -import { types, getSnapshot, applySnapshot, getParent, hasParent, onPatch, recordPatches, IJsonPatch } from ".."; +import { types, getParent, hasParent, recordPatches, IJsonPatch } from ".." import { test } from "ava" const randomUuid = () => Math.random() export const Box = types.model("Box", { id: types.identifier(), - name: '', + name: "", x: 0, y: 0, get width() { - return this.name.length * 15; + return this.name.length * 15 }, get isSelected() { if (!hasParent(this)) return false return getParent(getParent(this)).selection === this - }, + } +}, { move(dx, dy) { this.x += dx this.y += dy @@ -37,7 +38,8 @@ export const Arrow = types.model("Arrow", { export const Store = types.model("Store", { boxes: types.map(Box), arrows: types.array(Arrow), - selection: types.reference(Box, "./boxes"), + selection: types.reference(Box, "./boxes") +}, { addBox(name, x, y) { const box = Box.create({ name, x, y, id: randomUuid() }) this.boxes.put(box) diff --git a/test/hooks.ts b/test/hooks.ts index b05364846..3eb965f3b 100644 --- a/test/hooks.ts +++ b/test/hooks.ts @@ -3,7 +3,8 @@ import { test } from "ava" function createTestStore(listener) { const Todo = types.model("Todo", { - title: "", + title: "" + }, { setTitle(newTitle) { this.title = newTitle }, @@ -28,7 +29,8 @@ function createTestStore(listener) { }) const Store = types.model("Store", { - todos: types.array(Todo), + todos: types.array(Todo) + }, { afterCreate() { listener("new store: " + this.todos.length) addDisposer(this, () => { diff --git a/test/node.ts b/test/node.ts index 124ea8ece..1172e99db 100644 --- a/test/node.ts +++ b/test/node.ts @@ -215,7 +215,8 @@ test("make sure array filter works properly", (t) => { }) const Document = types.model({ - rows: types.array(Row), + rows: types.array(Row) + }, { clearDone() { this.rows.filter(row => row.done === true).forEach(destroy) } @@ -259,21 +260,23 @@ test("it can record and replay patches", (t) => { // === RECORD ACTIONS === test("it can record and replay actions", (t) => { const Row = types.model({ - article_id: 0, - setArticle(article_id){ + article_id: 0 + }, { + setArticle(article_id) { this.article_id = article_id } }) const Document = types.model({ customer_id: 0, + rows: types.array(Row) + }, { setCustomer(customer_id) { this.customer_id = customer_id }, addRow() { this.rows.push(Row.create()) - }, - rows: types.array(Row) + } }) const source = Document.create() diff --git a/test/object.ts b/test/object.ts index f061e0621..b64fa4e8a 100644 --- a/test/object.ts +++ b/test/object.ts @@ -1,5 +1,6 @@ import {onSnapshot, onPatch, onAction, applyPatch, applyPatches, applyAction, applyActions, getPath, IJsonPatch, applySnapshot, getSnapshot, types} from "../" import {test} from "ava" +import {autorun} from "mobx" interface ITestSnapshot{ to: string @@ -12,7 +13,8 @@ interface ITest{ const createTestFactories = () => { const Factory = types.model({ - to: 'world', + to: 'world' + }, { setTo(to) { this.to = to } @@ -179,7 +181,8 @@ test("it should throw if snapshot has computed properties", (t) => { test("it should throw if a replaced object is read or written to", (t) => { const Todo = types.model({ - title: "test", + title: "test" + }, { fn() { } @@ -235,3 +238,43 @@ test("it should check the type correctly", (t) => { t.deepEqual(Factory.is({wrongKey: true}), true) t.deepEqual(Factory.is({to: 3 }), false) }) + +// === VIEW FUNCTIONS === + +test("view functions should be tracked", (t) => { + const model = types.model({ + x: 3, + doubler() { + return this.x * 2 + } + }).create() + + const values: number[] = [] + const d = autorun(() => { + values.push(model.doubler()) + }) + + model.x = 7 + t.deepEqual(values, [6, 14]) +}) + +test("view functions should not be allowed to change state", (t) => { + const model = types.model({ + x: 3, + doubler() { + this.x *= 2 + } + }, { + anotherDoubler() { + this.x *= 2 + } + }).create() + + t.throws( + () => model.doubler(), + /Invariant failed: Side effects like changing state are not allowed at this point. Are you trying to modify state from, for example, the render function of a React component?/ + ) + + model.anotherDoubler() + t.is(model.x, 6) +}) diff --git a/test/protect.ts b/test/protect.ts index d658d2a4a..07649a1ec 100644 --- a/test/protect.ts +++ b/test/protect.ts @@ -3,7 +3,8 @@ import { test } from "ava" function createTestStore() { const Todo = types.model("Todo", { - title: "", + title: "" + }, { setTitle(newTitle) { this.title = newTitle } diff --git a/test/tsconfig.json b/test/tsconfig.json index 78ee5c2ea..696ccd93e 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -4,6 +4,7 @@ "module": "commonjs", "target": "es2015", "noImplicitAny": false, + "noImplicitThis": true, "strictNullChecks": true, "sourceMap": false, "outDir": "../test-lib" diff --git a/test/type-system.ts b/test/type-system.ts index 224190d0d..2fa2b8592 100644 --- a/test/type-system.ts +++ b/test/type-system.ts @@ -54,9 +54,17 @@ test("it should do typescript type inference correctly", (t) => { const A = types.model({ x: types.number, y: types.maybe(types.string), - method() { }, get z(): string { return "hi" }, set z(v: string) { } + }, { + method() { + // Correct this. Requires typescript 2.3 + const x: string = this.z + this.x + this.y + this.anotherMethod(x) + }, + anotherMethod(x: string) { + + } }) // factory is invokable @@ -183,7 +191,8 @@ test("can create factories with maybe primitives", t => { test("it is possible to refer to a type", t => { const Todo = types.model({ - title: types.string, + title: types.string + }, { setTitle(v: string) { } @@ -203,7 +212,8 @@ test("it is possible to refer to a type", t => { test(".Type should not be callable", t => { const Todo = types.model({ - title: types.string, + title: types.string + }, { setTitle(v: string) { } @@ -215,7 +225,8 @@ test(".Type should not be callable", t => { test(".SnapshotType should not be callable", t => { const Todo = types.model({ - title: types.string, + title: types.string + }, { setTitle(v: string) { } @@ -225,10 +236,10 @@ test(".SnapshotType should not be callable", t => { }) test("types instances with compatible snapshots should not be interchangeable", t => { - const A = types.model("A", { + const A = types.model("A", {}, { doA() {} }) - const B = types.model("B", { + const B = types.model("B", {}, { doB() {} }) const C = types.model("C", { @@ -249,3 +260,31 @@ test("types instances with compatible snapshots should not be interchangeable", // "[mobx-state-tree] Value of type B: '{}' is not assignable to type: A | null, expected an instance of A | null or a snapshot like '({ } | null)' instead. (Note that a snapshot of the provided value is compatible with the targeted type)" // ) }) + +test("it handles complex types correctly", t => { + const Todo = types.model({ + title: types.string + }, { + setTitle(v: string) { + + } + }) + + const Store = types.model({ + todos: types.map(Todo), + get amount() { + // double check, not available design time: + /// this.setAmount() + return this.todos.size + }, + getAmount(): number { + return this.todos.size + this.amount + } + }, { + setAmount() { + const x: number = this.todos.size + this.amount + this.getAmount + } + }) + + t.is(true, true) // supress no asserts warning +}) diff --git a/tsconfig.json b/tsconfig.json index 8f5ec8a76..42674cde8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ "experimentalDecorators": true, "strictNullChecks": true, "noFallthroughCasesInSwitch": true, - "noImplicitReturns": true + "noImplicitReturns": true, + "noImplicitThis": true }, "files": [ "src/index.ts" diff --git a/yarn.lock b/yarn.lock index ba42512ed..9edbc6f29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2854,14 +2854,14 @@ js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" -js-yaml@3.6.1, js-yaml@^3.3.1: +js-yaml@3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.6.1.tgz#6e5fe67d8b205ce4d22fad05b7781e8dadcc4b30" dependencies: argparse "^1.0.7" esprima "^2.6.0" -js-yaml@^3.8.2: +js-yaml@^3.3.1, js-yaml@^3.8.2: version "3.8.3" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.3.tgz#33a05ec481c850c8875929166fe1beb61c728766" dependencies: @@ -4732,9 +4732,9 @@ typedarray@^0.0.6, typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -typescript@2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.2.2.tgz#606022508479b55ffa368b58fee963a03dfd7b0c" +typescript@next: + version "2.3.0-dev.20170426" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.0-dev.20170426.tgz#d0cb8b4263dfe65363f2b2c4b6ab12182b586e68" uglify-js@^2.6, uglify-js@~2.7.3: version "2.7.5"