Add basic di in client side
This commit is contained in:
parent
f03ab2a03b
commit
69640da28e
@ -1,3 +1,13 @@
|
||||
{
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"src/**/*-vm.ts"
|
||||
],
|
||||
"rules": {
|
||||
"react-hooks/rules-of-hooks": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
|
12
package.json
12
package.json
@ -5,22 +5,24 @@
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start": "next start --port 4000",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "15.0.1",
|
||||
"react": "19.0.0-rc-69d4b800-20241021",
|
||||
"react-dom": "19.0.0-rc-69d4b800-20241021",
|
||||
"next": "15.0.1"
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"tsyringe": "^4.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.1",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.1"
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import "reflect-metadata"
|
||||
import type { Metadata } from "next";
|
||||
import localFont from "next/font/local";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = localFont({
|
||||
src: "./fonts/GeistVF.woff",
|
||||
variable: "--font-geist-sans",
|
||||
|
8
src/app/test/page.tsx
Normal file
8
src/app/test/page.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
"use client"
|
||||
import "reflect-metadata"
|
||||
import TestButtonVM from "@/app/test/vm/test-button-vm";
|
||||
import Button from "@/components/button/button";
|
||||
|
||||
export default function Page() {
|
||||
return <Button vmName={TestButtonVM.name} />
|
||||
}
|
17
src/app/test/vm/test-button-vm.ts
Normal file
17
src/app/test/vm/test-button-vm.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import BaseVM from "@/bootstrap/helpers/vm/base-vm";
|
||||
import injectableVm from "@/bootstrap/helpers/vm/vm-decorator";
|
||||
import ButtonVm from "@/components/button/button-vm";
|
||||
|
||||
@injectableVm()
|
||||
export default class TestButtonVM extends BaseVM<ButtonVm> {
|
||||
useVM(): ButtonVm {
|
||||
return {
|
||||
props: {
|
||||
title: "Test Button"
|
||||
},
|
||||
onClick: () => {
|
||||
console.log("clicked on the button");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
src/bootstrap/di/init-di.ts
Normal file
18
src/bootstrap/di/init-di.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import "reflect-metadata"
|
||||
|
||||
import { container, DependencyContainer } from "tsyringe";
|
||||
|
||||
/**
|
||||
* Serves as a central point for initializing and configuring
|
||||
* the DI container, ensuring that all necessary dependencies
|
||||
* are registered and available for injection throughout the application.
|
||||
*/
|
||||
const InitDI = (): DependencyContainer => {
|
||||
const di = container;
|
||||
|
||||
return di;
|
||||
};
|
||||
|
||||
const di = InitDI();
|
||||
|
||||
export default di;
|
8
src/bootstrap/helpers/type-helper.ts
Normal file
8
src/bootstrap/helpers/type-helper.ts
Normal file
@ -0,0 +1,8 @@
|
||||
declare const _: unique symbol;
|
||||
|
||||
type Forbidden = { [_]: typeof _ };
|
||||
|
||||
/**
|
||||
* You can use this type to make your parent class method forbidden to overwrite
|
||||
*/
|
||||
export type NoOverride<T = void> = T & Forbidden;
|
95
src/bootstrap/helpers/view/base-view.tsx
Normal file
95
src/bootstrap/helpers/view/base-view.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
"use client"
|
||||
/* eslint-disable react/display-name */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
import BaseVM from "@/bootstrap/helpers/vm/base-vm";
|
||||
import { Component, ReactNode, FC, PropsWithChildren, memo, MemoExoticComponent } from "react";
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Connector Component */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
interface IVvmConnector<IVM, PROPS> extends PropsWithChildren {
|
||||
View: FC<any & { vm: IVM }>;
|
||||
vmName: string;
|
||||
restProps?: PROPS;
|
||||
memoizedByVM?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is just will be used in
|
||||
*/
|
||||
const VvmConnector = memo(
|
||||
<IVM, PROPS>(props: IVvmConnector<IVM, PROPS>) => {
|
||||
const { View, vmName, restProps, children } = props;
|
||||
const VmInstance = di.resolve(vmName) as new () => BaseVM<IVM>;
|
||||
if (!VmInstance) throw new Error(`Provided vm as ${vmName} is not exists`)
|
||||
|
||||
const vm = new VmInstance().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> = {
|
||||
vmName: string;
|
||||
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;
|
||||
};
|
||||
|
||||
export type BuildProps<
|
||||
IVM extends IVMParent,
|
||||
PROPS extends IPropParent = undefined,
|
||||
> = {
|
||||
vm: IVM;
|
||||
restProps: PROPS;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export default abstract class BaseView<
|
||||
IVM extends IVMParent,
|
||||
PROPS extends IPropParent = undefined,
|
||||
> extends Component<BaseProps<PROPS>> {
|
||||
/* -------------------------------- Abstracts ------------------------------- */
|
||||
protected abstract Build(props: BuildProps<IVM, PROPS>): ReactNode;
|
||||
|
||||
/* -------------------------------- Renderer -------------------------------- */
|
||||
render(): ReactNode {
|
||||
const { vmName, restProps, memoizedByVM, children, ...rest } = this.props;
|
||||
|
||||
const Connector = VvmConnector as MemoExoticComponent<((props: IVvmConnector<IVM, PROPS>) => JSX.Element)>;
|
||||
|
||||
return (
|
||||
<Connector
|
||||
View={this.Build}
|
||||
vmName={vmName}
|
||||
memoizedByVM={typeof memoizedByVM === "undefined" ? true : memoizedByVM}
|
||||
restProps={{ ...restProps, ...rest } as PROPS}
|
||||
>
|
||||
{children}
|
||||
</Connector>
|
||||
);
|
||||
}
|
||||
/* -------------------------------------------------------------------------- */
|
||||
}
|
46
src/bootstrap/helpers/vm/base-vm.ts
Normal file
46
src/bootstrap/helpers/vm/base-vm.ts
Normal file
@ -0,0 +1,46 @@
|
||||
"use client"
|
||||
import { NoOverride } from "@/bootstrap/helpers/type-helper";
|
||||
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
|
||||
import { useState } from "react";
|
||||
|
||||
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;
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/**
|
||||
* 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>;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
}
|
3
src/bootstrap/helpers/vm/i-base-vm.ts
Normal file
3
src/bootstrap/helpers/vm/i-base-vm.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default interface IBaseVM<VM> {
|
||||
useVM(): VM;
|
||||
}
|
9
src/bootstrap/helpers/vm/vm-decorator.ts
Normal file
9
src/bootstrap/helpers/vm/vm-decorator.ts
Normal file
@ -0,0 +1,9 @@
|
||||
"use client"
|
||||
import di from "@/bootstrap/di/init-di";
|
||||
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
|
||||
|
||||
export default function injectableVm() {
|
||||
return function (target: new (...args: unknown[]) => IBaseVM<object>) {
|
||||
di.registerInstance(target.name, target);
|
||||
};
|
||||
}
|
6
src/components/button/button-vm.ts
Normal file
6
src/components/button/button-vm.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export default interface ButtonVm {
|
||||
props: {
|
||||
title: string
|
||||
}
|
||||
onClick(): void
|
||||
}
|
10
src/components/button/button.tsx
Normal file
10
src/components/button/button.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import BaseView, { BuildProps } from "@/bootstrap/helpers/view/base-view";
|
||||
import ButtonVm from "@/components/button/button-vm";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export default class Button extends BaseView<ButtonVm> {
|
||||
protected Build(props: BuildProps<ButtonVm>): ReactNode {
|
||||
const {vm} = props
|
||||
return <button onClick={vm.onClick} >{vm.props.title}</button>
|
||||
}
|
||||
}
|
@ -9,6 +9,8 @@
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
|
Loading…
x
Reference in New Issue
Block a user