# UI components

Source: https://developers.swell.is/apps/components

UI components are Preact components that an app adds to the Swell dashboard. A content or settings field with `type: "component"` renders one of the app's components in place of a standard input, and the dashboard saves the value the component sets along with the rest of the form. Use them for values that need their own editor, such as a color palette, a rule builder, or a map.

Each component runs in a frame on the app installation's own domain, separate from the dashboard page, and can call the app's functions on behalf of the admin user who is signed in.

### Creating a component

A component is a `.tsx` or `.jsx` file directly in the app's `components/` folder. You can create one with the following CLI command:

```txt
swell create component BrandColor
```

The command writes `components/BrandColor.tsx`. It also adds `preact` to the app's dependencies and `@swell/apps-sdk` to its dev dependencies, and creates `components/tsconfig.json` to type-check components with browser libraries. Then run `npm install`.

→ See the [CLI reference](https://developers.swell.is/apps/cli) for more details and options.

The component is the file's default export, and receives the field's value and the functions to change it as props:

**components/BrandColor.tsx**

```typescript
import type { ComponentProps } from '@swell/apps-sdk/components';

export const config = {
  description: 'Choose a brand color from a palette',
};

export default function BrandColor({
  value,
  setValue,
  readonly,
  params,
}: ComponentProps<string | null>) {
  const palette = (params.palette as string[] | undefined) ?? ['#111827', '#2563eb'];

  return (
    <div style={{ display: 'flex', gap: '8px' }}>
      {palette.map((color) => (
        <button
          key={color}
          type="button"
          title={color}
          aria-pressed={value === color}
          disabled={readonly}
          onClick={() => setValue(color)}
          style={{
            width: '32px',
            height: '32px',
            borderRadius: '50%',
            background: color,
            border: value === color ? '3px solid #2563eb' : '1px solid #d1d5db',
          }}
        />
      ))}
    </div>
  );
}
```

Import only the `ComponentProps` type from `@swell/apps-sdk/components`. The frame provides the SDK's runtime, so it isn't part of the component's build.

The file name without its extension is the component's name, so `components/BrandColor.tsx` is `BrandColor`. Names are case-sensitive. Use letters and digits: the CLI pushes an underscore in a file name as a hyphen, so `brand_color.tsx` is pushed as `brand-color`.

Other files under `components/`, such as `.ts` files and subfolders, are shared code that components can import. They aren't components themselves.

#### Config

A component can export a `config` object with a `description`. The CLI reads `config` without running the file, so it must be an object literal with literal values. References to variables, spreads, and computed keys are rejected when you push.

Don't set `extension` in the config of a field component. `extension` marks a checkout component of a payment extension, which the CLI builds differently. See [Extensions](https://developers.swell.is/apps/extensions).

### Adding a component field

To use a component, add a field with `type: "component"` to a content or settings file, and set `component` to the component's name. Set `params` to an object to configure the component for this field. The component receives it as the `params` prop:

**content/products.json**

```json
{
  "collection": "products",
  "fields": [
    {
      "id": "brand_color",
      "type": "component",
      "label": "Brand color",
      "component": "BrandColor",
      "required": true,
      "params": {
        "palette": ["#111827", "#2563eb", "#f59e0b"]
      }
    }
  ],
  "views": [
    {
      "id": "edit",
      "tabs": [
        {
          "id": "branding",
          "label": "Branding",
          "fields": [{ "id": "brand_color" }]
        }
      ]
    }
  ]
}
```

This example adds a Branding tab to product pages, with the `BrandColor` component as its field. Component fields also support the common field properties, such as `label`, `description`, `required`, `readonly`, `default`, `conditions`, and `admin_span`. They don't support `ui`.

Component fields can be used in:

- Content files, on the record pages of app collections, and in the app's tabs and sections on standard records such as products and orders. This includes fields inside `field_group`, `field_row`, and the rows of a `collection` field.
- [Settings](https://developers.swell.is/apps/settings) files, on the app's settings page and in the settings of its extensions.
- Action dialogs, in `modal.fields`. See [Actions](https://developers.swell.is/apps/actions).

In a view, refer to a component field by its `id`. The view takes `component` and `params` from the content field. Only apps can define component fields.

`swell app push` checks that the component fields in content and settings files name components the app has. The CLI pushes components before content and settings, so a new component and the field that uses it can be pushed together. Fields in action dialogs aren't checked when you push. A dialog field that names a missing component shows an error when the dialog opens.

#### Stored values

A component field stores its value as a string, unless the app's data model declares the field with another type. To store an object or a list, declare the field in the app's [models](https://developers.swell.is/apps/models):

**models/products.json**

```json
{
  "collection": "products",
  "fields": {
    "shipping_rules": {
      "type": "array",
      "value_type": "object"
    }
  }
}
```

When the field stores a string, the dashboard refuses other values. If a component sets a value that isn't a string, such as a number or an object, the value doesn't change, and the field shows a message such as `The component sent a number; this field stores text.`

Settings values are always stored as strings, so a component in a settings file should only set strings. In an action dialog, the value is sent to the function as the component set it.

An empty field's value is `null`, and setting the value to `null` clears the field. In list views, a component field's column shows its value when it's a string, a number, or a boolean. An object or a list shows an empty column, unless the view field sets a `template`.

### Props

Components receive the following props. Type them with `ComponentProps<TValue, TContext>`, for example `ComponentProps<string | null>`.

| Prop | Description |
| --- | --- |
| value | The field's value, or null when the field is empty. |
| setValue(value) | Changes the field's value. The component re-renders with the new value, and the dashboard saves it with the rest of the form. Use JSON values. |
| setValidity(error) | Marks the field invalid with a message, or valid again with null. See Validation. |
| readonly | true while the field can't be edited. The dashboard ignores setValue while it's true. |
| params | The field's params, or an empty object. |
| context | An object with record, the record being edited including unsaved changes, and field, with the field's id, label, required, and type. type is the type the value is stored as, such as string. On the settings page, record holds the settings being edited. |
| settings | Values of the app's settings fields that set public: true, grouped by settings file. Fields that are empty, false, or 0 are left out. |
| locale | The store's locale. |
| fetch | Works like fetch, and adds a token to requests to the component's own domain. See Calling app functions. |
| on(event, handler) | Handles an event from the page that shows the component. The Swell dashboard doesn't send events. |

The dashboard sends changes to `value`, `context`, and `readonly` as they happen, so a component re-renders when the admin user edits another field.

#### Validation

When the field sets `required: true`, the dashboard doesn't save the form while the value is empty: `null`, an empty string, an empty list, or `false`. For other rules, call `setValidity` with a message while the value is invalid, and with `null` once it's valid:

**Validating the value**

```typescript
import { useEffect } from 'preact/hooks';

// In the component
useEffect(() => {
  setValidity(value && !palette.includes(value) ? 'Choose a color from the palette' : null);
}, [value]);
```

When the admin user saves, the dashboard shows the message below the field and doesn't save the form. When the first invalid field is a component, the dashboard scrolls to it and focuses its first control.

### Layout

The frame is as wide as the field, and its height follows the component's content. The frame doesn't inherit the dashboard's styles or fonts, and its background is transparent. Components can't import CSS or other non-JavaScript files, so use inline styles or a `<style>` element.

Content drawn outside the component's own box, such as an absolutely positioned menu, is clipped at the edges of the frame. Keep menus in the component's layout, or use native controls such as `<select>`, which the browser draws outside the frame.

#### Modals

To open a modal over the dashboard, render an element with `position: fixed` and `inset: 0` as a direct child of `document.body`, for example with `createPortal`. While it's open, the frame covers the dashboard window, the rest of the dashboard can't be used, and keyboard focus moves into the modal. Removing the element closes it.

**components/NotesEditor.tsx**

```typescript
import { useState } from 'preact/hooks';
import { createPortal } from 'preact/compat';
import type { ComponentProps } from '@swell/apps-sdk/components';

export default function NotesEditor({ value, setValue, readonly }: ComponentProps<string | null>) {
  const [open, setOpen] = useState(false);

  return (
    <div>
      <button type="button" disabled={readonly} onClick={() => setOpen(true)}>
        Edit notes
      </button>
      {open &&
        createPortal(
          <div
            style={{
              position: 'fixed',
              inset: 0,
              display: 'grid',
              placeItems: 'center',
              background: 'rgba(0, 0, 0, 0.4)',
            }}
          >
            <div role="dialog" aria-modal="true" style={{ background: '#fff', padding: '24px' }}>
              <textarea
                rows={10}
                value={value ?? ''}
                onInput={(event) => setValue(event.currentTarget.value)}
              />
              <button type="button" onClick={() => setOpen(false)}>
                Done
              </button>
            </div>
          </div>,
          document.body,
        )}
    </div>
  );
}
```

Dialogs that a library adds to `document.body` the same way, such as a payment provider's 3-D Secure challenge, also cover the dashboard.

#### Lifecycle

The dashboard can remove a component's frame and create it again, for example when the admin user switches tabs. The component then starts again with the field's current value, so keep anything that must persist in the value.

Tab and Shift+Tab move focus into the component and out to the fields around it. The frame can use the `payment` and `publickey-credentials-get` browser features. Features that a frame needs other permissions for, such as the camera, aren't available.

### Calling app functions

A component calls the app's functions with its `fetch` prop, at `/functions/<app-id>/<function-name>` on its own domain, where `<app-id>` is the `id` in swell.json:

**Calling a function from a component**

```typescript
const [palette, setPalette] = useState<string[]>([]);

useEffect(() => {
  fetch('/functions/my_app/brand-palette')
    .then((response) => response.json())
    .then((data: { palette: string[] }) => setPalette(data.palette));
}, []);
```

The `fetch` prop adds a token to requests to the component's own domain. With the token, a request can call any function that has a `route`, without an API key and without `route.public`. Functions without a `route`, such as action and model event functions, can't be called this way. Responses to these requests aren't cached. The global `fetch` doesn't send the token.

The function receives the caller in `req.swellContext`. Its `storeUser.userId` is the admin user the component runs for, and its `surface` is `admin`. The platform verifies the token before it calls the function. Check `surface` and `storeUser` before doing anything only a store user may do:

**functions/brand-palette.ts**

```typescript
export const config: SwellConfig = {
  description: 'Palette for the BrandColor component',
  route: {
    methods: ['get'],
  },
};

export default async function (req: SwellRequest) {
  const context = req.swellContext;

  // Only for store users, through one of this app's components
  if (context?.surface !== 'admin' || !context.storeUser) {
    throw new SwellError('Forbidden', { status: 403 });
  }

  return { palette: ['#111827', '#2563eb', '#f59e0b'] };
}
```

Any admin user who can open the page gets a token, whatever their role. If a function should only run for some users, check `storeUser.userId`. See [Functions](https://developers.swell.is/apps/functions) for the other properties of `req.swellContext`.

Requests to other paths on the component's domain go to the app's [frontend](https://developers.swell.is/apps/frontend), with a `Swell-Context` header that identifies the admin user and has a `surface` claim of `admin`. The paths `/.swell/components` and `/.swell/sdk` on this domain are reserved for components.

### Local development

While `swell app dev` runs, the dashboard loads the app's components from your machine instead of the pushed build. A component must have been pushed once for this, which `swell app dev` does when it starts. Each frame loads its component once, so to see a change, reopen the record or reload the page. After adding a component, reload the page.

When you push, the CLI bundles each component with everything it imports, including Preact and other npm packages, into a single minified module. Imports of `react` and `react-dom` use `preact/compat`. The build doesn't check types, so check them with `tsc --noEmit -p components`. Unchanged components aren't pushed again, so after updating a dependency such as `preact`, run `swell app push --force`.

### Errors

`swell app push` rejects components and component fields with these errors. The messages say "Content field" for settings fields too.

| Message | Cause |
| --- | --- |
| Content field '<id>' uses component '<name>', which the app does not have. Add components/<name>.tsx (or .jsx) and push it. | The field names a component that hasn't been pushed. Names are case-sensitive, and an underscore in a file name is pushed as a hyphen. |
| Content field '<id>' property 'component' must be the name of a component in the app's components folder | component is missing, or contains characters other than letters, digits, hyphens, and underscores. |
| Content field '<id>' property 'params' must be an object | params isn't an object. |
| Unable to compile component <name> | The component failed to build. The message continues with the cause, such as an imported CSS file or a config that isn't static. |

In the dashboard, an error in a component shows below its field, with the error's message, and doesn't stop the admin user from saving the rest of the form. The platform reports these errors:

| Message | Cause |
| --- | --- |
| Component "<name>" not found in app "<app>" | The installed app has no pushed component with that name. |
| Component frame did not start | The component's frame didn't load within 10 seconds. |
| The component sent a number; this field stores text. | The component set a value that isn't a string, for a field stored as a string. The message names the kind of value: a number, a boolean, an object, or a list. |
