diff --git a/.changeset/afraid-cooks-nail.md b/.changeset/afraid-cooks-nail.md
new file mode 100644
index 0000000000..6abcf0a089
--- /dev/null
+++ b/.changeset/afraid-cooks-nail.md
@@ -0,0 +1,5 @@
+---
+"mobx": minor
+---
+
+Add 2022.3 Decorators support
diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml
index ef5866b6af..1bf246e948 100644
--- a/.github/workflows/build_and_test.yml
+++ b/.github/workflows/build_and_test.yml
@@ -5,6 +5,7 @@ on:
branches: [main]
pull_request:
branches: [main]
+ workflow_dispatch: {}
jobs:
build:
diff --git a/docs/enabling-decorators.md b/docs/enabling-decorators.md
index 805a07c678..596b1c9821 100644
--- a/docs/enabling-decorators.md
+++ b/docs/enabling-decorators.md
@@ -6,11 +6,69 @@ hide_title: true
-# Enabling decorators {🚀}
+# Enabling Decorators
-MobX before version 6 encouraged the use of ES.next decorators to mark things as `observable`, `computed` and `action`. However, decorators are currently not an ES standard, and the process of standardization is taking a long time. It also looks like the standard will be different from the way decorators were implemented previously. In the interest of compatibility we have chosen to move away from them in MobX 6, and recommend the use of [`makeObservable` / `makeAutoObservable`](observable-state.md) instead.
+After years of alterations, ES decorators have finally reached Stage 3 in the TC39 process, meaning that they are quite stable and won't undergo breaking changes again like the previous decorator proposals have. MobX has implemented support for this new "2022.3/Stage 3" decorator syntax. If you're looking for information on "legacy" decorators, look below or refer to older versions of the documentation.
-But many existing codebases use decorators, and a lot of the documentation and tutorial material online uses them as well. The rule is that anything you can use as an annotation to `makeObservable`, such as `observable`, `action` and `computed`, you can also use as a decorator. So let's examine what that looks like:
+2022.3 Decorators are supported in Babel (see https://babeljs.io/docs/babel-plugin-proposal-decorators) and in TypeScript (5.0+).
+
+## Usage
+
+```javascript
+import { makeObservable, observable, computed, action } from "mobx"
+
+class Todo {
+ id = Math.random()
+ @observable accessor title = ""
+ @observable accessor finished = false
+
+ @action
+ toggle() {
+ this.finished = !finished
+ }
+}
+
+class TodoList {
+ @observable accessor todos = []
+
+ @computed
+ get unfinishedTodoCount() {
+ return this.todos.filter(todo => !todo.finished).length
+ }
+}
+```
+
+Notice the usage of the new `accessor` keyword when using `@observable`.
+It is part of the 2022.3 spec and is required if you want to use decorators without `makeObservable()`.
+That being said, you _can_ do without it if you continue to call `makeObservable()` like so:
+
+```javascript
+class Todo {
+ @observable title = ""
+
+ constructor() {
+ makeObservable(this)
+ }
+}
+```
+
+## Changes/Gotchas
+
+MobX' 2022.3 Decorators are very similar to the pre-MobX 6 decorators, so usage is mostly the same, but there are some gotchas:
+
+- `@observable accessor` decorators are _not_ enumerable. `accessor`s do not have a direct equivalent in the past - they're a new concept in the language. We've chosen to make them non-enumerable, non-own properties in order to better follow the spirit of the ES language and what `accessor` means.
+ The main cases for enumerability seem to have been around serialization and rest destructuring.
+ - Regarding serialization, implicitly serializing all properties probably isn't ideal in an OOP-world anyway, so this doesn't seem like a substantial issue (consider implementing `toJSON` or using `serializr` as possible alternatives)
+ - Addressing rest-destructuring, such is an anti-pattern in MobX - doing so would (likely unwantedly) touch all observables and make the observer overly-reactive).
+- `@action some_field = () => {}` was and is valid usage (_if_ `makeObservable()` is also used). However, `@action accessor some_field = () => {}` is never valid.
+
+## Legacy Decorators
+
+We do not recommend new codebases that use MobX use legacy decorators until the point when they become an official part of the language, but you can still use them. It does require setup for transpilation so you have to use Babel or TypeScript.
+
+## MobX Core decorators {🚀}
+
+MobX before version 6 encouraged the use of ES.next decorators to mark things as `observable`, `computed` and `action`. While MobX 6 recomneds against using these decorators (an instead using [`makeObservable` / `makeAutoObservable`](observable-state.md)), it is still possible.
```javascript
import { makeObservable, observable, computed, action } from "mobx"
@@ -52,7 +110,7 @@ When migrating from MobX 4/5 to 6, we recommend to always run the code-mod, to m
Check out the [Migrating from MobX 4/5 {🚀}](migrating-from-4-or-5.md) section.
-## Using `observer` as a decorator
+### Using `observer` as a decorator
The `observer` function from `mobx-react` is both a function and a decorator that can be used on class components:
@@ -63,10 +121,6 @@ class Timer extends React.Component {
}
```
-## How to enable decorator support
-
-We do not recommend new codebases that use MobX use decorators until the point when they become an official part of the language, but you can still use them. It does require setup for transpilation so you have to use Babel or TypeScript.
-
### TypeScript
Enable the compiler option `"experimentalDecorators": true` and `"useDefineForClassFields": true` in your `tsconfig.json`.
@@ -89,7 +143,7 @@ Install support for decorators: `npm i --save-dev @babel/plugin-proposal-class-p
Decorators are only supported out of the box when using TypeScript in `create-react-app@^2.1.1` and newer. In older versions or when using vanilla JavaScript use eject, or the [customize-cra](https://github.com/arackaf/customize-cra) package.
-## Disclaimer: Limitations of the decorator syntax
+### Disclaimer: Limitations of the legacy decorator syntax
The current transpiler implementations of the decorator syntax are quite limited and don't behave exactly the same.
Also, many compositional patterns are currently not possible with decorators, until the stage-2 proposal has been implemented by all transpilers.
diff --git a/jest.base.config.js b/jest.base.config.js
index acd363122b..a63edb2489 100644
--- a/jest.base.config.js
+++ b/jest.base.config.js
@@ -1,9 +1,11 @@
const fs = require("fs")
const path = require("path")
-const tsConfig = "tsconfig.test.json"
-
-module.exports = function buildConfig(packageDirectory, pkgConfig) {
+module.exports = function buildConfig(
+ packageDirectory,
+ pkgConfig,
+ tsConfig = "tsconfig.test.json"
+) {
const packageName = require(`${packageDirectory}/package.json`).name
const packageTsconfig = path.resolve(packageDirectory, tsConfig)
return {
diff --git a/jest.config.js b/jest.config.js
index fb1ffb1f18..e40ab53e21 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,6 +1,6 @@
const buildConfig = require("./jest.base.config")
module.exports = buildConfig(__dirname, {
- projects: ["/packages/*/jest.config.js"]
+ projects: ["/packages/*/jest.config.js", "/packages/*/jest.config-*.js"]
// collectCoverageFrom: ["/packages/*/src/**/*.{ts,tsx}"]
})
diff --git a/package.json b/package.json
index 20de1105f1..9671ed58f8 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"packages/*"
],
"resolutions": {
- "typescript": "^4.0.2",
+ "typescript": "^5.0.0-beta",
"recast": "^0.23.1"
},
"repository": {
@@ -57,7 +57,7 @@
"lodash": "^4.17.4",
"minimist": "^1.2.5",
"mkdirp": "1.0.4",
- "prettier": "^2.0.5",
+ "prettier": "^2.8.4",
"pretty-quick": "3.1.0",
"prop-types": "15.6.2",
"react": "^18.0.0",
@@ -67,7 +67,7 @@
"tape": "^5.0.1",
"ts-jest": "26.4.1",
"tsdx": "^0.14.1",
- "typescript": "^4.0.2"
+ "typescript": "^5.0.0-beta"
},
"husky": {
"hooks": {
diff --git a/packages/mobx/__tests__/decorators_20223/stage3-decorators.ts b/packages/mobx/__tests__/decorators_20223/stage3-decorators.ts
new file mode 100644
index 0000000000..58ba800eaa
--- /dev/null
+++ b/packages/mobx/__tests__/decorators_20223/stage3-decorators.ts
@@ -0,0 +1,1108 @@
+"use strict"
+
+import {
+ observe,
+ computed,
+ observable,
+ autorun,
+ extendObservable,
+ action,
+ IObservableArray,
+ IArrayWillChange,
+ IArrayWillSplice,
+ IObservableValue,
+ isObservable,
+ isObservableProp,
+ isObservableObject,
+ transaction,
+ IObjectDidChange,
+ spy,
+ configure,
+ isAction,
+ IAtom,
+ createAtom,
+ runInAction,
+ makeObservable
+} from "../../src/mobx"
+import { type ObservableArrayAdministration } from "../../src/internal"
+import * as mobx from "../../src/mobx"
+
+const testFunction = function (a: any) {}
+
+// lazy wrapper around yest
+
+const t = {
+ equal(a: any, b: any) {
+ expect(a).toBe(b)
+ },
+ deepEqual(a: any, b: any) {
+ expect(a).toEqual(b)
+ },
+ notEqual(a: any, b: any) {
+ expect(a).not.toEqual(b)
+ },
+
+ throws(a: any, b: any) {
+ expect(a).toThrow(b)
+ }
+}
+
+test("decorators", () => {
+ class Order {
+ @observable accessor price: number = 3
+ @observable accessor amount: number = 2
+ @observable accessor orders: string[] = []
+ @observable accessor aFunction = testFunction
+
+ @computed
+ get total() {
+ return this.amount * this.price * (1 + this.orders.length)
+ }
+ }
+
+ const o = new Order()
+ t.equal(isObservableObject(o), true)
+ t.equal(isObservableProp(o, "amount"), true)
+ t.equal(isObservableProp(o, "total"), true)
+
+ const events: any[] = []
+ const d1 = observe(o, (ev: IObjectDidChange) => events.push(ev.name, (ev as any).oldValue))
+ const d2 = observe(o, "price", ev => events.push(ev.newValue, ev.oldValue))
+ const d3 = observe(o, "total", ev => events.push(ev.newValue, ev.oldValue))
+
+ o.price = 4
+
+ d1()
+ d2()
+ d3()
+
+ o.price = 5
+
+ t.deepEqual(events, [
+ 8, // new total
+ 6, // old total
+ 4, // new price
+ 3, // old price
+ "price", // event name
+ 3 // event oldValue
+ ])
+})
+
+test("annotations", () => {
+ const fn0 = () => 0
+ class Order {
+ @observable accessor price: number = 3
+ @observable accessor amount: number = 2
+ @observable accessor orders: string[] = []
+ @observable accessor aFunction = fn0
+
+ @computed
+ get total() {
+ return this.amount * this.price * (1 + this.orders.length)
+ }
+ }
+
+ const order1totals: number[] = []
+ const order1 = new Order()
+ const order2 = new Order()
+
+ const disposer = autorun(() => {
+ order1totals.push(order1.total)
+ })
+
+ order2.price = 4
+ order1.amount = 1
+
+ t.equal(order1.price, 3)
+ t.equal(order1.total, 3)
+ t.equal(order2.total, 8)
+ order2.orders.push("bla")
+ t.equal(order2.total, 16)
+
+ order1.orders.splice(0, 0, "boe", "hoi")
+ t.deepEqual(order1totals, [6, 3, 9])
+
+ disposer()
+ order1.orders.pop()
+ t.equal(order1.total, 6)
+ t.deepEqual(order1totals, [6, 3, 9])
+ expect(isAction(order1.aFunction)).toBe(true)
+ expect(order1.aFunction()).toBe(0)
+ order1.aFunction = () => 1
+ expect(isAction(order1.aFunction)).toBe(true)
+ expect(order1.aFunction()).toBe(1)
+})
+
+test("box", () => {
+ class Box {
+ @observable accessor uninitialized: any
+ @observable accessor height = 20
+ @observable accessor sizes = [2]
+ @observable accessor someFunc = function () {
+ return 2
+ }
+ @computed
+ get width() {
+ return this.height * this.sizes.length * this.someFunc() * (this.uninitialized ? 2 : 1)
+ }
+ @action("test")
+ addSize() {
+ this.sizes.push(3)
+ this.sizes.push(4)
+ }
+ }
+
+ const box = new Box()
+
+ const ar: number[] = []
+
+ autorun(() => {
+ ar.push(box.width)
+ })
+
+ t.deepEqual(ar.slice(), [40])
+ box.height = 10
+ t.deepEqual(ar.slice(), [40, 20])
+ box.sizes.push(3, 4)
+ t.deepEqual(ar.slice(), [40, 20, 60])
+ box.someFunc = () => 7
+ t.deepEqual(ar.slice(), [40, 20, 60, 210])
+ box.uninitialized = true
+ t.deepEqual(ar.slice(), [40, 20, 60, 210, 420])
+ box.addSize()
+ expect(ar.slice()).toEqual([40, 20, 60, 210, 420, 700])
+})
+
+test("computed setter should succeed", () => {
+ class Bla {
+ @observable accessor a = 3
+ @computed
+ get propX() {
+ return this.a * 2
+ }
+ set propX(v) {
+ this.a = v
+ }
+ }
+
+ const b = new Bla()
+ t.equal(b.propX, 6)
+ b.propX = 4
+ t.equal(b.propX, 8)
+})
+
+test("ClassFieldDecorators should work in conjunction with makeObservable()", () => {
+ class Order {
+ @observable price: number = 3
+ @observable amount: number = 2
+ @observable orders: string[] = []
+ @observable aFunction = testFunction
+
+ @computed
+ get total() {
+ return this.amount * this.price * (1 + this.orders.length)
+ }
+
+ constructor() {
+ makeObservable(this)
+ }
+ }
+
+ const o = new Order()
+ t.equal(isObservableObject(o), true)
+ t.equal(isObservableProp(o, "amount"), true)
+ t.equal(isObservableProp(o, "total"), true)
+
+ const events: any[] = []
+ const d1 = observe(o, (ev: IObjectDidChange) => events.push(ev.name, (ev as any).oldValue))
+ const d2 = observe(o, "price", ev => events.push(ev.newValue, ev.oldValue))
+ const d3 = observe(o, "total", ev => events.push(ev.newValue, ev.oldValue))
+
+ o.price = 4
+
+ d1()
+ d2()
+ d3()
+
+ o.price = 5
+
+ t.deepEqual(events, [
+ 8, // new total
+ 6, // old total
+ 4, // new price
+ 3, // old price
+ "price", // event name
+ 3 // event oldValue
+ ])
+})
+
+test("typescript: parameterized computed decorator", () => {
+ class TestClass {
+ @observable accessor x = 3
+ @observable accessor y = 3
+ @computed.struct
+ get boxedSum() {
+ return { sum: Math.round(this.x) + Math.round(this.y) }
+ }
+ }
+
+ const t1 = new TestClass()
+ const changes: { sum: number }[] = []
+ const d = autorun(() => changes.push(t1.boxedSum))
+
+ t1.y = 4 // change
+ t.equal(changes.length, 2)
+ t1.y = 4.2 // no change
+ t.equal(changes.length, 2)
+ transaction(() => {
+ t1.y = 3
+ t1.x = 4
+ }) // no change
+ t.equal(changes.length, 2)
+ t1.x = 6 // change
+ t.equal(changes.length, 3)
+ d()
+
+ t.deepEqual(changes, [{ sum: 6 }, { sum: 7 }, { sum: 9 }])
+})
+
+test("issue 165", () => {
+ function report(msg: string, value: T) {
+ // console.log(msg, ":", value)
+ return value
+ }
+
+ class Card {
+ constructor(public game: Game, public id: number) {
+ makeObservable(this)
+ }
+
+ @computed
+ get isWrong() {
+ return report(
+ "Computing isWrong for card " + this.id,
+ this.isSelected && this.game.isMatchWrong
+ )
+ }
+
+ @computed
+ get isSelected() {
+ return report(
+ "Computing isSelected for card" + this.id,
+ this.game.firstCardSelected === this || this.game.secondCardSelected === this
+ )
+ }
+ }
+
+ class Game {
+ @observable accessor firstCardSelected: Card | null = null
+ @observable accessor secondCardSelected: Card | null = null
+
+ @computed
+ get isMatchWrong() {
+ return report(
+ "Computing isMatchWrong",
+ this.secondCardSelected !== null &&
+ this.firstCardSelected!.id !== this.secondCardSelected.id
+ )
+ }
+ }
+
+ let game = new Game()
+ let card1 = new Card(game, 1),
+ card2 = new Card(game, 2)
+
+ autorun(() => {
+ card1.isWrong
+ card2.isWrong
+ // console.log("card1.isWrong =", card1.isWrong)
+ // console.log("card2.isWrong =", card2.isWrong)
+ // console.log("------------------------------")
+ })
+
+ // console.log("Selecting first card")
+ game.firstCardSelected = card1
+ // console.log("Selecting second card")
+ game.secondCardSelected = card2
+
+ t.equal(card1.isWrong, true)
+ t.equal(card2.isWrong, true)
+})
+
+test("issue 191 - shared initializers (2022.3)", () => {
+ class Test {
+ @observable accessor obj = { a: 1 }
+ @observable accessor array = [2]
+ }
+
+ const t1 = new Test()
+ t1.obj.a = 2
+ t1.array.push(3)
+
+ const t2 = new Test()
+ t2.obj.a = 3
+ t2.array.push(4)
+
+ t.notEqual(t1.obj, t2.obj)
+ t.notEqual(t1.array, t2.array)
+ t.equal(t1.obj.a, 2)
+ t.equal(t2.obj.a, 3)
+
+ t.deepEqual(t1.array.slice(), [2, 3])
+ t.deepEqual(t2.array.slice(), [2, 4])
+})
+
+function normalizeSpyEvents(events: any[]) {
+ events.forEach(ev => {
+ delete ev.fn
+ delete ev.time
+ })
+ return events
+}
+
+test("action decorator (2022.3)", () => {
+ class Store {
+ constructor(private multiplier: number) {
+ makeObservable(this)
+ }
+
+ @action
+ add(a: number, b: number): number {
+ return (a + b) * this.multiplier
+ }
+ }
+
+ const store1 = new Store(2)
+ const store2 = new Store(3)
+ const events: any[] = []
+ const d = spy(events.push.bind(events))
+ t.equal(store1.add(3, 4), 14)
+ t.equal(store2.add(2, 2), 12)
+ t.equal(store1.add(1, 1), 4)
+
+ t.deepEqual(normalizeSpyEvents(events), [
+ { arguments: [3, 4], name: "add", spyReportStart: true, object: store1, type: "action" },
+ { type: "report-end", spyReportEnd: true },
+ { arguments: [2, 2], name: "add", spyReportStart: true, object: store2, type: "action" },
+ { type: "report-end", spyReportEnd: true },
+ { arguments: [1, 1], name: "add", spyReportStart: true, object: store1, type: "action" },
+ { type: "report-end", spyReportEnd: true }
+ ])
+
+ d()
+})
+
+test("custom action decorator (2022.3)", () => {
+ class Store {
+ constructor(private multiplier: number) {
+ makeObservable(this)
+ }
+
+ @action("zoem zoem")
+ add(a: number, b: number): number {
+ return (a + b) * this.multiplier
+ }
+ }
+
+ const store1 = new Store(2)
+ const store2 = new Store(3)
+ const events: any[] = []
+ const d = spy(events.push.bind(events))
+ t.equal(store1.add(3, 4), 14)
+ t.equal(store2.add(2, 2), 12)
+ t.equal(store1.add(1, 1), 4)
+
+ t.deepEqual(normalizeSpyEvents(events), [
+ {
+ arguments: [3, 4],
+ name: "zoem zoem",
+ spyReportStart: true,
+ object: store1,
+ type: "action"
+ },
+ { type: "report-end", spyReportEnd: true },
+ {
+ arguments: [2, 2],
+ name: "zoem zoem",
+ spyReportStart: true,
+ object: store2,
+ type: "action"
+ },
+ { type: "report-end", spyReportEnd: true },
+ {
+ arguments: [1, 1],
+ name: "zoem zoem",
+ spyReportStart: true,
+ object: store1,
+ type: "action"
+ },
+ { type: "report-end", spyReportEnd: true }
+ ])
+
+ d()
+})
+
+test("action decorator on field (2022.3)", () => {
+ class Store {
+ constructor(private multiplier: number) {
+ makeObservable(this)
+ }
+
+ @action
+ add = (a: number, b: number) => {
+ return (a + b) * this.multiplier
+ }
+ }
+
+ const store1 = new Store(2)
+ const store2 = new Store(7)
+ expect(store1.add).not.toEqual(store2.add)
+
+ const events: any[] = []
+ const d = spy(events.push.bind(events))
+ t.equal(store1.add(3, 4), 14)
+ t.equal(store2.add(4, 5), 63)
+ t.equal(store1.add(2, 2), 8)
+
+ t.deepEqual(normalizeSpyEvents(events), [
+ { arguments: [3, 4], name: "add", spyReportStart: true, object: store1, type: "action" },
+ { type: "report-end", spyReportEnd: true },
+ { arguments: [4, 5], name: "add", spyReportStart: true, object: store2, type: "action" },
+ { type: "report-end", spyReportEnd: true },
+ { arguments: [2, 2], name: "add", spyReportStart: true, object: store1, type: "action" },
+ { type: "report-end", spyReportEnd: true }
+ ])
+
+ d()
+})
+
+test("custom action decorator on field (2022.3)", () => {
+ class Store {
+ constructor(private multiplier: number) {
+ makeObservable(this)
+ }
+
+ @action("zoem zoem")
+ add = (a: number, b: number) => {
+ return (a + b) * this.multiplier
+ }
+ }
+
+ const store1 = new Store(2)
+ const store2 = new Store(7)
+
+ const events: any[] = []
+ const d = spy(events.push.bind(events))
+ t.equal(store1.add(3, 4), 14)
+ t.equal(store2.add(4, 5), 63)
+ t.equal(store1.add(2, 2), 8)
+
+ t.deepEqual(normalizeSpyEvents(events), [
+ {
+ arguments: [3, 4],
+ name: "zoem zoem",
+ spyReportStart: true,
+ object: store1,
+ type: "action"
+ },
+ { type: "report-end", spyReportEnd: true },
+ {
+ arguments: [4, 5],
+ name: "zoem zoem",
+ spyReportStart: true,
+ object: store2,
+ type: "action"
+ },
+ { type: "report-end", spyReportEnd: true },
+ {
+ arguments: [2, 2],
+ name: "zoem zoem",
+ spyReportStart: true,
+ object: store1,
+ type: "action"
+ },
+ { type: "report-end", spyReportEnd: true }
+ ])
+
+ d()
+})
+
+test("267 (2022.3) should be possible to declare properties observable outside strict mode", () => {
+ configure({ enforceActions: "observed" })
+
+ class Store {
+ @observable accessor timer: number | null = null
+ }
+
+ configure({ enforceActions: "never" })
+})
+
+test("288 atom not detected for object property", () => {
+ class Store {
+ @observable accessor foo = ""
+ }
+
+ const store = new Store()
+
+ mobx.observe(
+ store,
+ "foo",
+ () => {
+ // console.log("Change observed")
+ },
+ true
+ )
+})
+
+test.skip("observable performance - ts - decorators", () => {
+ const AMOUNT = 100000
+
+ class A {
+ @observable accessor a = 1
+ @observable accessor b = 2
+ @observable accessor c = 3
+ @computed
+ get d() {
+ return this.a + this.b + this.c
+ }
+ }
+
+ const objs: any[] = []
+ const start = Date.now()
+
+ for (let i = 0; i < AMOUNT; i++) objs.push(new A())
+
+ console.log("created in ", Date.now() - start)
+
+ for (let j = 0; j < 4; j++) {
+ for (let i = 0; i < AMOUNT; i++) {
+ const obj = objs[i]
+ obj.a += 3
+ obj.b *= 4
+ obj.c = obj.b - obj.a
+ obj.d
+ }
+ }
+
+ console.log("changed in ", Date.now() - start)
+})
+
+test("unbound methods", () => {
+ class A {
+ // shared across all instances
+ @action
+ m1() {}
+ }
+
+ const a1 = new A()
+ const a2 = new A()
+
+ t.equal(a1.m1, a2.m1)
+ t.equal(Object.hasOwnProperty.call(a1, "m1"), false)
+ t.equal(Object.hasOwnProperty.call(a2, "m1"), false)
+})
+
+test("inheritance", () => {
+ class A {
+ @observable accessor a = 2
+ }
+
+ class B extends A {
+ @observable accessor b = 3
+ @computed
+ get c() {
+ return this.a + this.b
+ }
+ constructor() {
+ super()
+ makeObservable(this)
+ }
+ }
+ const b1 = new B()
+ const b2 = new B()
+ const values: any[] = []
+ mobx.autorun(() => values.push(b1.c + b2.c))
+
+ b1.a = 3
+ b1.b = 4
+ b2.b = 5
+ b2.a = 6
+
+ t.deepEqual(values, [10, 11, 12, 14, 18])
+})
+
+test("inheritance overrides observable", () => {
+ class A {
+ @observable accessor a = 2
+ }
+
+ class B {
+ @observable accessor a = 5
+ @observable accessor b = 3
+ @computed
+ get c() {
+ return this.a + this.b
+ }
+ }
+
+ const b1 = new B()
+ const b2 = new B()
+ const values: any[] = []
+ mobx.autorun(() => values.push(b1.c + b2.c))
+
+ b1.a = 3
+ b1.b = 4
+ b2.b = 5
+ b2.a = 6
+
+ t.deepEqual(values, [16, 14, 15, 17, 18])
+})
+
+test("reusing initializers", () => {
+ class A {
+ @observable accessor a = 3
+ @observable accessor b = this.a + 2
+ @computed
+ get c() {
+ return this.a + this.b
+ }
+ @computed
+ get d() {
+ return this.c + 1
+ }
+ }
+
+ const a = new A()
+ const values: any[] = []
+ mobx.autorun(() => values.push(a.d))
+
+ a.a = 4
+ t.deepEqual(values, [9, 10])
+})
+
+test("enumerability", () => {
+ class A {
+ @observable accessor a = 1 // enumerable, on proto
+ @computed
+ get b() {
+ return this.a
+ } // non-enumerable, (and, ideally, on proto)
+ @action
+ m() {} // non-enumerable, on proto
+ }
+
+ const a = new A()
+
+ // not initialized yet
+ let ownProps = Object.keys(a)
+ let enumProps: string[] = []
+ for (const key in a) enumProps.push(key)
+
+ t.deepEqual(ownProps, [])
+
+ t.deepEqual(enumProps, [])
+
+ t.equal("a" in a, true)
+ // eslint-disable-next-line
+ t.equal(a.hasOwnProperty("a"), false)
+ // eslint-disable-next-line
+ t.equal(a.hasOwnProperty("b"), false)
+ // eslint-disable-next-line
+ t.equal(a.hasOwnProperty("m"), false)
+
+ t.equal(mobx.isAction(a.m), true)
+
+ // after initialization
+ a.a
+ a.b
+ a.m
+
+ ownProps = Object.keys(a)
+ enumProps = []
+ for (const key in a) enumProps.push(key)
+
+ t.deepEqual(ownProps, [])
+
+ t.deepEqual(enumProps, [])
+
+ t.equal("a" in a, true)
+ // eslint-disable-next-line
+ t.equal(a.hasOwnProperty("a"), false)
+ // eslint-disable-next-line
+ t.equal(a.hasOwnProperty("b"), false)
+ // eslint-disable-next-line
+ t.equal(a.hasOwnProperty("m"), false)
+})
+
+test("issue 285 (2022.3) (legacy/field decorator)", () => {
+ const { observable, toJS } = mobx
+
+ class Todo {
+ id = 1
+ @observable title: string
+ @observable finished = false
+ @observable childThings = [1, 2, 3]
+ constructor(title: string) {
+ makeObservable(this)
+ this.title = title
+ }
+ }
+
+ const todo = new Todo("Something to do")
+
+ t.deepEqual(toJS(todo), {
+ id: 1,
+ title: "Something to do",
+ finished: false,
+ childThings: [1, 2, 3]
+ })
+})
+
+test("verify object assign (2022.3) (legacy/field decorator)", () => {
+ class Todo {
+ @observable title = "test"
+ @computed
+ get upperCase() {
+ return this.title.toUpperCase()
+ }
+ }
+
+ t.deepEqual((Object as any).assign({}, new Todo()), {
+ title: "test"
+ })
+})
+
+test("373 - fix isObservable for unused computed", () => {
+ class Bla {
+ @computed
+ get computedVal() {
+ return 3
+ }
+ constructor() {
+ makeObservable(this)
+ t.equal(isObservableProp(this, "computedVal"), true)
+ this.computedVal
+ t.equal(isObservableProp(this, "computedVal"), true)
+ }
+ }
+
+ new Bla()
+})
+
+test("705 - setter undoing caching (2022.3)", () => {
+ let recomputes = 0
+ let autoruns = 0
+
+ class Person {
+ @observable accessor name: string = ""
+ @observable accessor title: string = ""
+
+ // Typescript bug: if fullName is before the getter, the property is defined twice / incorrectly, see #705
+ // set fullName(val) {
+ // // Noop
+ // }
+ @computed
+ get fullName() {
+ recomputes++
+ return this.title + " " + this.name
+ }
+ // Should also be possible to define the setter _before_ the fullname
+ set fullName(val) {
+ // Noop
+ }
+ }
+
+ let p1 = new Person()
+ p1.name = "Tom Tank"
+ p1.title = "Mr."
+
+ t.equal(recomputes, 0)
+ t.equal(autoruns, 0)
+
+ const d1 = autorun(() => {
+ autoruns++
+ p1.fullName
+ })
+
+ const d2 = autorun(() => {
+ autoruns++
+ p1.fullName
+ })
+
+ t.equal(recomputes, 1)
+ t.equal(autoruns, 2)
+
+ p1.title = "Master"
+ t.equal(recomputes, 2)
+ t.equal(autoruns, 4)
+
+ d1()
+ d2()
+})
+
+test("@observable.ref (2022.3)", () => {
+ class A {
+ @observable.ref accessor ref = { a: 3 }
+ }
+
+ const a = new A()
+ t.equal(a.ref.a, 3)
+ t.equal(mobx.isObservable(a.ref), false)
+ t.equal(mobx.isObservableProp(a, "ref"), true)
+})
+
+test("@observable.shallow (2022.3)", () => {
+ class A {
+ @observable.shallow accessor arr = [{ todo: 1 }]
+ }
+
+ const a = new A()
+ const todo2 = { todo: 2 }
+ a.arr.push(todo2)
+ t.equal(mobx.isObservable(a.arr), true)
+ t.equal(mobx.isObservableProp(a, "arr"), true)
+ t.equal(mobx.isObservable(a.arr[0]), false)
+ t.equal(mobx.isObservable(a.arr[1]), false)
+ t.equal(a.arr[1] === todo2, true)
+})
+
+test("@observable.shallow - 2 (2022.3)", () => {
+ class A {
+ @observable.shallow accessor arr: Record = { x: { todo: 1 } }
+ }
+
+ const a = new A()
+ const todo2 = { todo: 2 }
+ a.arr.y = todo2
+ t.equal(mobx.isObservable(a.arr), true)
+ t.equal(mobx.isObservableProp(a, "arr"), true)
+ t.equal(mobx.isObservable(a.arr.x), false)
+ t.equal(mobx.isObservable(a.arr.y), false)
+ t.equal(a.arr.y === todo2, true)
+})
+
+test("@observable.deep (2022.3)", () => {
+ class A {
+ @observable.deep accessor arr = [{ todo: 1 }]
+ }
+
+ const a = new A()
+ const todo2 = { todo: 2 }
+ a.arr.push(todo2)
+
+ t.equal(mobx.isObservable(a.arr), true)
+ t.equal(mobx.isObservableProp(a, "arr"), true)
+ t.equal(mobx.isObservable(a.arr[0]), true)
+ t.equal(mobx.isObservable(a.arr[1]), true)
+ t.equal(a.arr[1] !== todo2, true)
+ t.equal(isObservable(todo2), false)
+})
+
+test("action.bound binds (2022.3)", () => {
+ class A {
+ @observable accessor x = 0
+ @action.bound
+ inc(value: number) {
+ this.x += value
+ }
+ }
+
+ const a = new A()
+ const runner = a.inc
+ runner(2)
+
+ t.equal(a.x, 2)
+})
+
+test("@computed.equals (2022.3)", () => {
+ const sameTime = (from: Time, to: Time) => from.hour === to.hour && from.minute === to.minute
+ class Time {
+ constructor(hour: number, minute: number) {
+ makeObservable(this)
+ this.hour = hour
+ this.minute = minute
+ }
+
+ @observable public accessor hour: number
+ @observable public accessor minute: number
+
+ @computed({ equals: sameTime })
+ public get time() {
+ return { hour: this.hour, minute: this.minute }
+ }
+ }
+ const time = new Time(9, 0)
+
+ const changes: Array<{ hour: number; minute: number }> = []
+ const disposeAutorun = autorun(() => changes.push(time.time))
+
+ t.deepEqual(changes, [{ hour: 9, minute: 0 }])
+ time.hour = 9
+ t.deepEqual(changes, [{ hour: 9, minute: 0 }])
+ time.minute = 0
+ t.deepEqual(changes, [{ hour: 9, minute: 0 }])
+ time.hour = 10
+ t.deepEqual(changes, [
+ { hour: 9, minute: 0 },
+ { hour: 10, minute: 0 }
+ ])
+ time.minute = 30
+ t.deepEqual(changes, [
+ { hour: 9, minute: 0 },
+ { hour: 10, minute: 0 },
+ { hour: 10, minute: 30 }
+ ])
+
+ disposeAutorun()
+})
+
+test("1072 - @observable accessor without initial value and observe before first access", () => {
+ class User {
+ @observable accessor loginCount: number = 0
+ }
+
+ const user = new User()
+ observe(user, "loginCount", () => {})
+})
+
+test("unobserved computed reads should warn with requiresReaction enabled", () => {
+ const consoleWarn = console.warn
+ const warnings: string[] = []
+ console.warn = function (...args) {
+ warnings.push(...args)
+ }
+ try {
+ class A {
+ @observable accessor x = 0
+
+ @computed({ requiresReaction: true })
+ get y() {
+ return this.x * 2
+ }
+ }
+
+ const a = new A()
+
+ a.y
+ const d = mobx.reaction(
+ () => a.y,
+ () => {}
+ )
+ a.y
+ d()
+ a.y
+
+ expect(warnings.length).toEqual(2)
+ expect(warnings[0]).toContain(
+ "is being read outside a reactive context. Doing a full recompute."
+ )
+ expect(warnings[1]).toContain(
+ "is being read outside a reactive context. Doing a full recompute."
+ )
+ } finally {
+ console.warn = consoleWarn
+ }
+})
+
+test("multiple inheritance should work", () => {
+ class A {
+ @observable accessor x = 1
+ }
+
+ class B extends A {
+ @observable accessor y = 1
+
+ constructor() {
+ super()
+ makeObservable(this)
+ }
+ }
+
+ const obsvKeys = [
+ ...(mobx._getAdministration(new B()) as ObservableArrayAdministration).values_.keys()
+ ]
+ expect(obsvKeys).toEqual(["x", "y"])
+})
+
+// 19.12.2020 @urugator:
+// All annotated non-observable fields are not writable.
+// All annotated fields of non-plain objects are non-configurable.
+// https://github.com/mobxjs/mobx/pull/2641
+test.skip("actions are reassignable", () => {
+ // See #1398 and #1545, make actions reassignable to support stubbing
+ class A {
+ @action
+ m1() {}
+ @action.bound
+ m3() {}
+ }
+
+ const a = new A()
+ expect(isAction(a.m1)).toBe(true)
+ expect(isAction(a.m3)).toBe(true)
+ a.m1 = () => {}
+ expect(isAction(a.m1)).toBe(false)
+ a.m3 = () => {}
+ expect(isAction(a.m3)).toBe(false)
+})
+
+test("it should support asyncAction as decorator (2022.3)", async () => {
+ mobx.configure({ enforceActions: "observed" })
+
+ class X {
+ @observable accessor a = 1
+
+ f = mobx.flow(function* f(this: X, initial: number) {
+ this.a = initial // this runs in action
+ this.a += yield Promise.resolve(5) as any
+ this.a = this.a * 2
+ return this.a
+ })
+ }
+
+ const x = new X()
+
+ expect(await x.f(3)).toBe(16)
+})
+
+test("toJS bug #1413 (2022.3)", () => {
+ class X {
+ @observable
+ accessor test = {
+ test1: 1
+ }
+ }
+
+ const x = new X()
+ const res = mobx.toJS(x.test) as any
+ expect(res).toEqual({ test1: 1 })
+ expect(res.__mobxDidRunLazyInitializers).toBe(undefined)
+})
+
+test("#2159 - computed property keys", () => {
+ const testSymbol = Symbol("test symbol")
+ const testString = "testString"
+
+ class TestClass {
+ @observable accessor [testSymbol] = "original symbol value"
+ @observable accessor [testString] = "original string value"
+ }
+
+ const o = new TestClass()
+
+ const events: any[] = []
+ observe(o, testSymbol, ev => events.push(ev.newValue, ev.oldValue))
+ observe(o, testString, ev => events.push(ev.newValue, ev.oldValue))
+
+ runInAction(() => {
+ o[testSymbol] = "new symbol value"
+ o[testString] = "new string value"
+ })
+
+ t.deepEqual(events, [
+ "new symbol value", // new symbol
+ "original symbol value", // original symbol
+ "new string value", // new string
+ "original string value" // original string
+ ])
+})
diff --git a/packages/mobx/__tests__/decorators_20223/tsconfig.json b/packages/mobx/__tests__/decorators_20223/tsconfig.json
new file mode 100644
index 0000000000..31758eb645
--- /dev/null
+++ b/packages/mobx/__tests__/decorators_20223/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": ["../../tsconfig.json", "../../../../tsconfig.test.json"],
+ "compilerOptions": {
+ "target": "ES6",
+ "experimentalDecorators": false,
+ "useDefineForClassFields": true,
+
+ "rootDir": "../../"
+ },
+ "exclude": ["__tests__"],
+ "include": ["./", "../../src"] // ["../../src", "./"]
+}
diff --git a/packages/mobx/jest.config-decorators.js b/packages/mobx/jest.config-decorators.js
new file mode 100644
index 0000000000..751c550a4f
--- /dev/null
+++ b/packages/mobx/jest.config-decorators.js
@@ -0,0 +1,11 @@
+const path = require("path")
+const buildConfig = require("../../jest.base.config")
+
+module.exports = buildConfig(
+ __dirname,
+ {
+ testRegex: "__tests__/decorators_20223/.*\\.(t|j)sx?$",
+ setupFilesAfterEnv: [`/jest.setup.ts`]
+ },
+ path.resolve(__dirname, "./__tests__/decorators_20223/tsconfig.json")
+)
diff --git a/packages/mobx/jest.config.js b/packages/mobx/jest.config.js
index f0af2b1e22..c5223d1fce 100644
--- a/packages/mobx/jest.config.js
+++ b/packages/mobx/jest.config.js
@@ -1,6 +1,7 @@
const buildConfig = require("../../jest.base.config")
module.exports = buildConfig(__dirname, {
+ projects: ["/jest.config.js", "/jest.config-decorators.js"],
testRegex: "__tests__/v[4|5]/base/.*\\.(t|j)sx?$",
setupFilesAfterEnv: [`/jest.setup.ts`]
})
diff --git a/packages/mobx/src/api/action.ts b/packages/mobx/src/api/action.ts
index ab5e386011..2fc2b519e7 100644
--- a/packages/mobx/src/api/action.ts
+++ b/packages/mobx/src/api/action.ts
@@ -7,9 +7,12 @@ import {
isFunction,
isStringish,
createDecoratorAnnotation,
- createActionAnnotation
+ createActionAnnotation,
+ is20223Decorator
} from "../internal"
+import type { ClassFieldDecorator, ClassMethodDecorator } from "../types/decorator_fills"
+
export const ACTION = "action"
export const ACTION_BOUND = "action.bound"
export const AUTOACTION = "autoAction"
@@ -29,17 +32,24 @@ const autoActionBoundAnnotation = createActionAnnotation(AUTOACTION_BOUND, {
bound: true
})
-export interface IActionFactory extends Annotation, PropertyDecorator {
+export interface IActionFactory
+ extends Annotation,
+ PropertyDecorator,
+ ClassMethodDecorator,
+ ClassFieldDecorator {
// nameless actions
(fn: T): T
// named actions
(name: string, fn: T): T
// named decorator
- (customName: string): PropertyDecorator & Annotation
+ (customName: string): PropertyDecorator &
+ Annotation &
+ ClassMethodDecorator &
+ ClassFieldDecorator
// decorator (name no longer supported)
- bound: Annotation & PropertyDecorator
+ bound: Annotation & PropertyDecorator & ClassMethodDecorator & ClassFieldDecorator
}
function createActionFactory(autoAction: boolean): IActionFactory {
@@ -52,6 +62,13 @@ function createActionFactory(autoAction: boolean): IActionFactory {
if (isFunction(arg2)) {
return createAction(arg1, arg2, autoAction)
}
+ // @action (2022.3 Decorators)
+ if (is20223Decorator(arg2)) {
+ return (autoAction ? autoActionAnnotation : actionAnnotation).decorate_20223_(
+ arg1,
+ arg2
+ )
+ }
// @action
if (isStringish(arg2)) {
return storeAnnotation(arg1, arg2, autoAction ? autoActionAnnotation : actionAnnotation)
diff --git a/packages/mobx/src/api/annotation.ts b/packages/mobx/src/api/annotation.ts
index 87a9ea607f..719c727402 100644
--- a/packages/mobx/src/api/annotation.ts
+++ b/packages/mobx/src/api/annotation.ts
@@ -20,6 +20,7 @@ export type Annotation = {
descriptor: PropertyDescriptor,
proxyTrap: boolean
): boolean | null
+ decorate_20223_(value: any, context: DecoratorContext)
options_?: any
}
diff --git a/packages/mobx/src/api/computed.ts b/packages/mobx/src/api/computed.ts
index 104932bd38..6ac31e8e04 100644
--- a/packages/mobx/src/api/computed.ts
+++ b/packages/mobx/src/api/computed.ts
@@ -10,19 +10,22 @@ import {
die,
IComputedValue,
createComputedAnnotation,
- comparer
+ comparer,
+ is20223Decorator
} from "../internal"
+import type { ClassGetterDecorator } from "../types/decorator_fills"
+
export const COMPUTED = "computed"
export const COMPUTED_STRUCT = "computed.struct"
-export interface IComputedFactory extends Annotation, PropertyDecorator {
+export interface IComputedFactory extends Annotation, PropertyDecorator, ClassGetterDecorator {
// @computed(opts)
- (options: IComputedValueOptions): Annotation & PropertyDecorator
+ (options: IComputedValueOptions): Annotation & PropertyDecorator & ClassGetterDecorator
// computed(fn, opts)
(func: () => T, options?: IComputedValueOptions): IComputedValue
- struct: Annotation & PropertyDecorator
+ struct: Annotation & PropertyDecorator & ClassGetterDecorator
}
const computedAnnotation = createComputedAnnotation(COMPUTED)
@@ -35,6 +38,10 @@ const computedStructAnnotation = createComputedAnnotation(COMPUTED_STRUCT, {
* For legacy purposes also invokable as ES5 observable created: `computed(() => expr)`;
*/
export const computed: IComputedFactory = function computed(arg1, arg2) {
+ if (is20223Decorator(arg2)) {
+ // @computed (2022.3 Decorators)
+ return computedAnnotation.decorate_20223_(arg1, arg2)
+ }
if (isStringish(arg2)) {
// @computed
return storeAnnotation(arg1, arg2, computedAnnotation)
diff --git a/packages/mobx/src/api/decorators.ts b/packages/mobx/src/api/decorators.ts
index 5e682c6c82..caa64d38ee 100644
--- a/packages/mobx/src/api/decorators.ts
+++ b/packages/mobx/src/api/decorators.ts
@@ -1,5 +1,7 @@
import { Annotation, addHiddenProp, AnnotationsMap, hasProp, die, isOverride } from "../internal"
+import type { Decorator } from "../types/decorator_fills"
+
export const storedAnnotationsSymbol = Symbol("mobx-stored-annotations")
/**
@@ -7,11 +9,17 @@ export const storedAnnotationsSymbol = Symbol("mobx-stored-annotations")
* - decorator
* - annotation object
*/
-export function createDecoratorAnnotation(annotation: Annotation): PropertyDecorator & Annotation {
+export function createDecoratorAnnotation(
+ annotation: Annotation
+): PropertyDecorator & Annotation & D {
function decorator(target, property) {
- storeAnnotation(target, property, annotation)
+ if (is20223Decorator(property)) {
+ return annotation.decorate_20223_(target, property)
+ } else {
+ storeAnnotation(target, property, annotation)
+ }
}
- return Object.assign(decorator, annotation)
+ return Object.assign(decorator, annotation) as any
}
/**
@@ -61,13 +69,26 @@ function assertNotDecorated(prototype: object, annotation: Annotation, key: Prop
*/
export function collectStoredAnnotations(target): AnnotationsMap {
if (!hasProp(target, storedAnnotationsSymbol)) {
- if (__DEV__ && !target[storedAnnotationsSymbol]) {
- die(
- `No annotations were passed to makeObservable, but no decorated members have been found either`
- )
- }
+ // if (__DEV__ && !target[storedAnnotationsSymbol]) {
+ // die(
+ // `No annotations were passed to makeObservable, but no decorated members have been found either`
+ // )
+ // }
// We need a copy as we will remove annotation from the list once it's applied.
addHiddenProp(target, storedAnnotationsSymbol, { ...target[storedAnnotationsSymbol] })
}
return target[storedAnnotationsSymbol]
}
+
+export function is20223Decorator(context): context is DecoratorContext {
+ return typeof context == "object" && typeof context["kind"] == "string"
+}
+
+export function assert20223DecoratorType(
+ context: DecoratorContext,
+ types: DecoratorContext["kind"][]
+) {
+ if (__DEV__ && !types.includes(context.kind)) {
+ die(`Decorator may not be used like this`)
+ }
+}
diff --git a/packages/mobx/src/api/flow.ts b/packages/mobx/src/api/flow.ts
index daad7a7e5a..ced847471c 100644
--- a/packages/mobx/src/api/flow.ts
+++ b/packages/mobx/src/api/flow.ts
@@ -7,9 +7,12 @@ import {
isStringish,
storeAnnotation,
createFlowAnnotation,
- createDecoratorAnnotation
+ createDecoratorAnnotation,
+ is20223Decorator
} from "../internal"
+import type { ClassMethodDecorator } from "../types/decorator_fills"
+
export const FLOW = "flow"
let generatorId = 0
@@ -25,11 +28,11 @@ export function isFlowCancellationError(error: Error) {
export type CancellablePromise = Promise & { cancel(): void }
-interface Flow extends Annotation, PropertyDecorator {
+interface Flow extends Annotation, PropertyDecorator, ClassMethodDecorator {
(
generator: (...args: Args) => Generator | AsyncGenerator
): (...args: Args) => CancellablePromise
- bound: Annotation & PropertyDecorator
+ bound: Annotation & PropertyDecorator & ClassMethodDecorator
}
const flowAnnotation = createFlowAnnotation("flow")
@@ -37,6 +40,10 @@ const flowBoundAnnotation = createFlowAnnotation("flow.bound", { bound: true })
export const flow: Flow = Object.assign(
function flow(arg1, arg2?) {
+ // @flow (2022.3 Decorators)
+ if (is20223Decorator(arg2)) {
+ return flowAnnotation.decorate_20223_(arg1, arg2)
+ }
// @flow
if (isStringish(arg2)) {
return storeAnnotation(arg1, arg2, flowAnnotation)
diff --git a/packages/mobx/src/api/observable.ts b/packages/mobx/src/api/observable.ts
index 8d808aee1d..78ed391246 100644
--- a/packages/mobx/src/api/observable.ts
+++ b/packages/mobx/src/api/observable.ts
@@ -29,9 +29,12 @@ import {
assign,
isStringish,
createObservableAnnotation,
- createAutoAnnotation
+ createAutoAnnotation,
+ is20223Decorator
} from "../internal"
+import type { ClassAccessorDecorator, ClassFieldDecorator } from "../types/decorator_fills"
+
export const OBSERVABLE = "observable"
export const OBSERVABLE_REF = "observable.ref"
export const OBSERVABLE_SHALLOW = "observable.shallow"
@@ -70,7 +73,8 @@ const observableShallowAnnotation = createObservableAnnotation(OBSERVABLE_SHALLO
const observableStructAnnotation = createObservableAnnotation(OBSERVABLE_STRUCT, {
enhancer: refStructEnhancer
})
-const observableDecoratorAnnotation = createDecoratorAnnotation(observableAnnotation)
+const observableDecoratorAnnotation =
+ createDecoratorAnnotation(observableAnnotation)
export function getEnhancerFromOptions(options: CreateObservableOptions): IEnhancer {
return options.deep === true
@@ -95,6 +99,11 @@ export function getEnhancerFromAnnotation(annotation?: Annotation): IEnhancer(value?: T, options?: CreateObservableOptions): IObservableValue
}
-export interface IObservableFactory extends Annotation, PropertyDecorator {
+export interface IObservableFactory
+ extends Annotation,
+ PropertyDecorator,
+ ClassAccessorDecorator,
+ ClassFieldDecorator {
(value: T[], options?: CreateObservableOptions): IObservableArray
(value: Set, options?: CreateObservableOptions): ObservableSet
(value: Map, options?: CreateObservableOptions): ObservableMap
@@ -170,13 +183,13 @@ export interface IObservableFactory extends Annotation, PropertyDecorator {
/**
* Decorator that creates an observable that only observes the references, but doesn't try to turn the assigned value into an observable.ts.
*/
- ref: Annotation & PropertyDecorator
+ ref: Annotation & PropertyDecorator & ClassAccessorDecorator & ClassFieldDecorator
/**
* Decorator that creates an observable converts its value (objects, maps or arrays) into a shallow observable structure
*/
- shallow: Annotation & PropertyDecorator
- deep: Annotation & PropertyDecorator
- struct: Annotation & PropertyDecorator
+ shallow: Annotation & PropertyDecorator & ClassAccessorDecorator & ClassFieldDecorator
+ deep: Annotation & PropertyDecorator & ClassAccessorDecorator & ClassFieldDecorator
+ struct: Annotation & PropertyDecorator & ClassAccessorDecorator & ClassFieldDecorator
}
const observableFactories: IObservableFactory = {
diff --git a/packages/mobx/src/types/actionannotation.ts b/packages/mobx/src/types/actionannotation.ts
index 7385c108b8..831cf52369 100644
--- a/packages/mobx/src/types/actionannotation.ts
+++ b/packages/mobx/src/types/actionannotation.ts
@@ -7,7 +7,9 @@ import {
isFunction,
Annotation,
globalState,
- MakeResult
+ MakeResult,
+ assert20223DecoratorType,
+ storeAnnotation
} from "../internal"
export function createActionAnnotation(name: string, options?: object): Annotation {
@@ -15,11 +17,13 @@ export function createActionAnnotation(name: string, options?: object): Annotati
annotationType_: name,
options_: options,
make_,
- extend_
+ extend_,
+ decorate_20223_
}
}
function make_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor,
@@ -49,6 +53,7 @@ function make_(
}
function extend_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor,
@@ -58,6 +63,47 @@ function extend_(
return adm.defineProperty_(key, actionDescriptor, proxyTrap)
}
+function decorate_20223_(this: Annotation, mthd, context: DecoratorContext) {
+ if (__DEV__) {
+ assert20223DecoratorType(context, ["method", "field"])
+ }
+ const { kind, name, addInitializer } = context
+ const ann = this
+
+ const _createAction = m =>
+ createAction(ann.options_?.name ?? name!.toString(), m, ann.options_?.autoAction ?? false)
+
+ // Backwards/Legacy behavior, expects makeObservable(this)
+ if (kind == "field") {
+ addInitializer(function () {
+ storeAnnotation(this, name, ann)
+ })
+ return
+ }
+
+ if (kind == "method") {
+ if (!isAction(mthd)) {
+ mthd = _createAction(mthd)
+ }
+
+ if (this.options_?.bound) {
+ addInitializer(function () {
+ const self = this as any
+ const bound = self[name].bind(self)
+ bound.isMobxAction = true
+ self[name] = bound
+ })
+ }
+
+ return mthd
+ }
+
+ die(
+ `Cannot apply '${ann.annotationType_}' to '${String(name)}' (kind: ${kind}):` +
+ `\n'${ann.annotationType_}' can only be used on properties with a function value.`
+ )
+}
+
function assertActionDescriptor(
adm: ObservableObjectAdministration,
{ annotationType_ }: Annotation,
diff --git a/packages/mobx/src/types/autoannotation.ts b/packages/mobx/src/types/autoannotation.ts
index 1be4e26363..c80f3b48d9 100644
--- a/packages/mobx/src/types/autoannotation.ts
+++ b/packages/mobx/src/types/autoannotation.ts
@@ -9,7 +9,8 @@ import {
computed,
autoAction,
isGenerator,
- MakeResult
+ MakeResult,
+ die
} from "../internal"
const AUTO = "true"
@@ -21,7 +22,8 @@ export function createAutoAnnotation(options?: object): Annotation {
annotationType_: AUTO,
options_: options,
make_,
- extend_
+ extend_,
+ decorate_20223_
}
}
@@ -105,3 +107,7 @@ function extend_(
let observableAnnotation = this.options_?.deep === false ? observable.ref : observable
return observableAnnotation.extend_(adm, key, descriptor, proxyTrap)
}
+
+function decorate_20223_(this: Annotation, desc, context: ClassGetterDecoratorContext) {
+ die(`'${this.annotationType_}' cannot be used as a decorator`)
+}
diff --git a/packages/mobx/src/types/computedannotation.ts b/packages/mobx/src/types/computedannotation.ts
index 4caa11f6eb..dd398741f4 100644
--- a/packages/mobx/src/types/computedannotation.ts
+++ b/packages/mobx/src/types/computedannotation.ts
@@ -1,15 +1,26 @@
-import { ObservableObjectAdministration, die, Annotation, MakeResult } from "../internal"
+import {
+ ObservableObjectAdministration,
+ die,
+ Annotation,
+ MakeResult,
+ assert20223DecoratorType,
+ $mobx,
+ asObservableObject,
+ ComputedValue
+} from "../internal"
export function createComputedAnnotation(name: string, options?: object): Annotation {
return {
annotationType_: name,
options_: options,
make_,
- extend_
+ extend_,
+ decorate_20223_
}
}
function make_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor
@@ -18,6 +29,7 @@ function make_(
}
function extend_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor,
@@ -35,6 +47,31 @@ function extend_(
)
}
+function decorate_20223_(this: Annotation, get, context: ClassGetterDecoratorContext) {
+ if (__DEV__) {
+ assert20223DecoratorType(context, ["getter"])
+ }
+ const ann = this
+ const { name: key, addInitializer } = context
+
+ addInitializer(function () {
+ const adm: ObservableObjectAdministration = asObservableObject(this)[$mobx]
+ const options = {
+ ...ann.options_,
+ get,
+ context: this
+ }
+ options.name ||= __DEV__
+ ? `${adm.name_}.${key.toString()}`
+ : `ObservableObject.${key.toString()}`
+ adm.values_.set(key, new ComputedValue(options))
+ })
+
+ return function () {
+ return this[$mobx].getObservablePropValue_(key)
+ }
+}
+
function assertComputedDescriptor(
adm: ObservableObjectAdministration,
{ annotationType_ }: Annotation,
diff --git a/packages/mobx/src/types/decorator_fills.ts b/packages/mobx/src/types/decorator_fills.ts
new file mode 100644
index 0000000000..084f023376
--- /dev/null
+++ b/packages/mobx/src/types/decorator_fills.ts
@@ -0,0 +1,33 @@
+// Hopefully these will be main-lined into Typescipt, but at the moment TS only declares the Contexts
+
+export type ClassAccessorDecorator = (
+ value: ClassAccessorDecoratorTarget,
+ context: ClassAccessorDecoratorContext
+) => ClassAccessorDecoratorResult | void
+
+export type ClassGetterDecorator = (
+ value: (this: This) => Value,
+ context: ClassGetterDecoratorContext
+) => ((this: This) => Value) | void
+
+export type ClassSetterDecorator = (
+ value: (this: This, value: Value) => void,
+ context: ClassSetterDecoratorContext
+) => ((this: This, value: Value) => void) | void
+
+export type ClassMethodDecorator any = any> = (
+ value: Value,
+ context: ClassMethodDecoratorContext
+) => Value | void
+
+export type ClassFieldDecorator any = any> = (
+ value: Value,
+ context: ClassFieldDecoratorContext
+) => Value | void
+
+export type Decorator =
+ | ClassAccessorDecorator
+ | ClassGetterDecorator
+ | ClassSetterDecorator
+ | ClassMethodDecorator
+ | ClassFieldDecorator
diff --git a/packages/mobx/src/types/flowannotation.ts b/packages/mobx/src/types/flowannotation.ts
index 42d6f3e109..530b95942c 100644
--- a/packages/mobx/src/types/flowannotation.ts
+++ b/packages/mobx/src/types/flowannotation.ts
@@ -8,7 +8,8 @@ import {
isFunction,
globalState,
MakeResult,
- hasProp
+ hasProp,
+ assert20223DecoratorType
} from "../internal"
export function createFlowAnnotation(name: string, options?: object): Annotation {
@@ -16,11 +17,13 @@ export function createFlowAnnotation(name: string, options?: object): Annotation
annotationType_: name,
options_: options,
make_,
- extend_
+ extend_,
+ decorate_20223_
}
}
function make_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor,
@@ -50,6 +53,7 @@ function make_(
}
function extend_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor,
@@ -59,6 +63,28 @@ function extend_(
return adm.defineProperty_(key, flowDescriptor, proxyTrap)
}
+function decorate_20223_(this: Annotation, mthd, context: ClassMethodDecoratorContext) {
+ if (__DEV__) {
+ assert20223DecoratorType(context, ["method"])
+ }
+ const { name, addInitializer } = context
+
+ if (!isFlow(mthd)) {
+ mthd = flow(mthd)
+ }
+
+ if (this.options_?.bound) {
+ addInitializer(function () {
+ const self = this as any
+ const bound = self[name].bind(self)
+ bound.isMobXFlow = true
+ self[name] = bound
+ })
+ }
+
+ return mthd
+}
+
function assertFlowDescriptor(
adm: ObservableObjectAdministration,
{ annotationType_ }: Annotation,
diff --git a/packages/mobx/src/types/observableannotation.ts b/packages/mobx/src/types/observableannotation.ts
index 4e5175e431..1f345011d9 100644
--- a/packages/mobx/src/types/observableannotation.ts
+++ b/packages/mobx/src/types/observableannotation.ts
@@ -3,7 +3,12 @@ import {
deepEnhancer,
die,
Annotation,
- MakeResult
+ MakeResult,
+ assert20223DecoratorType,
+ ObservableValue,
+ asObservableObject,
+ $mobx,
+ storeAnnotation
} from "../internal"
export function createObservableAnnotation(name: string, options?: object): Annotation {
@@ -11,11 +16,13 @@ export function createObservableAnnotation(name: string, options?: object): Anno
annotationType_: name,
options_: options,
make_,
- extend_
+ extend_,
+ decorate_20223_
}
}
function make_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor
@@ -24,6 +31,7 @@ function make_(
}
function extend_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor,
@@ -38,6 +46,71 @@ function extend_(
)
}
+function decorate_20223_(
+ this: Annotation,
+ desc,
+ context: ClassAccessorDecoratorContext | ClassFieldDecoratorContext
+) {
+ if (__DEV__) {
+ assert20223DecoratorType(context, ["accessor", "field"])
+ }
+
+ const ann = this
+ const { kind, name, addInitializer } = context
+
+ // Backwards/Legacy behavior, expects makeObservable(this)
+ if (kind == "field") {
+ addInitializer(function () {
+ storeAnnotation(this, name, ann)
+ })
+ return
+ }
+
+ // The laziness here is not ideal... It's a workaround to how 2022.3 Decorators are implemented:
+ // `addInitializer` callbacks are executed _before_ any accessors are defined (instead of the ideal-for-us right after each).
+ // This means that, if we were to do our stuff in an `addInitializer`, we'd attempt to read a private slot
+ // before it has been initialized. The runtime doesn't like that and throws a `Cannot read private member
+ // from an object whose class did not declare it` error.
+ const initializedObjects = new WeakSet()
+
+ function initializeObservable(target, value) {
+ const adm: ObservableObjectAdministration = asObservableObject(target)[$mobx]
+ const observable = new ObservableValue(
+ value,
+ ann.options_?.enhancer ?? deepEnhancer,
+ __DEV__ ? `${adm.name_}.${name.toString()}` : `ObservableObject.${name.toString()}`,
+ false
+ )
+ adm.values_.set(name, observable)
+ initializedObjects.add(target)
+ }
+
+ if (kind == "accessor") {
+ return {
+ get() {
+ if (!initializedObjects.has(this)) {
+ initializeObservable(this, desc.get.call(this))
+ }
+ return this[$mobx].getObservablePropValue_(name)
+ },
+ set(value) {
+ if (!initializedObjects.has(this)) {
+ initializeObservable(this, value)
+ }
+ return this[$mobx].setObservablePropValue_(name, value)
+ },
+ init(value) {
+ if (!initializedObjects.has(this)) {
+ initializeObservable(this, value)
+ }
+ return value
+ }
+ }
+ }
+
+ return
+}
+
function assertObservableDescriptor(
adm: ObservableObjectAdministration,
{ annotationType_ }: Annotation,
diff --git a/packages/mobx/src/types/observableobject.ts b/packages/mobx/src/types/observableobject.ts
index 0828577e54..75a63a4d69 100644
--- a/packages/mobx/src/types/observableobject.ts
+++ b/packages/mobx/src/types/observableobject.ts
@@ -707,6 +707,22 @@ function getCachedObservablePropDescriptor(key) {
)
}
+const fallthroughDescriptorCache = Object.create(null)
+
+export function getCachedFallthroughPropDescriptor(key) {
+ return (
+ fallthroughDescriptorCache[key] ||
+ (fallthroughDescriptorCache[key] = {
+ get() {
+ return Reflect.get(Object.getPrototypeOf(this), key, this)
+ },
+ set(v) {
+ return Reflect.set(Object.getPrototypeOf(this), key, v, this)
+ }
+ })
+ )
+}
+
export function isObservableObject(thing: any): boolean {
if (isObject(thing)) {
return isObservableObjectAdministration((thing as any)[$mobx])
diff --git a/packages/mobx/src/types/overrideannotation.ts b/packages/mobx/src/types/overrideannotation.ts
index 36fe60d555..f688c99630 100644
--- a/packages/mobx/src/types/overrideannotation.ts
+++ b/packages/mobx/src/types/overrideannotation.ts
@@ -7,19 +7,23 @@ import {
MakeResult
} from "../internal"
+import type { ClassMethodDecorator } from "./decorator_fills"
+
const OVERRIDE = "override"
-export const override: Annotation & PropertyDecorator = createDecoratorAnnotation({
- annotationType_: OVERRIDE,
- make_,
- extend_
-})
+export const override: Annotation & PropertyDecorator & ClassMethodDecorator =
+ createDecoratorAnnotation({
+ annotationType_: OVERRIDE,
+ make_,
+ extend_,
+ decorate_20223_
+ })
export function isOverride(annotation: Annotation): boolean {
return annotation.annotationType_ === OVERRIDE
}
-function make_(adm: ObservableObjectAdministration, key): MakeResult {
+function make_(this: Annotation, adm: ObservableObjectAdministration, key): MakeResult {
// Must not be plain object
if (__DEV__ && adm.isPlainObject_) {
die(
@@ -37,6 +41,10 @@ function make_(adm: ObservableObjectAdministration, key): MakeResult {
return MakeResult.Cancel
}
-function extend_(adm, key, descriptor, proxyTrap): boolean {
+function extend_(this: Annotation, adm, key, descriptor, proxyTrap): boolean {
die(`'${this.annotationType_}' can only be used with 'makeObservable'`)
}
+
+function decorate_20223_(this: Annotation, desc, context: DecoratorContext) {
+ console.warn(`'${this.annotationType_}' cannot be used with decorators - this is a no-op`)
+}
diff --git a/yarn.lock b/yarn.lock
index 34cd4b4b55..838f12c39e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11574,10 +11574,10 @@ prettier@^1.19.1:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
-prettier@^2.0.5:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
- integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
+prettier@^2.8.4:
+ version "2.8.4"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3"
+ integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==
pretty-format@^25.2.1, pretty-format@^25.5.0:
version "25.5.0"
@@ -13908,10 +13908,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
-typescript@^3.7.3, typescript@^4.0.2:
- version "4.5.4"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
- integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
+typescript@^3.7.3, typescript@^5.0.0-beta:
+ version "5.0.0-dev.20230222"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.0-dev.20230222.tgz#58809d36b989d244ef037ae5f869f0fc233a952c"
+ integrity sha512-OCNanAIcGf3Uy1aBvLbPNe524MnDEZChefbzgo9gvEZPYNG7Zma1C6dXuOBSCapLgbLHksOTyoPwixKTbDkZPQ==
uglify-js@^3.1.4:
version "3.14.5"