Skip to content
Merged
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
14 changes: 14 additions & 0 deletions .changeset/brave-bushes-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@buildnbuzz/buzzform": patch
---

**Features**
- Added **Output Configuration** support to transform form data into flat path-delimited keys (e.g., `person.address.street`).
- Introduced `output` configuration to `FormProvider` and `useForm` for global or per-form control of submission data shape.

**Internal Changes**
- Implemented `transformFormOutput` utility for flattening nested form objects into delimited path keys.
- Updated `useForm` to automatically apply output transformation to `getValues()`, `watch()`, and `onSubmit` data.

**Types**
- Exported new `OutputConfig`, `OutputType`, and `PathDelimiter` types.
2 changes: 1 addition & 1 deletion apps/web/.source/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ const create = browser<typeof Config, import("fumadocs-mdx/runtime/types").Inter
}
}>();
const browserCollections = {
docs: create.doc("docs", {"conditional-logic.mdx": () => import("../content/docs/conditional-logic.mdx?collection=docs"), "configuration.mdx": () => import("../content/docs/configuration.mdx?collection=docs"), "custom-rendering.mdx": () => import("../content/docs/custom-rendering.mdx?collection=docs"), "form-component.mdx": () => import("../content/docs/form-component.mdx?collection=docs"), "index.mdx": () => import("../content/docs/index.mdx?collection=docs"), "installation.mdx": () => import("../content/docs/installation.mdx?collection=docs"), "quick-start.mdx": () => import("../content/docs/quick-start.mdx?collection=docs"), "schema.mdx": () => import("../content/docs/schema.mdx?collection=docs"), "validation.mdx": () => import("../content/docs/validation.mdx?collection=docs"), "your-first-form.mdx": () => import("../content/docs/your-first-form.mdx?collection=docs"), "fields/types.mdx": () => import("../content/docs/fields/types.mdx?collection=docs"), "fields/layout/array.mdx": () => import("../content/docs/fields/layout/array.mdx?collection=docs"), "fields/layout/collapsible.mdx": () => import("../content/docs/fields/layout/collapsible.mdx?collection=docs"), "fields/layout/group.mdx": () => import("../content/docs/fields/layout/group.mdx?collection=docs"), "fields/layout/row.mdx": () => import("../content/docs/fields/layout/row.mdx?collection=docs"), "fields/layout/tabs.mdx": () => import("../content/docs/fields/layout/tabs.mdx?collection=docs"), "fields/data/checkbox-group.mdx": () => import("../content/docs/fields/data/checkbox-group.mdx?collection=docs"), "fields/data/checkbox.mdx": () => import("../content/docs/fields/data/checkbox.mdx?collection=docs"), "fields/data/date.mdx": () => import("../content/docs/fields/data/date.mdx?collection=docs"), "fields/data/number.mdx": () => import("../content/docs/fields/data/number.mdx?collection=docs"), "fields/data/password.mdx": () => import("../content/docs/fields/data/password.mdx?collection=docs"), "fields/data/radio.mdx": () => import("../content/docs/fields/data/radio.mdx?collection=docs"), "fields/data/select.mdx": () => import("../content/docs/fields/data/select.mdx?collection=docs"), "fields/data/switch.mdx": () => import("../content/docs/fields/data/switch.mdx?collection=docs"), "fields/data/tags.mdx": () => import("../content/docs/fields/data/tags.mdx?collection=docs"), "fields/data/text.mdx": () => import("../content/docs/fields/data/text.mdx?collection=docs"), "fields/data/textarea.mdx": () => import("../content/docs/fields/data/textarea.mdx?collection=docs"), "fields/data/upload.mdx": () => import("../content/docs/fields/data/upload.mdx?collection=docs"), }),
docs: create.doc("docs", {"conditional-logic.mdx": () => import("../content/docs/conditional-logic.mdx?collection=docs"), "configuration.mdx": () => import("../content/docs/configuration.mdx?collection=docs"), "custom-rendering.mdx": () => import("../content/docs/custom-rendering.mdx?collection=docs"), "form-component.mdx": () => import("../content/docs/form-component.mdx?collection=docs"), "index.mdx": () => import("../content/docs/index.mdx?collection=docs"), "installation.mdx": () => import("../content/docs/installation.mdx?collection=docs"), "output-configuration.mdx": () => import("../content/docs/output-configuration.mdx?collection=docs"), "quick-start.mdx": () => import("../content/docs/quick-start.mdx?collection=docs"), "schema.mdx": () => import("../content/docs/schema.mdx?collection=docs"), "validation.mdx": () => import("../content/docs/validation.mdx?collection=docs"), "your-first-form.mdx": () => import("../content/docs/your-first-form.mdx?collection=docs"), "fields/types.mdx": () => import("../content/docs/fields/types.mdx?collection=docs"), "fields/data/checkbox-group.mdx": () => import("../content/docs/fields/data/checkbox-group.mdx?collection=docs"), "fields/data/checkbox.mdx": () => import("../content/docs/fields/data/checkbox.mdx?collection=docs"), "fields/data/date.mdx": () => import("../content/docs/fields/data/date.mdx?collection=docs"), "fields/data/number.mdx": () => import("../content/docs/fields/data/number.mdx?collection=docs"), "fields/data/password.mdx": () => import("../content/docs/fields/data/password.mdx?collection=docs"), "fields/data/radio.mdx": () => import("../content/docs/fields/data/radio.mdx?collection=docs"), "fields/data/select.mdx": () => import("../content/docs/fields/data/select.mdx?collection=docs"), "fields/data/switch.mdx": () => import("../content/docs/fields/data/switch.mdx?collection=docs"), "fields/data/tags.mdx": () => import("../content/docs/fields/data/tags.mdx?collection=docs"), "fields/data/text.mdx": () => import("../content/docs/fields/data/text.mdx?collection=docs"), "fields/data/textarea.mdx": () => import("../content/docs/fields/data/textarea.mdx?collection=docs"), "fields/data/upload.mdx": () => import("../content/docs/fields/data/upload.mdx?collection=docs"), "fields/layout/array.mdx": () => import("../content/docs/fields/layout/array.mdx?collection=docs"), "fields/layout/collapsible.mdx": () => import("../content/docs/fields/layout/collapsible.mdx?collection=docs"), "fields/layout/group.mdx": () => import("../content/docs/fields/layout/group.mdx?collection=docs"), "fields/layout/row.mdx": () => import("../content/docs/fields/layout/row.mdx?collection=docs"), "fields/layout/tabs.mdx": () => import("../content/docs/fields/layout/tabs.mdx?collection=docs"), }),
};
export default browserCollections;
47 changes: 24 additions & 23 deletions apps/web/.source/server.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
// @ts-nocheck
import * as __fd_glob_31 from "../content/docs/fields/data/upload.mdx?collection=docs"
import * as __fd_glob_30 from "../content/docs/fields/data/textarea.mdx?collection=docs"
import * as __fd_glob_29 from "../content/docs/fields/data/text.mdx?collection=docs"
import * as __fd_glob_28 from "../content/docs/fields/data/tags.mdx?collection=docs"
import * as __fd_glob_27 from "../content/docs/fields/data/switch.mdx?collection=docs"
import * as __fd_glob_26 from "../content/docs/fields/data/select.mdx?collection=docs"
import * as __fd_glob_25 from "../content/docs/fields/data/radio.mdx?collection=docs"
import * as __fd_glob_24 from "../content/docs/fields/data/password.mdx?collection=docs"
import * as __fd_glob_23 from "../content/docs/fields/data/number.mdx?collection=docs"
import * as __fd_glob_22 from "../content/docs/fields/data/date.mdx?collection=docs"
import * as __fd_glob_21 from "../content/docs/fields/data/checkbox.mdx?collection=docs"
import * as __fd_glob_20 from "../content/docs/fields/data/checkbox-group.mdx?collection=docs"
import * as __fd_glob_19 from "../content/docs/fields/layout/tabs.mdx?collection=docs"
import * as __fd_glob_18 from "../content/docs/fields/layout/row.mdx?collection=docs"
import * as __fd_glob_17 from "../content/docs/fields/layout/group.mdx?collection=docs"
import * as __fd_glob_16 from "../content/docs/fields/layout/collapsible.mdx?collection=docs"
import * as __fd_glob_15 from "../content/docs/fields/layout/array.mdx?collection=docs"
import * as __fd_glob_14 from "../content/docs/fields/types.mdx?collection=docs"
import * as __fd_glob_13 from "../content/docs/your-first-form.mdx?collection=docs"
import * as __fd_glob_12 from "../content/docs/validation.mdx?collection=docs"
import * as __fd_glob_11 from "../content/docs/schema.mdx?collection=docs"
import * as __fd_glob_10 from "../content/docs/quick-start.mdx?collection=docs"
import * as __fd_glob_32 from "../content/docs/fields/layout/tabs.mdx?collection=docs"
import * as __fd_glob_31 from "../content/docs/fields/layout/row.mdx?collection=docs"
import * as __fd_glob_30 from "../content/docs/fields/layout/group.mdx?collection=docs"
import * as __fd_glob_29 from "../content/docs/fields/layout/collapsible.mdx?collection=docs"
import * as __fd_glob_28 from "../content/docs/fields/layout/array.mdx?collection=docs"
import * as __fd_glob_27 from "../content/docs/fields/data/upload.mdx?collection=docs"
import * as __fd_glob_26 from "../content/docs/fields/data/textarea.mdx?collection=docs"
import * as __fd_glob_25 from "../content/docs/fields/data/text.mdx?collection=docs"
import * as __fd_glob_24 from "../content/docs/fields/data/tags.mdx?collection=docs"
import * as __fd_glob_23 from "../content/docs/fields/data/switch.mdx?collection=docs"
import * as __fd_glob_22 from "../content/docs/fields/data/select.mdx?collection=docs"
import * as __fd_glob_21 from "../content/docs/fields/data/radio.mdx?collection=docs"
import * as __fd_glob_20 from "../content/docs/fields/data/password.mdx?collection=docs"
import * as __fd_glob_19 from "../content/docs/fields/data/number.mdx?collection=docs"
import * as __fd_glob_18 from "../content/docs/fields/data/date.mdx?collection=docs"
import * as __fd_glob_17 from "../content/docs/fields/data/checkbox.mdx?collection=docs"
import * as __fd_glob_16 from "../content/docs/fields/data/checkbox-group.mdx?collection=docs"
import * as __fd_glob_15 from "../content/docs/fields/types.mdx?collection=docs"
import * as __fd_glob_14 from "../content/docs/your-first-form.mdx?collection=docs"
import * as __fd_glob_13 from "../content/docs/validation.mdx?collection=docs"
import * as __fd_glob_12 from "../content/docs/schema.mdx?collection=docs"
import * as __fd_glob_11 from "../content/docs/quick-start.mdx?collection=docs"
import * as __fd_glob_10 from "../content/docs/output-configuration.mdx?collection=docs"
import * as __fd_glob_9 from "../content/docs/installation.mdx?collection=docs"
import * as __fd_glob_8 from "../content/docs/index.mdx?collection=docs"
import * as __fd_glob_7 from "../content/docs/form-component.mdx?collection=docs"
Expand All @@ -39,4 +40,4 @@ const create = server<typeof Config, import("fumadocs-mdx/runtime/types").Intern
}
}>({"doc":{"passthroughs":["extractedReferences"]}});

export const docs = await create.docs("docs", "content/docs", {"meta.json": __fd_glob_0, "fields/meta.json": __fd_glob_1, "fields/data/meta.json": __fd_glob_2, "fields/layout/meta.json": __fd_glob_3, }, {"conditional-logic.mdx": __fd_glob_4, "configuration.mdx": __fd_glob_5, "custom-rendering.mdx": __fd_glob_6, "form-component.mdx": __fd_glob_7, "index.mdx": __fd_glob_8, "installation.mdx": __fd_glob_9, "quick-start.mdx": __fd_glob_10, "schema.mdx": __fd_glob_11, "validation.mdx": __fd_glob_12, "your-first-form.mdx": __fd_glob_13, "fields/types.mdx": __fd_glob_14, "fields/layout/array.mdx": __fd_glob_15, "fields/layout/collapsible.mdx": __fd_glob_16, "fields/layout/group.mdx": __fd_glob_17, "fields/layout/row.mdx": __fd_glob_18, "fields/layout/tabs.mdx": __fd_glob_19, "fields/data/checkbox-group.mdx": __fd_glob_20, "fields/data/checkbox.mdx": __fd_glob_21, "fields/data/date.mdx": __fd_glob_22, "fields/data/number.mdx": __fd_glob_23, "fields/data/password.mdx": __fd_glob_24, "fields/data/radio.mdx": __fd_glob_25, "fields/data/select.mdx": __fd_glob_26, "fields/data/switch.mdx": __fd_glob_27, "fields/data/tags.mdx": __fd_glob_28, "fields/data/text.mdx": __fd_glob_29, "fields/data/textarea.mdx": __fd_glob_30, "fields/data/upload.mdx": __fd_glob_31, });
export const docs = await create.docs("docs", "content/docs", {"meta.json": __fd_glob_0, "fields/meta.json": __fd_glob_1, "fields/data/meta.json": __fd_glob_2, "fields/layout/meta.json": __fd_glob_3, }, {"conditional-logic.mdx": __fd_glob_4, "configuration.mdx": __fd_glob_5, "custom-rendering.mdx": __fd_glob_6, "form-component.mdx": __fd_glob_7, "index.mdx": __fd_glob_8, "installation.mdx": __fd_glob_9, "output-configuration.mdx": __fd_glob_10, "quick-start.mdx": __fd_glob_11, "schema.mdx": __fd_glob_12, "validation.mdx": __fd_glob_13, "your-first-form.mdx": __fd_glob_14, "fields/types.mdx": __fd_glob_15, "fields/data/checkbox-group.mdx": __fd_glob_16, "fields/data/checkbox.mdx": __fd_glob_17, "fields/data/date.mdx": __fd_glob_18, "fields/data/number.mdx": __fd_glob_19, "fields/data/password.mdx": __fd_glob_20, "fields/data/radio.mdx": __fd_glob_21, "fields/data/select.mdx": __fd_glob_22, "fields/data/switch.mdx": __fd_glob_23, "fields/data/tags.mdx": __fd_glob_24, "fields/data/text.mdx": __fd_glob_25, "fields/data/textarea.mdx": __fd_glob_26, "fields/data/upload.mdx": __fd_glob_27, "fields/layout/array.mdx": __fd_glob_28, "fields/layout/collapsible.mdx": __fd_glob_29, "fields/layout/group.mdx": __fd_glob_30, "fields/layout/row.mdx": __fd_glob_31, "fields/layout/tabs.mdx": __fd_glob_32, });
2 changes: 2 additions & 0 deletions apps/web/app/(builder)/components/builder-form-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function BuilderFormProvider({
}: BuilderFormProviderProps) {
const nodes = useBuilderStore((s) => s.nodes);
const rootIds = useBuilderStore((s) => s.rootIds);
const outputConfig = useBuilderStore((s) => s.outputConfig);

// Memoize derivation pipeline to prevent recomputation on unrelated store updates
const fields = React.useMemo(
Expand Down Expand Up @@ -76,6 +77,7 @@ export function BuilderFormProvider({
fields={fields}
defaultValues={defaultValues}
onSubmit={onSubmit ?? (() => {})}
output={outputConfig}
mode="onBlur"
showSubmit={false}
>
Expand Down
46 changes: 39 additions & 7 deletions apps/web/app/(builder)/components/canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useRef, useEffect } from "react";
import { useRef, useEffect, useState } from "react";
import { useDroppable } from "@dnd-kit/core";
import { useBuilderStore } from "../lib/store";
import { useBuilderKeyboardShortcuts } from "../lib/use-keyboard-shortcuts";
Expand All @@ -17,20 +17,21 @@ import {
EmptyTitle,
} from "@/components/ui/empty";
import { HugeiconsIcon } from "@hugeicons/react";
import { DragDropIcon } from "@hugeicons/core-free-icons";
import {
DragDropIcon,
Copy01Icon,
Tick01Icon,
} from "@hugeicons/core-free-icons";
import { PreviewForm } from "./preview/preview-form";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";

export function Canvas() {
useBuilderKeyboardShortcuts();
const onSubmit = async (data: Record<string, unknown>) => {
await new Promise((r) => setTimeout(r, 500));
toast("Form Submitted!", {
description: (
<pre className="mt-2 max-h-72 overflow-auto rounded-md bg-zinc-950 p-3 text-xs text-zinc-100">
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
),
description: <SubmitToastContent data={data} />,
duration: 10000,
});
};
Expand Down Expand Up @@ -135,3 +136,34 @@ const EmptyCanvas = () => {
</Empty>
);
};

function SubmitToastContent({ data }: { data: Record<string, unknown> }) {
const [copied, setCopied] = useState(false);

const handleCopy = () => {
navigator.clipboard.writeText(JSON.stringify(data, null, 2));
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};

return (
<div className="relative mt-2">
<Button
variant="ghost"
size="icon"
onClick={handleCopy}
className="absolute right-2 top-2 h-7 w-7"
title="Copy JSON"
>
{copied ? (
<HugeiconsIcon icon={Tick01Icon} size={14} className="text-primary" />
) : (
<HugeiconsIcon icon={Copy01Icon} size={14} />
)}
</Button>
<pre className="max-h-72 overflow-auto rounded-md bg-muted p-3 pt-6 text-xs text-muted-foreground sm:p-3 sm:pr-10 sm:pt-3">
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
</div>
);
}
5 changes: 3 additions & 2 deletions apps/web/app/(builder)/components/export-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export function ExportSheet() {
const rootIds = useBuilderStore((state) => state.rootIds);
const formId = useBuilderStore((state) => state.formId);
const formName = useBuilderStore((state) => state.formName);
const outputConfig = useBuilderStore((state) => state.outputConfig);

React.useEffect(() => {
if (!open) return;
Expand All @@ -65,8 +66,8 @@ export function ExportSheet() {

const componentCode = React.useMemo(() => {
if (!open) return "";
return generateComponentCode(nodes, rootIds, formName);
}, [open, nodes, rootIds, formName]);
return generateComponentCode(nodes, rootIds, formName, outputConfig);
}, [open, nodes, rootIds, formName, outputConfig]);

const schemaJson = React.useMemo(() => {
if (!open) return "";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"use client";

import * as React from "react";
import { useEffect, useMemo, useRef } from "react";
import {
createSchema,
useForm,
type FormAdapter,
type OutputConfig,
type PathDelimiter,
} from "@buildnbuzz/buzzform";
import { useBuilderStore } from "../../lib/store";
import { formSettingsProperties } from "../../lib/properties/form";
import { RenderFields } from "@/components/buzzform/fields/render";
import { FieldGroup } from "@/components/ui/field";
import { unflattenFormValues } from "../../lib/properties";

export function GlobalSettingsForm() {
const outputConfig = useBuilderStore((s) => s.outputConfig);
const updateFormSettings = useBuilderStore((s) => s.updateFormSettings);

const schema = useMemo(() => createSchema(formSettingsProperties), []);

const defaultValues = useMemo(() => {
return {
outputConfig: {
type: outputConfig?.type ?? "default",
delimiter: outputConfig?.delimiter ?? ".",
},
};
}, [outputConfig]);

const form = useForm({
schema,
defaultValues,
mode: "onBlur",
output: undefined,
});

const currentValues = form.watch() as Record<string, unknown>;

// Update store when form changes
useEffect(() => {
const nested = unflattenFormValues(currentValues);
const formOutputConfig = nested.outputConfig as
| { type: string; delimiter?: string }
| undefined;

const storeValue: OutputConfig | undefined =
formOutputConfig?.type === "path"
? {
type: "path",
delimiter: (formOutputConfig.delimiter as PathDelimiter) ?? ".",
}
: undefined;

// Compare with current store value
const currentStoreJson = JSON.stringify(outputConfig);
const newStoreJson = JSON.stringify(storeValue);

if (currentStoreJson !== newStoreJson) {
updateFormSettings({ outputConfig: storeValue });
}
}, [currentValues, outputConfig, updateFormSettings]);

// Sync form when store changes externally (undo/redo)
const prevOutputConfigRef = useRef(outputConfig);
useEffect(() => {
const prevJson = JSON.stringify(prevOutputConfigRef.current);
const currentJson = JSON.stringify(outputConfig);

if (prevJson !== currentJson) {
prevOutputConfigRef.current = outputConfig;

const nextValues = {
outputConfig: {
type: outputConfig?.type ?? "default",
delimiter: outputConfig?.delimiter ?? ".",
},
};

form.reset(nextValues);
}
}, [outputConfig, form]);

return (
<div className="px-4 py-2">
<form onSubmit={(e) => e.preventDefault()}>
<FieldGroup className="gap-2">
<RenderFields
fields={schema.fields}
form={form as unknown as FormAdapter}
/>
</FieldGroup>
</form>
</div>
);
}
Loading
Loading