/** * Form Demo Pattern * * Demonstrates the form buffering system with both create and edit modes: * - Create mode: Modal form with fresh data * - Edit mode: Modal form with existing data * - Cancel discards buffered changes * - Submit flushes all fields atomically * - Validation prevents invalid submissions */ import { action, computed, Default, equals, handler, ifElse, NAME, pattern, UI, type VNode, Writable, } from "commonfabric"; // ===== Types ===== export interface Person { name: string; email: string; role: "user" | "admin" | Default<"user">; } interface FormDemoInput { people: Writable>; } export interface FormDemoOutput { [NAME]: string; [UI]: VNode; people: Person[]; } const createEmptyPerson = (): Person => ({ name: "", email: "", role: "user", }); // Form submit handler - must be at module scope // cf-form flushes buffered values to bound cells before emitting cf-submit, // so handlers read from the cells directly (type-safe, no reconstruction needed) const handleFormSubmit = handler< unknown, { formData: Writable; people: Writable; editing: Writable<{ editing: Person | null }>; modalOpen: Writable; } >((_, { formData, people, editing, modalOpen }) => { const next = { ...formData.get() }; const target = editing.get().editing; if (target === null) { people.push(next); } else { const current = people.get(); const index = current.findIndex((p) => equals(p, target)); if (index >= 0) { const updated = [...current]; updated[index] = next; people.set(updated); } } modalOpen.set(false); editing.set({ editing: null }); }); export const EditPerson = pattern< { editing: Writable<{ editing: Person | null }>; formData: Writable; people: Writable; modalOpen: Writable; }, { [UI]: VNode } >( ({ editing, formData, people, modalOpen }) => { const isEditMode = computed(() => editing.get().editing !== null); // Cancel handler - close modal without saving const handleCancel = action(() => { modalOpen.set(false); editing.set({ editing: null }); // Form fields automatically reset via form.reset() }); return { [UI]: ( {/* Name field */} {/* Email field */} {/* Role field */} {/* Form actions */} Cancel {isEditMode ? "Save" : "Create"} ), }; }, ); // ===== Module-scope handlers ===== const startEdit = handler< unknown, { person: Person; people: Writable; editing: Writable<{ editing: Person | null }>; formData: Writable; modalOpen: Writable; } >((_event, { person, people, editing, formData, modalOpen }) => { const current = people.get(); const index = current.findIndex((p) => equals(p, person)); if (index >= 0) { formData.set({ ...current[index] }); editing.set({ editing: person }); modalOpen.set(true); } }); const deletePerson = handler< Event, { person: Person; people: Writable; } >((event, { person, people }) => { // Stop propagation to prevent card's onClick (startEdit) from firing event?.stopPropagation?.(); const current = people.get(); const filteredPeople = current.filter((p) => !equals(p, person)); people.set(filteredPeople); }); // ===== Pattern ===== export default pattern(({ people }) => { const editing = new Writable<{ editing: Person | null }>({ editing: null }); const formData = new Writable(createEmptyPerson()); const modalOpen = new Writable(false); // Computed values const peopleCount = computed(() => people.get().length); const isEditMode = computed(() => editing.get().editing !== null); const modalTitle = isEditMode ? "Edit Person" : "Add Person"; const showModal = computed(() => modalOpen.get()); // Open modal in create mode const startCreate = action(() => { formData.set(createEmptyPerson()); editing.set({ editing: null }); modalOpen.set(true); }); return { [NAME]: computed(() => `People Directory (${people.get().length})`), [UI]: ( {/* Header */} People Directory {peopleCount}{" "} {computed(() => people.get().length === 1 ? "person" : "people")} {/* Main content - list of people */} {people.map((person) => ( {person.name || "(unnamed)"} {person.email} {person.role} × ))} {ifElse( computed(() => people.get().length === 0), , null, )} {/* Footer - Add button */} + Add Person {/* Modal form */} { modalOpen.set(false); editing.set({ editing: null }); })} > {modalTitle} {ifElse( showModal, , null, )} ), people, }; });