import * as React from 'react'; import { getFormValues } from '@theoryofnekomata/formxtra'; import fetchPonyfill from 'fetch-ponyfill'; import {NextPage as DefaultNextPage} from 'next'; import {NextApiResponse, NextApiRequest} from './common'; const FormDerivedElementComponent = 'form' as const; type FormDerivedElement = HTMLElementTagNameMap[typeof FormDerivedElementComponent]; type BaseProps = React.HTMLProps; type EncTypeSerializer = (data: unknown) => string; type EncTypeSerializerMap = Record; const DEFAULT_ENCTYPE_SERIALIZERS: EncTypeSerializerMap = { 'application/json': (data: unknown) => JSON.stringify(data), }; type GetFormValuesOptions = Parameters[1]; interface FormProps extends Omit { action?: string; clientAction?: string; clientHeaders?: HeadersInit; clientMethod?: BaseProps['method']; invalidate?: (...args: unknown[]) => unknown; refresh?: (response: Response) => void; encTypeSerializers?: EncTypeSerializerMap; responseEncType?: string; serializerOptions?: GetFormValuesOptions; } export const useResponse = (res: NextApiResponse) => { const [response, setResponse] = React.useState( res.body ? new Response(res.body as any) : undefined ); const onStale = React.useCallback(() => { setResponse(undefined); }, []); const onFresh = React.useCallback((response: Response) => { setResponse(response); }, []); return React.useMemo(() => ({ response, refresh: onFresh, invalidate: onStale, }), [ response, ]); }; interface SerializeBodyParams { form: HTMLFormElement, encType: string, serializers: EncTypeSerializerMap, options?: GetFormValuesOptions, } const serializeBody = (params: SerializeBodyParams) => { const { form, encType, serializers, options } = params; if (encType === 'multipart/form-data') { // type error when provided a submitter element for some reason... const FormDataUnknown = FormData as unknown as { new(form?: HTMLElement, submitter?: HTMLElement ): BodyInit; }; return new FormDataUnknown(form, options?.submitter); } if (encType === 'application/x-www-form-urlencoded') { return new URLSearchParams(form); } if (typeof serializers[encType] === 'function') { return serializers[encType](getFormValues(form, options)); } throw new Error(`Unsupported encType: ${encType}`); } export const Form = React.forwardRef(({ children, onSubmit, action, method = 'get', clientAction = action, clientMethod = method, clientHeaders, encType = 'multipart/form-data', invalidate, refresh, encTypeSerializers = DEFAULT_ENCTYPE_SERIALIZERS, responseEncType = 'application/json', serializerOptions, ...etcProps }, forwardedRef) => { const handleSubmit: React.FormEventHandler = async (event) => { event.preventDefault(); const nativeEvent = event.nativeEvent as unknown as { submitter?: HTMLElement }; if (clientAction) { invalidate?.(); const { fetch } = fetchPonyfill(); const headers: HeadersInit = { ...(clientHeaders ?? {}), 'Accept': responseEncType, }; if (encType !== 'multipart/form-data') { // browser automatically generates content-type header for multipart/form-data (headers as unknown as Record)['Content-Type'] = encType; } const response = await fetch(clientAction, { method: clientMethod.toUpperCase(), body: serializeBody({ form: event.currentTarget, encType, serializers: encTypeSerializers, options: { ...serializerOptions, submitter: nativeEvent.submitter, }, }), headers, }); refresh?.(response); } onSubmit?.(event); }; // TODO how to display put/patch method in form? HTML only supports get/post // > throw error when not get/post // TODO handle "dialog" method as invalid const serverMethodRaw = method.toLowerCase(); const serverMethod = serverMethodRaw === 'get' ? 'get' : 'post'; const serverEncType = 'multipart/form-data'; return ( {children} ); }); export type NextPage = DefaultNextPage< T & { res: NextApiResponse; req: NextApiRequest; }, U >