diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index dc1dbaa..bfa17ed 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,13 +1,14 @@ -import React, { useRef } from "react"; +import React from "react"; import { themes } from '@storybook/theming'; import { ThemeProvider } from "../src/app/[lang]/dashboard/components/client/theme-provider/theme-provider"; -import { DARK_MODE_EVENT_NAME, UPDATE_DARK_MODE_EVENT_NAME } from 'storybook-dark-mode'; -import { initI18next, LANGS } from "../src/bootstrap/i18n/i18n" +import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode'; +import { getI18n, LANGS } from "../src/bootstrap/i18n/i18n" import { addons } from '@storybook/preview-api'; import { i18n } from "i18next"; import { I18nextProvider } from "react-i18next"; const channel = addons.getChannel(); import "../src/app/globals.css" + /** * * This function will expand the object with nested properties @@ -69,7 +70,7 @@ const preview = { React.useEffect(() => { (async () => { - setI18n((await initI18next({ lng: locale })).i18n); + setI18n((await getI18n({ lng: locale })).i18n); })() }, []) diff --git a/src/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice.tsx b/src/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice.tsx deleted file mode 100644 index 8db5bf3..0000000 --- a/src/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client"; - -import Button from "@/app/components/button/button"; -import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm"; -import { useDI } from "@/bootstrap/di/di-context"; -import { useRef } from "react"; - -/** - * From a parent component Vm and view will be connected together. - */ -export default function CreateRandomInvoiceContainer() { - const di = useDI(); - const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM)); - - return <Button vm={vm.current} />; -} diff --git a/src/app/[lang]/dashboard/components/server/latest-invoices.tsx b/src/app/[lang]/dashboard/components/server/latest-invoices.tsx index 4d54d2b..9a0583e 100644 --- a/src/app/[lang]/dashboard/components/server/latest-invoices.tsx +++ b/src/app/[lang]/dashboard/components/server/latest-invoices.tsx @@ -1,5 +1,6 @@ -import CreateRandomInvoiceContainer from "@/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice"; import latestInvoicesController from "@/app/[lang]/dashboard/controller/latest-invoices.controller"; +import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm"; +import Button from "@/app/components/button/button"; import { ArrowPathIcon } from "@heroicons/react/24/outline"; import clsx from "clsx"; import { isLeft } from "fp-ts/lib/Either"; @@ -39,6 +40,7 @@ export default async function LatestInvoices() { </p> </div> )); + return ( <div className="flex w-full flex-col md:col-span-4"> <h2 className="mb-4 text-xl md:text-2xl">Latest Invoices</h2> @@ -48,7 +50,7 @@ export default async function LatestInvoices() { <ArrowPathIcon className="h-5 w-5 text-gray-500" /> <h3 className="ml-2 text-sm text-gray-500 ">Updated just now</h3> </div> - <CreateRandomInvoiceContainer /> + <Button vmKey={CreateRandomInvoiceButtonVM} /> </div> </div> ); diff --git a/src/app/[lang]/dashboard/module/dashboard.app-module.ts b/src/app/[lang]/dashboard/module/dashboard.app-module.ts index b3ec20f..d87af96 100644 --- a/src/app/[lang]/dashboard/module/dashboard.app-module.ts +++ b/src/app/[lang]/dashboard/module/dashboard.app-module.ts @@ -1,3 +1,4 @@ +import createInvoiceController from "@/app/[lang]/dashboard/controller/create-invoice.controller"; import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm"; import di from "@/bootstrap/di/init-di"; @@ -8,8 +9,12 @@ export default function dashboardAppModule() { const dashboardDi = di.createChildContainer(); dashboardDi.register( - CreateRandomInvoiceButtonVM, + CreateRandomInvoiceButtonVM.name, CreateRandomInvoiceButtonVM, ); + + dashboardDi.register(createInvoiceController.name, { + useValue: createInvoiceController, + }); return dashboardDi; } 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 246508d..63e71ae 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 @@ -1,3 +1,5 @@ +"use client"; + import createInvoiceController from "@/app/[lang]/dashboard/controller/create-invoice.controller"; import ButtonVm from "@/app/components/button/button.i-vm"; import { useServerAction } from "@/bootstrap/helpers/hooks/use-server-action"; @@ -5,7 +7,6 @@ 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 { CreateInvoiceUsecase } from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase"; import { faker } from "@faker-js/faker"; import { useRouter } from "next/navigation"; import { useTranslation } from "react-i18next"; @@ -16,11 +17,13 @@ import { useTranslation } from "react-i18next"; * in this layer. */ export default class CreateRandomInvoiceButtonVM extends BaseVM<ButtonVm> { - private createInvoice: CreateInvoiceUsecase; + private createInvoice: typeof createInvoiceController; constructor() { super(); - this.createInvoice = createInvoiceController; + this.createInvoice = this.di.resolve( + createInvoiceController.prototype.name, + ); } useVM(): ButtonVm { diff --git a/src/app/components/button/stories/Button.stories.tsx b/src/app/components/button/stories/Button.stories.tsx index a68137b..06bf104 100644 --- a/src/app/components/button/stories/Button.stories.tsx +++ b/src/app/components/button/stories/Button.stories.tsx @@ -1,12 +1,7 @@ -import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm"; import Button from "@/app/components/button/button"; -import { DiContext, useDI } from "@/bootstrap/di/di-context"; -import mockedModuleDi from "@/bootstrap/di/mocked-module-di"; import Story from "@/bootstrap/helpers/view/storybook-base-template-type"; import getArgVM from "@/bootstrap/helpers/view/storybook-with-arg-vm"; -import { createInvoiceUsecaseKey } from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase"; import type { Meta } from "@storybook/react"; -import { useRef } from "react"; const meta: Meta = { title: "general/Button", @@ -32,36 +27,3 @@ export const Primary: Story = { return <Button vm={vm} memoizedByVM={false} />; }, }; - -export const WithVM: Story = { - decorators: [ - (Story) => { - const di = useRef( - mockedModuleDi([ - { - token: CreateRandomInvoiceButtonVM, - provider: CreateRandomInvoiceButtonVM, - }, - { - token: createInvoiceUsecaseKey, - // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-console - provider: (args: any) => console.log("clicked", args), - }, - ]), - ); - return ( - <DiContext.Provider value={di.current}> - <Story di={di.current} /> - </DiContext.Provider> - ); - }, - ], - render: () => { - function Child() { - const di = useDI(); - const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM)); - return <Button vm={vm.current} memoizedByVM={false} />; - } - return <Child />; - }, -}; diff --git a/src/bootstrap/helpers/view/base-view.tsx b/src/bootstrap/helpers/view/base-view.tsx index 914c9fc..e68a0fe 100644 --- a/src/bootstrap/helpers/view/base-view.tsx +++ b/src/bootstrap/helpers/view/base-view.tsx @@ -1,11 +1,14 @@ -"use client"; - -// import gdi from "@/bootstrap/di/init-di"; +/* 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 */ @@ -23,7 +26,6 @@ interface IVvmConnector<IVM, PROPS> extends PropsWithChildren { const VvmConnector = memo( <IVM, PROPS>(props: IVvmConnector<IVM, PROPS>) => { const { View, Vm, restProps, children } = props; - const vm = Vm.useVM(); const allProps = { @@ -45,8 +47,7 @@ const VvmConnector = memo( type IVMParent = Record<string, any>; type IPropParent = Record<string, any> | undefined; -type BaseProps<IVM extends IVMParent, PROPS extends IPropParent = undefined> = { - vm: IBaseVM<IVM>; +type BaseProps<PROPS extends IPropParent = undefined> = { restProps?: PROPS; /** * By default it's true. @@ -57,6 +58,24 @@ type BaseProps<IVM extends IVMParent, PROPS extends IPropParent = undefined> = { 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, @@ -66,6 +85,11 @@ export type BuildProps< 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 @@ -74,7 +98,23 @@ export type BuildProps< export default abstract class BaseView< IVM extends IVMParent, PROPS extends IPropParent = undefined, -> extends Component<BaseProps<IVM, PROPS>> { +> 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; } @@ -82,9 +122,16 @@ export default abstract class BaseView< protected abstract Build(props: BuildProps<IVM, PROPS>): ReactNode; render(): ReactNode { - const { vm, restProps, memoizedByVM, children, ...rest } = this.props; - + 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 @@ -97,5 +144,4 @@ export default abstract class BaseView< </VvmConnector> ); } - /* -------------------------------------------------------------------------- */ }