Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
<FilloutFullScreenEmbed
customFormLink="my-custom-form"
domain="example.com"
/>

// Using the full form link
// URL will be https://forms.mydomain.com/my-custom-form
<FilloutFullScreenEmbed
customFormLink="forms.mydomain.com/my-custom-form"
domain="example.com"
/>
);
}

export default App;
```
57 changes: 53 additions & 4 deletions src/embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,70 @@ const generateEmbedId = () => {

export type FormParams = Record<string, string | undefined>;

type EmbedOptions = {
type XOR<T1, T2> =
(T1 & {[k in Exclude<keyof T2, keyof T1>]?: never}) |
(T2 & {[k in Exclude<keyof T1, keyof T2>]?: 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<URLSearchParams>();
const [embedId, setEmbedId] = useState<string>();

Expand All @@ -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) {
Expand Down
11 changes: 4 additions & 7 deletions src/embeds/FullScreen.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -22,6 +18,7 @@ export const FullScreen = ({
}: FullScreenProps) => {
const [loading, setLoading] = useState(true);
const embed = useFilloutEmbed({
customFormLink,
filloutId,
domain,
inheritParameters,
Expand Down
10 changes: 4 additions & 6 deletions src/embeds/Popup.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -53,6 +49,7 @@ export const Popup = ({
};

const PopupContent = ({
customFormLink,
filloutId,
domain,
inheritParameters,
Expand All @@ -65,6 +62,7 @@ const PopupContent = ({
}: Omit<PopupProps, "isOpen" | "width" | "height">) => {
const [loading, setLoading] = useState(true);
const embed = useFilloutEmbed({
customFormLink,
filloutId,
domain,
inheritParameters,
Expand Down
13 changes: 5 additions & 8 deletions src/embeds/PopupButton.tsx
Original file line number Diff line number Diff line change
@@ -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<ButtonProps, "onClick">;

export const PopupButton = ({
filloutId,
domain,
inheritParameters,
parameters,
Expand All @@ -30,6 +25,8 @@ export const PopupButton = ({
color,
size,
float,
dynamicResize,
...embedIdentifiers
}: PopupButtonProps) => {
const [isOpen, setIsOpen] = useState(false);

Expand All @@ -44,7 +41,7 @@ export const PopupButton = ({
/>

<Popup
filloutId={filloutId}
{...embedIdentifiers}
domain={domain}
inheritParameters={inheritParameters}
parameters={parameters}
Expand Down
10 changes: 4 additions & 6 deletions src/embeds/Slider.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
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";

export type SliderDirection = "left" | "right";

type SliderProps = {
filloutId: string;
domain?: string;
inheritParameters?: boolean;
parameters?: FormParams;
type SliderProps = EmbedOptions & {
sliderDirection?: SliderDirection;
isOpen: boolean;
onClose: () => void;
Expand Down Expand Up @@ -50,6 +46,7 @@ export const Slider = ({
};

const SliderContent = ({
customFormLink,
filloutId,
domain,
inheritParameters,
Expand All @@ -63,6 +60,7 @@ const SliderContent = ({
}: Omit<SliderProps, "isOpen">) => {
const [loading, setLoading] = useState(true);
const embed = useFilloutEmbed({
customFormLink,
filloutId,
domain,
inheritParameters,
Expand Down
14 changes: 6 additions & 8 deletions src/embeds/SliderButton.tsx
Original file line number Diff line number Diff line change
@@ -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<ButtonProps, "onClick">;

export const SliderButton = ({
filloutId,

domain,
inheritParameters,
parameters,
Expand All @@ -28,6 +24,8 @@ export const SliderButton = ({
color,
size,
float,
dynamicResize,
...embedIdentifiers
}: SliderButtonProps) => {
const [isOpen, setIsOpen] = useState(false);

Expand All @@ -42,7 +40,7 @@ export const SliderButton = ({
/>

<Slider
filloutId={filloutId}
{...embedIdentifiers}
domain={domain}
inheritParameters={inheritParameters}
parameters={parameters}
Expand Down
12 changes: 4 additions & 8 deletions src/embeds/Standard.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
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 { useMessageListener } from "../messages.js";
import { EventProps, useFilloutEvents } from "../events.js";

type StandardProps = {
filloutId: string;
domain?: string;
inheritParameters?: boolean;
parameters?: FormParams;
dynamicResize?: boolean;
} & EventProps;
type StandardProps = EmbedOptions & EventProps;

export const Standard = ({
customFormLink,
filloutId,
domain,
inheritParameters,
Expand All @@ -25,6 +20,7 @@ export const Standard = ({
}: StandardProps) => {
const [loading, setLoading] = useState(true);
const embed = useFilloutEmbed({
customFormLink,
filloutId,
domain,
inheritParameters,
Expand Down