diff --git a/README.md b/README.md index 45e0e77..25d12fd 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,9 @@ Install with `npm i @fillout/react` ## Embed components -There is a component for each embed type. All of them require the `filloutId` prop, which is the id of your form. This code is easy to spot in the url of the editor or the live form, for example, `forms.fillout.com/t/foAdHjd1Duus`. +There is a component for each embed type. + All of them require **either** a `filloutId` (your form’s ID) **or** a [custom form link](https://www.fillout.com/help/customize-form-share-link). They are mutually exclusive. + +This code is easy to spot in the url of the editor or the live form, for example, `forms.fillout.com/t/foAdHjd1Duus`. All embed components allow you to pass URL parameters using the optional `parameters` prop, and you can also use `inheritParameters` to make the form inherit the parameters from the host page's url. @@ -214,3 +216,29 @@ function App() { export default App; ``` + +> ⚠️ If you pass `customFormLink` as a full URL, the full URL is used as-is, and `domain` will be ignored. + +```js +import { FilloutFullScreenEmbed } from "@fillout/react"; + +function App() { + return ( + // Using just the custom form ending + // URL will be https://example.com/my-custom-form + + + // Using the full form link + // URL will be https://forms.mydomain.com/my-custom-form + + ); +} + +export default App; +``` diff --git a/src/embed.ts b/src/embed.ts index 54a4f2b..9391eeb 100644 --- a/src/embed.ts +++ b/src/embed.ts @@ -11,21 +11,70 @@ const generateEmbedId = () => { export type FormParams = Record; -type EmbedOptions = { +type XOR = + (T1 & {[k in Exclude]?: never}) | + (T2 & {[k in Exclude]?: never}); + +type FormIdentifier = XOR<{ filloutId: string; - domain?: string; +},{ + customFormLink: string; +}> + +type CommonEmbedOptions = { inheritParameters?: boolean; parameters?: FormParams; dynamicResize?: boolean; + domain?: string; +} + +/** + * Strict exclusive OR for public-facing component API. + */ +export type EmbedOptions = FormIdentifier & CommonEmbedOptions; + +type NormalizeUrlParams = { + customFormLink?: string; + filloutId?: string; + origin: string; +}; + +const normalizeFormIdentifier = ({ + customFormLink, + filloutId, + origin, +}: NormalizeUrlParams): URL => { + if (customFormLink) { + // Accomodate full links or just the form identifier. + try { + return new URL(customFormLink); + } catch { + return new URL(customFormLink, origin); + } + } + + if (filloutId) { + return new URL(`/t/${encodeURIComponent(filloutId)}`, origin); + } + + throw new Error("Either filloutId or customFormLink must be provided."); }; +/** + * Loose type for internal use. + */ +type EmbedHookOptions = CommonEmbedOptions & { + customFormLink?: string; + filloutId?: string; +} export const useFilloutEmbed = ({ + customFormLink, filloutId, domain, inheritParameters, parameters, dynamicResize, -}: EmbedOptions) => { +}: EmbedHookOptions) => { const [searchParams, setSearchParams] = useState(); const [embedId, setEmbedId] = useState(); @@ -39,7 +88,7 @@ export const useFilloutEmbed = ({ // iframe url const origin = domain ? `https://${domain}` : FILLOUT_BASE_URL; - const iframeUrl = new URL(`${origin}/t/${encodeURIComponent(filloutId)}`); + const iframeUrl = normalizeFormIdentifier({filloutId, origin, customFormLink}); // inherit query params if (inheritParameters && searchParams) { diff --git a/src/embeds/FullScreen.tsx b/src/embeds/FullScreen.tsx index 1d49ab2..82409c1 100644 --- a/src/embeds/FullScreen.tsx +++ b/src/embeds/FullScreen.tsx @@ -1,16 +1,12 @@ import React, { useState } from "react"; -import { FormParams, useFilloutEmbed } from "../embed.js"; +import { EmbedOptions, FormParams, useFilloutEmbed } from "../embed.js"; import { Loading } from "../components/Loading.js"; import { EventProps, useFilloutEvents } from "../events.js"; -type FullScreenProps = { - filloutId: string; - domain?: string; - inheritParameters?: boolean; - parameters?: FormParams; -} & EventProps; +type FullScreenProps = EmbedOptions & EventProps; export const FullScreen = ({ + customFormLink, filloutId, domain, inheritParameters, @@ -22,6 +18,7 @@ export const FullScreen = ({ }: FullScreenProps) => { const [loading, setLoading] = useState(true); const embed = useFilloutEmbed({ + customFormLink, filloutId, domain, inheritParameters, diff --git a/src/embeds/Popup.tsx b/src/embeds/Popup.tsx index c9c4ecf..57633b0 100644 --- a/src/embeds/Popup.tsx +++ b/src/embeds/Popup.tsx @@ -1,14 +1,10 @@ import React, { ReactNode, useEffect, useState } from "react"; import { Loading } from "../components/Loading.js"; import { Portal } from "../components/Portal.js"; -import { FormParams, useFilloutEmbed } from "../embed.js"; +import { EmbedOptions, FormParams, useFilloutEmbed } from "../embed.js"; import { EventProps, useFilloutEvents } from "../events.js"; -type PopupProps = { - filloutId: string; - domain?: string; - inheritParameters?: boolean; - parameters?: FormParams; +type PopupProps = EmbedOptions & { isOpen: boolean; onClose: () => void; width?: number | string; @@ -53,6 +49,7 @@ export const Popup = ({ }; const PopupContent = ({ + customFormLink, filloutId, domain, inheritParameters, @@ -65,6 +62,7 @@ const PopupContent = ({ }: Omit) => { const [loading, setLoading] = useState(true); const embed = useFilloutEmbed({ + customFormLink, filloutId, domain, inheritParameters, diff --git a/src/embeds/PopupButton.tsx b/src/embeds/PopupButton.tsx index fbb2f3e..03578ff 100644 --- a/src/embeds/PopupButton.tsx +++ b/src/embeds/PopupButton.tsx @@ -1,21 +1,16 @@ import React, { useState } from "react"; -import { FormParams } from "../embed.js"; +import { EmbedOptions, FormParams } from "../embed.js"; import { Popup } from "./Popup.js"; import { Button, ButtonProps } from "../components/Button.js"; import { EventProps } from "../events.js"; -type PopupButtonProps = { - filloutId: string; - domain?: string; - inheritParameters?: boolean; - parameters?: FormParams; +type PopupButtonProps = EmbedOptions & { width?: number | string; height?: number | string; } & EventProps & Omit; export const PopupButton = ({ - filloutId, domain, inheritParameters, parameters, @@ -30,6 +25,8 @@ export const PopupButton = ({ color, size, float, + dynamicResize, + ...embedIdentifiers }: PopupButtonProps) => { const [isOpen, setIsOpen] = useState(false); @@ -44,7 +41,7 @@ export const PopupButton = ({ /> void; @@ -50,6 +46,7 @@ export const Slider = ({ }; const SliderContent = ({ + customFormLink, filloutId, domain, inheritParameters, @@ -63,6 +60,7 @@ const SliderContent = ({ }: Omit) => { const [loading, setLoading] = useState(true); const embed = useFilloutEmbed({ + customFormLink, filloutId, domain, inheritParameters, diff --git a/src/embeds/SliderButton.tsx b/src/embeds/SliderButton.tsx index a75b46f..cc61e0b 100644 --- a/src/embeds/SliderButton.tsx +++ b/src/embeds/SliderButton.tsx @@ -1,20 +1,16 @@ import React, { useState } from "react"; -import { FormParams } from "../embed.js"; +import { EmbedOptions, FormParams } from "../embed.js"; import { Slider, SliderDirection } from "./Slider.js"; import { Button, ButtonProps } from "../components/Button.js"; import { EventProps } from "../events.js"; -type SliderButtonProps = { - filloutId: string; - domain?: string; - inheritParameters?: boolean; - parameters?: FormParams; +type SliderButtonProps = EmbedOptions & { sliderDirection?: SliderDirection; } & EventProps & Omit; export const SliderButton = ({ - filloutId, + domain, inheritParameters, parameters, @@ -28,6 +24,8 @@ export const SliderButton = ({ color, size, float, + dynamicResize, + ...embedIdentifiers }: SliderButtonProps) => { const [isOpen, setIsOpen] = useState(false); @@ -42,7 +40,7 @@ export const SliderButton = ({ /> { const [loading, setLoading] = useState(true); const embed = useFilloutEmbed({ + customFormLink, filloutId, domain, inheritParameters,