Propel

Form

A native form that consolidates server-style field errors and lays out its field and action regions with consistent rhythm.

Basic

Select the resource profile for this server.

Storage type
Allowed protocols
Show 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 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 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 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

PropTypeRequiredDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, FormState>NoAllows 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.