Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/match-solid2-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@solid-primitives/match": major
---

Migrate to Solid.js v2.0 (beta.13)

## Breaking Changes

**Peer dependencies**: `solid-js@^2.0.0-beta.13` and `@solidjs/web@^2.0.0-beta.13` are now required.

- `JSX` types are now sourced from `@solidjs/web` per Solid 2.0 conventions
12 changes: 12 additions & 0 deletions packages/match/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# @solid-primitives/match

## 0.1.0

### Major Changes

Migrate to Solid.js v2.0 (beta.13)

**Peer dependencies**: `solid-js@^2.0.0-beta.13` and `@solidjs/web@^2.0.0-beta.13` are now required.

- `JSX` types are now sourced from `@solidjs/web` per Solid 2.0 conventions
- Signal writes are batched by default in Solid 2.0 — call `flush()` after signal writes in tests before reading reactive values
8 changes: 8 additions & 0 deletions packages/match/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ Use the `partial` prop to only handle some of the union members:
/>
```

> **Note:** `partial` is a TypeScript-only escape hatch — it switches the `case` mapped type from required to optional keys. It has no runtime effect; unmatched values fall through to `fallback` regardless of whether `partial` is set.

### Fallback

Provide a fallback element when no match is found or the value is `null`/`undefined`:
Expand Down Expand Up @@ -133,6 +135,8 @@ Use the `partial` prop to only handle some of the union members:
/>
```

> **Note:** `partial` is a TypeScript-only escape hatch — it has no runtime effect. See [`MatchTag` partial matching](#partial-matching) for details.

### Fallback

Provide a fallback element when no match is found or the value is `null`/`undefined`:
Expand All @@ -148,6 +152,10 @@ Provide a fallback element when no match is found or the value is `null`/`undefi
/>
```

## `MatchField` (deprecated)

`MatchField` is an alias for `MatchTag` kept for backwards compatibility. Use `MatchTag` in new code.

## Demo

[Deployed example](https://primitives.solidjs.community/playground/match) | [Source code](https://github.com/solidjs-community/solid-primitives/tree/main/packages/match/dev)
Expand Down
6 changes: 4 additions & 2 deletions packages/match/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@
"test:ssr": "pnpm run vitest --mode ssr"
},
"peerDependencies": {
"solid-js": "^1.6.12"
"@solidjs/web": "^2.0.0-beta.13",
"solid-js": "^2.0.0-beta.13"
},
"devDependencies": {
"solid-js": "^1.9.7"
"@solidjs/web": "2.0.0-beta.13",
"solid-js": "2.0.0-beta.13"
}
}
34 changes: 24 additions & 10 deletions packages/match/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type Accessor, type JSX, createMemo } from "solid-js";
import { type Accessor, createMemo } from "solid-js";
import type { JSX } from "@solidjs/web";

/**
* Control-flow component for matching discriminated union (tagged union) members.
Expand All @@ -15,42 +16,53 @@ import { type Accessor, type JSX, createMemo } from "solid-js";
*
* const [value, setValue] = createSignal<MyUnion>({type: 'foo', foo: 'foo-value'})
*
* <Match on={value()} case={{
* <MatchTag on={value()} case={{
* foo: v => <>{v().foo}</>,
* bar: v => <>{v().bar}</>,
* }} />
* ```
*/
export function MatchTag<T extends { [k in Tag]: PropertyKey }, Tag extends PropertyKey>(props: {
// Tag values are constrained to string | number rather than PropertyKey because symbol keys
// cannot be expressed in inline object literals, making symbol-tagged unions impossible to
// match in practice. The narrower constraint surfaces the error at the tag field rather than
// inside the mapped type where it is harder to diagnose.
export function MatchTag<T extends { [k in Tag]: string | number }, Tag extends string | number>(props: {
on: T | null | undefined;
tag: Tag;
case: { [K in T[Tag]]: (v: Accessor<T & { [k in Tag]: K }>) => JSX.Element };
fallback?: JSX.Element;
/** Type-only — no runtime effect. Relaxes `case` to allow partial union coverage. */
partial?: false;
}): JSX.Element;
export function MatchTag<T extends { type: PropertyKey }>(props: {
export function MatchTag<T extends { type: string | number }>(props: {
on: T | null | undefined;
case: { [K in T["type"]]: (v: Accessor<T & { [k in "type"]: K }>) => JSX.Element };
fallback?: JSX.Element;
/** Type-only — no runtime effect. Relaxes `case` to allow partial union coverage. */
partial?: false;
}): JSX.Element;
export function MatchTag<T extends { [k in Tag]: PropertyKey }, Tag extends PropertyKey>(props: {
export function MatchTag<T extends { [k in Tag]: string | number }, Tag extends string | number>(props: {
on: T | null | undefined;
tag: Tag;
case: { [K in T[Tag]]?: (v: Accessor<T & { [k in Tag]: K }>) => JSX.Element };
fallback?: JSX.Element;
/** Type-only — no runtime effect. Relaxes `case` to allow partial union coverage. */
partial: true;
}): JSX.Element;
export function MatchTag<T extends { type: PropertyKey }>(props: {
export function MatchTag<T extends { type: string | number }>(props: {
on: T | null | undefined;
case: { [K in T["type"]]?: (v: Accessor<T & { [k in "type"]: K }>) => JSX.Element };
fallback?: JSX.Element;
/** Type-only — no runtime effect. Relaxes `case` to allow partial union coverage. */
partial: true;
}): JSX.Element;
export function MatchTag(props: any): any {
const value = () => props.on;
const kind = createMemo(() => props.on?.[props.tag ?? "type"]);
return createMemo(() => props.case[kind()]?.(() => props.on) ?? props.fallback);
return createMemo(() => props.case[kind()]?.(value) ?? props.fallback);
}

/** @deprecated Use {@link MatchTag} instead. */
export { MatchTag as MatchField };

/**
Expand All @@ -62,22 +74,24 @@ export { MatchTag as MatchField };
*
* const [value, setValue] = createSignal<MyUnion>('foo')
*
* <Match on={value()} case={{
* <MatchValue on={value()} case={{
* foo: () => <p>foo</p>,
* bar: () => <p>bar</p>,
* }} />
* ```
*/
export function MatchValue<T extends PropertyKey>(props: {
export function MatchValue<T extends string | number>(props: {
on: T | null | undefined;
case: { [K in T]: (v: K) => JSX.Element };
fallback?: JSX.Element;
/** Type-only — no runtime effect. Relaxes `case` to allow partial union coverage. */
partial?: false;
}): JSX.Element;
export function MatchValue<T extends PropertyKey>(props: {
export function MatchValue<T extends string | number>(props: {
on: T | null | undefined;
case: { [K in T]?: (v: K) => JSX.Element };
fallback?: JSX.Element;
/** Type-only — no runtime effect. Relaxes `case` to allow partial union coverage. */
partial: true;
}): JSX.Element;
export function MatchValue(props: any): any {
Expand Down
20 changes: 20 additions & 0 deletions packages/match/test/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ v.describe("Match", () => {
v.expect(data.result()).toEqual(undefined);

setValue({ type: "foo", foo: "foo-value" });
s.flush();
v.expect(data.result()).toEqual(<>foo-value</>);

setValue({ type: "bar", bar: "bar-value" });
s.flush();
v.expect(data.result()).toEqual(<>bar-value</>);

data.dispose();
Expand Down Expand Up @@ -78,9 +80,11 @@ v.describe("Match", () => {
v.expect(data.result()).toEqual(undefined);

setValue({ kind: "foo", foo: "foo-value" });
s.flush();
v.expect(data.result()).toEqual(<>foo-value</>);

setValue({ kind: "bar", bar: "bar-value" });
s.flush();
v.expect(data.result()).toEqual(<>bar-value</>);

data.dispose();
Expand Down Expand Up @@ -119,9 +123,11 @@ v.describe("Match", () => {
v.expect(data.result()).toEqual(undefined);

setValue({ type: "foo", foo: "foo-value" });
s.flush();
v.expect(data.result()).toEqual(<>foo-value</>);

setValue({ type: "bar", bar: "bar-value" });
s.flush();
v.expect(data.result()).toEqual(undefined);

data.dispose();
Expand Down Expand Up @@ -161,9 +167,11 @@ v.describe("Match", () => {
v.expect(data.result()).toEqual(<>fallback</>);

setValue({ type: "foo", foo: "foo-value" });
s.flush();
v.expect(data.result()).toEqual(<>foo-value</>);

setValue(undefined);
s.flush();
v.expect(data.result()).toEqual(<>fallback</>);

data.dispose();
Expand All @@ -189,13 +197,17 @@ v.describe("MatchValue", () => {
)),
}));
v.expect(data.result()).toEqual(undefined);

setValue("foo");
s.flush();
v.expect(data.result()).toEqual(
<>
<p>foo</p>
</>,
);

setValue("bar");
s.flush();
v.expect(data.result()).toEqual(
<>
<p>bar</p>
Expand All @@ -222,13 +234,17 @@ v.describe("MatchValue", () => {
)),
}));
v.expect(data.result()).toEqual(undefined);

setValue("foo");
s.flush();
v.expect(data.result()).toEqual(
<>
<p>foo</p>
</>,
);

setValue("bar");
s.flush();
v.expect(data.result()).toEqual(undefined);
data.dispose();
});
Expand Down Expand Up @@ -256,13 +272,17 @@ v.describe("MatchValue", () => {
<p>fallback</p>
</>,
);

setValue("foo");
s.flush();
v.expect(data.result()).toEqual(
<>
<p>foo</p>
</>,
);

setValue(undefined);
s.flush();
v.expect(data.result()).toEqual(
<>
<p>fallback</p>
Expand Down
7 changes: 5 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.