Use forms with or without client-side JavaScript--no code duplication required!
Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 

163 linhas
4.0 KiB

  1. import * as React from 'react';
  2. import fetchPonyfill from 'fetch-ponyfill';
  3. import { NextPage as DefaultNextPage } from 'next';
  4. import {
  5. NextApiResponse,
  6. NextApiRequest,
  7. ENCTYPE_APPLICATION_JSON,
  8. ENCTYPE_MULTIPART_FORM_DATA,
  9. } from './common';
  10. import {
  11. DEFAULT_ENCTYPE_SERIALIZERS,
  12. EncTypeSerializerMap, serializeBody,
  13. SerializerOptions,
  14. } from './utils/serialization';
  15. const FormDerivedElementComponent = 'form' as const;
  16. type FormDerivedElement = HTMLElementTagNameMap[typeof FormDerivedElementComponent];
  17. const ALLOWED_SERVER_METHODS = ['get', 'post'] as const;
  18. type AllowedServerMethod = typeof ALLOWED_SERVER_METHODS[number];
  19. const ALLOWED_CLIENT_METHODS = [
  20. ...ALLOWED_SERVER_METHODS,
  21. 'put',
  22. 'patch',
  23. 'delete',
  24. ] as const;
  25. type AllowedClientMethod = typeof ALLOWED_CLIENT_METHODS[number];
  26. export interface FormProps extends Omit<React.HTMLProps<FormDerivedElement>, 'action' | 'method'> {
  27. action?: string;
  28. method?: AllowedServerMethod;
  29. clientAction?: string;
  30. clientHeaders?: HeadersInit;
  31. clientMethod?: AllowedClientMethod;
  32. invalidate?: (...args: unknown[]) => unknown;
  33. refresh?: (response: Response) => void;
  34. encTypeSerializers?: EncTypeSerializerMap;
  35. responseEncType?: string;
  36. serializerOptions?: SerializerOptions;
  37. }
  38. export const useResponse = (res: NextApiResponse) => {
  39. const [response, setResponse] = React.useState<Response | undefined>(
  40. res.body ? new Response(res.body as unknown as BodyInit) : undefined,
  41. );
  42. const invalidate = React.useCallback(() => {
  43. setResponse(undefined);
  44. }, []);
  45. const refresh = React.useCallback((newResponse: Response) => {
  46. setResponse(newResponse);
  47. }, []);
  48. return React.useMemo(() => ({
  49. response,
  50. refresh,
  51. invalidate,
  52. }), [
  53. response,
  54. refresh,
  55. invalidate,
  56. ]);
  57. };
  58. export const Form = React.forwardRef<FormDerivedElement, FormProps>(({
  59. children,
  60. onSubmit,
  61. action,
  62. method = 'get',
  63. clientAction = action,
  64. clientMethod = method,
  65. clientHeaders,
  66. encType = ENCTYPE_MULTIPART_FORM_DATA,
  67. invalidate,
  68. refresh,
  69. encTypeSerializers = DEFAULT_ENCTYPE_SERIALIZERS,
  70. responseEncType = ENCTYPE_APPLICATION_JSON,
  71. serializerOptions,
  72. ...etcProps
  73. }, forwardedRef) => {
  74. const handleSubmit: React.FormEventHandler<FormDerivedElement> = async (event) => {
  75. event.preventDefault();
  76. const nativeEvent = event.nativeEvent as unknown as { submitter?: HTMLElement };
  77. if (clientAction) {
  78. invalidate?.();
  79. const { fetch } = fetchPonyfill();
  80. const headers: HeadersInit = {
  81. ...(clientHeaders ?? {}),
  82. Accept: responseEncType,
  83. };
  84. if (encType !== ENCTYPE_MULTIPART_FORM_DATA) {
  85. // browser automatically generates content-type header for multipart/form-data
  86. (headers as unknown as Record<string, string>)['Content-Type'] = encType;
  87. }
  88. const fetchInit: RequestInit = {
  89. method: clientMethod.toUpperCase(),
  90. headers,
  91. };
  92. if (!['GET', 'HEAD'].includes(clientMethod.toUpperCase())) {
  93. fetchInit.body = serializeBody({
  94. form: event.currentTarget,
  95. encType,
  96. serializers: encTypeSerializers,
  97. options: {
  98. ...serializerOptions,
  99. submitter: nativeEvent.submitter,
  100. },
  101. });
  102. }
  103. const response = await fetch(clientAction, fetchInit);
  104. refresh?.(response);
  105. }
  106. onSubmit?.(event);
  107. };
  108. const serverMethodRaw = method.toLowerCase();
  109. const serverMethod = serverMethodRaw === 'get' ? 'get' : 'post';
  110. return (
  111. <FormDerivedElementComponent
  112. {...etcProps}
  113. ref={forwardedRef}
  114. onSubmit={handleSubmit}
  115. action={action}
  116. method={serverMethod}
  117. encType={ENCTYPE_MULTIPART_FORM_DATA}
  118. >
  119. {children}
  120. </FormDerivedElementComponent>
  121. );
  122. });
  123. Form.displayName = 'Form' as const;
  124. Form.defaultProps = {
  125. action: undefined,
  126. clientAction: undefined,
  127. clientHeaders: undefined,
  128. clientMethod: undefined,
  129. encTypeSerializers: DEFAULT_ENCTYPE_SERIALIZERS,
  130. invalidate: undefined,
  131. method: 'get' as const,
  132. refresh: undefined,
  133. responseEncType: ENCTYPE_APPLICATION_JSON,
  134. serializerOptions: undefined,
  135. };
  136. export type NextPage<T = NonNullable<unknown>, U = T> = DefaultNextPage<
  137. T & {
  138. res: NextApiResponse;
  139. req: NextApiRequest;
  140. }, U
  141. >