Skip to content
This repository was archived by the owner on Nov 12, 2025. It is now read-only.
Open
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
2 changes: 1 addition & 1 deletion examples/astro-forms-demo/src/components/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const FileUpload = ({
// Convert allowedFileFormats to accept string for HTML input
const acceptString = allowedFileFormats
?.map(format =>
format.startsWith('.') ? format : `.${format.toLowerCase()}`
format.startsWith('.') ? format : `.${format.toLowerCase()}/*`
)
.join(',');

Expand Down
11 changes: 3 additions & 8 deletions packages/headless-components/forms/src/react/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -853,20 +853,15 @@ interface FieldsProps {
*/
export const Fields = React.forwardRef<HTMLDivElement, FieldsProps>(
(props, ref) => {
const [formValues, setFormValues] = useState<FormValues>({});
const [formErrors, setFormErrors] = useState<FormError[]>([]);

const handleFormChange = useCallback((values: FormValues) => {
setFormValues(values);
}, []);

const handleFormValidate = useCallback((errors: FormError[]) => {
setFormErrors(errors);
}, []);

return (
<CoreFields>
{({ form, submitForm }) => {
{({ form, formValues, submitForm, handleForm }) => {
if (!form) return null;

return (
Expand All @@ -875,11 +870,11 @@ export const Fields = React.forwardRef<HTMLDivElement, FieldsProps>(
<FieldsWithForm
form={form}
values={formValues}
onChange={handleFormChange}
onChange={handleForm}
errors={formErrors}
onValidate={handleFormValidate}
fields={props.fieldMap}
submitForm={() => submitForm(formValues)}
submitForm={submitForm}
rowGapClassname={props.rowGapClassname}
columnGapClassname={props.columnGapClassname}
/>
Expand Down
11 changes: 9 additions & 2 deletions packages/headless-components/forms/src/react/core/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,10 @@ export function Submitted(props: FormSubmittedProps) {
export interface FieldsRenderProps {
/** The form data, or null if not loaded */
form: forms.Form | null;
formValues: FormValues;
/** Function to submit the form with values */
submitForm: (formValues: FormValues) => Promise<void>;
submitForm: () => Promise<void>;
handleForm: (formValues: FormValues) => Promise<void>;
}

/**
Expand Down Expand Up @@ -352,12 +354,17 @@ export interface FieldsProps {
* ```
*/
export function Fields(props: FieldsProps) {
const { formSignal, submitForm } = useService(FormServiceDefinition);
const { formSignal, submitForm, handleForm, formValuesSignal } = useService(
FormServiceDefinition,
);
const form = formSignal.get();
const formValues = formValuesSignal.get();

return props.children({
form,
formValues,
submitForm,
handleForm,
});
}

Expand Down
7 changes: 7 additions & 0 deletions packages/headless-components/forms/src/react/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,10 @@ export type {
};

export type FormValues = Record<string, any>;

export interface FileField {
fileId: string;
displayName: string;
url: string;
fileType: string;
}
15 changes: 15 additions & 0 deletions packages/headless-components/forms/src/react/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,18 @@ export function calculateGridStyles(layout: Layout) {
},
};
}

export function isFormFileField(value: any): boolean {
if (!value) {
return false;
}

if (!Array.isArray(value)) {
return false;
}
if (value.length === 0) {
return false;
}
const first = value[0];
return 'fileId' in first;
}
59 changes: 50 additions & 9 deletions packages/headless-components/forms/src/services/form-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {
SignalsServiceDefinition,
type ReadOnlySignal,
} from '@wix/services-definitions/core-services/signals';

import { FormValues } from '../react/types.js';
import { isFormFileField } from '../react/utils.js';
import { useUploadImage } from './hooks/index.js';

/**
* Response type for form submission operations.
Expand Down Expand Up @@ -32,8 +35,12 @@ export interface FormServiceAPI {
errorSignal: ReadOnlySignal<string | null>;
/** Reactive signal containing submission response state */
submitResponseSignal: ReadOnlySignal<SubmitResponse>;
/** Reactive signal that contains all actual formValues */
formValuesSignal: ReadOnlySignal<FormValues>;
/** Function to submit form with current values */
submitForm: (formValues: FormValues) => Promise<void>;
submitForm: () => Promise<void>;
/** Function to handle changed form with new values */
handleForm: (formValues: FormValues) => Promise<void>;
}

/**
Expand Down Expand Up @@ -101,6 +108,12 @@ export const FormService = implementService.withConfig<FormServiceConfig>()(
const formSignal = signalsService.signal<forms.Form | null>(
hasSchema ? config.form : null,
);
const formValuesSignal = signalsService.signal<FormValues>({});

const { handleFileFields } = useUploadImage({
setError: errorSignal.set,
formValues: formValuesSignal.get(),
});

if (!hasSchema) {
loadForm(config.formId);
Expand Down Expand Up @@ -135,6 +148,7 @@ export const FormService = implementService.withConfig<FormServiceConfig>()(
formId,
submissions: formValues,
});

// TODO: add message
return { type: 'success' };
} catch (error) {
Expand All @@ -147,7 +161,7 @@ export const FormService = implementService.withConfig<FormServiceConfig>()(
* Submits the form with the provided values.
* Uses custom handler if provided in config, otherwise uses default submission.
*/
async function submitForm(formValues: FormValues): Promise<void> {
async function submitForm(): Promise<void> {
const form = formSignal.get();
if (!form) {
console.error('Cannot submit: form not loaded');
Expand All @@ -159,8 +173,8 @@ export const FormService = implementService.withConfig<FormServiceConfig>()(
submitResponseSignal.set({ type: 'loading' });

try {
const handler = config.onSubmit || defaultSubmitHandler;
const response = await handler(formId, formValues);
const handler = (await config.onSubmit) || (await defaultSubmitHandler);
const response = await handler(formId, formValuesSignal.get());
submitResponseSignal.set(response);
} catch (error) {
console.error('Unexpected error during submission:', error);
Expand All @@ -171,12 +185,39 @@ export const FormService = implementService.withConfig<FormServiceConfig>()(
}
}

async function handleForm(formValues: FormValues) {
isLoadingSignal.set(true);
errorSignal.set(null);

const formId = formSignal.get()?._id;
if (!formId) {
return;
}

const newFormValues = await Object.fromEntries(
await Promise.all(
Object.entries(formValues).map(async ([key, value]) => {
if (!isFormFileField(value)) {
return [key, value];
}

return [key, await handleFileFields(formId, value)];
}),
),
);

isLoadingSignal.set(false);
formValuesSignal.set(newFormValues);
}

return {
formSignal: formSignal,
isLoadingSignal: isLoadingSignal,
errorSignal: errorSignal,
submitResponseSignal: submitResponseSignal,
submitForm: submitForm,
formSignal,
isLoadingSignal,
errorSignal,
submitResponseSignal,
formValuesSignal,
submitForm,
handleForm,
};
},
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useUploadImage } from './useUploadImage';
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { submissions } from '@wix/forms';
import { FileField } from '../../react';
import { FormValues } from '@wix/form-public';
import { isFormFileField } from '../../react/utils';

interface UseUploadImageProps {
setError(newValue: string | null): void;
formValues: FormValues;
}

const getFormFileFieldIds = (formValues: FormValues): string[] => {
const ids = Object.entries(formValues)
.map(([_, value]) => {
if (!isFormFileField(value)) {
return;
}

const fileIds = value.map((file: FileField) => file.fileId);

return fileIds;
})
.flat();

return ids;
};

export const useUploadImage = ({
setError,
formValues,
}: UseUploadImageProps) => {
const fileFieldIds = getFormFileFieldIds(formValues);

const getUploadUrl = async (
formId: string,
file: FileField,
): Promise<string | undefined> => {
try {
const { uploadUrl } = await submissions.getMediaUploadUrl(
formId,
file.displayName,
file.fileType,
);

return uploadUrl;
} catch {
return;
}
};

const uploadImage = async (
file: FileField,
uploadUrl: string,
): Promise<string | undefined> => {
const fileResponse = await fetch(file.url);
const blob = await fileResponse.blob();

const headers = {
'Content-Type': 'application/octet-stream',
};

try {
const response = await fetch(uploadUrl, {
headers,
method: 'PUT',
body: blob,
});

const { file } = await response.json();

if (!file) {
return;
}

return file.url;
} catch {
return;
}
};

const handleFileFields = async (
formId: string,
files: FileField[],
): Promise<FileField[]> => {
const newFileFields = await Promise.all(
files.map(async (fileField) => {
if (fileFieldIds.includes(fileField.fileId)) {
return fileField;
}

const uploadUrl = await getUploadUrl(formId, fileField);
if (uploadUrl === undefined) {
setError('An error occured while uploading the file!');
return fileField;
}

const newUrl = await uploadImage(fileField, uploadUrl);
if (newUrl === undefined) {
setError('An error occured while uploading the file!');
return fileField;
}

const newFileField = {
...fileField,
url: newUrl,
};

return newFileField;
}),
);

return newFileFields;
};

return {
handleFileFields,
};
};