|
- import * as React from 'react';
- import fetchPonyfill from 'fetch-ponyfill';
- import { NextPage as DefaultNextPage } from 'next';
- import {
- NextApiResponse,
- NextApiRequest,
- ENCTYPE_APPLICATION_JSON,
- ENCTYPE_MULTIPART_FORM_DATA,
- } from './common';
- import {
- DEFAULT_ENCTYPE_SERIALIZERS,
- EncTypeSerializerMap, serializeBody,
- SerializerOptions,
- } from './utils/serialization';
-
- const FormDerivedElementComponent = 'form' as const;
-
- type FormDerivedElement = HTMLElementTagNameMap[typeof FormDerivedElementComponent];
-
- const ALLOWED_SERVER_METHODS = ['get', 'post'] as const;
-
- type AllowedServerMethod = typeof ALLOWED_SERVER_METHODS[number];
-
- const ALLOWED_CLIENT_METHODS = [
- ...ALLOWED_SERVER_METHODS,
- 'put',
- 'patch',
- 'delete',
- ] as const;
-
- type AllowedClientMethod = typeof ALLOWED_CLIENT_METHODS[number];
-
- export interface FormProps extends Omit<React.HTMLProps<FormDerivedElement>, 'action' | 'method'> {
- action?: string;
- method?: AllowedServerMethod;
- clientAction?: string;
- clientHeaders?: HeadersInit;
- clientMethod?: AllowedClientMethod;
- invalidate?: (...args: unknown[]) => unknown;
- refresh?: (response: Response) => void;
- encTypeSerializers?: EncTypeSerializerMap;
- responseEncType?: string;
- serializerOptions?: SerializerOptions;
- }
-
- export const useResponse = (res: NextApiResponse) => {
- const [response, setResponse] = React.useState<Response | undefined>(
- res.body ? new Response(res.body as unknown as BodyInit) : undefined,
- );
-
- const invalidate = React.useCallback(() => {
- setResponse(undefined);
- }, []);
-
- const refresh = React.useCallback((newResponse: Response) => {
- setResponse(newResponse);
- }, []);
-
- return React.useMemo(() => ({
- response,
- refresh,
- invalidate,
- }), [
- response,
- refresh,
- invalidate,
- ]);
- };
-
- export const Form = React.forwardRef<FormDerivedElement, FormProps>(({
- children,
- onSubmit,
- action,
- method = 'get',
- clientAction = action,
- clientMethod = method,
- clientHeaders,
- encType = ENCTYPE_MULTIPART_FORM_DATA,
- invalidate,
- refresh,
- encTypeSerializers = DEFAULT_ENCTYPE_SERIALIZERS,
- responseEncType = ENCTYPE_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 !== 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 fetchInit: RequestInit = {
- method: clientMethod.toUpperCase(),
- headers,
- };
-
- if (!['GET', 'HEAD'].includes(clientMethod.toUpperCase())) {
- fetchInit.body = serializeBody({
- form: event.currentTarget,
- encType,
- serializers: encTypeSerializers,
- options: {
- ...serializerOptions,
- submitter: nativeEvent.submitter,
- },
- });
- }
- const response = await fetch(clientAction, fetchInit);
- refresh?.(response);
- }
-
- onSubmit?.(event);
- };
-
- const serverMethodRaw = method.toLowerCase();
- const serverMethod = serverMethodRaw === 'get' ? 'get' : 'post';
-
- return (
- <FormDerivedElementComponent
- {...etcProps}
- ref={forwardedRef}
- onSubmit={handleSubmit}
- action={action}
- method={serverMethod}
- encType={ENCTYPE_MULTIPART_FORM_DATA}
- >
- {children}
- </FormDerivedElementComponent>
- );
- });
-
- Form.displayName = 'Form' as const;
-
- Form.defaultProps = {
- action: undefined,
- clientAction: undefined,
- clientHeaders: undefined,
- clientMethod: undefined,
- encTypeSerializers: DEFAULT_ENCTYPE_SERIALIZERS,
- invalidate: undefined,
- method: 'get' as const,
- refresh: undefined,
- responseEncType: ENCTYPE_APPLICATION_JSON,
- serializerOptions: undefined,
- };
-
- export type NextPage<T = NonNullable<unknown>, U = T> = DefaultNextPage<
- T & {
- res: NextApiResponse;
- req: NextApiRequest;
- }, U
- >
|