diff --git a/package.json b/package.json index 59fef4e..accd399 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "react-cookie": "^7.2.2", "react-dom": "19.0.0-rc-69d4b800-20241021", "react-i18next": "^15.1.0", + "reactvvm": "^1.1.0", "reflect-metadata": "^0.2.2", "server-only": "^0.0.1", "tailwind-merge": "^2.5.4", diff --git a/src/app/[lang]/dashboard/components/client/nav-links/nav-link.personal-vm.ts b/src/app/[lang]/dashboard/components/client/nav-links/nav-link.personal-vm.ts index 50c4620..5e20d21 100644 --- a/src/app/[lang]/dashboard/components/client/nav-links/nav-link.personal-vm.ts +++ b/src/app/[lang]/dashboard/components/client/nav-links/nav-link.personal-vm.ts @@ -15,7 +15,7 @@ type LinkItem = { * so they come together always and there is no need to be connected with interface for reusable * vms. */ -export default function navLinkPersonalVM() { +export default function useNavLinkPersonalVM() { const pathname = usePathname(); // Map of links to display in the side navigation. // Depending on the size of the application, this would be stored in a database. diff --git a/src/app/[lang]/dashboard/components/client/nav-links/nav-links.tsx b/src/app/[lang]/dashboard/components/client/nav-links/nav-links.tsx index 8c2f587..f5ca830 100644 --- a/src/app/[lang]/dashboard/components/client/nav-links/nav-links.tsx +++ b/src/app/[lang]/dashboard/components/client/nav-links/nav-links.tsx @@ -1,11 +1,11 @@ "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 Link from "next/link"; export default function NavLinks() { - const { links, isLinkActive } = navLinkPersonalVM(); + const { links, isLinkActive } = useNavLinkPersonalVM(); return ( <> {links.map((link) => { diff --git a/src/app/[lang]/dashboard/layout.tsx b/src/app/[lang]/dashboard/layout.tsx index 41a0efc..f4851a1 100644 --- a/src/app/[lang]/dashboard/layout.tsx +++ b/src/app/[lang]/dashboard/layout.tsx @@ -2,13 +2,13 @@ import SideNav from "@/app/[lang]/dashboard/components/server/sidenav"; import dashboardAppModule from "@/app/[lang]/dashboard/module/dashboard.app-module"; -import { DiContext } from "@/bootstrap/di/di-context"; import { useRef } from "react"; +import { ReactVVMDiProvider } from "reactvvm"; export default function Layout({ children }: { children: React.ReactNode }) { const di = useRef(dashboardAppModule()); return ( - +
@@ -17,6 +17,6 @@ export default function Layout({ children }: { children: React.ReactNode }) { {children}
-
+ ); } diff --git a/src/app/[lang]/dashboard/vm/create-random-invoice-button-vm.ts b/src/app/[lang]/dashboard/vm/create-random-invoice-button-vm.ts index ccd0622..235e305 100644 --- a/src/app/[lang]/dashboard/vm/create-random-invoice-button-vm.ts +++ b/src/app/[lang]/dashboard/vm/create-random-invoice-button-vm.ts @@ -4,12 +4,12 @@ import createInvoiceController from "@/app/[lang]/dashboard/controller/create-in import ButtonVm from "@/app/components/button/button.i-vm"; import { useServerAction } from "@/bootstrap/helpers/hooks/use-server-action"; 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 { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param"; import { faker } from "@faker-js/faker"; import { useRouter } from "next/navigation"; import { useTranslation } from "react-i18next"; +import { BaseVM } from "reactvvm"; /** * Viewmodel for the button view to connect to business logics and all UI logics diff --git a/src/app/components/button/button.tsx b/src/app/components/button/button.tsx index 4cf3a5b..8e3da4c 100644 --- a/src/app/components/button/button.tsx +++ b/src/app/components/button/button.tsx @@ -1,12 +1,12 @@ "use client"; -import BaseView, { BuildProps } from "@/bootstrap/helpers/view/base-view"; import ButtonVm from "@/app/components/button/button.i-vm"; import { ReactNode } from "react"; import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/bootstrap/helpers/lib/ui-utils"; +import { BaseView, BuildProps } from "reactvvm"; export default class Button extends BaseView { protected Build(props: BuildProps): ReactNode { diff --git a/src/bootstrap/di/di-context.tsx b/src/bootstrap/di/di-context.tsx deleted file mode 100644 index 3505eff..0000000 --- a/src/bootstrap/di/di-context.tsx +++ /dev/null @@ -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(di); - -const useDI = () => { - const di = use(DiContext); - - if (!di) { - throw new Error("Di has not provided"); - } - - return di; -}; - -export { DiContext, useDI }; diff --git a/src/bootstrap/helpers/view/base-view.tsx b/src/bootstrap/helpers/view/base-view.tsx deleted file mode 100644 index e68a0fe..0000000 --- a/src/bootstrap/helpers/view/base-view.tsx +++ /dev/null @@ -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 extends PropsWithChildren { - View: FC; - Vm: IBaseVM; - restProps?: PROPS; - memoizedByVM?: boolean; -} - -/** - * This function is just will be used in - */ -const VvmConnector = memo( - (props: IVvmConnector) => { - const { View, Vm, restProps, children } = props; - const vm = Vm.useVM(); - - const allProps = { - restProps, - vm, - }; - - return {children}; - }, - (prevProps) => { - if (prevProps.memoizedByVM) return true; - return false; - }, -); - -/* -------------------------------------------------------------------------- */ -/* BaseView */ -/* -------------------------------------------------------------------------- */ -type IVMParent = Record; -type IPropParent = Record | undefined; - -type BaseProps = { - 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 & { - /** - * Directly instantiated vm - */ - vm: IBaseVM; -}; - -type BasePropsWithVMKey = - BaseProps & { - /** - * 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 | BasePropsWithVMKey; - -/** - * 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> { - private vm: IBaseVM | undefined; - - constructor(props: ViewProps) { - super(props); - this.vm = this.initVm; - } - - private get initVm() { - if (Object.hasOwn(this.props, "vmKey")) { - const { vmKey } = this.props as BasePropsWithVMKey; - const di = useDI(); - return di.resolve(vmKey) as IBaseVM; - } - return (this.props as BasePropsWithVM).vm; - } - - protected get componentName() { - return this.constructor.name; - } - - protected abstract Build(props: BuildProps): 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 ( - - {children} - - ); - } -} diff --git a/src/bootstrap/helpers/view/storybook-with-arg-vm.ts b/src/bootstrap/helpers/view/storybook-with-arg-vm.ts index 7a3387c..583c457 100644 --- a/src/bootstrap/helpers/view/storybook-with-arg-vm.ts +++ b/src/bootstrap/helpers/view/storybook-with-arg-vm.ts @@ -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 diff --git a/src/bootstrap/helpers/vm/base-vm.ts b/src/bootstrap/helpers/vm/base-vm.ts deleted file mode 100644 index d3b02c4..0000000 --- a/src/bootstrap/helpers/vm/base-vm.ts +++ /dev/null @@ -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 -{ - /* ------------------------------ 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>; - } - - /* -------------------------------------------------------------------------- */ -} diff --git a/src/bootstrap/helpers/vm/i-base-vm.ts b/src/bootstrap/helpers/vm/i-base-vm.ts deleted file mode 100644 index e2530bd..0000000 --- a/src/bootstrap/helpers/vm/i-base-vm.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * All viewmodels should implement this interface. - */ -export default interface IBaseVM { - useVM(): VM; -} diff --git a/yarn.lock b/yarn.lock index 413bcc0..16dae25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6493,6 +6493,11 @@ react@19.0.0-rc-69d4b800-20241021: dependencies: 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: version "1.0.0" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"