Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

fix: preserve files only on client #490

Merged
merged 1 commit into from
Mar 7, 2024
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
35 changes: 16 additions & 19 deletions packages/conform-dom/formdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,22 +165,28 @@ export function isFile(obj: unknown): obj is File {
* Normalize value by removing empty object or array, empty string and null values
*/
export function normalize<Type extends Record<string, unknown>>(
value: Type | null,
): Type | null | undefined;
value: Type,
acceptFile?: boolean,
): Type | undefined;
export function normalize<Type extends Array<unknown>>(
value: Type | null,
): Type | null | undefined;
export function normalize(value: unknown): unknown | undefined;
value: Type,
acceptFile?: boolean,
): Type | undefined;
export function normalize(
value: unknown,
acceptFile?: boolean,
): unknown | undefined;
export function normalize<
Type extends Record<string, unknown> | Array<unknown>,
>(
value: Type | null,
): Record<string, unknown> | Array<unknown> | null | undefined {
value: Type,
acceptFile = true,
): Record<string, unknown> | Array<unknown> | undefined {
if (isPlainObject(value)) {
const obj = Object.keys(value)
.sort()
.reduce<Record<string, unknown>>((result, key) => {
const data = normalize(value[key]);
const data = normalize(value[key], acceptFile);

if (typeof data !== 'undefined') {
result[key] = data;
Expand All @@ -201,26 +207,17 @@ export function normalize<
return undefined;
}

return value.map(normalize);
return value.map((item) => normalize(item, acceptFile));
}

if (
(typeof value === 'string' && value === '') ||
value === null ||
(isFile(value) && value.size === 0)
(isFile(value) && (!acceptFile || value.size === 0))
) {
return;
}

// We will skip serializing file if the result is sent to the client
if (isFile(value)) {
return Object.assign(value, {
toJSON() {
return;
},
});
}

return value;
}

Expand Down
7 changes: 6 additions & 1 deletion packages/conform-dom/submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,12 @@ export function replySubmission<FormError>(
return {
status: context.intent ? undefined : error ? 'error' : 'success',
intent: context.intent ? context.intent : undefined,
initialValue: normalize(context.payload) ?? {},
initialValue:
normalize(
context.payload,
// We can't serialize the file and send it back from the server, but we can preserve it in the client
typeof document !== 'undefined',
) ?? {},
error,
state: context.state,
fields: Array.from(context.fields),
Expand Down
51 changes: 49 additions & 2 deletions playground/app/routes/metadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,33 @@ const schema = z.object({
bookmarks.length,
'Bookmark URLs are repeated',
),
file: z.instanceof(File, { message: 'File is required' }),
files: z
.instanceof(File)
.array()
.min(1, 'At least 1 file is required')
.refine(
(files) => files.every((file) => file.type === 'application/json'),
'Only JSON file is accepted',
),
});

const getPrintableValue = (value: unknown) => {
if (typeof value === 'undefined') {
return;
}

return JSON.parse(
JSON.stringify(value, (key, value) => {
if (value instanceof File) {
return `${value.name} (${value.size} bytes)`;
}

return value;
}),
);
};

export async function loader({ request }: LoaderArgs) {
const url = new URL(request.url);

Expand Down Expand Up @@ -62,14 +87,14 @@ export default function Example() {
const bookmarks = fields.bookmarks.getFieldList();

return (
<Form method="post" {...getFormProps(form)}>
<Form method="post" {...getFormProps(form)} encType="multipart/form-data">
<Playground
title="Metadata"
result={{
form: {
status: form.status,
initialValue: form.initialValue,
value: form.value,
value: getPrintableValue(form.value),
dirty: form.dirty,
valid: form.valid,
errors: form.errors,
Expand Down Expand Up @@ -107,6 +132,22 @@ export default function Example() {
errors: bookmarks[1]?.errors,
allErrors: bookmarks[1]?.allErrors,
},
file: {
initialValue: fields.file.initialValue,
value: getPrintableValue(fields.file.value),
dirty: fields.file.dirty,
valid: fields.file.valid,
errors: fields.file.errors,
allErrors: fields.file.allErrors,
},
files: {
initialValue: fields.files.initialValue,
value: getPrintableValue(fields.files.value),
dirty: fields.files.dirty,
valid: fields.files.valid,
errors: fields.files.errors,
allErrors: fields.files.allErrors,
},
}}
>
<Field label="Title" meta={fields.title}>
Expand All @@ -128,6 +169,12 @@ export default function Example() {
</fieldset>
);
})}
<Field label="File" meta={fields.file}>
<input {...getInputProps(fields.file, { type: 'file' })} />
</Field>
<Field label="Files" meta={fields.files}>
<input {...getInputProps(fields.files, { type: 'file' })} multiple />
</Field>
</Playground>
</Form>
);
Expand Down
Loading
Loading