feature: Add reactvvm lib

This commit is contained in:
Behnamrhp74 2025-04-24 23:22:52 +03:00
parent 77b167c695
commit 2c9b9c5b8f
12 changed files with 15 additions and 240 deletions

View File

@ -32,6 +32,7 @@
"react-cookie": "^7.2.2", "react-cookie": "^7.2.2",
"react-dom": "19.0.0-rc-69d4b800-20241021", "react-dom": "19.0.0-rc-69d4b800-20241021",
"react-i18next": "^15.1.0", "react-i18next": "^15.1.0",
"reactvvm": "^1.1.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"server-only": "^0.0.1", "server-only": "^0.0.1",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.5.4",

View File

@ -15,7 +15,7 @@ type LinkItem = {
* so they come together always and there is no need to be connected with interface for reusable * so they come together always and there is no need to be connected with interface for reusable
* vms. * vms.
*/ */
export default function navLinkPersonalVM() { export default function useNavLinkPersonalVM() {
const pathname = usePathname(); const pathname = usePathname();
// Map of links to display in the side navigation. // Map of links to display in the side navigation.
// Depending on the size of the application, this would be stored in a database. // Depending on the size of the application, this would be stored in a database.

View File

@ -1,11 +1,11 @@
"use client"; "use client";
import navLinkPersonalVM from "@/app/[lang]/dashboard/components/client/nav-links/nav-link.personal-vm"; import useNavLinkPersonalVM from "@/app/[lang]/dashboard/components/client/nav-links/nav-link.personal-vm";
import clsx from "clsx"; import clsx from "clsx";
import Link from "next/link"; import Link from "next/link";
export default function NavLinks() { export default function NavLinks() {
const { links, isLinkActive } = navLinkPersonalVM(); const { links, isLinkActive } = useNavLinkPersonalVM();
return ( return (
<> <>
{links.map((link) => { {links.map((link) => {

View File

@ -2,13 +2,13 @@
import SideNav from "@/app/[lang]/dashboard/components/server/sidenav"; import SideNav from "@/app/[lang]/dashboard/components/server/sidenav";
import dashboardAppModule from "@/app/[lang]/dashboard/module/dashboard.app-module"; import dashboardAppModule from "@/app/[lang]/dashboard/module/dashboard.app-module";
import { DiContext } from "@/bootstrap/di/di-context";
import { useRef } from "react"; import { useRef } from "react";
import { ReactVVMDiProvider } from "reactvvm";
export default function Layout({ children }: { children: React.ReactNode }) { export default function Layout({ children }: { children: React.ReactNode }) {
const di = useRef(dashboardAppModule()); const di = useRef(dashboardAppModule());
return ( return (
<DiContext.Provider value={di.current}> <ReactVVMDiProvider diContainer={di.current}>
<div className="flex h-screen flex-col md:flex-row md:overflow-hidden"> <div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
<div className="w-full flex-none md:w-64"> <div className="w-full flex-none md:w-64">
<SideNav /> <SideNav />
@ -17,6 +17,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
{children} {children}
</div> </div>
</div> </div>
</DiContext.Provider> </ReactVVMDiProvider>
); );
} }

View File

@ -4,12 +4,12 @@ import createInvoiceController from "@/app/[lang]/dashboard/controller/create-in
import ButtonVm from "@/app/components/button/button.i-vm"; import ButtonVm from "@/app/components/button/button.i-vm";
import { useServerAction } from "@/bootstrap/helpers/hooks/use-server-action"; import { useServerAction } from "@/bootstrap/helpers/hooks/use-server-action";
import useThrottle from "@/bootstrap/helpers/hooks/use-throttle"; import useThrottle from "@/bootstrap/helpers/hooks/use-throttle";
import BaseVM from "@/bootstrap/helpers/vm/base-vm";
import langKey from "@/bootstrap/i18n/dictionaries/lang-key"; import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param"; import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
import { faker } from "@faker-js/faker"; import { faker } from "@faker-js/faker";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { BaseVM } from "reactvvm";
/** /**
* Viewmodel for the button view to connect to business logics and all UI logics * Viewmodel for the button view to connect to business logics and all UI logics

View File

@ -1,12 +1,12 @@
"use client"; "use client";
import BaseView, { BuildProps } from "@/bootstrap/helpers/view/base-view";
import ButtonVm from "@/app/components/button/button.i-vm"; import ButtonVm from "@/app/components/button/button.i-vm";
import { ReactNode } from "react"; import { ReactNode } from "react";
import * as React from "react"; import * as React from "react";
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/bootstrap/helpers/lib/ui-utils"; import { cn } from "@/bootstrap/helpers/lib/ui-utils";
import { BaseView, BuildProps } from "reactvvm";
export default class Button extends BaseView<ButtonVm> { export default class Button extends BaseView<ButtonVm> {
protected Build(props: BuildProps<ButtonVm>): ReactNode { protected Build(props: BuildProps<ButtonVm>): ReactNode {

View File

@ -1,19 +0,0 @@
"use client";
import di from "@/bootstrap/di/init-di";
import { createContext, use } from "react";
import { DependencyContainer } from "tsyringe";
const DiContext = createContext<null | DependencyContainer>(di);
const useDI = () => {
const di = use(DiContext);
if (!di) {
throw new Error("Di has not provided");
}
return di;
};
export { DiContext, useDI };

View File

@ -1,147 +0,0 @@
/* eslint-disable react-hooks/rules-of-hooks */
/* eslint-disable react/display-name */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react/jsx-props-no-spreading */
"use client";
import { useDI } from "@/bootstrap/di/di-context";
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
import { Component, ReactNode, FC, PropsWithChildren, memo } from "react";
import { InjectionToken } from "tsyringe";
/* -------------------------------------------------------------------------- */
/* Connector Component */
/* -------------------------------------------------------------------------- */
interface IVvmConnector<IVM, PROPS> extends PropsWithChildren {
View: FC<any & { vm: IVM }>;
Vm: IBaseVM<IVM>;
restProps?: PROPS;
memoizedByVM?: boolean;
}
/**
* This function is just will be used in
*/
const VvmConnector = memo(
<IVM, PROPS>(props: IVvmConnector<IVM, PROPS>) => {
const { View, Vm, restProps, children } = props;
const vm = Vm.useVM();
const allProps = {
restProps,
vm,
};
return <View {...allProps}>{children}</View>;
},
(prevProps) => {
if (prevProps.memoizedByVM) return true;
return false;
},
);
/* -------------------------------------------------------------------------- */
/* BaseView */
/* -------------------------------------------------------------------------- */
type IVMParent = Record<string, any>;
type IPropParent = Record<string, any> | undefined;
type BaseProps<PROPS extends IPropParent = undefined> = {
restProps?: PROPS;
/**
* By default it's true.
* If you pass true this view will update just by changes of vm not rest props
*
*/
memoizedByVM?: boolean;
children?: ReactNode;
};
type BasePropsWithVM<
IVM extends IVMParent,
PROPS extends IPropParent = undefined,
> = BaseProps<PROPS> & {
/**
* Directly instantiated vm
*/
vm: IBaseVM<IVM>;
};
type BasePropsWithVMKey<PROPS extends IPropParent = undefined> =
BaseProps<PROPS> & {
/**
* TSyringe key for vm to be injected
*/
vmKey: InjectionToken;
};
export type BuildProps<
IVM extends IVMParent,
PROPS extends IPropParent = undefined,
> = {
vm: IVM;
restProps: PROPS;
children?: ReactNode;
};
export type ViewProps<
IVM extends IVMParent,
PROPS extends IPropParent = undefined,
> = BasePropsWithVM<IVM, PROPS> | BasePropsWithVMKey<PROPS>;
/**
* Base view is base component for all views in mvvm architecture which gets
* vm as props and connect it to the view and memoize the component by default
* to just render just on changes of its vm.
*/
export default abstract class BaseView<
IVM extends IVMParent,
PROPS extends IPropParent = undefined,
> extends Component<ViewProps<IVM, PROPS>> {
private vm: IBaseVM<IVM> | undefined;
constructor(props: ViewProps<IVM, PROPS>) {
super(props);
this.vm = this.initVm;
}
private get initVm() {
if (Object.hasOwn(this.props, "vmKey")) {
const { vmKey } = this.props as BasePropsWithVMKey<PROPS>;
const di = useDI();
return di.resolve(vmKey) as IBaseVM<IVM>;
}
return (this.props as BasePropsWithVM<IVM, PROPS>).vm;
}
protected get componentName() {
return this.constructor.name;
}
protected abstract Build(props: BuildProps<IVM, PROPS>): ReactNode;
render(): ReactNode {
const { restProps, memoizedByVM, children, ...rest } = this.props;
VvmConnector.displayName = this.componentName;
const vm = memoizedByVM ? this.vm : this.initVm;
if (!vm) {
const isVmKey = Object.hasOwn(this.props, "vmKey");
const message = isVmKey
? "vm is not defined, check your di configuration"
: "pass correct vm";
throw new Error(`Vm is not defined${message}`);
}
return (
<VvmConnector
View={this.Build}
Vm={vm}
memoizedByVM={typeof memoizedByVM === "undefined" ? true : memoizedByVM}
restProps={{ ...restProps, ...rest }}
>
{children}
</VvmConnector>
);
}
}

View File

@ -1,4 +1,4 @@
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm"; import { IBaseVM } from "reactvvm";
/** /**
* To use with mvvm library to make a vm based on props so you can pass the result to the view * To use with mvvm library to make a vm based on props so you can pass the result to the view

View File

@ -1,59 +0,0 @@
"use client";
import { useDI } from "@/bootstrap/di/di-context";
import { NoOverride } from "@/bootstrap/helpers/type-helper";
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
import { useState } from "react";
/**
* Base class for all viewmodels. It provides
* - dependency injection: To get closes di which serves from di provider
* - rerender method: to rerender your component manually
* - produce method: to produce your vm dynamically by passing and attaching dependencies to it
*/
export default abstract class BaseVM<
IVM,
DEP extends object | undefined = undefined,
> implements IBaseVM<IVM>
{
/* ------------------------------ Dependencies ------------------------------ */
protected deps!: DEP;
/* -------------------------------- Abstracts ------------------------------- */
abstract useVM(): IVM;
/* -------------------------------------------------------------------------- */
produce(dep?: DEP) {
if (dep) this.deps = dep;
return this;
}
/* --------------------------------- Getters -------------------------------- */
/**
* You can pass your rerender method after calling useRerender on your vm
* so you can access to it in any method
*/
protected rerender?: () => void;
/* -------------------------------------------------------------------------- */
protected get di() {
return useDI();
}
/* -------------------------------------------------------------------------- */
/**
* You can use this hook in your useVm method to get rerender method
* @returns Rerender Method that when ever you call it you can rerender your component
* for showing new values
*/
protected useRerender(): NoOverride<() => void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, reState] = useState(false);
const rerender = () => reState((prev) => !prev);
return rerender as NoOverride<() => void>;
}
/* -------------------------------------------------------------------------- */
}

View File

@ -1,6 +0,0 @@
/**
* All viewmodels should implement this interface.
*/
export default interface IBaseVM<VM> {
useVM(): VM;
}

View File

@ -6493,6 +6493,11 @@ react@19.0.0-rc-69d4b800-20241021:
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
reactvvm@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/reactvvm/-/reactvvm-1.1.0.tgz#2cdb7ebb75ec0fed4121960077c13474536d18e1"
integrity sha512-HBb/9SN5gAFhYGaQ5qzxmT+YZ0ML+7hfFfAH0milq8T+hJWY9seUsHUUvYxROuxg1fY+WIR04ENGB1H4HhAYJg==
read-cache@^1.0.0: read-cache@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"