Create flow in a dialog
The shortest real call: a form, a title, the fields. Submit it empty to see the field errors, with a name ending in “Test” to see a refusal, or with a name to save and close.
import { useState } from 'react';
import type { ReactNode } from 'react';
import type { Key } from 'react-aria-components';
import { Button, SelectItem } from '@arbol-ai/ui';
import { FormDialog, useAppForm } from '@arbol-ai/ui/form';
const roundTrip = async () => {
// oxlint-disable-next-line promise/avoid-new -- a stand-in for the server the examples do not have.
await new Promise((resolve) => {
setTimeout(resolve, 900);
});
};
const Opener = ({ label, children }: { label: string; children: (close: () => void) => ReactNode }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button
onPress={() => {
setIsOpen(true);
}}
variant="outline"
>
{label}
</Button>
{isOpen
? children(() => {
setIsOpen(false);
})
: null}
</>
);
};
const NewContactExample = () => (
<Opener label="New contact">{(close) => <NewContactDialog onClose={close} />}</Opener>
);
const NewContactDialog = ({ onClose }: { onClose: () => void }) => {
const form = useAppForm({
defaultValues: { name: '', phone: '', channel: null as Key | null },
onSubmit: async () => {
await roundTrip();
},
validators: {
onSubmit: ({ value }) =>
value.name.endsWith('Test') ? 'Test contacts are turned off for this clinic.' : undefined,
},
});
return (
<FormDialog form={form} onClose={onClose} submitLabel="Add contact" title="New contact">
<form.AppField
name="name"
validators={{ onSubmit: ({ value }) => (value.trim() === '' ? 'Enter a name.' : undefined) }}
>
{(field) => <field.TextField isRequired label="Name" />}
</form.AppField>
<form.AppField
name="phone"
validators={{
onSubmit: ({ value }) =>
/^\+?[\d\s-]{7,}$/u.test(value) ? undefined : 'Enter a phone like +57 300 555 0101.',
}}
>
{(field) => <field.TextField isRequired label="Phone" type="tel" />}
</form.AppField>
<form.AppField name="channel">
{(field) => (
<field.Select label="Preferred channel">
<SelectItem id="whatsapp">WhatsApp</SelectItem>
<SelectItem id="phone">Phone call</SelectItem>
</field.Select>
)}
</form.AppField>
</FormDialog>
);
};
<NewContactExample />Editor in a sheet
Change the note, then press Esc to see the discard question. Delete, at the start of the footer, asks for the name first.
import { TrashIcon } from '@phosphor-icons/react';
import { useState } from 'react';
import type { ReactNode } from 'react';
import { Button, DeleteDialog, OverlayTrigger } from '@arbol-ai/ui';
import { FormSheet, useAppForm } from '@arbol-ai/ui/form';
const roundTrip = async () => {
// oxlint-disable-next-line promise/avoid-new -- a stand-in for the server the examples do not have.
await new Promise((resolve) => {
setTimeout(resolve, 900);
});
};
const Opener = ({ label, children }: { label: string; children: (close: () => void) => ReactNode }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button
onPress={() => {
setIsOpen(true);
}}
variant="outline"
>
{label}
</Button>
{isOpen
? children(() => {
setIsOpen(false);
})
: null}
</>
);
};
const ContactEditorExample = () => (
<Opener label="Edit Ana Torres">{(close) => <ContactEditor onClose={close} />}</Opener>
);
const ContactEditor = ({ onClose }: { onClose: () => void }) => {
const form = useAppForm({
defaultValues: { note: 'Prefers mornings.', receivesCampaigns: true },
onSubmit: async () => {
await roundTrip();
},
});
return (
<FormSheet
description="Changes apply to every campaign."
footerStart={
<OverlayTrigger>
<Button aria-label="Delete contact" size="icon" variant="ghost">
<TrashIcon aria-hidden />
</Button>
<DeleteDialog
body="Their conversations stay; they stop receiving campaigns."
name="Ana Torres"
onDelete={(close) => {
close();
onClose();
}}
/>
</OverlayTrigger>
}
form={form}
onClose={onClose}
size="md"
submitLabel="Save changes"
title="Ana Torres"
>
<form.AppField name="note">
{(field) => <field.TextField description="Only your team sees it." label="Note" multiline />}
</form.AppField>
<form.AppField name="receivesCampaigns">
{(field) => <field.Switch>Receives campaigns</field.Switch>}
</form.AppField>
</FormSheet>
);
};
<ContactEditorExample />Two steps
closeOnSubmit={false} on the first step and onBack on the second: Continue moves on, Back returns, and the last step closes once saved.
import { useState } from 'react';
import type { ReactNode } from 'react';
import { Button } from '@arbol-ai/ui';
import { FormDialog, useAppForm } from '@arbol-ai/ui/form';
const roundTrip = async () => {
// oxlint-disable-next-line promise/avoid-new -- a stand-in for the server the examples do not have.
await new Promise((resolve) => {
setTimeout(resolve, 900);
});
};
const Opener = ({ label, children }: { label: string; children: (close: () => void) => ReactNode }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button
onPress={() => {
setIsOpen(true);
}}
variant="outline"
>
{label}
</Button>
{isOpen
? children(() => {
setIsOpen(false);
})
: null}
</>
);
};
const MergeExample = () => (
<Opener label="Merge contacts">{(close) => <MergeDialog onClose={close} />}</Opener>
);
const MergeDialog = ({ onClose }: { onClose: () => void }) => {
const [step, setStep] = useState<'pick' | 'confirm'>('pick');
const form = useAppForm({
defaultValues: { duplicate: '' },
onSubmit: async () => {
if (step === 'pick') {
setStep('confirm');
return;
}
await roundTrip();
},
});
return (
<FormDialog
closeOnSubmit={step === 'confirm'}
description={
step === 'pick'
? 'Who is the same person as Ana Torres?'
: 'Their conversations, phones and facts move to Ana Torres.'
}
form={form}
onBack={
step === 'confirm'
? () => {
setStep('pick');
}
: undefined
}
onClose={onClose}
submitLabel={step === 'pick' ? 'Continue' : 'Merge contacts'}
title="Merge contacts"
>
{step === 'pick' ? (
<form.AppField
name="duplicate"
validators={{ onSubmit: ({ value }) => (value.trim() === '' ? 'Enter who to merge.' : undefined) }}
>
{(field) => <field.TextField description="Their name or phone." label="Duplicate contact" />}
</form.AppField>
) : (
<p className="text-sm">The duplicate is removed once everything moved.</p>
)}
</FormDialog>
);
};
<MergeExample />Source
The examples above are src/form/form-dialog.examples.tsx, which the library’s tests render too.