UNPKG

next

Version:

The React Framework

273 lines (272 loc) • 12.9 kB
'use client'; import { jsx as _jsx } from "react/jsx-runtime"; import { useEffect, useContext } from 'react'; import { addBasePath } from './add-base-path'; import { useIntersection } from './use-intersection'; import { useMergedRef } from './use-merged-ref'; import { AppRouterContext } from '../shared/lib/app-router-context.shared-runtime'; import { PrefetchKind } from './components/router-reducer/router-reducer-types'; import { RouterContext } from '../shared/lib/router-context.shared-runtime'; const DISALLOWED_FORM_PROPS = [ 'method', 'encType', 'target' ]; export default function Form(param) { let { replace, scroll, prefetch: prefetchProp, ref: externalRef, ...props } = param; const router = useAppOrPagesRouter(); const actionProp = props.action; const isNavigatingForm = typeof actionProp === 'string'; // Validate `action` if (process.env.NODE_ENV === 'development') { if (isNavigatingForm) { checkActionUrl(actionProp, 'action'); } } // Validate `prefetch` if (process.env.NODE_ENV === 'development') { if (!(prefetchProp === undefined || prefetchProp === false || prefetchProp === null)) { console.error('The `prefetch` prop of <Form> must be `false` or `null`'); } if (prefetchProp !== undefined) { if (!isAppRouter(router)) { console.error('Passing `prefetch` to a <Form> has no effect in the pages directory.'); } else if (!isNavigatingForm) { console.error('Passing `prefetch` to a <Form> whose `action` is a function has no effect.'); } } } const prefetch = prefetchProp === false || prefetchProp === null ? prefetchProp : null; // Validate `scroll` and `replace` if (process.env.NODE_ENV === 'development') { if (!isNavigatingForm && (replace !== undefined || scroll !== undefined)) { console.error('Passing `replace` or `scroll` to a <Form> whose `action` is a function has no effect.\n' + 'See the relevant docs to learn how to control this behavior for navigations triggered from actions:\n' + ' `redirect()` - https://nextjs.org/docs/app/api-reference/functions/redirect#parameters\n' + ' `router.replace()` - https://nextjs.org/docs/app/api-reference/functions/use-router#userouter\n'); } } // Clean up any unsupported form props (and warn if present) for (const key of DISALLOWED_FORM_PROPS){ if (key in props) { if (process.env.NODE_ENV === 'development') { console.error("<Form> does not support changing `" + key + "`. " + (isNavigatingForm ? "If you'd like to use it to perform a mutation, consider making `action` a function instead.\n" + "Learn more: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations" : '')); } delete props[key]; } } const isPrefetchEnabled = // there is no notion of instant loading states in pages dir, so prefetching is pointless isAppRouter(router) && // if we don't have an action path, we can't preload anything anyway. isNavigatingForm && prefetch === null; const [setIntersectionRef, isVisible] = useIntersection({ rootMargin: '200px', disabled: !isPrefetchEnabled }); const ownRef = useMergedRef(setIntersectionRef, externalRef != null ? externalRef : null); useEffect(()=>{ if (!isVisible || !isPrefetchEnabled) { return; } try { const prefetchKind = PrefetchKind.AUTO; router.prefetch(actionProp, { kind: prefetchKind }); } catch (err) { console.error(err); } }, [ isPrefetchEnabled, isVisible, actionProp, prefetch, router ]); if (!isNavigatingForm) { return /*#__PURE__*/ _jsx("form", { ...props, ref: ownRef }); } const actionHref = addBasePath(actionProp); return /*#__PURE__*/ _jsx("form", { ...props, ref: ownRef, action: actionHref, onSubmit: (event)=>onFormSubmit(event, { router, actionHref, replace, scroll, onSubmit: props.onSubmit }) }); } function onFormSubmit(event, param) { let { actionHref, onSubmit, replace, scroll, router } = param; if (typeof onSubmit === 'function') { onSubmit(event); // if the user called event.preventDefault(), do nothing. // (this matches what Link does for `onClick`) if (event.defaultPrevented) { return; } } const formElement = event.currentTarget; const submitter = event.nativeEvent.submitter; let action = actionHref; if (submitter) { if (process.env.NODE_ENV === 'development') { // the way server actions are encoded (e.g. `formMethod="post") // causes some unnecessary dev-mode warnings from `hasUnsupportedSubmitterAttributes`. // we'd bail out anyway, but we just do it silently. if (hasReactServerActionAttributes(submitter)) { return; } } if (hasUnsupportedSubmitterAttributes(submitter)) { return; } // client actions have `formAction="javascript:..."`. We obviously can't prefetch/navigate to that. if (hasReactClientActionAttributes(submitter)) { return; } // If the submitter specified an alternate formAction, // use that URL instead -- this is what a native form would do. // NOTE: `submitter.formAction` is unreliable, because it will give us `location.href` if it *wasn't* set // NOTE: this should not have `basePath` added, because we can't add it before hydration const submitterFormAction = submitter.getAttribute('formAction'); if (submitterFormAction !== null) { if (process.env.NODE_ENV === 'development') { checkActionUrl(submitterFormAction, 'formAction'); } action = submitterFormAction; } } let targetUrl; try { // NOTE: It might be more correct to resolve URLs relative to `document.baseURI`, // but we already do it relative to `location.href` elsewhere: // (see e.g. https://github.com/vercel/next.js/blob/bb0e6722f87ceb2d43015f5b8a413d0072f2badf/packages/next/src/client/components/app-router.tsx#L146) // so it's better to stay consistent. const base = window.location.href; targetUrl = new URL(action, base); } catch (err) { throw new Error('Cannot parse form action "' + action + '" as a URL', { cause: err }); } if (targetUrl.searchParams.size) { // url-encoded HTML forms *overwrite* any search params in the `action` url: // // "Let `query` be the result of running the application/x-www-form-urlencoded serializer [...]" // "Set parsed action's query component to `query`." // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action // // We need to match that. // (note that all other parts of the URL, like `hash`, are preserved) targetUrl.search = ''; } const formData = new FormData(formElement); for (let [name, value] of formData){ if (typeof value !== 'string') { // For file inputs, the native browser behavior is to use the filename as the value instead: // // "If entry's value is a File object, then let value be entry's value's name. Otherwise, let value be entry's value." // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs // if (process.env.NODE_ENV === 'development') { console.warn("<Form> only supports file inputs if `action` is a function. File inputs cannot be used if `action` is a string, " + "because files cannot be encoded as search params."); } value = value.name; } targetUrl.searchParams.append(name, value); } // Finally, no more reasons for bailing out. event.preventDefault(); const method = replace ? 'replace' : 'push'; const targetHref = targetUrl.href; if (isAppRouter(router)) { router[method](targetHref, { scroll }); } else { // TODO(form): Make this use a transition so that pending states work // // Unlike the app router, pages router doesn't use startTransition, // and can't easily be wrapped in one because of implementation details // (e.g. it doesn't use any react state) // But it's important to have this wrapped in a transition because // pending states from e.g. `useFormStatus` rely on that. // So this needs some follow up work. router[method](targetHref, undefined, { scroll }); } } function isAppRouter(router) { return !('asPath' in router); } function useAppOrPagesRouter() { const pagesRouter = useContext(RouterContext); const appRouter = useContext(AppRouterContext); if (pagesRouter) { return pagesRouter; } else { // We're in the app directory if there is no pages router. return appRouter; } } function checkActionUrl(action, source) { const aPropName = source === 'action' ? "an `action`" : "a `formAction`"; let testUrl; try { testUrl = new URL(action, 'http://n'); } catch (err) { console.error("<Form> received " + aPropName + ' that cannot be parsed as a URL: "' + action + '".'); return; } // url-encoded HTML forms ignore any queryparams in the `action` url. We need to match that. if (testUrl.searchParams.size) { console.warn("<Form> received " + aPropName + ' that contains search params: "' + action + '". This is not supported, and they will be ignored. ' + 'If you need to pass in additional search params, use an `<input type="hidden" />` instead.'); } } const isSupportedEncType = (value)=>value === 'application/x-www-form-urlencoded'; const isSupportedMethod = (value)=>value === 'get'; const isSupportedTarget = (value)=>value === '_self'; function hasUnsupportedSubmitterAttributes(submitter) { // A submitter can override `encType` for the form. const formEncType = submitter.getAttribute('formEncType'); if (formEncType !== null && !isSupportedEncType(formEncType)) { if (process.env.NODE_ENV === 'development') { console.error("<Form>'s `encType` was set to an unsupported value via `formEncType=\"" + formEncType + '"`. ' + "This will disable <Form>'s navigation functionality. If you need this, use a native <form> element instead."); } return true; } // A submitter can override `method` for the form. const formMethod = submitter.getAttribute('formMethod'); if (formMethod !== null && !isSupportedMethod(formMethod)) { if (process.env.NODE_ENV === 'development') { console.error("<Form>'s `method` was set to an unsupported value via `formMethod=\"" + formMethod + '"`. ' + "This will disable <Form>'s navigation functionality. If you need this, use a native <form> element instead."); } return true; } // A submitter can override `target` for the form. const formTarget = submitter.getAttribute('formTarget'); if (formTarget !== null && !isSupportedTarget(formTarget)) { if (process.env.NODE_ENV === 'development') { console.error("<Form>'s `target` was set to an unsupported value via `formTarget=\"" + formTarget + '"`. ' + "This will disable <Form>'s navigation functionality. If you need this, use a native <form> element instead."); } return true; } return false; } function hasReactServerActionAttributes(submitter) { // https://github.com/facebook/react/blob/942eb80381b96f8410eab1bef1c539bed1ab0eb1/packages/react-client/src/ReactFlightReplyClient.js#L931-L934 const name = submitter.getAttribute('name'); return name && (name.startsWith('$ACTION_ID_') || name.startsWith('$ACTION_REF_')); } function hasReactClientActionAttributes(submitter) { // CSR: https://github.com/facebook/react/blob/942eb80381b96f8410eab1bef1c539bed1ab0eb1/packages/react-dom-bindings/src/client/ReactDOMComponent.js#L482-L487 // SSR: https://github.com/facebook/react/blob/942eb80381b96f8410eab1bef1c539bed1ab0eb1/packages/react-dom-bindings/src/client/ReactDOMComponent.js#L2401 const action = submitter.getAttribute('formAction'); return action && /\s*javascript:/i.test(action); } //# sourceMappingURL=form.js.map