develop #3
@ -1,3 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"src/**/*-vm.ts"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"react-hooks/rules-of-hooks": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||||
}
|
}
|
||||||
|
12
package.json
12
package.json
@ -5,22 +5,24 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start --port 4000",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"next": "15.0.1",
|
||||||
"react": "19.0.0-rc-69d4b800-20241021",
|
"react": "19.0.0-rc-69d4b800-20241021",
|
||||||
"react-dom": "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": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "15.0.1",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"eslint": "^8",
|
"typescript": "^5"
|
||||||
"eslint-config-next": "15.0.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import "reflect-metadata"
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = localFont({
|
const geistSans = localFont({
|
||||||
src: "./fonts/GeistVF.woff",
|
src: "./fonts/GeistVF.woff",
|
||||||
variable: "--font-geist-sans",
|
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,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user