mobx-vue-bridge 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/package.json +65 -0
- package/src/mobxVueBridge.d.ts +33 -0
- package/src/mobxVueBridge.js +432 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2025-09-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release of mobx-vue-bridge
|
|
12
|
+
- Two-way data binding between MobX observables and Vue 3 reactivity
|
|
13
|
+
- Support for properties, getters, setters, and methods
|
|
14
|
+
- Deep object/array observation with proper reactivity
|
|
15
|
+
- Configurable mutation behavior via `allowDirectMutation` option
|
|
16
|
+
- TypeScript definitions for better developer experience
|
|
17
|
+
- Comprehensive test suite covering various scenarios
|
|
18
|
+
- Error handling for edge cases and circular references
|
|
19
|
+
- Performance optimizations with intelligent change detection
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
- `useMobxBridge()` - Main bridge function
|
|
23
|
+
- `usePresenterState()` - Alias for presenter pattern usage
|
|
24
|
+
- Automatic property type detection (observable, computed, methods)
|
|
25
|
+
- Intelligent synchronization preventing infinite loops
|
|
26
|
+
- Support for nested objects and arrays
|
|
27
|
+
- Graceful handling of uninitialized computed properties
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Visar Uruqi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# 🌉 MobX-Vue Bridge
|
|
2
|
+
|
|
3
|
+
A seamless bridge between MobX observables and Vue 3's reactivity system, enabling effortless two-way data binding and state synchronization.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/mobx-vue-bridge)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## ✨ Features
|
|
9
|
+
|
|
10
|
+
- 🔄 **Two-way data binding** between MobX observables and Vue reactive state
|
|
11
|
+
- 🎯 **Automatic property detection** (properties, getters, setters, methods)
|
|
12
|
+
- 🏗️ **Deep object/array observation** with proper reactivity
|
|
13
|
+
- ⚙️ **Configurable mutation behavior**
|
|
14
|
+
- 🔒 **Type-safe bridging** between reactive systems
|
|
15
|
+
- 🚀 **Optimized performance** with intelligent change detection
|
|
16
|
+
- 🛡️ **Error handling** for edge cases and circular references
|
|
17
|
+
|
|
18
|
+
## 📦 Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install mobx-vue-bridge
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Peer Dependencies:**
|
|
25
|
+
- Vue 3.x
|
|
26
|
+
- MobX 6.x
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install vue mobx
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 🚀 Quick Start
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
import { useMobxBridge } from 'mobx-vue-bridge'
|
|
36
|
+
import { makeAutoObservable } from 'mobx'
|
|
37
|
+
|
|
38
|
+
// Your MobX store/presenter
|
|
39
|
+
class UserPresenter {
|
|
40
|
+
constructor() {
|
|
41
|
+
this.name = 'John'
|
|
42
|
+
this.age = 25
|
|
43
|
+
this.emails = []
|
|
44
|
+
|
|
45
|
+
makeAutoObservable(this)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get displayName() {
|
|
49
|
+
return `${this.name} (${this.age})`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
addEmail(email) {
|
|
53
|
+
this.emails.push(email)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// In your Vue component
|
|
58
|
+
|
|
59
|
+
// Option 1: Modern <script setup> syntax (recommended)
|
|
60
|
+
<script setup>
|
|
61
|
+
import { useMobxBridge } from 'mobx-vue-bridge'
|
|
62
|
+
import { makeAutoObservable } from 'mobx'
|
|
63
|
+
|
|
64
|
+
// Your MobX presenter
|
|
65
|
+
class UserPresenter {
|
|
66
|
+
constructor() {
|
|
67
|
+
this.name = 'John'
|
|
68
|
+
this.age = 25
|
|
69
|
+
this.emails = []
|
|
70
|
+
|
|
71
|
+
makeAutoObservable(this)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get displayName() {
|
|
75
|
+
return `${this.name} (${this.age})`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
addEmail(email) {
|
|
79
|
+
this.emails.push(email)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const userPresenter = new UserPresenter()
|
|
84
|
+
|
|
85
|
+
// Bridge MobX observable to Vue reactive state
|
|
86
|
+
const state = useMobxBridge(userPresenter)
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
// Option 2: Traditional Composition API
|
|
90
|
+
<script>
|
|
91
|
+
import { useMobxBridge } from 'mobx-vue-bridge'
|
|
92
|
+
import { makeAutoObservable } from 'mobx'
|
|
93
|
+
|
|
94
|
+
class UserPresenter {
|
|
95
|
+
constructor() {
|
|
96
|
+
this.name = 'John'
|
|
97
|
+
this.age = 25
|
|
98
|
+
this.emails = []
|
|
99
|
+
|
|
100
|
+
makeAutoObservable(this)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get displayName() {
|
|
104
|
+
return `${this.name} (${this.age})`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
addEmail(email) {
|
|
108
|
+
this.emails.push(email)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default {
|
|
113
|
+
setup() {
|
|
114
|
+
const userPresenter = new UserPresenter()
|
|
115
|
+
|
|
116
|
+
// Bridge MobX observable to Vue reactive state
|
|
117
|
+
const state = useMobxBridge(userPresenter)
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
state
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
</script>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```vue
|
|
128
|
+
<template>
|
|
129
|
+
<div>
|
|
130
|
+
<!-- Two-way binding works seamlessly -->
|
|
131
|
+
<input v-model="state.name" />
|
|
132
|
+
<input v-model.number="state.age" />
|
|
133
|
+
|
|
134
|
+
<!-- Computed properties are reactive -->
|
|
135
|
+
<p>{{ state.displayName }}</p>
|
|
136
|
+
|
|
137
|
+
<!-- Methods are properly bound -->
|
|
138
|
+
<button @click="state.addEmail('new@email.com')">
|
|
139
|
+
Add Email
|
|
140
|
+
</button>
|
|
141
|
+
|
|
142
|
+
<!-- Arrays/objects are deeply reactive -->
|
|
143
|
+
<ul>
|
|
144
|
+
<li v-for="email in state.emails" :key="email">
|
|
145
|
+
{{ email }}
|
|
146
|
+
</li>
|
|
147
|
+
</ul>
|
|
148
|
+
</div>
|
|
149
|
+
</template>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 📚 API Reference
|
|
153
|
+
|
|
154
|
+
### `useMobxBridge(mobxObject, options?)`
|
|
155
|
+
|
|
156
|
+
Bridges a MobX observable object with Vue's reactivity system.
|
|
157
|
+
|
|
158
|
+
**Parameters:**
|
|
159
|
+
- `mobxObject` - The MobX observable object to bridge
|
|
160
|
+
- `options` - Configuration options (optional)
|
|
161
|
+
|
|
162
|
+
**Options:**
|
|
163
|
+
- `allowDirectMutation` (boolean, default: `true`) - Whether to allow direct mutation of properties
|
|
164
|
+
|
|
165
|
+
**Returns:** Vue reactive state object
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
// With configuration
|
|
169
|
+
const state = useMobxBridge(store, {
|
|
170
|
+
allowDirectMutation: false // Prevents direct mutations
|
|
171
|
+
})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `usePresenterState(presenter, options?)`
|
|
175
|
+
|
|
176
|
+
Alias for `useMobxBridge` - commonly used with presenter pattern.
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
const state = usePresenterState(presenter, options)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## 🎯 Use Cases
|
|
183
|
+
|
|
184
|
+
### Presenter Pattern
|
|
185
|
+
```javascript
|
|
186
|
+
class TodoPresenter {
|
|
187
|
+
constructor(todoService) {
|
|
188
|
+
this.todoService = todoService
|
|
189
|
+
this.todos = []
|
|
190
|
+
this.filter = 'all'
|
|
191
|
+
this.loading = false
|
|
192
|
+
|
|
193
|
+
makeAutoObservable(this)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get filteredTodos() {
|
|
197
|
+
switch (this.filter) {
|
|
198
|
+
case 'active': return this.todos.filter(t => !t.completed)
|
|
199
|
+
case 'completed': return this.todos.filter(t => t.completed)
|
|
200
|
+
default: return this.todos
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async loadTodos() {
|
|
205
|
+
this.loading = true
|
|
206
|
+
try {
|
|
207
|
+
this.todos = await this.todoService.fetchTodos()
|
|
208
|
+
} finally {
|
|
209
|
+
this.loading = false
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// In component
|
|
215
|
+
const presenter = new TodoPresenter(todoService)
|
|
216
|
+
const state = usePresenterState(presenter)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Store Integration
|
|
220
|
+
```javascript
|
|
221
|
+
// MobX store
|
|
222
|
+
class AppStore {
|
|
223
|
+
constructor() {
|
|
224
|
+
this.user = null
|
|
225
|
+
this.theme = 'light'
|
|
226
|
+
this.notifications = []
|
|
227
|
+
|
|
228
|
+
makeAutoObservable(this)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
get isAuthenticated() {
|
|
232
|
+
return !!this.user
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
setTheme(theme) {
|
|
236
|
+
this.theme = theme
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Bridge in component
|
|
241
|
+
const state = useMobxBridge(appStore)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## 🔧 Advanced Features
|
|
245
|
+
|
|
246
|
+
### Deep Reactivity
|
|
247
|
+
The bridge automatically handles deep changes in objects and arrays:
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
// These mutations are automatically synced
|
|
251
|
+
state.user.profile.name = 'New Name' // Object mutation
|
|
252
|
+
state.todos.push(newTodo) // Array mutation
|
|
253
|
+
state.settings.colors[0] = '#FF0000' // Nested array mutation
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Error Handling
|
|
257
|
+
The bridge gracefully handles edge cases:
|
|
258
|
+
|
|
259
|
+
- Uninitialized computed properties
|
|
260
|
+
- Circular references
|
|
261
|
+
- Failed setter operations
|
|
262
|
+
- Missing dependencies
|
|
263
|
+
|
|
264
|
+
### Performance Optimization
|
|
265
|
+
- Intelligent change detection prevents unnecessary updates
|
|
266
|
+
- Efficient shallow/deep equality checks
|
|
267
|
+
- Minimal overhead for large object graphs
|
|
268
|
+
|
|
269
|
+
## 🧪 Testing
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
npm test # Run tests
|
|
273
|
+
npm run test:watch # Watch mode
|
|
274
|
+
npm run test:coverage # Coverage report
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## 🤝 Contributing
|
|
278
|
+
|
|
279
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
280
|
+
|
|
281
|
+
## 📄 License
|
|
282
|
+
|
|
283
|
+
MIT © [Visar Uruqi](https://github.com/visaruruqi)
|
|
284
|
+
|
|
285
|
+
## 🔗 Links
|
|
286
|
+
|
|
287
|
+
- [GitHub Repository](https://github.com/visaruruqi/mobx-vue-bridge)
|
|
288
|
+
- [NPM Package](https://www.npmjs.com/package/mobx-vue-bridge)
|
|
289
|
+
- [Issues](https://github.com/visaruruqi/mobx-vue-bridge/issues)
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mobx-vue-bridge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A bridge between MobX observables and Vue 3 reactivity system, enabling seamless two-way data binding and state synchronization.",
|
|
5
|
+
"main": "src/mobxVueBridge.js",
|
|
6
|
+
"module": "src/mobxVueBridge.js",
|
|
7
|
+
"types": "src/mobxVueBridge.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./src/mobxVueBridge.js",
|
|
12
|
+
"require": "./src/mobxVueBridge.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src/",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "vitest",
|
|
23
|
+
"test:watch": "vitest --watch",
|
|
24
|
+
"test:coverage": "vitest --coverage",
|
|
25
|
+
"prepublishOnly": "npm test && node scripts/pre-publish.js",
|
|
26
|
+
"publish:dry": "npm publish --dry-run"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/visaruruqi/mobx-vue-bridge.git"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"mobx",
|
|
34
|
+
"vue",
|
|
35
|
+
"vue3",
|
|
36
|
+
"reactivity",
|
|
37
|
+
"state-management",
|
|
38
|
+
"bridge",
|
|
39
|
+
"observable",
|
|
40
|
+
"reactive",
|
|
41
|
+
"two-way-binding",
|
|
42
|
+
"composition-api"
|
|
43
|
+
],
|
|
44
|
+
"author": "Visar Uruqi",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/visaruruqi/mobx-vue-bridge/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/visaruruqi/mobx-vue-bridge#readme",
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"vue": "^3.0.0",
|
|
52
|
+
"mobx": "^6.0.0"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"mobx-utils": "^6.0.0",
|
|
56
|
+
"clone": "^2.1.2"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"vitest": "^1.0.0",
|
|
60
|
+
"@vitest/coverage-v8": "^1.0.0"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=16.0.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Ref, UnwrapRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
export interface MobxBridgeOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Whether to allow direct mutation of properties
|
|
6
|
+
* @default true
|
|
7
|
+
*/
|
|
8
|
+
allowDirectMutation?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Bridge between MobX observables and Vue 3 reactivity system
|
|
13
|
+
*
|
|
14
|
+
* @param mobxObject - The MobX observable object to bridge
|
|
15
|
+
* @param options - Configuration options
|
|
16
|
+
* @returns Vue reactive state object
|
|
17
|
+
*/
|
|
18
|
+
export function useMobxBridge<T extends object>(
|
|
19
|
+
mobxObject: T,
|
|
20
|
+
options?: MobxBridgeOptions
|
|
21
|
+
): UnwrapRef<T>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Helper alias for useMobxBridge - commonly used with presenter objects
|
|
25
|
+
*
|
|
26
|
+
* @param presenter - The MobX presenter object to bridge
|
|
27
|
+
* @param options - Configuration options
|
|
28
|
+
* @returns Vue reactive state object
|
|
29
|
+
*/
|
|
30
|
+
export function usePresenterState<T extends object>(
|
|
31
|
+
presenter: T,
|
|
32
|
+
options?: MobxBridgeOptions
|
|
33
|
+
): UnwrapRef<T>
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { reactive, onUnmounted, ref } from 'vue';
|
|
2
|
+
import { toJS, reaction, observe, isComputedProp, isObservableProp } from 'mobx';
|
|
3
|
+
import { deepObserve } from 'mobx-utils';
|
|
4
|
+
import clone from 'clone';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 🌉 MobX-Vue Bridge
|
|
8
|
+
*
|
|
9
|
+
* @param {object} mobxObject - The MobX observable object to bridge
|
|
10
|
+
* @param {object} options - Configuration options
|
|
11
|
+
* @param {boolean} options.allowDirectMutation - Whether to allow direct mutation of properties
|
|
12
|
+
* @returns {object} Vue reactive state object
|
|
13
|
+
*/
|
|
14
|
+
export function useMobxBridge(mobxObject, options = {}) {
|
|
15
|
+
const safeOptions = options || {};
|
|
16
|
+
// Use explicit boolean conversion to handle truthy/falsy values properly
|
|
17
|
+
const allowDirectMutation = safeOptions.allowDirectMutation !== undefined
|
|
18
|
+
? Boolean(safeOptions.allowDirectMutation)
|
|
19
|
+
: true; // Keep the original default of true
|
|
20
|
+
const vueState = reactive({});
|
|
21
|
+
|
|
22
|
+
// Discover props/methods via MobX introspection (don’t rely on raw descriptors)
|
|
23
|
+
const props = Object.getOwnPropertyNames(mobxObject)
|
|
24
|
+
.concat(Object.getOwnPropertyNames(Object.getPrototypeOf(mobxObject)))
|
|
25
|
+
.filter(p => p !== 'constructor' && !p.startsWith('_'));
|
|
26
|
+
|
|
27
|
+
const members = {
|
|
28
|
+
getters: props.filter(p => {
|
|
29
|
+
try {
|
|
30
|
+
// First try to check if it's a computed property via MobX introspection
|
|
31
|
+
try {
|
|
32
|
+
return isComputedProp(mobxObject, p);
|
|
33
|
+
} catch (computedError) {
|
|
34
|
+
// If isComputedProp fails (e.g., due to uninitialized nested objects),
|
|
35
|
+
// fall back to checking if it has a getter descriptor
|
|
36
|
+
const descriptor = Object.getOwnPropertyDescriptor(mobxObject, p) ||
|
|
37
|
+
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(mobxObject), p);
|
|
38
|
+
|
|
39
|
+
// If it has a getter but no corresponding property, it's likely a computed getter
|
|
40
|
+
return descriptor && typeof descriptor.get === 'function' &&
|
|
41
|
+
!isObservableProp(mobxObject, p);
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}),
|
|
47
|
+
setters: props.filter(p => {
|
|
48
|
+
try {
|
|
49
|
+
// Check if it has a setter descriptor
|
|
50
|
+
const descriptor = Object.getOwnPropertyDescriptor(mobxObject, p) ||
|
|
51
|
+
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(mobxObject), p);
|
|
52
|
+
|
|
53
|
+
// Must have a setter
|
|
54
|
+
if (!descriptor || typeof descriptor.set !== 'function') return false;
|
|
55
|
+
|
|
56
|
+
// Exclude methods
|
|
57
|
+
if (typeof mobxObject[p] === 'function') return false;
|
|
58
|
+
|
|
59
|
+
// For MobX objects with makeAutoObservable, we need to distinguish:
|
|
60
|
+
// 1. Regular observable properties (handled separately)
|
|
61
|
+
// 2. Computed properties with setters (getter/setter pairs)
|
|
62
|
+
// 3. Setter-only properties
|
|
63
|
+
|
|
64
|
+
// Include if it's a computed property with a WORKING setter (getter/setter pair)
|
|
65
|
+
try {
|
|
66
|
+
if (isComputedProp(mobxObject, p)) {
|
|
67
|
+
// For computed properties, test if the setter actually works
|
|
68
|
+
try {
|
|
69
|
+
const originalValue = mobxObject[p];
|
|
70
|
+
descriptor.set.call(mobxObject, originalValue); // Try to set to same value
|
|
71
|
+
return true; // Setter works, it's a getter/setter pair
|
|
72
|
+
} catch (setterError) {
|
|
73
|
+
return false; // Setter throws error, it's a computed-only property
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// If isComputedProp fails, check if it has a getter and test the setter
|
|
78
|
+
if (descriptor.get) {
|
|
79
|
+
try {
|
|
80
|
+
// Try to get the current value and set it back
|
|
81
|
+
const currentValue = mobxObject[p];
|
|
82
|
+
descriptor.set.call(mobxObject, currentValue);
|
|
83
|
+
return true; // Setter works
|
|
84
|
+
} catch (setterError) {
|
|
85
|
+
return false; // Setter throws error
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Include if it's NOT an observable property (setter-only or other cases)
|
|
91
|
+
if (!isObservableProp(mobxObject, p)) return true;
|
|
92
|
+
|
|
93
|
+
// Exclude regular observable properties (they're handled separately)
|
|
94
|
+
return false;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
properties: props.filter(p => {
|
|
100
|
+
try {
|
|
101
|
+
// Check if it's an observable property
|
|
102
|
+
if (!isObservableProp(mobxObject, p)) return false;
|
|
103
|
+
|
|
104
|
+
// Check if it's a function (method)
|
|
105
|
+
if (typeof mobxObject[p] === 'function') return false;
|
|
106
|
+
|
|
107
|
+
// Check if it's a computed property - if so, it's a getter, not a property
|
|
108
|
+
const isComputed = isComputedProp(mobxObject, p);
|
|
109
|
+
if (isComputed) return false;
|
|
110
|
+
|
|
111
|
+
return true; // Regular observable property
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}),
|
|
116
|
+
methods: props.filter(p => {
|
|
117
|
+
try {
|
|
118
|
+
return typeof mobxObject[p] === 'function';
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
// ---- utils: guards + equality --------------------------------------------
|
|
127
|
+
const updatingFromMobx = new Set();
|
|
128
|
+
const updatingFromVue = new Set();
|
|
129
|
+
|
|
130
|
+
const isEqual = (a, b) => {
|
|
131
|
+
if (Object.is(a, b)) return true;
|
|
132
|
+
|
|
133
|
+
// Handle null/undefined cases
|
|
134
|
+
if (a == null || b == null) return a === b;
|
|
135
|
+
|
|
136
|
+
// Different types are not equal
|
|
137
|
+
if (typeof a !== typeof b) return false;
|
|
138
|
+
|
|
139
|
+
// For primitives, Object.is should have caught them
|
|
140
|
+
if (typeof a !== 'object') return false;
|
|
141
|
+
|
|
142
|
+
// Fast array comparison
|
|
143
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
144
|
+
if (a.length !== b.length) return false;
|
|
145
|
+
return a.every((val, i) => isEqual(val, b[i]));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fast object comparison - check keys first
|
|
149
|
+
const aKeys = Object.keys(a);
|
|
150
|
+
const bKeys = Object.keys(b);
|
|
151
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
152
|
+
|
|
153
|
+
// Check if all keys match
|
|
154
|
+
if (!aKeys.every(key => bKeys.includes(key))) return false;
|
|
155
|
+
|
|
156
|
+
// Check values (recursive)
|
|
157
|
+
return aKeys.every(key => isEqual(a[key], b[key]));
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Warning helpers to reduce duplication
|
|
161
|
+
const warnDirectMutation = (prop) => console.warn(`Direct mutation of '${prop}' is disabled`);
|
|
162
|
+
const warnSetterMutation = (prop) => console.warn(`Direct mutation of setter '${prop}' is disabled`);
|
|
163
|
+
const warnMethodAssignment = (prop) => console.warn(`Cannot assign to method '${prop}'`);
|
|
164
|
+
|
|
165
|
+
// Helper to create deep proxies for nested objects and arrays
|
|
166
|
+
const createDeepProxy = (value, prop) => {
|
|
167
|
+
// Don't proxy built-in objects that should remain unchanged
|
|
168
|
+
if (value instanceof Date || value instanceof RegExp || value instanceof Map ||
|
|
169
|
+
value instanceof Set || value instanceof WeakMap || value instanceof WeakSet) {
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return new Proxy(value, {
|
|
174
|
+
get: (target, key) => {
|
|
175
|
+
const result = target[key];
|
|
176
|
+
// If the result is an object/array, wrap it in a proxy too (but not built-ins)
|
|
177
|
+
if (result && typeof result === 'object' &&
|
|
178
|
+
!(result instanceof Date || result instanceof RegExp || result instanceof Map ||
|
|
179
|
+
result instanceof Set || result instanceof WeakMap || result instanceof WeakSet)) {
|
|
180
|
+
return createDeepProxy(result, prop);
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
},
|
|
184
|
+
set: (target, key, val) => {
|
|
185
|
+
target[key] = val;
|
|
186
|
+
// Update the Vue ref to trigger reactivity
|
|
187
|
+
propertyRefs[prop].value = clone(propertyRefs[prop].value);
|
|
188
|
+
// Update MobX immediately
|
|
189
|
+
mobxObject[prop] = clone(propertyRefs[prop].value);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// ---- properties (two-way) -------------------------------------------------
|
|
196
|
+
const propertyRefs = {};
|
|
197
|
+
|
|
198
|
+
members.properties.forEach(prop => {
|
|
199
|
+
propertyRefs[prop] = ref(toJS(mobxObject[prop]));
|
|
200
|
+
|
|
201
|
+
Object.defineProperty(vueState, prop, {
|
|
202
|
+
get: () => {
|
|
203
|
+
const value = propertyRefs[prop].value;
|
|
204
|
+
// For objects/arrays, return a deep proxy that syncs mutations back
|
|
205
|
+
if (value && typeof value === 'object') {
|
|
206
|
+
return createDeepProxy(value, prop);
|
|
207
|
+
}
|
|
208
|
+
return value;
|
|
209
|
+
},
|
|
210
|
+
set: allowDirectMutation
|
|
211
|
+
? (value) => {
|
|
212
|
+
// Update Vue ref
|
|
213
|
+
const cloned = clone(value);
|
|
214
|
+
if (!isEqual(propertyRefs[prop].value, cloned)) {
|
|
215
|
+
propertyRefs[prop].value = cloned;
|
|
216
|
+
}
|
|
217
|
+
// ALSO update MobX immediately (synchronous)
|
|
218
|
+
if (!isEqual(mobxObject[prop], cloned)) {
|
|
219
|
+
mobxObject[prop] = cloned;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
: () => warnDirectMutation(prop),
|
|
223
|
+
enumerable: true,
|
|
224
|
+
configurable: true,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ---- getters and setters (handle both computed and two-way binding) ------
|
|
230
|
+
const getterRefs = {};
|
|
231
|
+
const setterRefs = {};
|
|
232
|
+
|
|
233
|
+
// First, handle properties that have BOTH getters and setters (getter/setter pairs)
|
|
234
|
+
const getterSetterPairs = members.getters.filter(prop => members.setters.includes(prop));
|
|
235
|
+
const gettersOnly = members.getters.filter(prop => !members.setters.includes(prop));
|
|
236
|
+
const settersOnly = members.setters.filter(prop => !members.getters.includes(prop));
|
|
237
|
+
|
|
238
|
+
// Getter/setter pairs: writable with reactive updates
|
|
239
|
+
getterSetterPairs.forEach(prop => {
|
|
240
|
+
// Get initial value from getter
|
|
241
|
+
let initialValue;
|
|
242
|
+
try {
|
|
243
|
+
initialValue = toJS(mobxObject[prop]);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
initialValue = undefined;
|
|
246
|
+
}
|
|
247
|
+
getterRefs[prop] = ref(initialValue);
|
|
248
|
+
setterRefs[prop] = ref(initialValue);
|
|
249
|
+
|
|
250
|
+
Object.defineProperty(vueState, prop, {
|
|
251
|
+
get: () => getterRefs[prop].value,
|
|
252
|
+
set: allowDirectMutation
|
|
253
|
+
? (value) => {
|
|
254
|
+
// Update both refs
|
|
255
|
+
setterRefs[prop].value = value;
|
|
256
|
+
// Call the MobX setter immediately
|
|
257
|
+
try {
|
|
258
|
+
mobxObject[prop] = value;
|
|
259
|
+
// The getter ref will be updated by the reaction
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.warn(`Failed to set property '${prop}':`, error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
: () => warnDirectMutation(prop),
|
|
265
|
+
enumerable: true,
|
|
266
|
+
configurable: true,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Getter-only properties: read-only computed
|
|
271
|
+
gettersOnly.forEach(prop => {
|
|
272
|
+
// Safely get initial value of computed property, handle errors gracefully
|
|
273
|
+
let initialValue;
|
|
274
|
+
try {
|
|
275
|
+
initialValue = toJS(mobxObject[prop]);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
// If computed property throws during initialization (e.g., accessing null.property),
|
|
278
|
+
// set initial value to undefined and let the reaction handle updates later
|
|
279
|
+
initialValue = undefined;
|
|
280
|
+
}
|
|
281
|
+
getterRefs[prop] = ref(initialValue);
|
|
282
|
+
|
|
283
|
+
Object.defineProperty(vueState, prop, {
|
|
284
|
+
get: () => getterRefs[prop].value,
|
|
285
|
+
set: () => {
|
|
286
|
+
throw new Error(`Cannot assign to computed property '${prop}'`)
|
|
287
|
+
},
|
|
288
|
+
enumerable: true,
|
|
289
|
+
configurable: true,
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Setter-only properties: write-only
|
|
294
|
+
settersOnly.forEach(prop => {
|
|
295
|
+
// For setter-only properties, track the last set value
|
|
296
|
+
setterRefs[prop] = ref(undefined);
|
|
297
|
+
|
|
298
|
+
Object.defineProperty(vueState, prop, {
|
|
299
|
+
get: () => setterRefs[prop].value,
|
|
300
|
+
set: allowDirectMutation
|
|
301
|
+
? (value) => {
|
|
302
|
+
// Update the setter ref
|
|
303
|
+
setterRefs[prop].value = value;
|
|
304
|
+
|
|
305
|
+
// Call the MobX setter immediately
|
|
306
|
+
try {
|
|
307
|
+
mobxObject[prop] = value;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.warn(`Failed to set property '${prop}':`, error);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
: () => warnSetterMutation(prop),
|
|
313
|
+
enumerable: true,
|
|
314
|
+
configurable: true,
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ---- methods (bound) ------------------------------------------------------
|
|
319
|
+
members.methods.forEach(prop => {
|
|
320
|
+
// Cache the bound method to avoid creating new functions on every access
|
|
321
|
+
const boundMethod = mobxObject[prop].bind(mobxObject);
|
|
322
|
+
Object.defineProperty(vueState, prop, {
|
|
323
|
+
get: () => boundMethod,
|
|
324
|
+
set: () => warnMethodAssignment(prop),
|
|
325
|
+
enumerable: true,
|
|
326
|
+
configurable: true,
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ---- MobX → Vue: property observation ----------------------------------------
|
|
331
|
+
const subscriptions = [];
|
|
332
|
+
|
|
333
|
+
setupStandardPropertyObservers();
|
|
334
|
+
|
|
335
|
+
// Standard property observation implementation
|
|
336
|
+
function setupStandardPropertyObservers() {
|
|
337
|
+
// Use individual observe for each property to avoid circular reference issues
|
|
338
|
+
members.properties.forEach(prop => {
|
|
339
|
+
try {
|
|
340
|
+
const sub = observe(mobxObject, prop, (change) => {
|
|
341
|
+
if (!propertyRefs[prop]) return;
|
|
342
|
+
if (updatingFromVue.has(prop)) return; // avoid echo
|
|
343
|
+
updatingFromMobx.add(prop);
|
|
344
|
+
try {
|
|
345
|
+
const next = toJS(mobxObject[prop]);
|
|
346
|
+
if (!isEqual(propertyRefs[prop].value, next)) {
|
|
347
|
+
propertyRefs[prop].value = next;
|
|
348
|
+
}
|
|
349
|
+
} finally {
|
|
350
|
+
updatingFromMobx.delete(prop);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
subscriptions.push(sub);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
// Silently ignore non-observable properties
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// For nested objects and arrays, use deepObserve to handle deep changes
|
|
360
|
+
// This handles both object properties and array mutations
|
|
361
|
+
members.properties.forEach(prop => {
|
|
362
|
+
const value = mobxObject[prop];
|
|
363
|
+
if (value && typeof value === 'object') { // Include both objects AND arrays
|
|
364
|
+
try {
|
|
365
|
+
const sub = deepObserve(value, (change, path) => {
|
|
366
|
+
if (!propertyRefs[prop]) return;
|
|
367
|
+
if (updatingFromVue.has(prop)) return; // avoid echo
|
|
368
|
+
updatingFromMobx.add(prop);
|
|
369
|
+
try {
|
|
370
|
+
const next = toJS(mobxObject[prop]);
|
|
371
|
+
if (!isEqual(propertyRefs[prop].value, next)) {
|
|
372
|
+
propertyRefs[prop].value = next;
|
|
373
|
+
}
|
|
374
|
+
} finally {
|
|
375
|
+
updatingFromMobx.delete(prop);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
subscriptions.push(sub);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
// Silently ignore if deepObserve fails (e.g., circular references in nested objects)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Getters: keep them in sync via reaction (both getter-only and getter/setter pairs)
|
|
387
|
+
[...gettersOnly, ...getterSetterPairs].forEach(prop => {
|
|
388
|
+
const sub = reaction(
|
|
389
|
+
() => {
|
|
390
|
+
try {
|
|
391
|
+
return toJS(mobxObject[prop]);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
// If computed property throws (e.g., accessing null.property), return undefined
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
(next) => {
|
|
398
|
+
if (!getterRefs[prop]) return;
|
|
399
|
+
if (!isEqual(getterRefs[prop].value, next)) {
|
|
400
|
+
getterRefs[prop].value = next;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
subscriptions.push(sub);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Cleanup
|
|
408
|
+
onUnmounted(() => {
|
|
409
|
+
subscriptions.forEach(unsub => {
|
|
410
|
+
try {
|
|
411
|
+
if (typeof unsub === 'function') {
|
|
412
|
+
unsub();
|
|
413
|
+
}
|
|
414
|
+
} catch {
|
|
415
|
+
// Silently ignore cleanup errors
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
return vueState;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Helper alias for useMobxBridge - commonly used with presenter objects
|
|
425
|
+
*
|
|
426
|
+
* @param {object} presenter - The MobX presenter object to bridge
|
|
427
|
+
* @param {object} options - Configuration options
|
|
428
|
+
* @returns {object} Vue reactive state object
|
|
429
|
+
*/
|
|
430
|
+
export function usePresenterState(presenter, options = {}) {
|
|
431
|
+
return useMobxBridge(presenter, options);
|
|
432
|
+
}
|