Autocomponents 

Gadget provides high-level, super-automatic components for assembling working user interfaces very quickly called autocomponents. Autocomponents are React components that implement forms and tables that can read and write to Gadget data in your app's database. Autocomponents use a design system under the hood which means they look and feel like the rest of your app.

When to use autocomponents 

Autocomponents are intended to be high-quality interfaces for common tasks. While autocomponents offer a remarkably high degree of customizability, they are designed to help you build fast rather than build perfect. They are not intended to be a replacement for fully custom or artisanally designed user interfaces. If you need fine-grained control over your UX, Gadget recommends using the headless hooks that power autocomponents with head-ful design elements on top!

Installing autocomponents 

Because autocomponents use a design system, you need to install the design system's dependencies to use them.

Currently, Gadget supports the following design systems:

Support for Material UI is coming soon!

Autocomponents are only available in @gadgetinc/react version 0.16.0 and later. To upgrade to the latest version, run the following in the Gadget command palette:

Upgrade @gadgetinc/react to >0.16.0
yarn add @gadgetinc/react

Installing Shopify Polaris autocomponents 

If you did not select the Shopify app type when creating your app, you will need to install Shopify Polaris and set up the Polaris provider and stylesheet in your app.

Install the following dependencies:

install the Polaris library and the latest version of @gadgetinc/react
yarn add @shopify/polaris @shopify/polaris-icons

Now import the Polaris provider and required style.css file. Add the following imports to the top of your web/components/App.jsx file:

web/components/App.jsx
React
import { AppProvider } from "@shopify/polaris";
import "@shopify/polaris/build/esm/styles.css";
import { AppProvider } from "@shopify/polaris";
import "@shopify/polaris/build/esm/styles.css";

Then replace your Layout component in the same file with the following code to make use of the Polaris provider:

web/components/App.jsx
React
1const Layout = () => {
2 const navigate = useNavigate();
3
4 return (
5 <AppProvider>
6 <Provider api={api} navigate={navigate} auth={window.gadgetConfig.authentication}>
7 <Header />
8 <div className="app">
9 <div className="app-content">
10 <div className="main">
11 <Outlet />
12 </div>
13 </div>
14 </div>
15 </Provider>
16 </AppProvider>
17 );
18};
1const Layout = () => {
2 const navigate = useNavigate();
3
4 return (
5 <AppProvider>
6 <Provider api={api} navigate={navigate} auth={window.gadgetConfig.authentication}>
7 <Header />
8 <div className="app">
9 <div className="app-content">
10 <div className="main">
11 <Outlet />
12 </div>
13 </div>
14 </div>
15 </Provider>
16 </AppProvider>
17 );
18};

<AutoForm /> 

<AutoForm /> renders a form that calls one of your app's backend API actions. On form submit, <AutoForm /> can call one of your model actions such as create, update, or a custom action. Global actions can also be run by <AutoForm />.

<AutoForm /> renders the correct HTML input element for each backend field and includes support for form validation, relationship fields, error handling, autocomplete, and file uploads.

For example, if you have a backend widget model with a name and inventoryCount field, you can render a form to create a new widget like so:

web/components/example.jsx
React
1// Render an AutoForm using Shopify's Polaris design system
2import { AutoForm } from "@gadgetinc/react/auto/polaris";
3import { api } from "../api";
4
5export default function Example() {
6 return <AutoForm action={api.widget.create} />;
7}
1// Render an AutoForm using Shopify's Polaris design system
2import { AutoForm } from "@gadgetinc/react/auto/polaris";
3import { api } from "../api";
4
5export default function Example() {
6 return <AutoForm action={api.widget.create} />;
7}

AutoForm components can also be given an explicit set of children which lets you re-order, wrap, and replace child fields as needed:

web/components/example.jsx
React
1import { AutoForm, AutoInput, AutoSubmit } from "@gadgetinc/react/auto/polaris";
2import { InlineStack } from "@shopify/polaris";
3import { api } from "../api";
4
5export default function Example() {
6 return (
7 <AutoForm action={api.widget.create}>
8 <InlineStack gap="400">
9 {/** put these two auto inputs in a wrapper component to align them horizontally */}
10 <AutoInput field="name" />
11 <AutoInput field="inventoryCount" />
12 </InlineStack>
13 {/** render a submit button */}
14 <AutoSubmit />
15 </AutoForm>
16 );
17}
1import { AutoForm, AutoInput, AutoSubmit } from "@gadgetinc/react/auto/polaris";
2import { InlineStack } from "@shopify/polaris";
3import { api } from "../api";
4
5export default function Example() {
6 return (
7 <AutoForm action={api.widget.create}>
8 <InlineStack gap="400">
9 {/** put these two auto inputs in a wrapper component to align them horizontally */}
10 <AutoInput field="name" />
11 <AutoInput field="inventoryCount" />
12 </InlineStack>
13 {/** render a submit button */}
14 <AutoSubmit />
15 </AutoForm>
16 );
17}

Using <AutoForm /> without specifying the included fields or explicitly passing children will render a form with all fields from the model or all params for an action. This means that adding a new field to a model or params to a global action will also add an input for that field to the form.

Make sure you test any default <AutoForm /> components after making changes to your models or actions to ensure they still work as expected.

Import AutoForm 

Import Shopify Polaris <AutoForm> components from @gadgetinc/react/auto/polaris:

import the AutoForm components into your frontend files
React
import { AutoForm, AutoInput, AutoSubmit } from "@gadgetinc/react/auto/polaris";
import { AutoForm, AutoInput, AutoSubmit } from "@gadgetinc/react/auto/polaris";

Choosing fields for the form 

By default, <AutoForm /> will render input components to set or edit data for each field on a model record, or each global action parameter. Form inputs can be removed, re-ordered, or grouped.

To include only some of the fields of a model or action in your form, pass the include option with a list of fields to render:

render only the name and inventoryCount fields
React
<AutoForm action={api.widget.create} include={["name", "inventoryCount"]} />
<AutoForm action={api.widget.create} include={["name", "inventoryCount"]} />

include can also be used to quickly re-order the list of fields, as fields will be rendered in the order specified in the include list:

render the inventoryCount field before the name field
React
<AutoForm action={api.widget.create} include={["inventoryCount", "name"]} />
<AutoForm action={api.widget.create} include={["inventoryCount", "name"]} />

To exclude some fields from your model and render all the others, pass the exclude options with a list of fields to omit from the form:

exclude some fields
React
<AutoForm action={api.widget.create} exclude={["inventoryCount"]} />
<AutoForm action={api.widget.create} exclude={["inventoryCount"]} />

You cannot have include and exclude defined for the same form.

<AutoForm /> does not currently support custom params with the object or array types.

AutoForm rich text fields 

The auto-generated input for rich text fields in AutoForms require an extra dependency to be installed:

install the rich text editor dependency
yarn add -D @mdxeditor/editor

Then import the stylesheet into your app:

web/components/App.jsx
React
import "@mdxeditor/editor/style.css";
import "@mdxeditor/editor/style.css";

Custom form components 

<AutoForm /> can be passed an explicit set of children components or HTML elements to render instead of the default auto-generated components. This allows full control of field order and layout, and provides the ability to mix and match autocomponents with custom inputs.

To begin customizing the children, open the <AutoForm /> tag and pass the children components or HTML elements you'd like to render, like so:

add custom components or HTML elements to an AutoForm
React
<AutoForm action={api.widget.create}>
<h1>My form title</h1>
{/* more form elements*/}
</AutoForm>
<AutoForm action={api.widget.create}>
<h1>My form title</h1>
{/* more form elements*/}
</AutoForm>

To keep your form looking like the fully-automatic version of an <AutoForm/>, replace each field with an <AutoInput field="..."/> element and add an <AutoSubmit/> element:

render only the name and inventoryCount fields with AutoInput children
React
<AutoForm action={api.widget.create}>
<AutoInput field="name" />
<AutoInput field="inventoryCount" />
<AutoSubmit />
</AutoForm>
<AutoForm action={api.widget.create}>
<AutoInput field="name" />
<AutoInput field="inventoryCount" />
<AutoSubmit />
</AutoForm>

Custom form title 

<AutoForm /> has a title prop that can be used to override the default form title. This is useful when you want a custom form title but want to use the auto-generated input components for a form:

set a custom form title
React
<AutoForm action={api.widget.create} title="The widget factory" />
<AutoForm action={api.widget.create} title="The widget factory" />

The title can also be hidden by passing title={false}:

remove the form title
React
<AutoForm action={api.widget.create} title={false} />
<AutoForm action={api.widget.create} title={false} />

Inputs with <AutoInput/> 

<AutoInput /> renders an automatic input for the provided field, selecting the right kind of control, formatting, and experience for inputting data for a given field type.

<AutoForm/>'s automatic input selection uses <AutoInput/> under the hood, so if you want to take over and customize the layout of your form while using the same input components, you can keep using a fully automatic version of each field with <AutoInput/>.

For example, you can convert the form from the previous section to use explicit children like so:

AutoForm vs manually built AutoForm using AutoInput and AutoSubmit
React
1// before
2() => <AutoForm action={api.widget.create} include={["name", "inventoryCount"]} />;
3
4// after
5() => (
6 <AutoForm action={api.widget.create}>
7 <AutoInput field="name" />
8 <AutoInput field="inventoryCount" />
9 <AutoSubmit />
10 </AutoForm>
11);
1// before
2() => <AutoForm action={api.widget.create} include={["name", "inventoryCount"]} />;
3
4// after
5() => (
6 <AutoForm action={api.widget.create}>
7 <AutoInput field="name" />
8 <AutoInput field="inventoryCount" />
9 <AutoSubmit />
10 </AutoForm>
11);

The field prop for each <AutoInput/> specifies the model field to accept input for. You can also add HTML elements or layout components, such as the Polaris FormLayout component:

render an AutoForm using a @shopify/polaris FormLayout component
React
1() => (
2 <AutoForm action={api.widget.create}>
3 <FormLayout>
4 <FormLayout.Group>
5 <AutoInput field="name" />
6 <AutoInput field="inventoryCount" />
7 </FormLayout.Group>
8 </FormLayout>
9 <AutoSubmit />
10 </AutoForm>
11);
1() => (
2 <AutoForm action={api.widget.create}>
3 <FormLayout>
4 <FormLayout.Group>
5 <AutoInput field="name" />
6 <AutoInput field="inventoryCount" />
7 </FormLayout.Group>
8 </FormLayout>
9 <AutoSubmit />
10 </AutoForm>
11);

Customizing Shopify Polaris <AutoInput /> 

If you would like to have more control over the input's appearance, you can use the individual input types directly:

manually select the input component that is used
React
1// before
2() => (
3 <AutoForm action={api.widget.create}>
4 <AutoInput field="name" />
5 <AutoInput field="inventoryCount" />
6 <AutoSubmit />
7 </AutoForm>
8);
9
10// after
11() => (
12 <AutoForm action={api.widget.create}>
13 <AutoStringInput field="name" />
14 <AutoNumberInput field="inventoryCount" />
15 <AutoSubmit />
16 </AutoForm>
17);
1// before
2() => (
3 <AutoForm action={api.widget.create}>
4 <AutoInput field="name" />
5 <AutoInput field="inventoryCount" />
6 <AutoSubmit />
7 </AutoForm>
8);
9
10// after
11() => (
12 <AutoForm action={api.widget.create}>
13 <AutoStringInput field="name" />
14 <AutoNumberInput field="inventoryCount" />
15 <AutoSubmit />
16 </AutoForm>
17);

These inputs map 1:1 to the corresponding Polaris input and can accept any parameters that the Polaris input could:

example using Polaris params on an AutoStringInput
React
1() => (
2 <AutoForm action={api.widget.create}>
3 <AutoStringInput field="name" selectTextOnFocus clearButton />
4 <AutoSubmit />
5 </AutoForm>
6);
1() => (
2 <AutoForm action={api.widget.create}>
3 <AutoStringInput field="name" selectTextOnFocus clearButton />
4 <AutoSubmit />
5 </AutoForm>
6);

To see a full list of input components available, see the autocomponent reference.

Call update on submit and fetch existing records 

Autoform can also be used for update actions.

An existing record can be automatically fetched by passing the record id into the findBy prop. You need to make sure your current user role has read access to the model you are trying to fetch.

render a form for updating a widget with id=123
React
<AutoForm action={api.widget.update} findBy="123" />
<AutoForm action={api.widget.update} findBy="123" />

To fetch an existing record by something other than id, pass the findBy prop a set of conditions to find by:

render a form for updating a widget with slug="foobar"
React
<AutoForm action={api.widget.update} findBy={{ slug: "foobar" }} />
<AutoForm action={api.widget.update} findBy={{ slug: "foobar" }} />

When using a findBy for a field other than id, it requires a by-field record finder like .findBySlug to exist for your model, which is generated by adding a uniqueness validation to a field.

Call custom model actions on submit 

<AutoForm /> can also be used to call custom model actions. Appropriate input components will be rendered for any custom params defined for the action. These custom actions require a record to be passed in, so you must pass the findBy prop to fetch the record to run the action on.

example of a custom action called on form submit
React
<AutoForm action={api.widget.reconfigure} findBy="123" />
<AutoForm action={api.widget.reconfigure} findBy="123" />

For example, if you have a custom widget.reconfigure action that has state: string and value: number params defined, the default <AutoForm> will render with a text input for name and a number input for value.

A screenshot of a form for the Upgrade Widget action, with a text input for the Upgrade state param, a number input for the Value param, and a button for form submission.

To fetch an existing record by something other than id, pass the findBy prop a set of conditions to find by:

render a form that runs a custom action for widget record with name="foobar"
React
<AutoForm action={api.widget.reconfigure} findBy={{ name: "foobar" }} />
<AutoForm action={api.widget.reconfigure} findBy={{ name: "foobar" }} />

Call a global action on submit 

<AutoForm /> can also be used to call global actions. Appropriate input components will be rendered for any custom params defined for the action.

example of a global action called on form submit
React
<AutoForm action={api.sendEmail} />
<AutoForm action={api.sendEmail} />

Setting initial values for create forms 

Forms for create actions will automatically populate with default values set for fields in your backend Gadget models.

For example, if you have set the default inventoryCount for the widget model to 0 in the model schema page, the form will start with the inventoryCount field set to 0.

If you'd like to pre-populate the form with specific values when creating new records, you can also pass the defaultValues prop to set explicit initial values for the form:

set default form values explicitly
React
1<AutoForm
2 action={api.widget.create}
3 defaultValues={{
4 widget: { name: "foobar", inventoryCount: 0 },
5 }}
6/>
1<AutoForm
2 action={api.widget.create}
3 defaultValues={{
4 widget: { name: "foobar", inventoryCount: 0 },
5 }}
6/>

defaultValues must be passed in the fully-qualified form that includes the model name, like widget.name, instead of just name. This wrapper object allows the form to store other state beyond just the model data, and matches your app's API format under the hood.

correct form for defaultValues
React
<AutoForm defaultValues={{ widget: { name: "foobar", inventoryCount: 0 } }} />
<AutoForm defaultValues={{ widget: { name: "foobar", inventoryCount: 0 } }} />

Don't do this:

incorrect form for defaultValues
React
<AutoForm defaultValues={{ name: "foobar", inventoryCount: 0 }} />
<AutoForm defaultValues={{ name: "foobar", inventoryCount: 0 }} />

Hidden inputs 

You can submit hardcoded hidden values along with the form for values you do not want the user to edit.

render a form with a hidden status field
React
1import { AutoForm, AutoInput, AutoHiddenInput, AutoSubmit } from "@gadgetinc/react/auto/polaris";
2
3() => (
4 <AutoForm action={api.widget.create}>
5 <AutoInput field="name" />
6 <AutoHiddenInput field="status" value="started" />
7 <AutoSubmit />
8 </AutoForm>
9);
1import { AutoForm, AutoInput, AutoHiddenInput, AutoSubmit } from "@gadgetinc/react/auto/polaris";
2
3() => (
4 <AutoForm action={api.widget.create}>
5 <AutoInput field="name" />
6 <AutoHiddenInput field="status" value="started" />
7 <AutoSubmit />
8 </AutoForm>
9);

Form validation 

Validation is automatically applied to all <AutoForm/> inputs. The default validation rules are set using the field validation rules set on a model's schema.

Validating custom params 

For custom parameters on model or global actions, you can manually validate params passed into the action from an <AutoForm />. If a value is invalid, you can return an InvalidRecordError with a message to display to the user.

This validation is done in your actions, not in the frontend.

Here's a sample action that validates a custom params object:

validate custom params on a global action
JavaScript
1// import InvalidRecordError from gadget-server
2import { InvalidRecordError } from "gadget-server";
3
4export const run: ActionRun = async ({ params, logger, api, connections }) => {
5 // validate the param manually
6 if (params.count < 0 || params.count > 100) {
7 throw new InvalidRecordError("count param not valid", [
8 { apiIdentifier: "customUpload", message: "count must be between 0 and 100" },
9 ]);
10 }
11
12 // ... custom action code
13};
14
15export const params = {
16 count: { type: "number" },
17};
1// import InvalidRecordError from gadget-server
2import { InvalidRecordError } from "gadget-server";
3
4export const run: ActionRun = async ({ params, logger, api, connections }) => {
5 // validate the param manually
6 if (params.count < 0 || params.count > 100) {
7 throw new InvalidRecordError("count param not valid", [
8 { apiIdentifier: "customUpload", message: "count must be between 0 and 100" },
9 ]);
10 }
11
12 // ... custom action code
13};
14
15export const params = {
16 count: { type: "number" },
17};

You can call this action from an <AutoForm /> and the validation error will be displayed:

calling the action with custom params
React
<AutoForm action={api.customAction} />
<AutoForm action={api.customAction} />

To learn about setting validations on fields, see the field validation guide.

<AutoForm /> does not support custom client-side validation.

Manual form control using react-hook-form 

If you need to manually control the form state, you can use the react-hook-form hooks directly. This is useful if you need to integrate with form state that is not provided by <AutoForm />, or if you need to manually control the form state for some other reason.

For example, I want to use Shopify's AppBridge resourcePicker to select a product, and then save the selected product ID to the form state. I can use the useFormContext hook from react-hook-form to get access to the form state APIs and manually set the selected product ID:

manually control form state with react-hook-form
React
1import { Button } from "@shopify/polaris";
2import { AutoForm, AutoInput, AutoSubmit } from "@gadgetinc/react/auto/polaris";
3import { api } from "../api";
4import { useAppBridge } from "@shopify/app-bridge-react";
5// import useFormContext to get access to react-hook-form API
6import { useFormContext } from "react-hook-form";
7
8export default function () {
9 return (
10 <AutoForm action={api.saveProductOffer}>
11 <ResourcePicker />
12 <AutoInput field="message" />
13 <AutoSubmit />
14 </AutoForm>
15 );
16}
17
18// custom component that selects a product using an external resource picker
19const ResourcePicker = () => {
20 const shopify = useAppBridge();
21
22 // use setValue to manually manage form state for the selected product
23 const { setValue } = useFormContext();
24
25 return (
26 <Button
27 onClick={async () => {
28 const selection = await shopify.resourcePicker({
29 type: "product",
30 multiple: false,
31 action: "select",
32 });
33
34 // save the selected productId to the form state
35 if (selection && selection.length) {
36 setValue("productId", selection[0].id);
37 }
38 }}
39 >
40 Select product
41 </Button>
42 );
43};
1import { Button } from "@shopify/polaris";
2import { AutoForm, AutoInput, AutoSubmit } from "@gadgetinc/react/auto/polaris";
3import { api } from "../api";
4import { useAppBridge } from "@shopify/app-bridge-react";
5// import useFormContext to get access to react-hook-form API
6import { useFormContext } from "react-hook-form";
7
8export default function () {
9 return (
10 <AutoForm action={api.saveProductOffer}>
11 <ResourcePicker />
12 <AutoInput field="message" />
13 <AutoSubmit />
14 </AutoForm>
15 );
16}
17
18// custom component that selects a product using an external resource picker
19const ResourcePicker = () => {
20 const shopify = useAppBridge();
21
22 // use setValue to manually manage form state for the selected product
23 const { setValue } = useFormContext();
24
25 return (
26 <Button
27 onClick={async () => {
28 const selection = await shopify.resourcePicker({
29 type: "product",
30 multiple: false,
31 action: "select",
32 });
33
34 // save the selected productId to the form state
35 if (selection && selection.length) {
36 setValue("productId", selection[0].id);
37 }
38 }}
39 >
40 Select product
41 </Button>
42 );
43};

Running code after form submission 

The <AutoForm> also has an onSuccess callback hook that allows you to run code after the form has been successfully submitted. This can be useful for things like redirecting the user to a new page, showing a success message, or updating the UI in some way.

run code after successful form submit
React
1() => (
2 <AutoForm
3 action={api.widget.create}
4 onSuccess={(record: { inventoryCount: number }) => {
5 console.log("created record:", record.inventoryCount);
6 }}
7 />
8);
1() => (
2 <AutoForm
3 action={api.widget.create}
4 onSuccess={(record: { inventoryCount: number }) => {
5 console.log("created record:", record.inventoryCount);
6 }}
7 />
8);

<AutoButton /> 

<AutoButton /> renders a button that when clicked calls one of your app's backend API actions with variables passed as a prop. You can use <AutoButton /> for model actions to create, update, or delete records, or to call a global action.

<AutoButton /> renders a button from the design system you're using, so it fits right in with the rest of your application and doesn't require any additional styling.

To render an <AutoButton/>, pass the action prop with the backend API action you want to call.

render a button that when clicked calls the create action
React
<AutoButton action={api.widget.create} />
// when clicked, will run api.widget.create()
<AutoButton action={api.widget.create} />
// when clicked, will run api.widget.create()

When clicked, this button will run the action, and show a success or error toast when it completes. It will also show a nice loading spinner while the action is running.

Often, you'll need to pass variables to your action. Pass them in the same style as you would pass them to your app's JS client:

render a button that when clicked calls the create action
React
<AutoButton action={api.widget.create} variables={{ name: "foobar", inventoryCount: 0 }} />
// when clicked, will run api.widget.create({ name: "foobar", inventoryCount: 0 })
<AutoButton action={api.widget.create} variables={{ name: "foobar", inventoryCount: 0 }} />
// when clicked, will run api.widget.create({ name: "foobar", inventoryCount: 0 })

Variables can be set dynamically using the normal React state management tools, like a useState hook:

render a button that when clicked calls the create action with settable variables
React
const CreateWidgetButton = () => {
const [variables, setVariables] = useState({ name: "foobar", inventoryCount: 0 });
return <AutoButton action={api.widget.create} variables={variables} />;
};
const CreateWidgetButton = () => {
const [variables, setVariables] = useState({ name: "foobar", inventoryCount: 0 });
return <AutoButton action={api.widget.create} variables={variables} />;
};

Button labels and styling 

<AutoButton/> accepts all the props from the design system you're using, so you can control its look and feel in the same way you would control the <Button/> component from the underlying design system.

For example, to set a Polaris <AutoButton>'s label, you can pass label as children:

set the button label
React
<AutoButton action={api.widget.create}>Create widget</AutoButton>
<AutoButton action={api.widget.create}>Create widget</AutoButton>

Or to set the size and variant of a Polaris <AutoButton>, you can pass the size and variant props:

render a large primary button
React
<AutoButton action={api.widget.create} size="large" variant="primary" />
<AutoButton action={api.widget.create} size="large" variant="primary" />

Running code after form submission 

<AutoButton/> has onSuccess and onError callback props that are triggered when the action succeeds or fails. This can be useful for things like redirecting the user to a new page, showing a success message, or updating the UI in some way.

For example, you can run code after a button is clicked like so:

run code after successful action
React
1() => (
2 <AutoButton
3 action={api.widget.create}
4 onSuccess={(result) => {
5 const { data } = result;
6 // onSuccess is passed the same object that calling an action
7 // with `useAction` would return, a `{data, fetching, error}` object
8 console.log("created record:", data.inventoryCount);
9 // navigate to the new record
10 navigate(`/widgets/${data.id}`);
11 }}
12 />
13);
1() => (
2 <AutoButton
3 action={api.widget.create}
4 onSuccess={(result) => {
5 const { data } = result;
6 // onSuccess is passed the same object that calling an action
7 // with `useAction` would return, a `{data, fetching, error}` object
8 console.log("created record:", data.inventoryCount);
9 // navigate to the new record
10 navigate(`/widgets/${data.id}`);
11 }}
12 />
13);

You can also pass custom error handlers with onError, which will be passed an Error object describing the error:

run code after failed action
React
1() => {
2 const [error, setError] = useState(null);
3
4 return (
5 <AutoButton
6 action={api.widget.create}
7 onError={(err: { message: string }) => {
8 // onError is passed an Error object
9 console.log("failed to create record:", err.message);
10 setError(err.message);
11 }}
12 />
13 );
14};
1() => {
2 const [error, setError] = useState(null);
3
4 return (
5 <AutoButton
6 action={api.widget.create}
7 onError={(err: { message: string }) => {
8 // onError is passed an Error object
9 console.log("failed to create record:", err.message);
10 setError(err.message);
11 }}
12 />
13 );
14};

When onSuccess or onError is passed, the default behavior of rendering a toast in the selected design system is overridden. If you want to maintain that behavior, you need to trigger it yourself.

For example, if you want to show a Shopify Polaris toast after the button is clicked, you can call the toast function:

React
1// Show a toast after the button is clicked
2() => (
3 <AutoButton
4 action={api.widget.create}
5 onSuccess={() => {
6 shopify.toast.show(`New widget created.`);
7 }}
8 />
9);
1// Show a toast after the button is clicked
2() => (
3 <AutoButton
4 action={api.widget.create}
5 onSuccess={() => {
6 shopify.toast.show(`New widget created.`);
7 }}
8 />
9);

<AutoTable /> 

<AutoTable /> components generate data tables for your React frontends and are pre-configured to read and display data from your data models.

<AutoTable /> reads each record, and renders the right kind of cell. This autocomponent supports pagination, filtering, searching, and sorting out of the box.

simple AutoTable example: build a data table for a customer model
React
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2// your app's auto-generated API client
3import { api } from "../api";
4
5export const CustomersTable = () => {
6 return <AutoTable model={api.customer} />;
7};
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2// your app's auto-generated API client
3import { api } from "../api";
4
5export const CustomersTable = () => {
6 return <AutoTable model={api.customer} />;
7};

Using <AutoTable /> without specifying the included columns will render a table with all fields from the model. This means that adding a new field to a model will also add a column to the table.

Make sure that you test any default <AutoTable /> components after making changes to your models to ensure they still work as expected.

Import AutoTable 

Import Shopify Polaris <AutoTable> components from @gadgetinc/react/auto/polaris:

import the AutoTable component into your frontend files
React
import { AutoTable } from "@gadgetinc/react/auto/polaris";
import { AutoTable } from "@gadgetinc/react/auto/polaris";

AutoTable uses the useFindMany hook to fetch data from the backend under the hood, so AutoTable has the same filter prop as useFindMany. The sort prop from useFindMany is also available, but has been renamed to initialSort since users could choose a different column to sort by after the table is rendered.

filter and sort records
React
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2import { api } from "../api";
3
4export const RecentTable = () => {
5 return (
6 <AutoTable
7 model={api.widget}
8 filter={{
9 createdAt: { greaterThan: new Date(2024, 1, 1) },
10 }}
11 initialSort={{ updatedAt: "Ascending" }}
12 />
13 );
14};
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2import { api } from "../api";
3
4export const RecentTable = () => {
5 return (
6 <AutoTable
7 model={api.widget}
8 filter={{
9 createdAt: { greaterThan: new Date(2024, 1, 1) },
10 }}
11 initialSort={{ updatedAt: "Ascending" }}
12 />
13 );
14};

AutoTable provides a search bar that filters records based on the query. It uses the same full-text search feature found in the API client. It is enabled by default, but can be disabled by passing searchable={false} to the AutoTable component.

React
// to disable the search bar
export const WidgetTable = () => {
return <AutoTable model={api.widget} searchable={false} />;
};
// to disable the search bar
export const WidgetTable = () => {
return <AutoTable model={api.widget} searchable={false} />;
};

To programmatically control the search value from outside of the AutoTable component, you can use the searchValue prop.

programmatically control the search value
React
1export const WidgetTable = () => {
2 const [searchValue, setSearchValue] = useState("");
3 return (
4 <>
5 <input value={searchValue} onChange={(e) => setSearchValue(e.target.value)} />
6 <AutoTable model={api.widget} searchValue={searchValue} />
7 </>
8 );
9};
1export const WidgetTable = () => {
2 const [searchValue, setSearchValue] = useState("");
3 return (
4 <>
5 <input value={searchValue} onChange={(e) => setSearchValue(e.target.value)} />
6 <AutoTable model={api.widget} searchValue={searchValue} />
7 </>
8 );
9};

Filtering has many through 

Suppose we have a data model where many people can belong to many chat rooms, and PersonChatRoom is a has many through relation.

We can show the chat rooms that belong to person 1 like so:

hasManyThrough filtering example
React
1import { Page, Text } from "@shopify/polaris";
2import { AutoTable } from "@gadgetinc/react/auto/polaris";
3import { useFindMany } from "@gadgetinc/react";
4
5export const Person1ChatRooms = () => {
6 const person1Id = 1;
7 const [{ data: chatRooms, fetching }] = useFindMany(api.personChatRoom, {
8 filter: {
9 personId: { equals: person1Id },
10 },
11 select: {
12 chatRoomId: true,
13 },
14 });
15
16 if (fetching) return "Loading chat rooms...";
17
18 const chatRoomIds = chatRooms?.map((chatRoom) => chatRoom.chatRoomId);
19
20 return (
21 <Page>
22 <Text variant="headingXl" as="h4">
23 Person 1's Chat Rooms
24 </Text>
25 <AutoTable
26 model={api.chatRooms}
27 filter={{
28 id: { in: chatRoomIds },
29 }}
30 columns={["chatRoomName"]}
31 />
32 </Page>
33 );
34};
1import { Page, Text } from "@shopify/polaris";
2import { AutoTable } from "@gadgetinc/react/auto/polaris";
3import { useFindMany } from "@gadgetinc/react";
4
5export const Person1ChatRooms = () => {
6 const person1Id = 1;
7 const [{ data: chatRooms, fetching }] = useFindMany(api.personChatRoom, {
8 filter: {
9 personId: { equals: person1Id },
10 },
11 select: {
12 chatRoomId: true,
13 },
14 });
15
16 if (fetching) return "Loading chat rooms...";
17
18 const chatRoomIds = chatRooms?.map((chatRoom) => chatRoom.chatRoomId);
19
20 return (
21 <Page>
22 <Text variant="headingXl" as="h4">
23 Person 1's Chat Rooms
24 </Text>
25 <AutoTable
26 model={api.chatRooms}
27 filter={{
28 id: { in: chatRoomIds },
29 }}
30 columns={["chatRoomName"]}
31 />
32 </Page>
33 );
34};

Customize AutoTable 

AutoTable supports params from the underlying components in the design system you're using, so you can control its look and feel in the same way you would when building with the system's native table component.

See the AutoTable reference for a full list of supported props.

Customize table 

Select fields to display 

You can specify which columns to display. Pass an array of their API identifiers to the columns prop. The columns are displayed in the order in which they are listed.

include columns
React
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2import { api } from "../api";
3
4export const CustomColumnsTable = () => {
5 // Renders a table with "First Name", "Last Name", and "Email" as the only columns
6 return <AutoTable model={api.customer} columns={["firstName", "lastName", "email"]} />;
7};
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2import { api } from "../api";
3
4export const CustomColumnsTable = () => {
5 // Renders a table with "First Name", "Last Name", and "Email" as the only columns
6 return <AutoTable model={api.customer} columns={["firstName", "lastName", "email"]} />;
7};

Exclude columns from the table like so:

exclude columns
React
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2import { api } from "../api";
3
4export const ExcludeColumnsTable = () => {
5 // Renders all customer fields except "First Name", "Last Name", and "Email"
6 return <AutoTable model={api.customer} excludeColumns={["firstName", "lastName", "email"]} />;
7};
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2import { api } from "../api";
3
4export const ExcludeColumnsTable = () => {
5 // Renders all customer fields except "First Name", "Last Name", and "Email"
6 return <AutoTable model={api.customer} excludeColumns={["firstName", "lastName", "email"]} />;
7};

Related fields (fields that are of type has one, belongs to, or has many) are not rendered by default. You can render them using dot notation in the columns prop, using the same syntax as your app's GraphQL queries.

For has one or belongs to relationships, you can directly access fields on the related model:

show email of the customer that the purchase belongs to
React
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2import { api } from "../api";
3
4export const PurchaseTable = () => {
5 return (
6 <AutoTable
7 model={api.purchase}
8 columns={[
9 "id",
10 "purchaseLocation",
11 "customer.email", // for "has one" or "belongs to" relationships
12 ]}
13 />
14 );
15};
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2import { api } from "../api";
3
4export const PurchaseTable = () => {
5 return (
6 <AutoTable
7 model={api.purchase}
8 columns={[
9 "id",
10 "purchaseLocation",
11 "customer.email", // for "has one" or "belongs to" relationships
12 ]}
13 />
14 );
15};

PurchaseTable renders like so:

For example, CustomerTable displays the records associated with the customer model. Each customer has many purchases. Because GraphQL query syntax is used, edges.node goes between the related model name and the field name, like so: purchases.edges.node.purchaseLocation.

show all purchase locations associated with each customer
React
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2import { api } from "../api";
3
4export const CustomerTable = () => {
5 return (
6 <AutoTable
7 model={api.customer}
8 columns={[
9 "email",
10 "purchases.edges.node.purchaseLocation", // for "has many" relationships
11 ]}
12 />
13 );
14};
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2import { api } from "../api";
3
4export const CustomerTable = () => {
5 return (
6 <AutoTable
7 model={api.customer}
8 columns={[
9 "email",
10 "purchases.edges.node.purchaseLocation", // for "has many" relationships
11 ]}
12 />
13 );
14};

CustomerTable renders like so:

Render a custom cell 

If you would like to change the way a field is being rendered or add a new column to your table, you can an object to the columns prop:

example of a custom cell renderer in an AutoTable
React
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2
3export const CustomTable = () => {
4 // Renders a table with a custom column "Customer name", and the email column
5 return (
6 <AutoTable
7 model={api.customer}
8 columns={[
9 {
10 header: "Customer name",
11 render: ({ record }) => {
12 // Displays the name like so: A. Turing
13 return <div>{record.firstName[0].toUpperCase + ". " + record.lastName}</div>;
14 },
15 },
16 "email",
17 ]}
18 />
19 );
20};
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2
3export const CustomTable = () => {
4 // Renders a table with a custom column "Customer name", and the email column
5 return (
6 <AutoTable
7 model={api.customer}
8 columns={[
9 {
10 header: "Customer name",
11 render: ({ record }) => {
12 // Displays the name like so: A. Turing
13 return <div>{record.firstName[0].toUpperCase + ". " + record.lastName}</div>;
14 },
15 },
16 "email",
17 ]}
18 />
19 );
20};

Manually set sortable columns 

By default, columns for field types that are able to be sorted alphanumerically will be sortable in an AutoTable. A list of sortable fields can be found in your API documentation.

If you want to manually set columns that are sortable in an AutoTable you can use the sortable boolean in a custom column configuration:

specify certain columns as sortable
React
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2import { api } from "../api";
3
4export const RecentTable = () => {
5 return (
6 <AutoTable
7 model={api.widget}
8 columns={[
9 // name will not be a sortable column
10 { field: "name", sortable: false },
11 // type will be sortable
12 { field: "type", sortable: true },
13 // code will be sortable if the field type is sortable
14 "code",
15 ]}
16 />
17 );
18};
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2import { api } from "../api";
3
4export const RecentTable = () => {
5 return (
6 <AutoTable
7 model={api.widget}
8 columns={[
9 // name will not be a sortable column
10 { field: "name", sortable: false },
11 // type will be sortable
12 { field: "type", sortable: true },
13 // code will be sortable if the field type is sortable
14 "code",
15 ]}
16 />
17 );
18};

Realtime query support for <AutoTable /> 

By default, an AutoTable does not re-render when a record is created/updated/deleted on the backend.

If you want to keep your frontend data table up to date with the data in your database, AutoTable components support realtime queries. Anytime a record is updated in the model powering the AutoTable, the AutoTable will automatically re-render to reflect the changes.

You can enable realtime query support on your AutoTable with the live prop:

React
// This component will rerender every time a customer is added, removed or updated
<AutoTable model={api.customer} live />
// This component will rerender every time a customer is added, removed or updated
<AutoTable model={api.customer} live />

Custom onClick 

The onClick prop is a callback that is fired when the row is clicked. Including onClick overrides the default click behavior, which is to select the row.

Note that even when onClick is included as a prop, rows can still be selected by pressing directly on the checkbox:

Polaris table with an arrow pointing to the checkbox beside the first row
React
<AutoTable model={api.customer} onClick={(row) => console.log(row)} />
<AutoTable model={api.customer} onClick={(row) => console.log(row)} />

The row parameter is an object that describes a row the user selected. Say I have a row with columns "First name", "Last name" and "Email", (row) => console.log(row) prints this object to the console:

json
1{
2 "id": "147",
3 "firstName": "example value for firstName",
4 "lastName": "example value for lastName",
5 "email": "[email protected]"
6}

Bulk selection 

You can trigger bulk actions from AutoTable out of the box. Upon selecting a row, a bulk action can be run by selecting the three dots, then selecting the name of the bulk action. The action is run for each selected record in the table.

Users can trigger actions that have delete and custom action types. AutoTable cannot trigger create and update action types. Instead, use AutoForm to create or update a record.

By default, all delete and custom actions are included in the table. You can include actions by passing their action API identifiers:

React
// myCustomAction is the API identifier of the action
<AutoTable actions={["myCustomAction"]} />
// myCustomAction is the API identifier of the action
<AutoTable actions={["myCustomAction"]} />

Actions can also be excluded:

React
// myOtherCustomAction is the API identifier of the action
<AutoTable excludeActions={["myOtherCustomAction"]} />
// myOtherCustomAction is the API identifier of the action
<AutoTable excludeActions={["myOtherCustomAction"]} />

When a user selects an action, the run() method of the action is called, and each row is passed in as a record.

Custom bulk action 

The actions prop allows for a custom callback or renderer. A bulk action cannot have both a custom callback and renderer.

Custom callback 

By default, when a user clicks on an action, a confirmation window appears. To override this behavior, use a custom callback.

To describe your custom callback, include an object in the actions prop:

action with a custom callback
React
1<AutoTable
2 actions={[
3 {
4 // Name of the custom action
5 label: "Custom action name",
6 // Prints the list of records that were selected to the console
7 action: (records) => {
8 console.log("records: ", JSON.stringify(records, null, 2));
9 },
10 },
11 ]}
12/>
1<AutoTable
2 actions={[
3 {
4 // Name of the custom action
5 label: "Custom action name",
6 // Prints the list of records that were selected to the console
7 action: (records) => {
8 console.log("records: ", JSON.stringify(records, null, 2));
9 },
10 },
11 ]}
12/>

You can then use your api client to run an action on the selected records:

run a global action on the selected records
React
1<AutoTable
2 actions={[
3 {
4 label: "Export",
5 action: async (records) => {
6 // call the exportData global action, passing the selected records
7 await api.exportData({ records });
8 },
9 },
10 ]}
11/>
1<AutoTable
2 actions={[
3 {
4 label: "Export",
5 action: async (records) => {
6 // call the exportData global action, passing the selected records
7 await api.exportData({ records });
8 },
9 },
10 ]}
11/>
Custom action modal 

To render a custom popup when a user selects an action, create a custom action and use it to show a modal:

action with a custom render
React
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2import { api } from "../api";
3import { Modal, TitleBar, useAppBridge } from "@shopify/app-bridge-react";
4
5export default function () {
6 const shopify = useAppBridge();
7
8 return (
9 <>
10 <AutoTable
11 model={api.product}
12 actions={[
13 {
14 label: "Export",
15 action: async (records) => {
16 // show the modal when the action is clicked
17 shopify.modal.show("my-modal");
18 },
19 },
20 ]}
21 />
22 {/** use the Shopify AppBridge modal component */}
23 <Modal id="my-modal">
24 <p>Message</p>
25 <TitleBar title="My modal">
26 <button onClick={() => shopify.modal.hide("my-modal")}>Close</button>
27 </TitleBar>
28 </Modal>
29 </>
30 );
31}
1import { AutoTable } from "@gadgetinc/react/auto/polaris";
2import { api } from "../api";
3import { Modal, TitleBar, useAppBridge } from "@shopify/app-bridge-react";
4
5export default function () {
6 const shopify = useAppBridge();
7
8 return (
9 <>
10 <AutoTable
11 model={api.product}
12 actions={[
13 {
14 label: "Export",
15 action: async (records) => {
16 // show the modal when the action is clicked
17 shopify.modal.show("my-modal");
18 },
19 },
20 ]}
21 />
22 {/** use the Shopify AppBridge modal component */}
23 <Modal id="my-modal">
24 <p>Message</p>
25 <TitleBar title="My modal">
26 <button onClick={() => shopify.modal.hide("my-modal")}>Close</button>
27 </TitleBar>
28 </Modal>
29 </>
30 );
31}

For more information on passing input to modals, see Shopify's documentation on AppBridge modals.

Was this page helpful?