mobx-view-model
Version:
MobX ViewModel power for ReactJS
222 lines (221 loc) • 7.82 kB
JavaScript
import { action, computed, observable, runInAction } from 'mobx';
import { mergeVMConfigs, applyObservable, } from '../config/index.js';
import { generateVmId } from '../utils/index.js';
export class ViewModelStoreBase {
config;
viewModels;
linkedComponentVMClasses;
viewModelIdsByClasses;
instanceAttachedCount;
/**
* It is temp heap which is needed to get access to view model instance before all initializations happens
*/
viewModelsTempHeap;
/**
* Views waiting for mount
*/
mountingViews;
/**
* Views waiting for unmount
*/
unmountingViews;
vmConfig;
constructor(config) {
this.config = config;
this.viewModels = observable.map([], { deep: false });
this.linkedComponentVMClasses = observable.map([], { deep: false });
this.viewModelIdsByClasses = observable.map([], { deep: true });
this.instanceAttachedCount = observable.map([], { deep: false });
this.mountingViews = observable.set([], { deep: false });
this.unmountingViews = observable.set([], { deep: false });
this.vmConfig = mergeVMConfigs(config?.vmConfig);
this.viewModelsTempHeap = new Map();
applyObservable(this, [
['mountedViewsCount', computed],
['mount', action],
['unmount', action],
['attachVMConstructor', action],
['attach', action],
['detach', action],
['linkComponents', action],
['unlinkComponents', action],
], this.vmConfig.observable.viewModelStores);
}
get mountedViewsCount() {
return [...this.instanceAttachedCount.values()].reduce((sum, count) => sum + count, 0);
}
processCreateConfig(config) {
this.linkComponents(config.VM, config.component, config.ctx?.externalComponent);
}
createViewModel(config) {
const VMConstructor = config.VM;
const vmConfig = mergeVMConfigs(this.vmConfig, config.config ?? config.vmConfig);
const vmParams = {
...config,
vmConfig,
};
if (vmConfig.factory) {
return vmConfig.factory(vmParams);
}
return new VMConstructor(vmParams);
}
generateViewModelId(config) {
if (config.id) {
return config.id;
}
else if (this.vmConfig.generateId) {
return this.vmConfig.generateId(config.ctx);
}
else {
return generateVmId(config.ctx);
}
}
linkComponents(VM, ...components) {
components.forEach((component) => {
if (component && !this.linkedComponentVMClasses.has(component)) {
this.linkedComponentVMClasses.set(component, VM);
}
});
}
unlinkComponents(...components) {
components.forEach((component) => {
if (component && this.linkedComponentVMClasses.has(component)) {
this.linkedComponentVMClasses.delete(component);
}
});
}
getIds(vmLookup) {
if (!vmLookup)
return [];
if (typeof vmLookup === 'string') {
return [vmLookup];
}
const viewModelClass = (this.linkedComponentVMClasses.get(vmLookup) || vmLookup);
const viewModelIds = this.viewModelIdsByClasses.get(viewModelClass) || [];
return viewModelIds;
}
getId(vmLookup) {
const viewModelIds = this.getIds(vmLookup);
if (viewModelIds.length === 0)
return null;
if (process.env.NODE_ENV !== 'production' && viewModelIds.length > 1) {
console.warn(`Found more than 1 view model with the same identifier. Last instance will been returned`);
}
return viewModelIds.at(-1);
}
has(vmLookup) {
const id = this.getId(vmLookup);
if (!id)
return false;
return this.viewModels.has(id);
}
get(vmLookup) {
// helps to users of this method to better observe changes in view models
// this.viewModels.keys();
const id = this.getId(vmLookup);
if (!id)
return null;
const observedVM = this.viewModels.get(id);
return observedVM ?? this.viewModelsTempHeap.get(id) ?? null;
}
getAll(vmLookup) {
const viewModelIds = this.getIds(vmLookup);
return viewModelIds.map((id) => this.viewModels.get(id));
}
async mount(model) {
this.mountingViews.add(model.id);
await model.mount?.();
runInAction(() => {
this.mountingViews.delete(model.id);
});
}
async unmount(model) {
this.unmountingViews.add(model.id);
await model.unmount?.();
runInAction(() => {
this.unmountingViews.delete(model.id);
});
}
attachVMConstructor(model) {
const constructor = model.constructor;
if (this.viewModelIdsByClasses.has(constructor)) {
const vmIds = this.viewModelIdsByClasses.get(constructor);
if (!vmIds.includes(model.id)) {
vmIds.push(model.id);
}
}
else {
this.viewModelIdsByClasses.set(constructor, [model.id]);
}
}
dettachVMConstructor(model) {
const constructor = model.constructor;
if (this.viewModelIdsByClasses.has(constructor)) {
const vmIds = this.viewModelIdsByClasses
.get(constructor)
.filter((it) => it !== model.id);
if (vmIds.length > 0) {
this.viewModelIdsByClasses.set(constructor, vmIds);
}
else {
this.viewModelIdsByClasses.delete(constructor);
}
}
}
markToBeAttached(model) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
this.viewModelsTempHeap.set(model.id, model);
if ('linkStore' in model) {
model.linkStore(this);
}
else if ('attachViewModelStore' in model) {
model.attachViewModelStore(this);
}
this.attachVMConstructor(model);
}
async attach(model) {
const attachedCount = this.instanceAttachedCount.get(model.id) ?? 0;
this.instanceAttachedCount.set(model.id, attachedCount + 1);
if (this.viewModels.has(model.id)) {
return;
}
this.viewModels.set(model.id, model);
this.attachVMConstructor(model);
await this.mount(model);
this.viewModelsTempHeap.delete(model.id);
}
async detach(id) {
const attachedCount = this.instanceAttachedCount.get(id) ?? 0;
this.viewModelsTempHeap.delete(id);
const model = this.viewModels.get(id);
if (!model) {
return;
}
const nextInstanceAttachedCount = attachedCount - 1;
this.instanceAttachedCount.set(id, nextInstanceAttachedCount);
if (nextInstanceAttachedCount <= 0) {
if ('willUnmount' in model) {
model.willUnmount();
}
this.instanceAttachedCount.delete(id);
this.viewModels.delete(id);
this.dettachVMConstructor(model);
await this.unmount(model);
}
}
isAbleToRenderView(id) {
const isViewMounting = this.mountingViews.has(id);
const hasViewModel = this.viewModels.has(id);
return !!id && hasViewModel && !isViewMounting;
}
clean() {
this.viewModels.clear();
this.linkedComponentVMClasses.clear();
this.viewModelIdsByClasses.clear();
this.instanceAttachedCount.clear();
this.mountingViews.clear();
this.unmountingViews.clear();
this.viewModelsTempHeap.clear();
}
}