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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,39 @@ This can be used in conjunction with the [@fillout/api](https://npmjs.org/packag

> ⚠️ The `onPageChange` may be triggered for any page on your form, including the starting page when the embed first loads. You should always check the page ID instead of making assumptions about where the user is in the form just because the function was triggered.

## Custom loading states

While the form loads, the embeds show a built-in loading spinner. If you are placing an embed inside your own modal or layout, you can replace that with your own loading UI using the `hideLoading` and `onReady` props.

`hideLoading` stops the built-in spinner from rendering, and `onReady` is called once the form has finished loading and is visible.

```js
import { useState } from "react";
import { FilloutStandardEmbed } from "@fillout/react";

function App() {
const [ready, setReady] = useState(false);

return (
<div>
{!ready && <MyOwnSpinner />}

<FilloutStandardEmbed
filloutId="4SVaJQNVdrus"
hideLoading
onReady={() => setReady(true)}
/>
</div>
);
}

export default App;
```

Both props are optional and are supported by every embed component.

> ℹ️ `onReady` tracks the embed's own loading state — it fires at the exact moment the built-in spinner would be hidden and the form becomes visible. `onInit` is a separate event sent by the form itself, and gives you the submission UUID. Use `onReady` to decide when to show the form, and `onInit` when you need the submission UUID.

## Custom domains

If you want to take advantage of features that are only available with forms hosted on your own domain, such as custom JS, you can use the `domain` prop to change the embedded url.
Expand Down
16 changes: 12 additions & 4 deletions src/embeds/FullScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import React, { useState } from "react";
import { FormParams, useFilloutEmbed } from "../embed.js";
import { Loading } from "../components/Loading.js";
import { EventProps, useFilloutEvents } from "../events.js";
import { EventProps, LoadingProps, useFilloutEvents } from "../events.js";

type FullScreenProps = {
filloutId: string;
domain?: string;
inheritParameters?: boolean;
parameters?: FormParams;
} & EventProps;
} & EventProps &
LoadingProps;

export const FullScreen = ({
filloutId,
domain,
inheritParameters,
parameters,
hideLoading,

onInit,
onPageChange,
onSubmit,
onReady,
}: FullScreenProps) => {
const [loading, setLoading] = useState(true);
const embed = useFilloutEmbed({
Expand All @@ -30,6 +33,11 @@ export const FullScreen = ({

useFilloutEvents(embed, { onInit, onPageChange, onSubmit });

const onIframeLoad = () => {
setLoading(false);
onReady?.();
};
Comment on lines +36 to +39

return (
<div
style={{
Expand All @@ -40,7 +48,7 @@ export const FullScreen = ({
height: "100%",
}}
>
{loading && (
{loading && !hideLoading && (
<div
style={{
display: "flex",
Expand All @@ -65,7 +73,7 @@ export const FullScreen = ({
height: !loading ? "100%" : 0,
opacity: !loading ? 1 : 0,
}}
onLoad={() => setLoading(false)}
onLoad={onIframeLoad}
/>
)}
</div>
Expand Down
16 changes: 12 additions & 4 deletions src/embeds/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 { EventProps, useFilloutEvents } from "../events.js";
import { EventProps, LoadingProps, useFilloutEvents } from "../events.js";

type PopupProps = {
filloutId: string;
Expand All @@ -13,7 +13,8 @@ type PopupProps = {
onClose: () => void;
width?: number | string;
height?: number | string;
} & EventProps;
} & EventProps &
LoadingProps;

// This is exposed as a standalone embed component,
// but can also be used indirectly with PopupButton
Expand Down Expand Up @@ -58,10 +59,12 @@ const PopupContent = ({
inheritParameters,
parameters,
onClose,
hideLoading,

onInit,
onPageChange,
onSubmit,
onReady,
}: Omit<PopupProps, "isOpen" | "width" | "height">) => {
const [loading, setLoading] = useState(true);
const embed = useFilloutEmbed({
Expand All @@ -73,6 +76,11 @@ const PopupContent = ({

useFilloutEvents(embed, { onInit, onPageChange, onSubmit });

const onIframeLoad = () => {
setLoading(false);
onReady?.();
};
Comment on lines +79 to +82

return (
<>
{!loading && <CloseButton onClick={onClose} />}
Expand All @@ -88,11 +96,11 @@ const PopupContent = ({
width: "100%",
opacity: !loading ? 1 : 0,
}}
onLoad={() => setLoading(false)}
onLoad={onIframeLoad}
/>
)}

{loading && (
{loading && !hideLoading && (
<div
style={{
position: "fixed",
Expand Down
7 changes: 6 additions & 1 deletion src/embeds/PopupButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState } from "react";
import { FormParams } from "../embed.js";
import { Popup } from "./Popup.js";
import { Button, ButtonProps } from "../components/Button.js";
import { EventProps } from "../events.js";
import { EventProps, LoadingProps } from "../events.js";

type PopupButtonProps = {
filloutId: string;
Expand All @@ -12,6 +12,7 @@ type PopupButtonProps = {
width?: number | string;
height?: number | string;
} & EventProps &
LoadingProps &
Omit<ButtonProps, "onClick">;

export const PopupButton = ({
Expand All @@ -21,10 +22,12 @@ export const PopupButton = ({
parameters,
width,
height,
hideLoading,

onInit,
onPageChange,
onSubmit,
onReady,

text,
color,
Expand Down Expand Up @@ -53,6 +56,8 @@ export const PopupButton = ({
onInit={onInit}
onPageChange={onPageChange}
onSubmit={onSubmit}
onReady={onReady}
hideLoading={hideLoading}
width={width}
height={height}
/>
Expand Down
16 changes: 12 additions & 4 deletions src/embeds/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 { EventProps, useFilloutEvents } from "../events.js";
import { EventProps, LoadingProps, useFilloutEvents } from "../events.js";

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

Expand All @@ -14,7 +14,8 @@ type SliderProps = {
sliderDirection?: SliderDirection;
isOpen: boolean;
onClose: () => void;
} & EventProps;
} & EventProps &
LoadingProps;

// This is exposed as a standalone embed component,
// but can also be used indirectly with SliderButton
Expand Down Expand Up @@ -56,10 +57,12 @@ const SliderContent = ({
parameters,
sliderDirection = "right",
onClose,
hideLoading,

onInit,
onPageChange,
onSubmit,
onReady,
}: Omit<SliderProps, "isOpen">) => {
const [loading, setLoading] = useState(true);
const embed = useFilloutEmbed({
Expand All @@ -71,12 +74,17 @@ const SliderContent = ({

useFilloutEvents(embed, { onInit, onPageChange, onSubmit });

const onIframeLoad = () => {
setLoading(false);
onReady?.();
};
Comment on lines +77 to +80

const sliderLeft = sliderDirection === "left";
const sliderOpen = !loading;

return (
<>
{loading && (
{loading && !hideLoading && (
<div
style={{
display: "flex",
Expand Down Expand Up @@ -122,7 +130,7 @@ const SliderContent = ({
height: !loading ? "100%" : 0,
opacity: !loading ? 1 : 0,
}}
onLoad={() => setLoading(false)}
onLoad={onIframeLoad}
/>
)}

Expand Down
7 changes: 6 additions & 1 deletion src/embeds/SliderButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState } from "react";
import { FormParams } from "../embed.js";
import { Slider, SliderDirection } from "./Slider.js";
import { Button, ButtonProps } from "../components/Button.js";
import { EventProps } from "../events.js";
import { EventProps, LoadingProps } from "../events.js";

type SliderButtonProps = {
filloutId: string;
Expand All @@ -11,6 +11,7 @@ type SliderButtonProps = {
parameters?: FormParams;
sliderDirection?: SliderDirection;
} & EventProps &
LoadingProps &
Omit<ButtonProps, "onClick">;

export const SliderButton = ({
Expand All @@ -19,10 +20,12 @@ export const SliderButton = ({
inheritParameters,
parameters,
sliderDirection,
hideLoading,

onInit,
onPageChange,
onSubmit,
onReady,

text,
color,
Expand Down Expand Up @@ -52,6 +55,8 @@ export const SliderButton = ({
onInit={onInit}
onPageChange={onPageChange}
onSubmit={onSubmit}
onReady={onReady}
hideLoading={hideLoading}
/>
</>
);
Expand Down
16 changes: 12 additions & 4 deletions src/embeds/Standard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,29 @@ import React, { useState } from "react";
import { FormParams, useFilloutEmbed } from "../embed.js";
import { Loading } from "../components/Loading.js";
import { useMessageListener } from "../messages.js";
import { EventProps, useFilloutEvents } from "../events.js";
import { EventProps, LoadingProps, useFilloutEvents } from "../events.js";

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

export const Standard = ({
filloutId,
domain,
inheritParameters,
parameters,
dynamicResize,
hideLoading,

onInit,
onPageChange,
onSubmit,
onReady,
}: StandardProps) => {
const [loading, setLoading] = useState(true);
const embed = useFilloutEmbed({
Expand All @@ -34,6 +37,11 @@ export const Standard = ({

useFilloutEvents(embed, { onInit, onPageChange, onSubmit });

const onIframeLoad = () => {
setLoading(false);
onReady?.();
};
Comment on lines +40 to +43

// dynamic resize
const [height, setHeight] = useState<number>();
useMessageListener(
Expand All @@ -60,7 +68,7 @@ export const Standard = ({
transition: dynamicResize ? "height 150ms ease" : undefined,
}}
>
{loading && (
{loading && !hideLoading && (
<div
style={{
display: "flex",
Expand All @@ -80,7 +88,7 @@ export const Standard = ({
src={embed.iframeUrl}
allow="microphone; camera; geolocation"
title="Embedded Form"
onLoad={() => setLoading(false)}
onLoad={onIframeLoad}
style={{
width: !loading ? "100%" : 0,
height: !loading ? "100%" : 0,
Expand Down
12 changes: 12 additions & 0 deletions src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ export type EventProps = {
onInit?: (submissionUuid: string) => void;
onPageChange?: (submissionUuid: string, pageId: string) => void;
onSubmit?: (submissionUuid: string, data?: Record<string, unknown>) => void;
// Fired once the embed has finished loading and the form is visible.
// Unlike the events above, this is not driven by a postMessage from the
// form — each embed component calls it when its iframe fires `load`,
// at the same moment the built-in loading indicator is hidden.
onReady?: () => void;
};

// Display options shared by every embed component.
export type LoadingProps = {
// Skip rendering the built-in <Loading /> indicator, so the consumer can
// show their own while waiting for `onReady`.
hideLoading?: boolean;
};

export const useFilloutEvents = (
Expand Down