Skip to content

Commit 0cfc8b8

Browse files
committed
feat(Case): add as props & refactor code. (#45)
1 parent 7b9e5d8 commit 0cfc8b8

File tree

5 files changed

+159
-39
lines changed

5 files changed

+159
-39
lines changed

core/README.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ import { Switch, Case, Default } from '@uiw/react-only-when/switch'
100100

101101
<Switch>
102102
<Case condition={age < 6}>preschool</Case>
103-
<Case condition={age >= 6}>primary school</Case>
103+
<Case as="div" condition={age >= 6}>primary school</Case>
104104
<Default>you graduated</Default>
105105
</Switch>
106106
```
@@ -113,18 +113,41 @@ export default function App() {
113113
const [age, setAge] = useState(19)
114114
return (
115115
<Fragment>
116-
<input type="range" onChange={(evn) => setAge(Number(evn.target.value))} /> {age}
116+
<input type="range" onChange={(evn) => setAge(Number(evn.target.value))} /> {age}<br />
117117
<Switch>
118118
<Case condition={age < 6}>Preschool</Case>
119119
<Case condition={age >= 6 && age < 18}>Primary school</Case>
120-
<Case condition={age >= 18}>Went to college</Case>
120+
<Case condition={age >= 18 && age < 60}>Went to college</Case>
121121
<Default>you graduated</Default>
122122
</Switch>
123123
</Fragment>
124124
);
125125
}
126126
```
127127

128+
Defaults to specifying a wrapped HTML Element.
129+
130+
```jsx mdx:preview&background=#fff&codePen=true
131+
import React, { useState, Fragment } from 'react';
132+
import { Switch, Case, Default } from '@uiw/react-only-when/switch'
133+
134+
export default function App() {
135+
const [age, setAge] = useState(19)
136+
return (
137+
<Fragment>
138+
<input type="range" onChange={(evn) => setAge(Number(evn.target.value))} /> {age}
139+
<br />
140+
<Switch>
141+
<Case as="span" condition={age < 6}>Preschool</Case>
142+
<Case as="em" condition={age >= 6 && age < 18}>Primary school</Case>
143+
<Case as="div" condition={age >= 18 && age < 60}>Went to college</Case>
144+
<Default as="p">you graduated</Default>
145+
</Switch>
146+
</Fragment>
147+
);
148+
}
149+
```
150+
128151
## `<Only />` props
129152

130153
| prop name | type | default | isRequired | description |

core/src/case.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useEffect, useId } from 'react';
2+
import { type InitialState, useSwitchDispatch } from './switch.store';
3+
4+
type TagType = React.ElementType | keyof JSX.IntrinsicElements;
5+
interface CaseElementProps<T extends TagType> {
6+
as?: T;
7+
readonly condition?: boolean;
8+
}
9+
10+
export type CaseProps<T extends TagType> = CaseElementProps<T> & React.ComponentPropsWithoutRef<T>;
11+
12+
export const Case = <T extends TagType>(props: CaseProps<T>) => {
13+
const ids = useId();
14+
const dispatch = useSwitchDispatch();
15+
const { children, condition, as: Comp, ...reset } = props;
16+
const Elm = Comp as TagType;
17+
const child = Elm ? <Elm {...reset}>{children}</Elm> : children;
18+
const state: InitialState = { [ids]: child, active: { [ids]: !!condition } };
19+
useEffect(() => dispatch(state), [state]);
20+
return null;
21+
};
22+
23+
export const Default = <T extends TagType>(props: CaseProps<T>) => {
24+
const dispatch = useSwitchDispatch();
25+
const { children, as: Comp, ...reset } = props;
26+
const Elm = Comp as TagType;
27+
const child = Elm ? <Elm {...reset}>{children}</Elm> : children;
28+
useEffect(() => dispatch({ default: child }), [props]);
29+
return null;
30+
};

core/src/switch.store.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { createContext, useContext, useReducer } from 'react';
2+
3+
export type InitialState = {
4+
[key: string]: React.ReactNode;
5+
} & {
6+
default?: React.ReactNode;
7+
active?: Record<string, boolean>;
8+
};
9+
10+
const initialState: InitialState = {};
11+
export const Context = createContext<InitialState>(initialState);
12+
13+
const reducer = (state: InitialState, action: InitialState) => {
14+
action.active = action.active ?? {};
15+
action.active = { ...state.active, ...action.active };
16+
return {
17+
...state,
18+
...action,
19+
};
20+
};
21+
22+
export const useSwitchStore = () => {
23+
return useContext(Context);
24+
};
25+
26+
export function useSwitch() {
27+
return useReducer(reducer, initialState);
28+
}
29+
30+
type Dispatch = React.Dispatch<InitialState>;
31+
export const DispatchSwitch = createContext<Dispatch>(() => {});
32+
DispatchSwitch.displayName = 'OW.DispatchSwitch';
33+
34+
export function useSwitchDispatch() {
35+
return useContext(DispatchSwitch);
36+
}

core/src/switch.tsx

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,37 @@
1-
import { ReactElement, Children } from 'react';
2-
import { FC, PropsWithChildren } from 'react';
1+
import { type FC, type PropsWithChildren } from 'react';
2+
import { DispatchSwitch, Context, useSwitch, useSwitchStore } from './switch.store';
3+
import { Case, Default } from './case';
4+
5+
export * from './case';
36

47
export const Switch: FC<PropsWithChildren<{}>> = ({ children }) => {
5-
let matchChild: ReactElement | null = null;
6-
let defaultCase: ReactElement | null = null;
7-
Children.forEach(Children.toArray(children) as ReactElement<PropsWithChildren<CaseProps>>[], (child) => {
8-
if (!matchChild && child.type === Case) {
9-
const { condition } = child.props;
10-
const conditionIsTrue = Boolean(condition);
11-
if (conditionIsTrue) {
12-
matchChild = child;
8+
const [state, dispatch] = useSwitch();
9+
const filteredChildren = [];
10+
const childs = Array.isArray(children) ? children : children ? [children] : null;
11+
if (childs) {
12+
for (let i = 0; i < childs.length; i++) {
13+
const child = childs[i];
14+
if (child && (child.type === Case || child.type === Default)) {
15+
filteredChildren.push(child);
1316
}
14-
} else if (!defaultCase && child.type === Default) {
15-
defaultCase = child;
1617
}
17-
});
18-
return matchChild ?? defaultCase ?? null;
18+
}
19+
return (
20+
<Context.Provider value={state}>
21+
<DispatchSwitch.Provider value={dispatch}>{childs}</DispatchSwitch.Provider>
22+
<Render />
23+
</Context.Provider>
24+
);
1925
};
2026

21-
export interface CaseProps {
22-
readonly condition?: boolean;
23-
}
24-
25-
export const Case: FC<PropsWithChildren<CaseProps>> = ({ children }) => children;
26-
export const Default: FC<PropsWithChildren> = ({ children }) => children;
27+
const Render = () => {
28+
const state = useSwitchStore();
29+
let activeKey;
30+
for (var key in state.active) {
31+
if (state.active[key] === true) {
32+
activeKey = key;
33+
break;
34+
}
35+
}
36+
return state[activeKey ?? 'default'] ?? null;
37+
};

test/switch.test.tsx

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
/* eslint-disable jest/no-conditional-expect */
21
import renderer from 'react-test-renderer';
2+
import { render, screen } from '@testing-library/react';
33
import { Switch, Case, Default } from '../core/src/switch';
44

5-
65
it('<Switch />', () => {
76
const component = renderer.create(
87
<Switch></Switch>
@@ -12,46 +11,67 @@ it('<Switch />', () => {
1211
});
1312

1413
it('<Default />', () => {
15-
const component = renderer.create(
14+
const { container } = render(
1615
<Switch>
1716
<Default>you graduated</Default>
1817
</Switch>
1918
);
20-
const only = component.toJSON();
21-
expect(only).toEqual('you graduated');
19+
expect(container.innerHTML).toEqual('you graduated');
2220
});
2321

2422
it('<Case />', () => {
25-
const component = renderer.create(
23+
const { container } = render(
2624
<Switch>
2725
<Case condition={true}>preschool</Case>
2826
<Default>you graduated</Default>
2927
</Switch>
3028
);
31-
const only = component.toJSON();
32-
expect(only).toEqual('preschool');
29+
expect(container.innerHTML).toEqual('preschool');
3330
});
3431

35-
it('<Case /> condition=true', () => {
36-
const component = renderer.create(
32+
it('<Case />', () => {
33+
const { container } = render(
3734
<Switch>
3835
<Case condition={true}>preschool</Case>
3936
<Case condition={true}>primary school</Case>
4037
<Default>you graduated</Default>
4138
</Switch>
4239
);
43-
const only = component.toJSON();
44-
expect(only).toEqual('preschool');
40+
expect(container.innerHTML).toEqual('preschool');
4541
});
4642

47-
it('<Case /> condition=false', () => {
48-
const component = renderer.create(
43+
it('<Case />', () => {
44+
const { container } = render(
4945
<Switch>
5046
<Case condition={false}>preschool</Case>
5147
<Case condition={false}>primary school</Case>
5248
<Default>you graduated</Default>
5349
</Switch>
5450
);
55-
const only = component.toJSON();
56-
expect(only).toEqual('you graduated');
51+
expect(container.innerHTML).toEqual('you graduated');
52+
});
53+
54+
55+
it('<Case as="span" />', () => {
56+
render(
57+
<Switch>
58+
<Case as="span" data-testid="span" condition={true}>preschool</Case>
59+
</Switch>
60+
);
61+
const span = screen.getByTestId('span');
62+
expect(span.tagName).toEqual('SPAN');
63+
expect(span.innerHTML).toEqual('preschool');
64+
});
65+
66+
67+
it('<Default as="p" />', () => {
68+
render(
69+
<Switch>
70+
<Default as="p" title="test case" data-testid="elm">you graduated</Default>
71+
</Switch>
72+
);
73+
const elm = screen.getByTestId('elm');
74+
expect(elm.tagName).toEqual('P');
75+
expect(elm.innerHTML).toEqual('you graduated');
76+
expect(elm.title).toEqual('test case');
5777
});

0 commit comments

Comments
 (0)