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:
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/reactyarn 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:
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:
1const Layout = () => {2 const navigate = useNavigate();34 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();34 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:
1// Render an AutoForm using Shopify's Polaris design system2import { AutoForm } from "@gadgetinc/react/auto/polaris";3import { api } from "../api";45export default function Example() {6 return <AutoForm action={api.widget.create} />;7}
1// Render an AutoForm using Shopify's Polaris design system2import { AutoForm } from "@gadgetinc/react/auto/polaris";3import { api } from "../api";45export 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:
1import { AutoForm, AutoInput, AutoSubmit } from "@gadgetinc/react/auto/polaris";2import { InlineStack } from "@shopify/polaris";3import { api } from "../api";45export 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";45export 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 { 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:
<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:
<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:
<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 dependencyyarn add -D @mdxeditor/editor
Then import the stylesheet into your app:
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:
<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:
<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:
<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}
:
<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:
1// before2() => <AutoForm action={api.widget.create} include={["name", "inventoryCount"]} />;34// after5() => (6 <AutoForm action={api.widget.create}>7 <AutoInput field="name" />8 <AutoInput field="inventoryCount" />9 <AutoSubmit />10 </AutoForm>11);
1// before2() => <AutoForm action={api.widget.create} include={["name", "inventoryCount"]} />;34// after5() => (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:
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:
1// before2() => (3 <AutoForm action={api.widget.create}>4 <AutoInput field="name" />5 <AutoInput field="inventoryCount" />6 <AutoSubmit />7 </AutoForm>8);910// after11() => (12 <AutoForm action={api.widget.create}>13 <AutoStringInput field="name" />14 <AutoNumberInput field="inventoryCount" />15 <AutoSubmit />16 </AutoForm>17);
1// before2() => (3 <AutoForm action={api.widget.create}>4 <AutoInput field="name" />5 <AutoInput field="inventoryCount" />6 <AutoSubmit />7 </AutoForm>8);910// after11() => (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:
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.
<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:
<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.
<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
.
To fetch an existing record by something other than id
, pass the findBy
prop a set of conditions to find by:
<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.
<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:
1<AutoForm2 action={api.widget.create}3 defaultValues={{4 widget: { name: "foobar", inventoryCount: 0 },5 }}6/>
1<AutoForm2 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.
<AutoForm defaultValues={{ widget: { name: "foobar", inventoryCount: 0 } }} />
<AutoForm defaultValues={{ widget: { name: "foobar", inventoryCount: 0 } }} />
Don't do this:
<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.
1import { AutoForm, AutoInput, AutoHiddenInput, AutoSubmit } from "@gadgetinc/react/auto/polaris";23() => (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";23() => (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:
1// import InvalidRecordError from gadget-server2import { InvalidRecordError } from "gadget-server";34export const run: ActionRun = async ({ params, logger, api, connections }) => {5 // validate the param manually6 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 }1112 // ... custom action code13};1415export const params = {16 count: { type: "number" },17};
1// import InvalidRecordError from gadget-server2import { InvalidRecordError } from "gadget-server";34export const run: ActionRun = async ({ params, logger, api, connections }) => {5 // validate the param manually6 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 }1112 // ... custom action code13};1415export const params = {16 count: { type: "number" },17};
You can call this action from an <AutoForm />
and the validation error will be displayed:
<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:
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 API6import { useFormContext } from "react-hook-form";78export default function () {9 return (10 <AutoForm action={api.saveProductOffer}>11 <ResourcePicker />12 <AutoInput field="message" />13 <AutoSubmit />14 </AutoForm>15 );16}1718// custom component that selects a product using an external resource picker19const ResourcePicker = () => {20 const shopify = useAppBridge();2122 // use setValue to manually manage form state for the selected product23 const { setValue } = useFormContext();2425 return (26 <Button27 onClick={async () => {28 const selection = await shopify.resourcePicker({29 type: "product",30 multiple: false,31 action: "select",32 });3334 // save the selected productId to the form state35 if (selection && selection.length) {36 setValue("productId", selection[0].id);37 }38 }}39 >40 Select product41 </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 API6import { useFormContext } from "react-hook-form";78export default function () {9 return (10 <AutoForm action={api.saveProductOffer}>11 <ResourcePicker />12 <AutoInput field="message" />13 <AutoSubmit />14 </AutoForm>15 );16}1718// custom component that selects a product using an external resource picker19const ResourcePicker = () => {20 const shopify = useAppBridge();2122 // use setValue to manually manage form state for the selected product23 const { setValue } = useFormContext();2425 return (26 <Button27 onClick={async () => {28 const selection = await shopify.resourcePicker({29 type: "product",30 multiple: false,31 action: "select",32 });3334 // save the selected productId to the form state35 if (selection && selection.length) {36 setValue("productId", selection[0].id);37 }38 }}39 >40 Select product41 </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.
1() => (2 <AutoForm3 action={api.widget.create}4 onSuccess={(record: { inventoryCount: number }) => {5 console.log("created record:", record.inventoryCount);6 }}7 />8);
1() => (2 <AutoForm3 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.
<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:
<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:
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:
<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:
<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:
1() => (2 <AutoButton3 action={api.widget.create}4 onSuccess={(result) => {5 const { data } = result;6 // onSuccess is passed the same object that calling an action7 // with `useAction` would return, a `{data, fetching, error}` object8 console.log("created record:", data.inventoryCount);9 // navigate to the new record10 navigate(`/widgets/${data.id}`);11 }}12 />13);
1() => (2 <AutoButton3 action={api.widget.create}4 onSuccess={(result) => {5 const { data } = result;6 // onSuccess is passed the same object that calling an action7 // with `useAction` would return, a `{data, fetching, error}` object8 console.log("created record:", data.inventoryCount);9 // navigate to the new record10 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:
1() => {2 const [error, setError] = useState(null);34 return (5 <AutoButton6 action={api.widget.create}7 onError={(err: { message: string }) => {8 // onError is passed an Error object9 console.log("failed to create record:", err.message);10 setError(err.message);11 }}12 />13 );14};
1() => {2 const [error, setError] = useState(null);34 return (5 <AutoButton6 action={api.widget.create}7 onError={(err: { message: string }) => {8 // onError is passed an Error object9 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:
1// Show a toast after the button is clicked2() => (3 <AutoButton4 action={api.widget.create}5 onSuccess={() => {6 shopify.toast.show(`New widget created.`);7 }}8 />9);
1// Show a toast after the button is clicked2() => (3 <AutoButton4 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.
1import { AutoTable } from "@gadgetinc/react/auto/polaris";2// your app's auto-generated API client3import { api } from "../api";45export const CustomersTable = () => {6 return <AutoTable model={api.customer} />;7};
1import { AutoTable } from "@gadgetinc/react/auto/polaris";2// your app's auto-generated API client3import { api } from "../api";45export 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 { AutoTable } from "@gadgetinc/react/auto/polaris";
import { AutoTable } from "@gadgetinc/react/auto/polaris";
Filter, sort, and search
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.
1import { AutoTable } from "@gadgetinc/react/auto/polaris";2import { api } from "../api";34export const RecentTable = () => {5 return (6 <AutoTable7 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";34export const RecentTable = () => {5 return (6 <AutoTable7 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.
// to disable the search barexport const WidgetTable = () => {return <AutoTable model={api.widget} searchable={false} />;};
// to disable the search barexport const WidgetTable = () => {return <AutoTable model={api.widget} searchable={false} />;};
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:
1import { Page, Text } from "@shopify/polaris";2import { AutoTable } from "@gadgetinc/react/auto/polaris";3import { useFindMany } from "@gadgetinc/react";45export 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 });1516 if (fetching) return "Loading chat rooms...";1718 const chatRoomIds = chatRooms?.map((chatRoom) => chatRoom.chatRoomId);1920 return (21 <Page>22 <Text variant="headingXl" as="h4">23 Person 1's Chat Rooms24 </Text>25 <AutoTable26 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";45export 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 });1516 if (fetching) return "Loading chat rooms...";1718 const chatRoomIds = chatRooms?.map((chatRoom) => chatRoom.chatRoomId);1920 return (21 <Page>22 <Text variant="headingXl" as="h4">23 Person 1's Chat Rooms24 </Text>25 <AutoTable26 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.
1import { AutoTable } from "@gadgetinc/react/auto/polaris";2import { api } from "../api";34export const CustomColumnsTable = () => {5 // Renders a table with "First Name", "Last Name", and "Email" as the only columns6 return <AutoTable model={api.customer} columns={["firstName", "lastName", "email"]} />;7};
1import { AutoTable } from "@gadgetinc/react/auto/polaris";2import { api } from "../api";34export const CustomColumnsTable = () => {5 // Renders a table with "First Name", "Last Name", and "Email" as the only columns6 return <AutoTable model={api.customer} columns={["firstName", "lastName", "email"]} />;7};
Exclude columns from the table like so:
1import { AutoTable } from "@gadgetinc/react/auto/polaris";2import { api } from "../api";34export 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";34export 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};
Render related fields
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:
1import { AutoTable } from "@gadgetinc/react/auto/polaris";2import { api } from "../api";34export const PurchaseTable = () => {5 return (6 <AutoTable7 model={api.purchase}8 columns={[9 "id",10 "purchaseLocation",11 "customer.email", // for "has one" or "belongs to" relationships12 ]}13 />14 );15};
1import { AutoTable } from "@gadgetinc/react/auto/polaris";2import { api } from "../api";34export const PurchaseTable = () => {5 return (6 <AutoTable7 model={api.purchase}8 columns={[9 "id",10 "purchaseLocation",11 "customer.email", // for "has one" or "belongs to" relationships12 ]}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
.
1import { AutoTable } from "@gadgetinc/react/auto/polaris";2import { api } from "../api";34export const CustomerTable = () => {5 return (6 <AutoTable7 model={api.customer}8 columns={[9 "email",10 "purchases.edges.node.purchaseLocation", // for "has many" relationships11 ]}12 />13 );14};
1import { AutoTable } from "@gadgetinc/react/auto/polaris";2import { api } from "../api";34export const CustomerTable = () => {5 return (6 <AutoTable7 model={api.customer}8 columns={[9 "email",10 "purchases.edges.node.purchaseLocation", // for "has many" relationships11 ]}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:
1import { AutoTable } from "@gadgetinc/react/auto/polaris";23export const CustomTable = () => {4 // Renders a table with a custom column "Customer name", and the email column5 return (6 <AutoTable7 model={api.customer}8 columns={[9 {10 header: "Customer name",11 render: ({ record }) => {12 // Displays the name like so: A. Turing13 return <div>{record.firstName[0].toUpperCase + ". " + record.lastName}</div>;14 },15 },16 "email",17 ]}18 />19 );20};
1import { AutoTable } from "@gadgetinc/react/auto/polaris";23export const CustomTable = () => {4 // Renders a table with a custom column "Customer name", and the email column5 return (6 <AutoTable7 model={api.customer}8 columns={[9 {10 header: "Customer name",11 render: ({ record }) => {12 // Displays the name like so: A. Turing13 return <div>{record.firstName[0].toUpperCase + ". " + record.lastName}</div>;14 },15 },16 "email",17 ]}18 />19 );20};
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:
// 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:
<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:
json1{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:
// 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:
// 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:
1<AutoTable2 actions={[3 {4 // Name of the custom action5 label: "Custom action name",6 // Prints the list of records that were selected to the console7 action: (records) => {8 console.log("records: ", JSON.stringify(records, null, 2));9 },10 },11 ]}12/>
1<AutoTable2 actions={[3 {4 // Name of the custom action5 label: "Custom action name",6 // Prints the list of records that were selected to the console7 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:
1<AutoTable2 actions={[3 {4 label: "Export",5 action: async (records) => {6 // call the exportData global action, passing the selected records7 await api.exportData({ records });8 },9 },10 ]}11/>
1<AutoTable2 actions={[3 {4 label: "Export",5 action: async (records) => {6 // call the exportData global action, passing the selected records7 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:
1import { AutoTable } from "@gadgetinc/react/auto/polaris";2import { api } from "../api";3import { Modal, TitleBar, useAppBridge } from "@shopify/app-bridge-react";45export default function () {6 const shopify = useAppBridge();78 return (9 <>10 <AutoTable11 model={api.product}12 actions={[13 {14 label: "Export",15 action: async (records) => {16 // show the modal when the action is clicked17 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";45export default function () {6 const shopify = useAppBridge();78 return (9 <>10 <AutoTable11 model={api.product}12 actions={[13 {14 label: "Export",15 action: async (records) => {16 // show the modal when the action is clicked17 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.