Hierarchical Combobox
import { storeApi as createStoreApi, defineStates, effect, eventApi, matchina, setup } from "matchina";import { nestedHsmRoot, submachine } from "matchina/hsm";import { createComboboxStore } from "../store";
export function createComboboxMachine() { const store = createComboboxStore(); const storeApi = createStoreApi(store);
const rootMachine = matchina( defineStates({ Inactive: undefined, // Define the Child Machine Active: submachine( () => matchina( defineStates({ Empty: undefined, Suggesting: undefined, }), // Child machine transitions { Empty: { type: "Suggesting", select: "Empty", }, Suggesting: { select: "Empty", type: "Suggesting", }, }, "Empty" ) ), }), // Parent machine transitions { Inactive: { focus: "Active" }, Active: { blur: "Inactive", }, }, "Inactive" );
const hsm = nestedHsmRoot(rootMachine);
// Effects coordinate machine transitions with store updates setup(hsm)( effect((ev) => { // HSM emits 'child.change' pseudo-event for child state changes // Use type assertion since it's a runtime HSM feature not in static types (ev.match as any)({ focus: storeApi.clear, blur: storeApi.clear, "child.change": (data: { target: any; type: string; params: any[] }) => { const target = data?.target; if (target && typeof target.getChange === 'function') { const change = target.getChange(); change.match({ type: storeApi.setInput, select: storeApi.selectHighlighted, }, false); } }, }, false); }) );
const combobox = Object.assign(hsm, { model: store, ...storeApi, ...eventApi(hsm), select: () => { storeApi.selectHighlighted(); hsm.send("select"); }, });
return combobox;}
export type NestedComboboxMachine = ReturnType<typeof createComboboxMachine>;import { useRef, useEffect } from "react";import { setup, effect } from "matchina";import { useMachine } from "matchina/react";import { ComboboxView } from "../../combobox-common/ComboboxView";import type { NestedComboboxMachine } from "./machine";
interface ComboboxViewProps { machine: NestedComboboxMachine;}
export function ComboboxViewNested({ machine }: ComboboxViewProps) { useMachine(machine); useMachine(machine.model); const { input, selectedTags, suggestions, highlightedIndex } = machine.model.getState(); const searchRef = useRef<HTMLInputElement>(null);
const state = machine.getState(); const isActive = state.key !== "Inactive"; useEffect(() => setup(machine)( effect((ev: any) => { if (ev.type === "focus") searchRef.current?.focus(); if (ev.type === "blur") searchRef.current?.blur(); }) ), [machine] );
return ( <ComboboxView stateKey={state.key} isActive={isActive} input={input} selectedTags={selectedTags} suggestions={suggestions} highlightedIndex={highlightedIndex} searchRef={searchRef} onFocus={() => machine.send("focus")} onBlur={() => machine.send("blur")} onType={(v) => machine.send("type", v)} onSelect={(i) => { machine.setHighlighted(i); machine.select(); }} onAdd={(tag) => machine.addTag(tag)} onRemove={(tag) => machine.removeTag(tag)} onHighlight={(dir) => machine.highlight(dir)} /> );}import { useMemo, useState } from "react";import { createFlatComboboxMachine } from "./flattened/machine";import { createComboboxMachine } from "./nested/machine";import { ComboboxViewNested } from "./nested/ComboboxView";import { ComboboxViewFlat } from "./flattened/ComboboxView";import { MachineVisualizer } from "@components/MachineVisualizer";
type Mode = "flat" | "nested";
export default function HSMComboboxIndex() { const [mode, setMode] = useState<Mode>("flat");
// Re-create machine when mode changes const { flatMachine, hierarchicalMachine } = useMemo(() => { return { flatMachine: createFlatComboboxMachine(), hierarchicalMachine: createComboboxMachine(), }; }, [mode]);
const machine = mode === "flat" ? flatMachine : hierarchicalMachine;
return ( <div className="space-y-6"> {/* Mode Toggle - Sticky below header */} <div className="flex justify-center mb-6 sticky top-16 z-10 py-2 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm -mx-4 px-4"> <div className="inline-flex bg-gray-100 dark:bg-gray-800 p-1 rounded-lg"> <button onClick={() => setMode("flat")} className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${ mode === "flat" ? "bg-white dark:bg-gray-700 text-blue-600 dark:text-blue-400 shadow-sm" : "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" }`} > Flattened </button> <button onClick={() => setMode("nested")} className={`px-4 py-2 rounded text-sm font-medium transition-colors ${ mode === "nested" ? "bg-white dark:bg-gray-700 text-blue-600 dark:text-blue-400 shadow-sm" : "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" }`} > Nested (Hierarchical) </button> </div> </div>
{mode === "flat" ? ( <ComboboxViewFlat machine={flatMachine} /> ) : ( <ComboboxViewNested machine={hierarchicalMachine} /> )}
<MachineVisualizer key={mode} // Force re-mount of visualizer when mode changes machine={machine} title={`HSM Combobox (${mode === "flat" ? "Flattened" : "Nested"})`} interactive={true} layout="stacked" /> </div> );}