Skip to content

Commit 1563d4d

Browse files
committed
feat: MatchOption component
1 parent 0658786 commit 1563d4d

6 files changed

Lines changed: 147 additions & 1 deletion

File tree

src/components/MatchOption.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Children, isValidElement } from "react";
2+
import { MatchProps, OptionProps } from "../models";
3+
4+
/**
5+
* **`Match`**: Component used inside _MatchOption_ component to represent a match construct.
6+
*
7+
* @see [📖 Documentation](https://react-tools.ndria.dev/components/Match)
8+
* @template T - The type of the `value` to be compared.
9+
* @param {MatchProps<T>} props - {@link MatchProps}
10+
* @returns {JSX.Element|null} element
11+
*/
12+
const Match = <T,>({ value, children, fallback }: MatchProps<T>) => {
13+
const childrenArray = Children.toArray(children);
14+
15+
const match = childrenArray.find(
16+
(child) => {
17+
let result = false;
18+
if (isValidElement(child)) {
19+
if ("is" in child.props) {
20+
result = typeof child.props.is === "function"
21+
? child.props.is(value)
22+
: child.props.is === value;
23+
}
24+
}
25+
return result;
26+
}
27+
);
28+
29+
return match ?? fallback ?? null;
30+
};
31+
32+
/**
33+
* **`Option`**: Component used inside _MatchOption_ component to represent an option construct.
34+
*
35+
* @see [📖 Documentation](https://react-tools.ndria.dev/components/MatchOption)
36+
* @template T - The type of the `is` value, should match the `value` type of the parent [Match].
37+
* @param {OptionProps<T>} props - {@link OptionProps}
38+
* @returns {JSX.Element|null} element
39+
*/
40+
const Option = <T,>({ children }: OptionProps<T>) => {
41+
return children;
42+
};
43+
44+
/**
45+
* **`MatchOption`**: Provides a declarative switch-case pattern. It compares the [Match] component's `value` against the `is` prop of its [Option] children.
46+
* The first matching [Option] is rendered; otherwise, the `fallback` is displayed.
47+
* @see [📖 Documentation](https://react-tools.ndria.dev/components/MatchOption)
48+
*/
49+
export const MatchOption = { Match, Option };
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { memo } from "react";
2+
import { MatchOption } from "./MatchOption";
3+
4+
//#IGNORE
5+
6+
/**
7+
* **`MatchOptionMemoized`**: Memoized version of _MatchOption_ component.
8+
* Both [Match] and [Option] are wrapped with `React.memo`, preventing re-renders
9+
* when their props have not changed.
10+
* Prefer this over [MatchOption](https://react-tools.ndria.dev/components/MatchOption)
11+
* in performance-sensitive trees where the parent re-renders frequently.
12+
*
13+
* @see [📖 Documentation](https://react-tools.ndria.dev/components/MatchOptionMemoized)
14+
*/
15+
export const MatchOptionMemoized = {
16+
Switch: memo(MatchOption.Match),
17+
Case: memo(MatchOption.Option)
18+
};

src/components/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ export { For } from './For';
77
export { ForMemoized } from './ForMemoized';
88
export { ErrorBoundary } from './ErrorBoundary';
99
export { Activity } from './Activity';
10-
export { Suspense } from './Suspense';
10+
export { Suspense } from './Suspense';
11+
export { MatchOption } from './MatchOption';
12+
export { MatchOptionMemoized } from './MatchOptionMemoized';
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useEffect, useRef, useState } from "react";
2+
import { MatchOption } from "../../../";
3+
4+
/**
5+
The component has an array of numbers and a variable state number used like index of array. Every 2 seconds index changes value. It uses __MatchOption__ component to render different p element with a text indicating the value of array with the current index.
6+
*/
7+
export default function SM() {
8+
const valuesCounter = useRef([1, 2, 3]);
9+
const [indexCounter, setIndexCounter] = useState(0);
10+
11+
useEffect(() => {
12+
const id = setInterval(() => setIndexCounter(i => i%4+1 === 4 ? 0 : i%4+1), 2000);
13+
return () => clearInterval(id);
14+
}, []);
15+
16+
return (<div style={{textAlign: "left", display: "flex", flexDirection: "column", alignItems: "center"}}>
17+
<p style={{width: 230}}>Counter values: {JSON.stringify(valuesCounter.current, null, 2)}</p>
18+
<p style={{width: 230}}>Counter index: {indexCounter}</p>
19+
<MatchOption.Match value={indexCounter} fallback={<p style={{color: "darkorange", width: 230}}>Counter value unsetted.</p>}>
20+
<MatchOption.Option is={0}>
21+
<p style={{color: "darkturquoise", width: 230, fontWeight: 800}}>Counter value is 1.</p>
22+
</MatchOption.Option>
23+
<MatchOption.Option is={(value) => value === 1}>
24+
<p style={{color: "darkkhaki", width: 230, fontWeight: 800}}>Counter value is 2.</p>
25+
</MatchOption.Option>
26+
<MatchOption.Option is={2}>
27+
<p style={{color: "darkcyan", width: 230, fontWeight: 800}}>Counter value is 3.</p>
28+
</MatchOption.Option>
29+
</MatchOption.Match>
30+
</div>);
31+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Memoized implementation of _MatchOption_ component.
2+
3+
Please visit [MatchOption component](https://react-tools.ndria.dev/components/MatchOption) example to see how it works.

src/models/MatchOption.model.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { PropsWithChildren, ReactElement, ReactNode } from "react";
2+
3+
/**
4+
* Props accepted by the [MatchOption](https://react-tools.ndria.dev/components/MatchOption) component.
5+
*
6+
* @template T - The type of the `value` to be compared.
7+
*/
8+
export interface MatchProps<T> {
9+
/**
10+
* The central value to compare against the `is` prop of each <MatchOption.Option> child.
11+
* The first <MatchOption.Option> whose `is` value or function strictly matches (`===`) this `value`
12+
* will be rendered.
13+
*/
14+
value: T;
15+
16+
/**
17+
* One or more {@link Option} elements to evaluate in order. The first `Option`
18+
* whose `is` prop is truthy is rendered; all others are ignored.
19+
*/
20+
children?:
21+
| ReactElement<OptionProps<T>>
22+
| ReactElement<OptionProps<T>>[]
23+
| undefined;
24+
25+
/**
26+
* Optional content rendered when no <MatchOption.Option> matches the provided `value`.
27+
* Accepts any valid React node.
28+
*/
29+
fallback?: ReactNode;
30+
}
31+
32+
/**
33+
* Props accepted by the [MatchOption](https://react-tools.ndria.dev/components/MatchOption) component.
34+
*
35+
* @template T - The type of the `is` value, should match the `value` type of the parent <MatchOption.Match>.
36+
*/
37+
export interface OptionProps<T> extends PropsWithChildren {
38+
/**
39+
* The value used to determine if this option should be rendered.
40+
* If `is === value` (from the parent [Match]), the `children` are displayed.
41+
*/
42+
is: T | ((value:T) => boolean);
43+
}

0 commit comments

Comments
 (0)