Skip to content

Commit

Permalink
parseFormData (#129)
Browse files Browse the repository at this point in the history
* wip

* overhaul

* fixes

* fix

* use key to parse type

* fix build

* fix fix fix

* fix?

* fix build

* safe return

* .

* ?

* revert

* revert unstable_Form to not depend on EventKeys and parseFormData

* fix

* nit

---------

Co-authored-by: Julian Benegas <[email protected]>
  • Loading branch information
justkahdri and julianbenegas authored Jan 3, 2025
1 parent db17479 commit c826c61
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 7 deletions.
125 changes: 124 additions & 1 deletion packages/basehub/src/events/primitive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
// eslint-disable-next-line import/no-unresolved
} from "../index";
import type { ResolvedRef } from "../common-types";
import type { Field } from "../react/form/primitive";

/* -------------------------------------------------------------------------------------------------
* Client
Expand Down Expand Up @@ -42,7 +43,7 @@ type ExtractEventKey<T extends string> = T extends `${infer Base}:${string}`
: T;

// Get all event key types (bshb_event_*)
type EventKeys = KeysStartingWith<Scalars, "bshb_event">;
export type EventKeys = KeysStartingWith<Scalars, "bshb_event">;

// Map from event key to its schema type
type EventSchemaMap = {
Expand Down Expand Up @@ -278,3 +279,125 @@ export async function deleteEvent<Key extends `${EventKeys}:${string}`>(
| { success: true }
| { success: false; error: string };
}

// PARSE FORM DATA HELPER ------------------------------------------------------------------------
type SafeReturn<T> =
| { success: true; data: T }
| { success: false; errors: Record<string, string> };

export function parseFormData<
Key extends `${EventKeys}:${string}`,
Schema extends Field[],
>(
key: Key,
schema: Schema,
formData: FormData
): SafeReturn<EventSchemaMap[ExtractEventKey<Key>]> {
const formattedData: Record<string, unknown> = {};
const errors: Record<string, string> = {};

schema.forEach((field) => {
const key = field.name;

// Handle multiple values (like multiple select or checkboxes)
if ((field.type === "select" || field.type === "radio") && field.multiple) {
const values = formData.getAll(key).filter(Boolean);

if (field.required && values.length === 0) {
errors[key] = `${field.label || key} is required`;
}

formattedData[key] = values.map(String);
return;
}

const value = formData.get(key);

// Required field validation
if (field.required && (value === null || value === "")) {
errors[key] = `${field.label || key} is required`;
return;
}

// Handle empty optional fields
if (value === null || value === "") {
formattedData[key] = field.defaultValue ?? null;
return;
}

try {
switch (field.type) {
case "checkbox":
formattedData[key] = value === "on" || value === "true";
break;

case "email": {
const email = String(value);
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
errors[key] = `${field.label || key} must be a valid email address`;
}
formattedData[key] = email;
break;
}

case "select":
case "radio": {
const stringValue = String(value);
if (field.options.length && !field.options.includes(stringValue)) {
errors[key] = `${
field.label || key
} must be one of the available options`;
}
formattedData[key] = stringValue;
break;
}

case "date":
case "datetime": {
const date = new Date(value as string);
if (isNaN(date.getTime())) {
errors[key] = `${field.label || key} must be a valid date`;
break;
}
formattedData[key] = date.toISOString();
break;
}

case "number": {
const num = Number(value);
if (isNaN(num)) {
errors[key] = `${field.label || key} must be a valid number`;
break;
}
formattedData[key] = num;
break;
}

case "file": {
const file = value as File;
if (!(file instanceof File)) {
errors[key] = `${field.label || key} must be a valid file`;
break;
}
formattedData[key] = file;
break;
}

default:
formattedData[key] = String(value);
}
} catch (error) {
errors[key] = `Invalid value for ${field.label || key}`;
}
});

if (Object.keys(errors).length > 0) {
return { success: false, errors };
}

return {
data: formattedData as EventSchemaMap[ExtractEventKey<Key>],
success: true,
};
}
14 changes: 8 additions & 6 deletions packages/basehub/src/react/form/primitive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export type Field = {
| { type: "radio"; options: string[]; multiple: boolean }
);

// FORM COMPONENT ------------------------------------------------------------------------

type Handlers = {
text: (props: Extract<Field, { type: "text" }>) => ReactNode;
textarea: (props: Extract<Field, { type: "textarea" }>) => ReactNode;
Expand All @@ -51,19 +53,19 @@ export type HandlerProps<Key extends keyof Handlers> = ExtractPropsForHandler<
type CustomBlockBase = { readonly __typename: string };
export type CustomBlocksBase = readonly CustomBlockBase[];

export type FormProps = {
export type FormProps<Key extends string> = {
schema: Field[];
components?: Partial<Handlers>;
disableDefaultComponents?: boolean;
children?: ReactNode;
action:
| {
type: "send";
ingestKey: string;
ingestKey: Key;
}
| {
type: "update";
adminKey: string;
adminKey: Key;
eventId: string;
};
} & Omit<
Expand All @@ -74,14 +76,14 @@ export type FormProps = {
"action" | "onSubmit" | "children"
>;

export const unstable_Form = ({
export function unstable_Form<T extends string>({
schema,
components,
disableDefaultComponents,
children,
action,
...rest
}: FormProps): ReactNode => {
}: FormProps<T>): ReactNode {
const fields = schema as Field[] | undefined;

// eslint-disable-next-line react-hooks/rules-of-hooks
Expand Down Expand Up @@ -143,7 +145,7 @@ export const unstable_Form = ({
{children ?? <button type="submit">Submit</button>}
</form>
);
};
}

const defaultHandlers: Handlers = {
text: (props) => (
Expand Down

0 comments on commit c826c61

Please sign in to comment.