|
- 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<FormDerivedElement>;
-
- type EncTypeSerializer = (data: unknown) => string;
-
- type EncTypeSerializerMap = Record<string, EncTypeSerializer>;
-
- const DEFAULT_ENCTYPE_SERIALIZERS: EncTypeSerializerMap = {
- 'application/json': (data: unknown) => JSON.stringify(data),
- };
-
- type GetFormValuesOptions = Parameters<typeof getFormValues>[1];
-
- interface FormProps extends Omit<BaseProps, 'action'> {
- 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<Response | undefined>(
- 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<FormDerivedElement, FormProps>(({
- 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<FormDerivedElement> = 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<string, string>)['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 (
- <FormDerivedElementComponent
- {...etcProps}
- ref={forwardedRef}
- onSubmit={handleSubmit}
- action={action}
- method={serverMethod}
- encType={serverEncType}
- >
- {children}
- </FormDerivedElementComponent>
- );
- });
-
- export type NextPage<T = {}, U = T> = DefaultNextPage<
- T & {
- res: NextApiResponse;
- req: NextApiRequest;
- }, U
- >
|