feature/research-di #1

Merged
behnam merged 37 commits from feature/research-di into develop 2024-11-21 15:50:19 +00:00
26 changed files with 355 additions and 72 deletions
Showing only changes of commit f6fba115e3 - Show all commits

20
components.json Normal file
View File

@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

View File

@ -9,10 +9,16 @@
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-icons": "^1.3.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.454.0",
"next": "15.0.1",
"react": "19.0.0-rc-69d4b800-20241021",
"react-dom": "19.0.0-rc-69d4b800-20241021",
"reflect-metadata": "^0.2.2",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"tsyringe": "^4.8.0"
},
"devDependencies": {

View File

@ -1,10 +1,12 @@
"use client"
import BaseView, { BuildProps } from "@/bootstrap/helpers/view/base-view";
import ButtonVm from "@/components/button/button-vm";
import ButtonVm from "@/app/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>
}
}

View File

@ -2,20 +2,71 @@
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
body {
font-family: Arial, Helvetica, sans-serif;
}
@media (prefers-color-scheme: dark) {
@layer base {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -1,4 +1,3 @@
import "reflect-metadata"
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";

View File

@ -0,0 +1,13 @@
"use client"
import Button from "@/app/components/button/button"
import { TestButtonVM } from "@/app/test/client/vm/test-button-vm"
import { useDI } from "@/bootstrap/di/di-context"
import { useRef } from "react"
export default function ParentView() {
const di = useDI()
const vmRef = useRef(di.resolve(TestButtonVM))
return <Button vm={vmRef.current} />
}

View File

@ -0,0 +1,34 @@
"use client"
import BaseVM from "@/bootstrap/helpers/vm/base-vm";
import ButtonVm from "@/app/components/button/button-vm";
import { useEffect, useState } from "react";
import getButtonTitle from "@/feature/domain/test/service/test-get-button-title-service";
export class TestButtonVM extends BaseVM<ButtonVm> {
private getButtonTitle: () => Promise<string>
constructor() {
super()
this.getButtonTitle = this.di.resolve(getButtonTitle.name)
}
useVM(): ButtonVm {
const [ buttonTitle, setTitle ] = useState("Default title")
useEffect(() => {
(async () => {
const title = await this.getButtonTitle()
setTitle(title)
})()
}, [])
return {
props: {
title: buttonTitle
},
onClick: () => {
console.log("clicked on the button");
}
}
}
}
export const testKey = "testKey"

10
src/app/test/layout.tsx Normal file
View File

@ -0,0 +1,10 @@
"use client"
import testAppModule from "@/app/test/modules/test-app-module";
import { DiContext } from "@/bootstrap/di/di-context";
import { PropsWithChildren, useRef } from "react";
export default function WithDILayout(props: PropsWithChildren) {
const testDi = useRef(testAppModule())
return <DiContext.Provider value={testDi.current}>{props.children}</DiContext.Provider>
}

View File

@ -0,0 +1,13 @@
import { TestButtonVM, testKey } from "@/app/test/client/vm/test-button-vm";
import di from "@/bootstrap/di/init-di"
import getButtonTitle from "@/feature/domain/test/service/test-get-button-title-service";
export default function testAppModule() {
const testDi = di.createChildContainer()
testDi.registerInstance(testKey, TestButtonVM);
testDi.register(getButtonTitle.name, {
useValue: getButtonTitle
})
return testDi
}

View File

@ -1,8 +1,5 @@
"use client"
import "reflect-metadata"
import TestButtonVM from "@/app/test/vm/test-button-vm";
import Button from "@/components/button/button";
import ParentView from "@/app/test/client/view/parent-view";
export default function Page() {
return <Button vmName={TestButtonVM.name} />
return <ParentView />
}

View File

@ -1,17 +0,0 @@
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");
}
}
}
}

View File

@ -0,0 +1,21 @@
"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,5 +1,5 @@
// "use client"
import "reflect-metadata"
import { container, DependencyContainer } from "tsyringe";
/**
@ -8,7 +8,7 @@ import { container, DependencyContainer } from "tsyringe";
* are registered and available for injection throughout the application.
*/
const InitDI = (): DependencyContainer => {
const di = container;
const di = container.createChildContainer();
return di;
};

View File

@ -1,17 +1,17 @@
"use client"
// import gdi from "@/bootstrap/di/init-di";
/* 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";
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
import { Component, ReactNode, FC, PropsWithChildren, memo } from "react";
/* -------------------------------------------------------------------------- */
/* Connector Component */
/* -------------------------------------------------------------------------- */
interface IVvmConnector<IVM, PROPS> extends PropsWithChildren {
View: FC<any & { vm: IVM }>;
vmName: string;
Vm: IBaseVM<IVM>;
restProps?: PROPS;
memoizedByVM?: boolean;
}
@ -21,11 +21,9 @@ interface IVvmConnector<IVM, PROPS> extends PropsWithChildren {
*/
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 { View, Vm, restProps, children } = props;
const vm = new VmInstance().useVM()
const vm = Vm.useVM()
const allProps = {
restProps,
@ -46,8 +44,8 @@ const VvmConnector = memo(
type IVMParent = Record<string, any>;
type IPropParent = Record<string, any> | undefined;
type BaseProps<PROPS extends IPropParent = undefined> = {
vmName: string;
type BaseProps<IVM extends IVMParent, PROPS extends IPropParent = undefined> = {
vm: IBaseVM<IVM>;
restProps?: PROPS;
/**
* By default it's true.
@ -70,25 +68,23 @@ export type BuildProps<
export default abstract class BaseView<
IVM extends IVMParent,
PROPS extends IPropParent = undefined,
> extends Component<BaseProps<PROPS>> {
> extends Component<BaseProps<IVM, 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)>;
const { vm, restProps, memoizedByVM, children, ...rest } = this.props;
return (
<Connector
<VvmConnector
View={this.Build}
vmName={vmName}
Vm={vm}
memoizedByVM={typeof memoizedByVM === "undefined" ? true : memoizedByVM}
restProps={{ ...restProps, ...rest } as PROPS}
restProps={{ ...restProps, ...rest }}
>
{children}
</Connector>
</VvmConnector>
);
}
/* -------------------------------------------------------------------------- */

View File

@ -1,4 +1,5 @@
"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";
@ -28,6 +29,10 @@ export default abstract class BaseVM<
*/
protected rerender?: () => void;
/* -------------------------------------------------------------------------- */
protected get di() {
return useDI()
}
/* -------------------------------------------------------------------------- */
/**
* You can use this hook in your useVm method to get rerender method

View File

@ -1,9 +0,0 @@
"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);
};
}

View File

@ -0,0 +1,13 @@
import { testModuleKey } from "@/feature/domain/test/test-module-key";
import getTestModule from "@/feature/infra/test/module/test-module";
import { DependencyContainer } from "tsyringe";
export default function serverDi(module: string): DependencyContainer {
const getDi = {
[testModuleKey]: getTestModule
}[module]
if (!getDi) throw new Error("Server Di didn't found for module: " + module)
return getDi()
}

View File

@ -0,0 +1,9 @@
"use server"
import serverDi from "@/feature/common/server-di";
import TestRepo, { testRepoKey } from "@/feature/domain/test/service/test-service-repo"
import { testModuleKey } from "@/feature/domain/test/test-module-key";
export default async function getButtonTitle() {
const repo = serverDi(testModuleKey).resolve<TestRepo>(testRepoKey)
return repo.getButtonTitle()
}

View File

@ -0,0 +1,5 @@
export default interface TestRepo {
getButtonTitle(): Promise<string>
}
export const testRepoKey = "restRepoKey"

View File

@ -0,0 +1 @@
export const testModuleKey = "testModuleKey"

View File

@ -0,0 +1,12 @@
import di from "@/bootstrap/di/init-di"
import { testRepoKey } from "@/feature/domain/test/service/test-service-repo"
import TestRepoImpl from "@/feature/infra/test/repo/test-repo-iml"
export default function getTestModule() {
const testDi = di.createChildContainer()
di.register(testRepoKey, {
useClass: TestRepoImpl
})
return testDi
}

View File

@ -0,0 +1,14 @@
import TestRepo from "@/feature/domain/test/service/test-service-repo";
export default class TestRepoImpl implements TestRepo {
async getButtonTitle(): Promise<string> {
await new Promise((res) => {
setTimeout(() => {
res(true)
}, 3000)
})
console.log('hereee');
return Promise.resolve("Button title")
}
}

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-require-imports */
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
@ -9,11 +11,54 @@ const config: Config = {
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
plugins: [],
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
},
plugins: [require("tailwindcss-animate")],
};
export default config;

View File

@ -305,6 +305,11 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@radix-ui/react-icons@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.1.tgz#462c85fd726a77854cd5956e29eb19a575aa7ce5"
integrity sha512-QvYompk0X+8Yjlo/Fv4McrzxohDdM5GgLHyQcPpcsPvlOSXCGFjdbuyGL5dzRbg0GpknAjQJJZzdiRK7iWVuFQ==
"@rtsao/scc@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
@ -714,11 +719,28 @@ chokidar@^3.5.3:
optionalDependencies:
fsevents "~2.3.2"
class-variance-authority@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.0.tgz#1c3134d634d80271b1837452b06d821915954522"
integrity sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==
dependencies:
clsx "2.0.0"
client-only@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
clsx@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==
clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
@ -1871,6 +1893,11 @@ lru-cache@^10.2.0:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
lucide-react@^0.454.0:
version "0.454.0"
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.454.0.tgz#a81b9c482018720f07ead0503ae502d94d528444"
integrity sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==
merge2@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
@ -2599,6 +2626,16 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tailwind-merge@^2.5.4:
version "2.5.4"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.5.4.tgz#4bf574e81fa061adeceba099ae4df56edcee78d1"
integrity sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==
tailwindcss-animate@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4"
integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==
tailwindcss@^3.4.1:
version "3.4.14"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.14.tgz#6dd23a7f54ec197b19159e91e3bb1e55e7aa73ac"