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,