UNPKG

mobx-keystone-mindreframer

Version:

A MobX powered state management solution based on data trees with first class support for Typescript, snapshots, patches and much more

189 lines (163 loc) 6.34 kB
import { HookAction } from "../action/hookActions" import { wrapModelMethodInActionIfNeeded } from "../action/wrapInAction" import type { AnyDataModel } from "../dataModel/BaseDataModel" import { isDataModelClass } from "../dataModel/utils" import { getGlobalConfig } from "../globalConfig" import type { AnyModel } from "../model/BaseModel" import { modelTypeKey } from "../model/metadata" import { isModelClass } from "../model/utils" import { ModelClass, modelInitializedSymbol } from "../modelShared/BaseModelShared" import { modelInfoByClass, modelInfoByName } from "../modelShared/modelInfo" import { modelUnwrappedClassSymbol, runAfterModelDecoratorSymbol, } from "../modelShared/modelSymbols" import { addHiddenProp, failure, getMobxVersion, logWarning, mobx6, runAfterNewSymbol, runLateInitializationFunctions, } from "../utils" /** * Decorator that marks this class (which MUST inherit from the `Model` or `DataModel` abstract classes) * as a model. * * @param name Unique name for the model type. Note that this name must be unique for your whole * application, so it is usually a good idea to use some prefix unique to your application domain. */ export const model = (name: string) => <MC extends ModelClass<AnyModel | AnyDataModel>>( clazz: MC ): MC => { return internalModel(name)(clazz) } const internalModel = (name: string) => <MC extends ModelClass<AnyModel | AnyDataModel>>( clazz: MC ): MC => { const type = isModelClass(clazz) ? "class" : isDataModelClass(clazz) ? "data" : undefined if (!type) { throw failure(`clazz must be a class that extends from Model/DataModel`) } if (modelInfoByName[name]) { if (getGlobalConfig().showDuplicateModelNameWarnings) { logWarning( "warn", `a model with name "${name}" already exists (if you are using hot-reloading you may safely ignore this warning)`, `duplicateModelName - ${name}` ) } } if ((clazz as any)[modelUnwrappedClassSymbol]) { throw failure("a class already decorated with `@model` cannot be re-decorated") } // trick so plain new works const newClazz: any = function (this: any, initialData: any, modelConstructorOptions: any) { const instance = new (clazz as any)(initialData, modelConstructorOptions) // set or else it points to the undecorated class Object.defineProperty(instance, "constructor", { configurable: true, writable: true, enumerable: false, value: newClazz, }) runLateInitializationFunctions(instance, runAfterNewSymbol) // compatibility with mobx 6 if (getMobxVersion() >= 6) { try { mobx6.makeObservable(instance) } catch (err) { // sadly we need to use this hack since the PR to do this the proper way // was rejected on the mobx side if ( err.message !== "[MobX] No annotations were passed to makeObservable, but no decorator members have been found either" && err.message !== "[MobX] No annotations were passed to makeObservable, but no decorated members have been found either" ) { throw err } } } // the object is ready addHiddenProp(instance, modelInitializedSymbol, true, false) if (type === "class" && instance.onInit) { wrapModelMethodInActionIfNeeded(instance, "onInit", HookAction.OnInit) instance.onInit() } if (type === "data" && instance.onLazyInit) { wrapModelMethodInActionIfNeeded(instance, "onLazyInit", HookAction.OnLazyInit) instance.onLazyInit() } return instance } clazz.toString = () => `class ${clazz.name}#${name}` if (type === "class") { ;(clazz as any)[modelTypeKey] = name } // this also gives access to modelInitializersSymbol, modelPropertiesSymbol, modelDataTypeCheckerSymbol Object.setPrototypeOf(newClazz, clazz) newClazz.prototype = clazz.prototype Object.defineProperty(newClazz, "name", { ...Object.getOwnPropertyDescriptor(newClazz, "name"), value: clazz.name, }) newClazz[modelUnwrappedClassSymbol] = clazz const modelInfo = { name, class: newClazz, } modelInfoByName[name] = modelInfo modelInfoByClass.set(newClazz, modelInfo) modelInfoByClass.set(clazz, modelInfo) runLateInitializationFunctions(clazz, runAfterModelDecoratorSymbol) return newClazz } // basically taken from TS function tsDecorate(decorators: any, target: any, key: any, desc: any) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d if (typeof Reflect === "object" && typeof (Reflect as any).decorate === "function") r = (Reflect as any).decorate(decorators, target, key, desc) else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r // eslint-disable-next-line no-sequences return c > 3 && r && Object.defineProperty(target, key, r), r } /** * Marks a class (which MUST inherit from the `Model` abstract class) * as a model and decorates some of its methods/properties. * * @param name Unique name for the model type. Note that this name must be unique for your whole * application, so it is usually a good idea to use some prefix unique to your application domain. * If you don't want to assign a name yet (e.g. for a base model) pass `undefined`. * @param clazz Model class. * @param decorators Decorators. */ export function decoratedModel<MC extends ModelClass<AnyModel>>( name: string | undefined, clazz: MC, decorators: { [k in keyof InstanceType<MC>]?: | ((...args: any[]) => any) | ReadonlyArray<(...args: any[]) => any> } ): MC { // decorate class members for (const [k, decorator] of Object.entries(decorators)) { const prototypeValueDesc = Object.getOwnPropertyDescriptor(clazz.prototype, k) // TS seems to send null for methods in the prototype // (which we substitute for the descriptor to avoid a double look-up) and void 0 (undefined) for props tsDecorate( Array.isArray(decorator) ? decorator : [decorator], clazz.prototype, k, prototypeValueDesc ? prototypeValueDesc : void 0 ) } return name ? model(name)(clazz) : clazz }