Form
A native form that consolidates server-style field errors and lays out its field and action regions with consistent rhythm.
Basic
Show codeHide code
import { Button } from "@makeplane/propel/components/button";
import {
CheckboxGroupField,
CheckboxGroupFieldOption,
} from "@makeplane/propel/components/checkbox-group-field";
import { Form, FormActions, FormBody } from "@makeplane/propel/components/form";
import { InputField } from "@makeplane/propel/components/input-field";
import {
RadioGroupField,
RadioGroupFieldOption,
} from "@makeplane/propel/components/radio-group-field";
import { SelectField } from "@makeplane/propel/components/select-field";
import { SwitchField } from "@makeplane/propel/components/switch-field";
import * as React from "react";
const SERVER_TYPES = [
{ label: "General purpose", value: "general" },
{ label: "Compute optimized", value: "compute" },
{ label: "Memory optimized", value: "memory" },
];
type LaunchServerValues = {
allowedProtocols: string[];
homepage: string;
restartOnFailure: boolean;
serverType: string;
storageType: string;
};
export default function BasicDemo() {
const [homepage, setHomepage] = React.useState("https://example.com");
const [errors, setErrors] = React.useState<Record<string, string | string[]>>({});
return (
<Form<LaunchServerValues>
errors={errors}
onFormSubmit={(values) => {
if (values.homepage.includes("example.com")) {
setErrors({ homepage: "The example domain is not allowed." });
return;
}
setErrors({});
}}
>
<FormBody layout="single">
<InputField
magnitude="md"
orientation="vertical"
name="homepage"
label="Homepage"
type="url"
required
placeholder="https://plane.so"
value={homepage}
onValueChange={(nextHomepage) => {
setHomepage(nextHomepage);
if (errors.homepage) {
setErrors({});
}
}}
/>
<SelectField
name="serverType"
label="Server type"
magnitude="md"
options={SERVER_TYPES}
defaultValue="general"
required
description="Select the resource profile for this server."
/>
<RadioGroupField
name="storageType"
label="Storage type"
magnitude="md"
density="comfortable"
defaultValue="ssd"
required
>
<RadioGroupFieldOption value="ssd" label="SSD" />
<RadioGroupFieldOption value="hdd" label="HDD" />
</RadioGroupField>
<CheckboxGroupField
name="allowedProtocols"
label="Allowed protocols"
magnitude="md"
density="comfortable"
defaultValue={["https"]}
>
<CheckboxGroupFieldOption value="http" label="HTTP" />
<CheckboxGroupFieldOption value="https" label="HTTPS" />
<CheckboxGroupFieldOption value="ssh" label="SSH" />
</CheckboxGroupField>
<SwitchField
name="restartOnFailure"
label="Restart on failure"
magnitude="md"
defaultChecked
/>
</FormBody>
<FormActions layout="inline">
<Button
sizing="hug"
type="submit"
prominence="secondary"
tone="neutral"
magnitude="md"
label="Submit"
/>
</FormActions>
</Form>
);
}Async validation
The submit button holds its pending state while a server-style check resolves, then surfaces any returned error on the matching field.
Show codeHide code
import { Button } from "@makeplane/propel/components/button";
import { Form, FormActions, FormBody } from "@makeplane/propel/components/form";
import { InputField } from "@makeplane/propel/components/input-field";
import * as React from "react";
type CreateAccountValues = {
username: string;
};
function checkUsernameAvailability(username: string): Promise<{ error?: string }> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(username === "admin" ? { error: "This username is already taken." } : {});
}, 300);
});
}
export default function AsyncValidationDemo() {
const [pending, setPending] = React.useState(false);
const [errors, setErrors] = React.useState<Record<string, string | string[]>>({});
return (
<Form<CreateAccountValues>
errors={errors}
onFormSubmit={async (values) => {
setPending(true);
const { error } = await checkUsernameAvailability(values.username);
setPending(false);
if (error) {
setErrors({ username: error });
return;
}
setErrors({});
}}
>
<FormBody layout="single">
<InputField
magnitude="md"
orientation="vertical"
name="username"
label="Username"
required
defaultValue="admin"
placeholder="e.g. alice"
/>
</FormBody>
<FormActions layout="inline">
<Button
sizing="hug"
type="submit"
prominence="primary"
tone="neutral"
magnitude="md"
loading={pending}
label="Create account"
/>
</FormActions>
</Form>
);
}Server function
Submitting through the form action with React.useActionState maps the action’s returned errors back onto the field by name.
Show codeHide code
import { Button } from "@makeplane/propel/components/button";
import { Form, FormActions, FormBody } from "@makeplane/propel/components/form";
import { InputField } from "@makeplane/propel/components/input-field";
import * as React from "react";
type ReserveUsernameState = {
serverErrors?: Record<string, string | string[]>;
};
async function reserveUsername(
_previousState: ReserveUsernameState,
formData: FormData,
): Promise<ReserveUsernameState> {
await new Promise((resolve) => {
setTimeout(resolve, 300);
});
if (formData.get("username") === "admin") {
return { serverErrors: { username: "'admin' is reserved for system use." } };
}
return {};
}
export default function ServerFunctionDemo() {
const [state, formAction, pending] = React.useActionState(reserveUsername, {});
return (
<Form action={formAction} errors={state.serverErrors}>
<FormBody layout="single">
<InputField
magnitude="md"
orientation="vertical"
name="username"
label="Username"
required
defaultValue="admin"
placeholder="e.g. alice"
/>
</FormBody>
<FormActions layout="inline">
<Button
sizing="hug"
type="submit"
prominence="primary"
tone="neutral"
magnitude="md"
loading={pending}
label="Reserve username"
/>
</FormActions>
</Form>
);
}Schema validation
One schema-style parse consolidates per-field errors and applies them across every field at once.
Show codeHide code
import { Button } from "@makeplane/propel/components/button";
import { Form, FormActions, FormBody } from "@makeplane/propel/components/form";
import { InputField } from "@makeplane/propel/components/input-field";
import * as React from "react";
type ProfileValues = {
name: string;
age: string;
};
function parseProfile(values: ProfileValues): Record<string, string[]> {
const fieldErrors: Record<string, string[]> = {};
if (values.name.length === 0) {
fieldErrors.name = ["Name is required"];
}
const age = Number(values.age);
if (Number.isNaN(age)) {
fieldErrors.age = ["Age must be a number"];
} else if (age <= 0) {
fieldErrors.age = ["Age must be a positive number"];
}
return fieldErrors;
}
export default function SchemaValidationDemo() {
const [errors, setErrors] = React.useState<Record<string, string | string[]>>({});
return (
<Form<ProfileValues>
errors={errors}
onFormSubmit={(values) => {
const fieldErrors = parseProfile(values);
if (Object.keys(fieldErrors).length > 0) {
setErrors(fieldErrors);
return;
}
setErrors({});
}}
>
<FormBody layout="single">
<InputField
magnitude="md"
orientation="vertical"
name="name"
label="Name"
placeholder="Enter name"
/>
<InputField
magnitude="md"
orientation="vertical"
name="age"
label="Age"
placeholder="Enter age"
/>
</FormBody>
<FormActions layout="inline">
<Button
sizing="hug"
type="submit"
prominence="primary"
tone="neutral"
magnitude="md"
label="Submit"
/>
</FormActions>
</Form>
);
}Installation
import { Form, FormActions, FormBody } from "@makeplane/propel/components/form";
Props
Form
| Prop | Type | Required | Description |
|---|---|---|---|
| render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, FormState> | No | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |