Skip to content

Commit

Permalink
♻️ [refactor]: Add playground app for local development, add abstract…
Browse files Browse the repository at this point in the history
…ed hooks
  • Loading branch information
brunotot committed Sep 6, 2023
1 parent cae2382 commit a7a5416
Show file tree
Hide file tree
Showing 35 changed files with 724 additions and 61 deletions.
9 changes: 8 additions & 1 deletion packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,34 @@ import {
} from "./src/decorators/types/DecoratorContext.type";
import { ValidationGroup } from "./src/decorators/types/DecoratorProps.type";
import { Locale, getLocale, setLocale } from "./src/messages/model/Locale";
import EntityProcessor from "./src/model/processor/EntityProcessor";
import EntityProcessor, {
EntityProcessorConfig,
} from "./src/model/processor/EntityProcessor";
import { Class, StripClass } from "./src/types/Class.type";
import { DetailedErrors } from "./src/types/DetailedErrors.type";
import { Errors } from "./src/types/Errors.type";
import { Payload } from "./src/types/Payload.type";
import { ValidationEvaluator } from "./src/types/ValidationEvaluator.type";
import { ValidationResult } from "./src/types/ValidationResult.type";
import { Condition } from "./src/types/namespace/Condition.ns";
import { $ } from "./src/types/namespace/Utility.ns";
import validators from "./validators";
import Rule from "./validators/any/Rule";

export interface PrimitiveSetAppend {}

export type {
Class,
Condition,
DecoratorContext,
DecoratorContextMetadata,
DetailedErrors,
EntityProcessorConfig,
Errors,
Locale,
Payload,
StripClass,
$ as TypeUtils,
ValidationEvaluator,
ValidationGroup,
ValidationResult,
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"scripts": {
"clean": "rm -rf dist",
"build": "npm run test && npm run clean && tsc && cp polyfill.d.ts ./dist && cp polyfill.d.ts ./dist/types",
"build:noTest": "npm run clean && tsc && cp polyfill.d.ts ./dist && cp polyfill.d.ts ./dist/types",
"deploy:minor": "bash ../../scripts/deploy.sh core minor",
"deploy:major": "bash ../../scripts/deploy.sh core major",
"deploy:patch": "bash ../../scripts/deploy.sh core patch",
Expand Down
21 changes: 15 additions & 6 deletions packages/core/src/model/processor/EntityProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import MetadataProcessor from "./MetadataProcessor";

(Symbol as any).metadata ??= Symbol("Symbol.metadata");

type EntityProcessorConfig<TBody> = {
export type EntityProcessorConfig<TBody> = {
defaultValue?: TBody;
groups?: ValidationGroup[];
};
Expand Down Expand Up @@ -58,20 +58,29 @@ export default class EntityProcessor<TClass, TBody = TClass> {
return this.#noArgsInstance;
}

#buildEmptyInstance(
public static buildEmptyInstance<TClass, TBody = TClass>(
clazz: Class<TClass>,
config?: EntityProcessorConfig<TBody>
defaultValue?: TBody | undefined
) {
return (config?.defaultValue ?? new clazz()) as TBody;
return (defaultValue ?? new clazz()) as TBody;
}

public static getClassFieldNames<TClass, TBody = TClass>(
clazz: Class<TClass>
) {
return getClassFieldNames(clazz) as (keyof TBody)[];
}

constructor(clazz: Class<TClass>, config?: EntityProcessorConfig<TBody>) {
const groups = config?.groups ?? [];
this.#noArgsInstance = this.#buildEmptyInstance(clazz, config);
this.#noArgsInstance = EntityProcessor.buildEmptyInstance(
clazz,
config?.defaultValue
);
this.#clazz = clazz;
this.#groups = Array.from(new Set(groups));
this.#cache = {} as EntityProcessorCache<TClass>;
this.#fields = getClassFieldNames(clazz) as (keyof TBody)[];
this.#fields = EntityProcessor.getClassFieldNames(clazz);
this.#setMetadata(this.#noArgsInstance as Payload<TClass>);
}

Expand Down
7 changes: 1 addition & 6 deletions packages/react/examples/basic-example-form/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "tdv-core/dist/polyfill.d.ts";
import App from "./App.tsx";

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
18 changes: 18 additions & 0 deletions packages/react/examples/playground/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
24 changes: 24 additions & 0 deletions packages/react/examples/playground/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
27 changes: 27 additions & 0 deletions packages/react/examples/playground/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:

- Configure the top-level `parserOptions` property like this:

```js
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
```

- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
39 changes: 39 additions & 0 deletions packages/react/examples/playground/dev-watch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { exec } from "child_process";
import { watch } from "chokidar";

// Specify the directories you want to watch for changes
const directoriesToWatch = ["./../../../core/src", "./../../src"]; // Adjust as needed

// Define the command to restart your Vite development server
const restartCommand = "npm run dev";

// Create a chokidar watcher
const watcher = watch(directoriesToWatch, {
ignoreInitial: true, // Ignore initial scan to avoid unnecessary restarts
});

// Start the Vite development server
exec(restartCommand, (error, stdout, stderr) => {
if (error) {
console.error(`Error starting the server: ${error}`);
return;
}
console.log(`Server started successfully.`);
});

// When a change is detected, execute the restart command
watcher.on("change", (path) => {
console.log(`Change detected in ${path}. Restarting server...`);
exec(restartCommand, (error, stdout, stderr) => {
if (error) {
console.error(`Error during server restart: ${error}`);
return;
}
console.log(`Server restarted successfully.`);
});
});

// Handle errors
watcher.on("error", (error) => {
console.error(`Watcher error: ${error}`);
});
12 changes: 12 additions & 0 deletions packages/react/examples/playground/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>tdv-react example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
33 changes: 33 additions & 0 deletions packages/react/examples/playground/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "basic-example-form",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"start": "npm run dev",
"dev:watch": "node dev-watch.js",
"dev": "npm i --silent && npm run build:noTest --prefix=../../../core && npm run build:noTest --prefix=../.. && vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tdv-core": "file:../../../core/dist",
"tdv-react": "file:../../dist"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
"chokidar-cli": "^3.0.0",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"typescript": "^5.2.2",
"vite": "^4.4.5"
}
}
11 changes: 11 additions & 0 deletions packages/react/examples/playground/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import UserFormControls from "./components/controls/UserFormControls";

function App() {
return (
<div style={{ padding: "1rem" }}>
<UserFormControls />
</div>
);
}

export default App;
1 change: 1 addition & 0 deletions packages/react/examples/playground/src/assets/react.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useState } from "react";
import { FormProvider, useForm } from "tdv-react";
import UserForm from "../../models/UserForm";
import Input from "../shared/Input";

const SUBMIT_BUTTON_TRIGGERS_VALIDATION = true;
const VALIDATION_GROUPS_FACTORY: "native" | "custom" | "both" = "both";
const GROUPS =
VALIDATION_GROUPS_FACTORY === "both"
? ["custom", "native"]
: VALIDATION_GROUPS_FACTORY === "custom"
? ["custom"]
: ["native"];

function onSubmitValidationFail(e: any) {
alert(
"Form has invalid data. Please correct them\n" + JSON.stringify(e, null, 2)
);
}

export default function UserFormInput() {
const [numberOfStateChanges, setNumberOfStateChanges] = useState(0);
const [
form,
_setForm,
{ providerProps, errors, isSubmitted, isValid, onSubmit, reset, mutations },
] = useForm<UserForm>(UserForm, {
validationGroups: GROUPS,
onSubmit: () => alert("Congrats, you don't have any validation errors!"),
whenChanged: () => setNumberOfStateChanges((p) => ++p),
defaultValue: undefined,
standalone: true,
validateImmediately: !SUBMIT_BUTTON_TRIGGERS_VALIDATION,
onSubmitValidationFail,
});

console.count("UserFormControls");

const dateOfBirthValue = form!.dateOfBirth
? form.dateOfBirth.toISOString().substring(0, 10)
: "";

return (
<>
<FormProvider {...providerProps}>
<div
style={{ display: "flex", flexDirection: "column", gap: "0.25rem" }}
>
<Input
value={form.testEmail}
onChange={mutations.testEmail}
errors={errors.testEmail}
label="Test email"
/>
<Input
value={form.age}
onChange={mutations.age}
errors={errors.age}
label="Age"
/>
<Input
value={form.password}
onChange={mutations.password}
errors={errors.password}
type="password"
label="Password"
/>
<Input
value={form.confirmPassword}
onChange={mutations.confirmPassword}
errors={errors.confirmPassword}
type="password"
label="Confirm password"
/>
<Input
value={dateOfBirthValue}
onChange={(v) => mutations.dateOfBirth!(v ? new Date(v) : null!)}
errors={errors.dateOfBirth}
type="date"
label="Date of birth"
/>
{SUBMIT_BUTTON_TRIGGERS_VALIDATION && (
<button
style={{ padding: "0.5rem", fontSize: 18, marginTop: 8 }}
onClick={onSubmit}
disabled={isSubmitted && !isValid}
>
<strong>Submit</strong>
</button>
)}
<p>Form changed state count: {numberOfStateChanges}</p>
<button
style={{ padding: "0.5rem", fontSize: 18, marginTop: 8 }}
onClick={() => reset("testEmail")}
>
<strong>Reset form</strong>
</button>
</div>
</FormProvider>
</>
);
}
Loading

0 comments on commit a7a5416

Please sign in to comment.