Skip to content

Commit

Permalink
Added useViewModel hook
Browse files Browse the repository at this point in the history
  • Loading branch information
gius committed Jun 12, 2023
1 parent adf52b1 commit ba127b3
Show file tree
Hide file tree
Showing 19 changed files with 202 additions and 127 deletions.
4 changes: 2 additions & 2 deletions examples/navigation/src/customers/customersViewModel.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { IViewModel, NavigationContext, NoParams } from "@frui.ts/views";
import type { IRouteViewModel, NavigationContext, NoParams } from "@frui.ts/views";
import { makeObservable, observable, runInAction } from "mobx";

type SearchScheme = { name?: string };

export default class CustomersViewModel implements IViewModel<NoParams, SearchScheme> {
export default class CustomersViewModel implements IRouteViewModel<NoParams, SearchScheme> {
@observable
search?: string;

Expand Down
5 changes: 5 additions & 0 deletions examples/navigation/src/home/homeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export const HomeView = registerViewComponent(HomeViewModel, vm => {
</li>
</ul>
</li>
<li>
<Link to="/settings" {...linkProps}>
Settings
</Link>
</li>
</ul>
</nav>
</header>
Expand Down
4 changes: 2 additions & 2 deletions examples/navigation/src/home/homeViewModel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IViewModel } from "@frui.ts/views";
import type { IRouteViewModel } from "@frui.ts/views";
import { action, makeObservable, observable } from "mobx";
export default class HomeViewModel implements IViewModel {
export default class HomeViewModel implements IRouteViewModel {
text = "foo";

@observable
Expand Down
4 changes: 2 additions & 2 deletions examples/navigation/src/invoices/invoiceDetailViewModel.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { IViewModel, NavigationContext } from "@frui.ts/views";
import type { IRouteViewModel, NavigationContext } from "@frui.ts/views";
import { makeObservable, observable, runInAction } from "mobx";

type ParamsScheme = Record<"invoiceId", string>;

export default class InvoiceDetailViewModel implements IViewModel<ParamsScheme> {
export default class InvoiceDetailViewModel implements IRouteViewModel<ParamsScheme> {
@observable
id = -1;

Expand Down
4 changes: 2 additions & 2 deletions examples/navigation/src/invoices/invoicesViewModel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IViewModel, NavigationContext } from "@frui.ts/views";
import type { IRouteViewModel, NavigationContext } from "@frui.ts/views";

export default class InvoicesViewModel implements IViewModel {
export default class InvoicesViewModel implements IRouteViewModel {
onInitialize(context: NavigationContext) {
console.log("invoices on initialize", context);
}
Expand Down
8 changes: 8 additions & 0 deletions examples/navigation/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import HomeViewModel from "./home/homeViewModel";
import "./index.css";
import InvoiceDetailViewModel from "./invoices/invoiceDetailViewModel";
import InvoicesViewModel from "./invoices/invoicesViewModel";
import { Settings } from "./settings/Settings";
import "./viewsRegistry";

const homeRoute = buildRootRoute(() => new HomeViewModel(), {});
Expand Down Expand Up @@ -35,10 +36,17 @@ const customersRoute = buildRoute(() => new CustomersViewModel(), {
validateSearch: CustomersViewModel.validateSearch,
});

const settingsRoute = new Route({
getParentRoute: () => homeRoute,
path: "settings",
component: Settings,
});

const routeTree = homeRoute.addChildren([
indexRoute,
invoicesRoute.addChildren([invoiceDetailRoute, defaultInvoiceRoute]),
customersRoute,
settingsRoute,
]);

const router = new ReactRouter({ routeTree });
Expand Down
14 changes: 14 additions & 0 deletions examples/navigation/src/settings/Settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useViewModel } from "@frui.ts/views";
import { Observer } from "mobx-react-lite";
import React from "react";
import SettingsViewModel from "./settingsViewModel";

export function Settings() {
const vm = useViewModel(() => new SettingsViewModel(), {}, []);

return (
<p>
<Observer>{() => <span>{vm.text}</span>}</Observer>
</p>
);
}
23 changes: 23 additions & 0 deletions examples/navigation/src/settings/settingsViewModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { IViewModel } from "@frui.ts/views";
import { makeObservable, observable } from "mobx";

export default class SettingsViewModel implements IViewModel<unknown> {
@observable
text = crypto.randomUUID();

constructor() {
makeObservable(this);
}

onInitialize(context: unknown) {
console.log("settings on initialize", this.text, context);
}

onActivate(context: unknown) {
console.log("settings on activate", this.text, context);
}

onDeactivate(context: unknown) {
console.log("settings on deactivate", this.text, context);
}
}
4 changes: 2 additions & 2 deletions examples/todolist/src/list/todoListViewModel.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { IViewModel, NavigationContext } from "@frui.ts/views";
import type { IRouteViewModel, NavigationContext } from "@frui.ts/views";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { v4 as uuid } from "uuid";
import { type TodoItem } from "../models/todoItem";

type FilterType = "all" | "completed" | "active";
type ParamsScheme = Record<"filter", string>;
export default class TodoListViewModel implements IViewModel<ParamsScheme> {
export default class TodoListViewModel implements IRouteViewModel<ParamsScheme> {
name = "TODO List";
@observable list: TodoItem[] = [];
@observable newItem!: TodoItem;
Expand Down
1 change: 1 addition & 0 deletions packages/views/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"dependencies": {
"@frui.ts/helpers": "^999.0.0",
"@tanstack/react-router": "^0.0.1-beta.82",
"async-mutex": "^0.4.0",
"inversify": "^6.0.1",
"lodash-es": "^4.17.21"
},
Expand Down
83 changes: 83 additions & 0 deletions packages/views/src/helpers/viewModelLifecycleManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Mutex } from "async-mutex";
import type { IViewModel } from "../types";

export class ViewModelLifecycleManager<TContext, TViewModel extends IViewModel<TContext>> {
#instance: TViewModel | undefined;
#lock = new Mutex();

_isInitialized = false;

_isActive = false;

get instance() {
this.#instance ??= this.factory();
return this.#instance;
}

constructor(private factory: () => TViewModel) {}

resetInstance() {
this.#instance = undefined;
this._isInitialized = false;
this._isActive = false;
}

async initialize(context: TContext) {
await this.#lock.runExclusive(async () => {
await this.callInitialize(context);
});
}

private async callInitialize(context: TContext) {
if (!this._isInitialized) {
await this.instance.onInitialize?.(context);
this._isInitialized = true;
}
}

async activate(context: TContext) {
await this.#lock.runExclusive(async () => {
await this.callInitialize(context);
await this.callActivate(context);
});
}

private async callActivate(context: TContext) {
if (!this._isActive) {
await this.instance.onActivate?.(context);
this._isActive = true;
}
}

async navigate(context: TContext) {
await this.#lock.runExclusive(async () => {
await this.callInitialize(context);
await this.callActivate(context);
await this.callNavigate(context);
});
}

private async callNavigate(context: TContext) {
await this.instance.onNavigate?.(context);
}

async deactivate(context: TContext) {
await this.#lock.runExclusive(async () => {
await this.callDeactivate(context);
});
}

private async callDeactivate(context: TContext) {
if (this._isActive) {
await this.instance.onDeactivate?.(context);
this._isActive = false;
}
}

async close(context: TContext) {
await this.#lock.runExclusive(async () => {
await this.callDeactivate(context);
this.resetInstance();
});
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect } from "react";
import type { IDisposable } from "@frui.ts/helpers";

export default function useDisposable(...dependencies: IDisposable[]) {
export function useDisposable(...dependencies: IDisposable[]) {
useEffect(
() => () => {
dependencies.forEach(x => x.dispose());
Expand Down
29 changes: 29 additions & 0 deletions packages/views/src/hooks/useViewModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { DependencyList } from "react";
import { useEffect, useRef } from "react";
import { ViewModelLifecycleManager } from "../helpers/viewModelLifecycleManager";
import type { IViewModel } from "../types";

export function useViewModel<TContext, TViewModel extends IViewModel<TContext>>(
factory: () => TViewModel,
context: TContext,
dependencies?: DependencyList
) {
const vmManager = useRef(new ViewModelLifecycleManager(factory));

const currentContext = useRef(context);
currentContext.current = context;

useEffect(() => {
void vmManager.current.initialize(currentContext.current);

return () => {
void vmManager.current.close(currentContext.current);
};
}, []);

useEffect(() => {
void vmManager.current.navigate(currentContext.current);
}, dependencies ?? [context]);

return vmManager.current.instance;
}
3 changes: 2 additions & 1 deletion packages/views/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ export { default as assignDefaultProps } from "./helpers/assignDefaultProps";
export { default as createDataIdHandler } from "./helpers/dataIdHandler";
export { default as createMemoizedHandler } from "./helpers/memoizedDataHandler";
export { default as preventDefault } from "./helpers/preventDefault";
export * from "./hooks/useViewModel";
export * from "./router/router";
export * from "./router/types";
export * from "./types";
export * from "./view/helpers";
export { default as useDisposable } from "./view/useDisposable";
export * from "./hooks/useDisposable";
export { default as View } from "./view/view";
export * from "./view/viewLocator";
12 changes: 6 additions & 6 deletions packages/views/src/router/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import type {
} from "@tanstack/react-router";
import { Outlet, RootRoute, Route } from "@tanstack/react-router";
import React from "react";
import { ViewModelLifecycleManager } from "../helpers/viewModelLifecycleManager";
import View from "../view/view";
import type { IViewModel, NoParams } from "./types";
import { ViewModelLifecycleManager } from "./viewModelLifecycleManager";
import type { IRouteViewModel, NavigationContext, NoParams } from "./types";

function buildViewModelOptions<
TParentRoute extends AnyRoute = AnyRoute,
Expand All @@ -41,9 +41,9 @@ function buildViewModelOptions<
// TRouterContext extends AnyContext = AnyContext,
// TChildren = unknown,
// TRoutesInfo extends DefaultRoutesInfo = DefaultRoutesInfo,
TViewModel extends IViewModel<TAllParams, TFullSearchSchema> = IViewModel<TAllParams, TFullSearchSchema>
TViewModel extends IRouteViewModel<TAllParams, TFullSearchSchema> = IRouteViewModel<TAllParams, TFullSearchSchema>
>(vmFactory: () => TViewModel) {
const vmManager = new ViewModelLifecycleManager<TAllParams, TFullSearchSchema, TViewModel>(vmFactory);
const vmManager = new ViewModelLifecycleManager<NavigationContext<TAllParams, TFullSearchSchema>, TViewModel>(vmFactory);

// since the current router does not help here,
// we need to manually handle situation when a route remains selected,
Expand Down Expand Up @@ -118,7 +118,7 @@ export function buildRoute<
// TRouterContext extends AnyContext = AnyContext,
// TChildren = unknown,
// TRoutesInfo extends DefaultRoutesInfo = DefaultRoutesInfo,
TViewModel extends IViewModel<TAllParams, TFullSearchSchema> = IViewModel<TAllParams, TFullSearchSchema>
TViewModel extends IRouteViewModel<TAllParams, TFullSearchSchema> = IRouteViewModel<TAllParams, TFullSearchSchema>
>(
vmFactory: () => TViewModel,
options: RouteOptions<
Expand Down Expand Up @@ -146,7 +146,7 @@ export function buildRootRoute<
TSearchSchema extends AnySearchSchema = {},
TContext extends RouteContext = RouteContext,
TRouterContext extends RouterContext = RouterContext,
TViewModel extends IViewModel<NoParams, TSearchSchema> = IViewModel<NoParams, TSearchSchema>
TViewModel extends IRouteViewModel<NoParams, TSearchSchema> = IRouteViewModel<NoParams, TSearchSchema>
>(
vmFactory: () => TViewModel,
options: Omit<
Expand Down
12 changes: 5 additions & 7 deletions packages/views/src/router/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { IViewModel } from "../types";

export type NoParams = Record<never, string>;

export interface NavigationContext<TParams extends Record<string, string> = NoParams, TSearch = unknown> {
Expand All @@ -6,10 +8,6 @@ export interface NavigationContext<TParams extends Record<string, string> = NoPa
search: TSearch;
}

export interface IViewModel<TParams extends Record<string, string> = NoParams, TSearch = unknown> {
onInitialize?(context: NavigationContext<TParams, TSearch>): Promise<unknown> | unknown;
onActivate?(context: NavigationContext<TParams, TSearch>): Promise<unknown> | unknown;
onNavigate?(context: NavigationContext<TParams, TSearch>): Promise<unknown> | unknown;
onSearchChanged?(context: NavigationContext<TParams, TSearch>): Promise<unknown> | unknown;
onDeactivate?(context: NavigationContext<TParams, TSearch>): Promise<unknown> | unknown;
}
export type IRouteViewModel<TParams extends Record<string, string> = NoParams, TSearch = unknown> = IViewModel<
NavigationContext<TParams, TSearch>
>;
Loading

0 comments on commit ba127b3

Please sign in to comment.