Skip to content

Commit f6080d3

Browse files
committed
feat: if control
1 parent 68ebc9e commit f6080d3

19 files changed

+346
-0
lines changed

index.html

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta name="author" content="SkyCodr (aka: Dulan Sudasinghe)" />
7+
<title>Typescript React Directives</title>
8+
<script type="module" src="/src/main.tsx" defer></script>
9+
</head>
10+
<body>
11+
<div id="root"></div>
12+
</body>
13+
</html>

src/App.tsx

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useState } from "react";
2+
import { Switch, If, Else, ElseIf } from "./directives";
3+
4+
function App() {
5+
const [val, setVal] = useState(-1);
6+
const [val2, setVal2] = useState(false);
7+
return (
8+
<div>
9+
<h1>
10+
Selected value {val} - sub value {val2.toString()}
11+
</h1>
12+
<button onClick={() => setVal(0)}>0</button>
13+
<button onClick={() => setVal(1)}>1</button>
14+
<button onClick={() => setVal(2)}>2</button>
15+
<button onClick={() => setVal(3)}>3</button>
16+
<Switch>
17+
<If condition={val === 0}>
18+
<div>if val = 0</div>
19+
</If>
20+
<ElseIf condition={val === 1}>
21+
<div>if val = 1</div>
22+
<button onClick={() => setVal2((prev) => !prev)}>sub</button>
23+
<Switch>
24+
<If condition={val2}>
25+
<div>if sub</div>
26+
</If>
27+
</Switch>
28+
</ElseIf>
29+
<ElseIf condition={val === 2}>
30+
<div>if val = 2</div>
31+
</ElseIf>
32+
<Else>
33+
<div>Else val = {val}</div>
34+
</Else>
35+
</Switch>
36+
</div>
37+
);
38+
}
39+
40+
export default App;

src/assets/index.css

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
:root {
2+
--color: 255, 100, 200;
3+
--bg-color: 0, 100, 200;
4+
}
5+
6+
ol.error-list {
7+
border: 0.2rem double rgb(var(--bg-color));
8+
background-color: rgba(var(--bg-color), 0.1);
9+
list-style-type: none;
10+
box-sizing: border-box;
11+
padding: 0.5rem;
12+
margin: 0;
13+
}
14+
15+
.error-list > li {
16+
color: black;
17+
background-color: rgba(var(--color), 0.1);
18+
padding: 0.125rem;
19+
margin: 0.125rem;
20+
border: 2px solid rgb(var(--color));
21+
font-size: 1.5rem;
22+
font-family: monospace;
23+
}
24+
25+
.error-list > li::before {
26+
content: "\1f6c8 ";
27+
font-size: 1.5rem;
28+
padding-left: 0.125rem;
29+
margin-right: 0.125rem;
30+
}

src/components/Errors.tsx

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { FC } from "react";
2+
import { LogicErrors } from "../fixtures";
3+
4+
import "../assets/index.css";
5+
6+
const ERRORS: Record<LogicErrors, string> = {
7+
[LogicErrors.IfBlockExpected]: "Missing 'If'",
8+
[LogicErrors.OnlyOneIfBlockExpected]: "Can only have one 'If'",
9+
[LogicErrors.OnlyOneElseBlockExpected]: "Can only have one 'Else'",
10+
[LogicErrors.SwitchBlockExpected]: "'If', 'ElseIf', 'Else' need to be wrapped in 'Switch'",
11+
[LogicErrors.InvalidElseBlockOrdinal]: "Invalid ordinal, 'Else' should be the last",
12+
[LogicErrors.InvalidIfBlockOrdinal]: "Invalid ordinal, 'If' block should be the first",
13+
[LogicErrors.InvalidElement]: "Invalid element",
14+
[LogicErrors.ChildrenExpected]: "Should at least have one child",
15+
[LogicErrors.InvalidElseIfBlockOrdinal]: "Cannot have 'ElseIf' before 'If'",
16+
};
17+
18+
const Errors: FC<{ errors: LogicErrors[] }> = ({ errors }) => (
19+
<ol className="error-list">
20+
{errors.map((error, index) => (
21+
<li key={`${error}-${index}`}>{ERRORS[error]}</li>
22+
))}
23+
</ol>
24+
);
25+
26+
export default Errors;

src/components/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as Errors } from "./Errors";

src/directives/hooks/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { useValidate } from "./useValidate";

src/directives/hooks/useValidate.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Children, PropsWithChildren, ReactNode, useMemo } from "react";
2+
import { ValidationFactory } from "../../utils";
3+
4+
export const useValidate = <T extends ReactNode>(props: PropsWithChildren<T>, name: string) => {
5+
const { children } = props;
6+
7+
const _children = Children.toArray(children);
8+
return useMemo(() => ValidationFactory.get(name)(_children), [_children]);
9+
};

src/directives/if/Else.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { FC } from "react";
2+
import { useValidate } from "../hooks";
3+
import { Errors } from "../../components";
4+
5+
const Else: FC<ElseProps> = (props) => {
6+
const errors = useValidate<ElseIfProps>(props, Else.name);
7+
const children = errors.length === 0 ? props.children : <Errors errors={errors} />;
8+
9+
return <>{children}</>;
10+
};
11+
12+
export default Else;

src/directives/if/ElseIf.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { FC } from "react";
2+
import { useValidate } from "../hooks";
3+
import { Errors } from "../../components";
4+
5+
const ElseIf: FC<ElseIfProps> = (props) => {
6+
const errors = useValidate<ElseIfProps>(props, ElseIf.name);
7+
const children = errors.length === 0 ? props.children : <Errors errors={errors} />;
8+
9+
return <>{children}</>;
10+
};
11+
12+
export default ElseIf;

src/directives/if/If.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { FC } from "react";
2+
import { useValidate } from "../hooks";
3+
import { Errors } from "../../components";
4+
5+
const If: FC<IfProps> = (props) => {
6+
const errors = useValidate<IfProps>(props, If.name);
7+
const children = errors.length === 0 ? props.children : <Errors errors={errors} />;
8+
9+
return <>{children}</>;
10+
};
11+
12+
export default If;

src/directives/if/Switch.tsx

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Children, createElement, FC, memo, ReactNode } from "react";
2+
import { useValidate } from "../hooks";
3+
import { Errors } from "../../components";
4+
5+
const useSwitch = (props: SwitchProps) => {
6+
const errors = useValidate<SwitchProps>(props, Switch.name);
7+
if (errors.length) {
8+
return { children: createElement(Errors, { errors }) };
9+
}
10+
11+
const { children: oChildren } = props;
12+
const _children = Children.toArray(oChildren);
13+
const _child = _children.reduce<ReactNode | null>((acc, curr) => {
14+
// @ts-expect-error
15+
if (acc?.props?.condition) {
16+
return acc;
17+
}
18+
// @ts-expect-error
19+
if (curr?.props?.condition || curr?.type.name === "Else") {
20+
return curr;
21+
}
22+
23+
return acc;
24+
}, null);
25+
26+
return { children: _child };
27+
};
28+
29+
const Switch: FC<SwitchProps> = (props) => {
30+
const { children } = useSwitch(props);
31+
32+
return <>{children}</>;
33+
};
34+
35+
export default memo(Switch);

src/directives/if/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { default as Switch } from "./Switch";
2+
export { default as If } from "./If";
3+
export { default as ElseIf } from "./ElseIf";
4+
export { default as Else } from "./Else";

src/directives/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./if";

src/fixtures/enums.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export enum LogicErrors {
2+
// Switch
3+
IfBlockExpected = 2001,
4+
OnlyOneIfBlockExpected = 2002,
5+
OnlyOneElseBlockExpected = 2003,
6+
SwitchBlockExpected = 2005,
7+
InvalidIfBlockOrdinal = 2004,
8+
InvalidElseBlockOrdinal = 2006,
9+
InvalidElseIfBlockOrdinal = 2007,
10+
11+
// General
12+
13+
InvalidElement = 3002,
14+
ChildrenExpected = 3003,
15+
}

src/fixtures/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./enums";

src/main.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { StrictMode } from "react";
2+
import { createRoot } from "react-dom/client";
3+
4+
import App from "./App.tsx";
5+
6+
createRoot(document.getElementById("root")!).render(
7+
<StrictMode>
8+
<App />
9+
</StrictMode>
10+
);

src/types/globals.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
type SwitchProps<T extends React.ReactNode = any> = React.PropsWithChildren<T>;
2+
type IfProps<T extends React.ReactNode = any> = React.PropsWithChildren<T> & { condition: boolean };
3+
type ElseIfProps<T extends React.ReactNode = any> = IfProps<T>;
4+
type ElseProps<T extends React.ReactNode = any> = React.PropsWithChildren<T>;
5+
6+
type ErrorProps = { errors: number[] };
7+
8+
type ValidatorFn = (children: Array<Exclude<ReactNode, boolean | null | undefined>>) => number[];

src/utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ValidationFactory } from "./validators";

src/utils/validators.ts

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Children, isValidElement } from "react";
2+
import { LogicErrors } from "../fixtures";
3+
4+
/**
5+
* Validate if the children of the If directive are valid.
6+
* @param children
7+
* @returns
8+
*/
9+
const validateSwitchBlocks: ValidatorFn = (children) => {
10+
const _children = Children.toArray(children);
11+
const errors: LogicErrors[] = [];
12+
13+
if (_children.length === 0) {
14+
errors.push(LogicErrors.ChildrenExpected);
15+
return errors;
16+
}
17+
18+
_children.forEach((child) => {
19+
// @ts-expect-error
20+
const typeName = isValidElement(child) ? child.type.name : "unknown";
21+
22+
// If can't have a direct child of If, ElseIf, Else
23+
if (typeName === "If" || typeName === "ElseIf" || typeName === "Else") {
24+
errors.push(LogicErrors.SwitchBlockExpected);
25+
}
26+
});
27+
28+
return errors;
29+
};
30+
31+
const validateSwitch: ValidatorFn = (children) => {
32+
const _children = Children.toArray(children);
33+
const errors: LogicErrors[] = [];
34+
const elements: Record<string, number> = {};
35+
36+
if (_children.length === 0) {
37+
errors.push(LogicErrors.IfBlockExpected);
38+
return errors;
39+
}
40+
41+
_children.forEach((child, index) => {
42+
// @ts-expect-error
43+
const typeName = isValidElement(child) ? child.type.name : "unknown";
44+
45+
const count = elements[typeName] ?? 0;
46+
elements[typeName] = count + 1;
47+
48+
validateIfBlock(typeName, index, elements, errors);
49+
validateElseBlock(typeName, index, _children.length, elements, errors);
50+
validateElseIfBlock(typeName, index, errors);
51+
validateInvalidElement(typeName, errors);
52+
});
53+
54+
if (!elements["If"]) {
55+
errors.push(LogicErrors.IfBlockExpected);
56+
}
57+
58+
return errors;
59+
};
60+
61+
const validateIfBlock = (typeName: string, index: number, elements: Record<string, number>, errors: LogicErrors[]) => {
62+
if (typeName === "If") {
63+
if (index !== 0) {
64+
errors.push(LogicErrors.InvalidIfBlockOrdinal);
65+
}
66+
67+
if (elements["If"] > 1) {
68+
errors.push(LogicErrors.OnlyOneIfBlockExpected);
69+
}
70+
}
71+
};
72+
73+
const validateElseBlock = (
74+
typeName: string,
75+
index: number,
76+
length: number,
77+
elements: Record<string, number>,
78+
errors: LogicErrors[]
79+
) => {
80+
if (typeName === "Else") {
81+
if (elements["Else"] > 1) {
82+
errors.push(LogicErrors.OnlyOneElseBlockExpected);
83+
}
84+
if (index === 0 || index !== length - 1) {
85+
errors.push(LogicErrors.InvalidElseBlockOrdinal);
86+
}
87+
}
88+
};
89+
90+
const validateElseIfBlock = (typeName: string, index: number, errors: LogicErrors[]) => {
91+
if (typeName === "ElseIf" && index === 0) {
92+
errors.push(LogicErrors.InvalidElseIfBlockOrdinal);
93+
}
94+
};
95+
96+
const validateInvalidElement = (typeName: string, errors: LogicErrors[]) => {
97+
if (typeName !== "If" && typeName !== "ElseIf" && typeName !== "Else") {
98+
errors.push(LogicErrors.InvalidElement);
99+
}
100+
};
101+
102+
export class ValidationFactory {
103+
static get(validator: string) {
104+
switch (validator) {
105+
case "Switch":
106+
return validateSwitch;
107+
case "If":
108+
case "ElseIf":
109+
case "Else":
110+
return validateSwitchBlocks;
111+
default:
112+
return () => [];
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)