Skip to content

Commit be5e956

Browse files
committed
match: Rename Match to MatchTag and add MatchValue
1 parent 1b3ed9b commit be5e956

File tree

4 files changed

+186
-42
lines changed

4 files changed

+186
-42
lines changed

packages/match/README.md

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
[![version](https://img.shields.io/npm/v/@solid-primitives/match?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/match)
99
[![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives#contribution-process)
1010

11-
Control-flow component for matching discriminated union (tagged union) members.
11+
Control-flow components for matching discriminated union (tagged union) members and union literals.
1212

1313
## Installation
1414

@@ -20,10 +20,10 @@ yarn add @solid-primitives/match
2020
pnpm add @solid-primitives/match
2121
```
2222

23-
## `Match`
23+
## `MatchTag`
2424

2525
Control-flow component for matching discriminated union (tagged union) members.
26-
26+
2727
### How to use it
2828

2929
```tsx
@@ -37,7 +37,7 @@ type MyUnion = {
3737

3838
const [value, setValue] = createSignal<MyUnion>({type: "foo", foo: "foo-value"})
3939

40-
<Match on={value()} case={{
40+
<MatchTag on={value()} case={{
4141
foo: v => <>{v().foo}</>,
4242
bar: v => <>{v().bar}</>,
4343
}} />
@@ -56,7 +56,7 @@ type MyUnion = {
5656
bar: "bar-value",
5757
}
5858

59-
<Match on={value()} tag="kind" case={{
59+
<MatchTag on={value()} tag="kind" case={{
6060
foo: v => <>{v().foo}</>,
6161
bar: v => <>{v().bar}</>,
6262
}} />
@@ -67,7 +67,7 @@ type MyUnion = {
6767
Use the `partial` prop to only handle some of the union members:
6868

6969
```tsx
70-
<Match partial on={value()} case={{
70+
<MatchTag partial on={value()} case={{
7171
foo: v => <>{v().foo}</>,
7272
// bar case is not handled
7373
}} />
@@ -78,12 +78,51 @@ Use the `partial` prop to only handle some of the union members:
7878
Provide a fallback element when no match is found or the value is `null`/`undefined`:
7979

8080
```tsx
81-
<Match on={value()} case={{
81+
<MatchTag on={value()} case={{
8282
foo: v => <>{v().foo}</>,
8383
bar: v => <>{v().bar}</>,
8484
}} fallback={<div>No match found</div>} />
8585
```
8686

87+
## `MatchValue`
88+
89+
Control-flow component for matching union literals.
90+
91+
### How to use it
92+
93+
```tsx
94+
type MyUnion = "foo" | "bar";
95+
96+
const [value, setValue] = createSignal<MyUnion>("foo");
97+
98+
<MatchValue on={value()} case={{
99+
foo: () => <p>foo</p>,
100+
bar: () => <p>bar</p>,
101+
}} />
102+
```
103+
104+
### Partial matching
105+
106+
Use the `partial` prop to only handle some of the union members:
107+
108+
```tsx
109+
<MatchValue partial on={value()} case={{
110+
foo: () => <p>foo</p>,
111+
// bar case is not handled
112+
}} />
113+
```
114+
115+
### Fallback
116+
117+
Provide a fallback element when no match is found or the value is `null`/`undefined`:
118+
119+
```tsx
120+
<MatchValue on={value()} case={{
121+
foo: () => <p>foo</p>,
122+
bar: () => <p>bar</p>,
123+
}} fallback={<div>No match found</div>} />
124+
```
125+
87126
## Demo
88127

89128
[Deployed example](https://primitives.solidjs.community/playground/match) | [Source code](https://github.com/solidjs-community/solid-primitives/tree/main/packages/match/dev)

packages/match/dev/index.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Component, createSignal } from "solid-js";
2-
import { Match } from "../src/index.js"
2+
import { MatchTag, MatchValue } from "../src/index.js";
33

44
type AnimalDog = {type: 'dog', breed: string};
55
type AnimalCat = {type: 'cat', lives: number};
@@ -75,7 +75,7 @@ const App: Component = () => {
7575
<h2 class="text-xl font-semibold mb-1">Complete Match</h2>
7676
<p class="text-sm mb-4">Handles all union members with fallback</p>
7777
<div class="border border-gray-500 border-2 rounded p-4 min-h-[100px] flex items-center justify-center">
78-
<Match on={animal()} case={{
78+
<MatchTag on={animal()} case={{
7979
dog: v => <DogDisplay animal={v()} />,
8080
cat: v => <CatDisplay animal={v()} />,
8181
bird: v => <BirdDisplay animal={v()} />,
@@ -87,13 +87,25 @@ const App: Component = () => {
8787
<h2 class="text-xl font-semibold mb-1">Partial Match</h2>
8888
<p class="text-sm mb-4">Only handles dogs and cats</p>
8989
<div class="border border-gray-500 border-2 rounded p-4 min-h-[100px] flex items-center justify-center">
90-
<Match partial on={animal()} case={{
90+
<MatchTag partial on={animal()} case={{
9191
dog: v => <DogDisplay animal={v()} />,
9292
cat: v => <CatDisplay animal={v()} />,
9393
}} fallback={<FallbackDisplay />} />
9494
</div>
9595
</div>
9696
</div>
97+
98+
<div class="mt-8 border border-gray-500 border-2 rounded-lg p-6">
99+
<h2 class="text-xl font-semibold mb-1">Value Match</h2>
100+
<p class="text-sm mb-4">Match on union literals</p>
101+
<div class="border border-gray-500 border-2 rounded p-4 min-h-[100px] flex items-center justify-center">
102+
<MatchValue on={animal()?.type} case={{
103+
dog: () => <p>🐕</p>,
104+
cat: () => <p>🐱</p>,
105+
bird: () => <p>🐦</p>,
106+
}} fallback={<FallbackDisplay />} />
107+
</div>
108+
</div>
97109
</div>
98110
</div>
99111
);

packages/match/src/index.ts

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,43 +21,75 @@ import { type Accessor, type JSX, createMemo } from "solid-js"
2121
* }} />
2222
* ```
2323
*/
24-
export function Match<
25-
T extends {[k in Tag]: PropertyKey},
26-
Tag extends PropertyKey,
24+
export function MatchTag<
25+
T extends {[k in Tag]: PropertyKey},
26+
Tag extends PropertyKey,
2727
>(props: {
28-
on: T | null | undefined,
29-
tag: Tag,
30-
case: {[K in T[Tag]]: (v: Accessor<T & {[k in Tag]: K}>) => JSX.Element},
28+
on: T | null | undefined,
29+
tag: Tag,
30+
case: {[K in T[Tag]]: (v: Accessor<T & {[k in Tag]: K}>) => JSX.Element},
3131
fallback?: JSX.Element,
32-
partial?: false,
32+
partial?: false,
3333
}): JSX.Element
34-
export function Match<
35-
T extends {type: PropertyKey},
34+
export function MatchTag<
35+
T extends {type: PropertyKey},
3636
>(props: {
37-
on: T | null | undefined,
38-
case: {[K in T['type']]: (v: Accessor<T & {[k in 'type']: K}>) => JSX.Element},
37+
on: T | null | undefined,
38+
case: {[K in T['type']]: (v: Accessor<T & {[k in 'type']: K}>) => JSX.Element},
3939
fallback?: JSX.Element,
40-
partial?: false,
40+
partial?: false,
4141
}): JSX.Element
42-
export function Match<
43-
T extends {[k in Tag]: PropertyKey},
44-
Tag extends PropertyKey,
42+
export function MatchTag<
43+
T extends {[k in Tag]: PropertyKey},
44+
Tag extends PropertyKey,
4545
>(props: {
46-
on: T | null | undefined,
47-
tag: Tag,
48-
case: {[K in T[Tag]]?: (v: Accessor<T & {[k in Tag]: K}>) => JSX.Element},
46+
on: T | null | undefined,
47+
tag: Tag,
48+
case: {[K in T[Tag]]?: (v: Accessor<T & {[k in Tag]: K}>) => JSX.Element},
4949
fallback?: JSX.Element,
50-
partial: true,
50+
partial: true,
5151
}): JSX.Element
52-
export function Match<
53-
T extends {type: PropertyKey},
52+
export function MatchTag<
53+
T extends {type: PropertyKey},
5454
>(props: {
55-
on: T | null | undefined,
56-
case: {[K in T['type']]?: (v: Accessor<T & {[k in 'type']: K}>) => JSX.Element},
55+
on: T | null | undefined,
56+
case: {[K in T['type']]?: (v: Accessor<T & {[k in 'type']: K}>) => JSX.Element},
5757
fallback?: JSX.Element,
58-
partial: true,
58+
partial: true,
5959
}): JSX.Element
60-
export function Match(props: any): any {
61-
const kind = createMemo(() => props.on?.[props.tag ?? 'type'])
62-
return createMemo(() => props.case[kind()]?.(() => props.on) ?? props.fallback)
60+
export function MatchTag(props: any): any {
61+
const kind = createMemo(() => props.on?.[props.tag ?? 'type'])
62+
return createMemo(() => props.case[kind()]?.(() => props.on) ?? props.fallback)
63+
}
64+
65+
/**
66+
* Control-flow component for matching union literals.
67+
*
68+
* @example
69+
* ```tsx
70+
* type MyUnion = 'foo' | 'bar'
71+
*
72+
* const [value, setValue] = createSignal<MyUnion>('foo')
73+
*
74+
* <Match on={value()} case={{
75+
* foo: () => <p>foo</p>,
76+
* bar: () => <p>bar</p>,
77+
* }} />
78+
* ```
79+
*/
80+
export function MatchValue<T extends PropertyKey>(props: {
81+
on: T | null | undefined,
82+
case: {[K in T]: (v: K) => JSX.Element},
83+
fallback?: JSX.Element,
84+
partial?: false,
85+
}): JSX.Element
86+
export function MatchValue<T extends PropertyKey>(props: {
87+
on: T | null | undefined,
88+
case: {[K in T]?: (v: K) => JSX.Element},
89+
fallback?: JSX.Element,
90+
partial: true,
91+
}): JSX.Element
92+
export function MatchValue(props: any): any {
93+
const kind = createMemo(() => props.on)
94+
return createMemo(() => props.case[kind()]?.(kind()) ?? props.fallback)
6395
}

packages/match/test/index.test.tsx

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as v from "vitest";
22
import * as s from "solid-js";
3-
import { Match } from "../src/index.js";
3+
import { MatchTag, MatchValue } from "../src/index.js";
44

55
v.describe("Match", () => {
66
v.test("match on type field", () => {
@@ -19,7 +19,7 @@ v.describe("Match", () => {
1919
return {
2020
dispose,
2121
result: s.children(() => <>
22-
<Match on={value()} case={{
22+
<MatchTag on={value()} case={{
2323
foo: v => <>{v().foo}</>,
2424
bar: v => <>{v().bar}</>,
2525
}} />
@@ -54,7 +54,7 @@ v.describe("Match", () => {
5454
return {
5555
dispose,
5656
result: s.children(() => <>
57-
<Match on={value()} tag='kind' case={{
57+
<MatchTag on={value()} tag='kind' case={{
5858
foo: v => <>{v().foo}</>,
5959
bar: v => <>{v().bar}</>,
6060
}} />
@@ -89,7 +89,7 @@ v.describe("Match", () => {
8989
return {
9090
dispose,
9191
result: s.children(() => <>
92-
<Match partial on={value()} case={{
92+
<MatchTag partial on={value()} case={{
9393
foo: v => <>{v().foo}</>,
9494
}} />
9595
</>)
@@ -123,7 +123,7 @@ v.describe("Match", () => {
123123
return {
124124
dispose,
125125
result: s.children(() => <>
126-
<Match on={value()} case={{
126+
<MatchTag on={value()} case={{
127127
foo: v => <>{v().foo}</>,
128128
bar: v => <>{v().bar}</>,
129129
}} fallback={<>fallback</>} />
@@ -142,3 +142,64 @@ v.describe("Match", () => {
142142
data.dispose();
143143
});
144144
});
145+
146+
v.describe("MatchValue", () => {
147+
v.test("match on union literal", () => {
148+
type MyUnion = 'foo' | 'bar'
149+
const [value, setValue] = s.createSignal<MyUnion>()
150+
const data = s.createRoot(dispose => ({
151+
dispose,
152+
result: s.children(() => <>
153+
<MatchValue on={value()} case={{
154+
foo: () => <p>foo</p>,
155+
bar: () => <p>bar</p>,
156+
}} />
157+
</>)
158+
}))
159+
v.expect(data.result()).toEqual(undefined)
160+
setValue('foo')
161+
v.expect(data.result()).toEqual(<><p>foo</p></>)
162+
setValue('bar')
163+
v.expect(data.result()).toEqual(<><p>bar</p></>)
164+
data.dispose()
165+
})
166+
167+
v.test("partial match", () => {
168+
type MyUnion = 'foo' | 'bar'
169+
const [value, setValue] = s.createSignal<MyUnion>()
170+
const data = s.createRoot(dispose => ({
171+
dispose,
172+
result: s.children(() => <>
173+
<MatchValue partial on={value()} case={{
174+
foo: () => <p>foo</p>,
175+
}} />
176+
</>)
177+
}))
178+
v.expect(data.result()).toEqual(undefined)
179+
setValue('foo')
180+
v.expect(data.result()).toEqual(<><p>foo</p></>)
181+
setValue('bar')
182+
v.expect(data.result()).toEqual(undefined)
183+
data.dispose()
184+
})
185+
186+
v.test("fallback", () => {
187+
type MyUnion = 'foo' | 'bar'
188+
const [value, setValue] = s.createSignal<MyUnion>()
189+
const data = s.createRoot(dispose => ({
190+
dispose,
191+
result: s.children(() => <>
192+
<MatchValue on={value()} case={{
193+
foo: () => <p>foo</p>,
194+
bar: () => <p>bar</p>,
195+
}} fallback={<p>fallback</p>} />
196+
</>)
197+
}))
198+
v.expect(data.result()).toEqual(<><p>fallback</p></>)
199+
setValue('foo')
200+
v.expect(data.result()).toEqual(<><p>foo</p></>)
201+
setValue(undefined)
202+
v.expect(data.result()).toEqual(<><p>fallback</p></>)
203+
data.dispose()
204+
})
205+
})

0 commit comments

Comments
 (0)