-
Notifications
You must be signed in to change notification settings - Fork 641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Alternative model definition API #592
Comments
I know its really unlikely that we'll change the default API, but since lots of people are coming up with alternatives (I remember MST-Classy) is one... perhaps we can document all the alternative styles somewhere with MST being the base-project? |
I'll create a repository with this code. |
Hey @s-panferov! As suggested above we cannot make this the default syntax in MST (it has seemingly no full feature parity, and would complicate the build setup of any consumer). So shipping as separate package (or join forces with mst-classy) would be encouraged. Make sure to mention the link here: mobxjs/awesome-mobx#39 Thanks |
I'm very interested in this @s-panferov if you set it up please let me know! |
@steve8708 Here's my ES6 version in case you or someone else who comes across this thread is still interested. Couldn't find a working version from @s-panferov anywhere and I don't use typescript. Note that I added prototype walking so you can extend from other classes, use a decorator to apply it, auto-optional/maybe every field, and I auto-unprotect every instance in an import {types, flow, unprotect} from 'mobx-state-tree'
const introSymbol = Symbol.for('mstIntrospection')
function ensureMSTIntrospection(object) {
if (object[introSymbol]) {
return object[introSymbol]
} else {
const intro = {
fields: {},
views: {},
actions: {},
volatile: {},
}
object[introSymbol] = intro
return intro
}
}
export const field = type => {
return (target, property) => {
const intro = ensureMSTIntrospection(target)
intro.fields[property.toString()] = type
}
}
export const computed = (target, property) => {
const intro = ensureMSTIntrospection(target)
intro.views[property.toString()] = property
}
export const action = (target, property) => {
const intro = ensureMSTIntrospection(target)
intro.actions[property.toString()] = property
}
export const volatile = (target, property) => {
const intro = ensureMSTIntrospection(target)
intro.volatile[property.toString()] = property
}
const getPropDescriptor = (obj, property) => {
let supe, desc = Object.getOwnPropertyDescriptor(obj.prototype, property)
while(!desc && (supe = Object.getPrototypeOf(obj))) {
desc = getPropDescriptor(supe, property)
}
return desc
}
export function mst(Type) {
const intro = ensureMSTIntrospection(Type.prototype)
const instance = new Type()
const fields = {}
Object.keys(intro.fields).forEach(key => {
if (instance.hasOwnProperty(key) && typeof instance[key] !== 'undefined') {
const defaultValue = instance[key]
fields[key] = types.optional(intro.fields[key], defaultValue)
} else {
fields[key] = types.maybe(intro.fields[key])
}
})
const model = types
.model(Type.name, fields)
.extend(self => {
const views = {}
Object.keys(intro.views).forEach(key => {
const desc = getPropDescriptor(Type, key)
if (desc && desc.value) {
views[key] = desc.value.bind(self)
} else if (desc.get) {
Object.defineProperty(views, key, {
get: desc.get.bind(self),
})
}
})
const actions = {}
Object.keys(intro.actions).forEach(key => {
const desc = getPropDescriptor(Type, key)
if (desc && desc.value) {
const func = desc.value
if (
func.prototype &&
func.prototype.toString() === '[object Generator]'
) {
actions[key] = flow(func.bind(self))
} else {
actions[key] = func.bind(self)
}
}
})
actions.afterCreate = function() {
unprotect(self)
}
return {
views,
actions,
}
})
.volatile(_self => {
const volatile = {}
const instance = new Type()
Object.keys(intro.volatile).forEach(key => {
const defaultValue = instance[key]
volatile[key] = defaultValue
})
return volatile
})
return model
} And then you use it almost like vanilla mobx: import {mst, computed, field, action, volatile} from 'mst'
@mst
export class Crew extends Model {
@field(types.string) name
@field(types.string) email
@field(types.string) phone
@field(types.string) position
@field(types.array(Event)) events = []
@computed
get total() {
return this.things.reduce((a, c) => a + c, 0)
}
@action doStuff() {
}
} |
Hi! I just released the mst-decorators library to define class-based models. I used this library for 1.5 years for dozens of projects but it may still contain some bugs. And there is no TS defs yet. Feel free to open issues/PRs. Some features:
@model class User {}
const Author = maybe(ref(User))
@model class Message {
@Author author
} Several examples: import {
model, view, action, flow, ref, bool, array, map, maybe, id, str, jsonDate,
} from 'mst-decorators'
@model class BaseUser {
@id id
@str username
@str password
}
@model class User extends BaseUser {
@maybe(str) phone
@maybe(str) firstName
@maybe(str) lastName
@view get fullName() {
if (!this.firstName && !this.lastName) return
if (!this.lastName) return this.firstName
if (!this.firstName) return this.lastName
return `${this.firstName} ${this.lastName}`
}
@action setPhone(phone) {
this.phone = phone
}
}
@model class Location {
@num long
@num lat
}
@model class Message {
@id id
@ref(User) sender
@str text
@jsonDate date
@bool unread
@Location location
static preProcessSnapshot(snap) {
//...
}
static postProcessSnapshot(snap) {
//...
}
onPatch(patch, reversePatch) {
//...
}
onSnapshot(snapshot) {
//...
}
onAction(call) {
//...
}
}
@model class Chat {
@id id
@array(Message) messages
@map(User) users
@action afterCreate() {
this.fetchMessages()
}
@flow fetchMessages = function* () {
this.messages = yield Api.fetchMessages()
}
}
const chat = Chat.create({
id: '1',
}) |
I want to share my small research for those who interested. It appeared very hard to me to use default model definition API, because:
yield
expression returns cannot be typed in TypeScript, soawait
syntax is more preferable for async actions.self
andthis
So for my project I decided to go with decorator+transformations approach. I wrote simple
@action
,@field
,@view
and@volatile
decorators and acreateModel
function:Click to expand the implementation
The code above can be used to write an API like this:
createModel
automatically does all the transformations and binding and even supports a private state:The biggest challenge is to remap
await
toyield
calls. This is achieved with a simple Babel@7 plugin, which is invoked before typescript:Click to expand webpack.config.json
Click to expand transformer.ts
It's better to run this transformation after TypeScript, but unfortunately there is no way to disable decorator emit right now in TypeScript.
Cons of this approach:
Pros of this approach:
this
supportawait
keyword instead ofyield
t.optional
using a native property initialization API.I share this not because I want to propose to change the default API, but maybe to give an alternative way to work with
mobx-state-tree
for those who experience the same problems as me. And maybe this approach can be adopted somehow in the library.The text was updated successfully, but these errors were encountered: