React Admin Advanced Recipes: Creating a Record Related to the Current One

Gildas Garcia
Gildas GarciaJuly 09, 2018
#react-admin#react#tutorial

This is the first article in a new serie of advanced tutorials for React-admin. It assumes you already understand the basics of react-admin and have at least completed the official tutorial.

Today, we'll see how to add a button on a show or edit page to create a new resource related to the one displayed. For example, a new comment from the post show or edit pages. The create form should have its post already set when coming from the post page and it should redirect to the original post after creation.

Initializing the project

For the purpose of this tutorial, we'll keep our resources very simple:

  • the post resource will only have a title, a teaser and a body
  • the comment resource will only have a creation date, a body and a reference to its post

We won't go into the details of all the components here as they are pretty straightforward but you can explore the code in this codesandbox.

Moreover, as we don't want to initialize a real API, we'll be using ra-data-fakerest as our dataProvider. It generates a fake API server from an object containing our data. In our case, this object looks like this:

const data = {
    posts: [
        {
            id: 1,
            title: "...",
            teaser: "...",
            body: "<p>...</p>"
        },
        ...
    ],
    comments: [
        {
            id: 1,
            post_id: 6,
            body: "...",
            created_at: new Date("2012-08-02")
        },
        ...
    ]
}

ra-data-fakerest will fake an API returning JSON data for routes such as:

  • GET /posts,
  • GET /posts/1
  • PUT /posts/1
  • POST /posts

Setting default values on the CommentCreate component

Back to our problem now. How can we set the post when creating a new comment? The answer lies in the code we already have for the CommentCreate component:

const CommentCreate = props => {
  // ...
  return (
    <Create {...props}>
      <SimpleForm
        defaultValue={{ created_at: today }} // Spoiler alert!
        redirect="show"
      >
        // ...
      </SimpleForm>
    </Create>
  );
};

We must set the post_id field default value somehow:

<SimpleForm defaultValue={{ created_at: today, post_id }} redirect="show">
  // ...
</SimpleForm>

One way would be to parse it from the url right?

//...
import { parse } from "query-string";

const CommentCreate = props => {
  // Read the post_id from the location which is injected by React Router and passed to our component by react-admin automatically
  const { post_id: post_id_string } = parse(props.location.search);

  // ra-data-fakerest uses integers as identifiers, we need to parse the querystring
  // We also must ensure we can still create a new comment without having a post_id
  // from the url by returning an empty string if post_id isn't specified
  const post_id = post_id_string ? parseInt(post_id_string, 10) : "";

  return (
    <Create {...props}>
      <SimpleForm defaultValue={{ created_at: today, post_id }}>
        // ...
      </SimpleForm>
    </Create>
  );
};

Note: We could also have used the location state property instead of search. Same result but the post_id wouldn't appear in the url.

We should also redirect to the original post page when a comment is created with a post_id already defined. Thanks to react-admin 2.1.0, we can even redirect to the comments tab directly!

//...
const CommentCreate = props => {
  //...
  const redirect = post_id ? `/posts/${post_id}/show/comments` : "show";

  return (
    <Create {...props}>
      <SimpleForm
        defaultValue={{ created_at: today, post_id }}
        redirect={redirect}
      >
        // ...
      </SimpleForm>
    </Create>
  );
};

That should do it and it's already testable if you browse to an url like /#/comments/create?post_id=1;

Creating a new comment from the post show page

We must now find a way to call this page from the PostShow component.

Let's start with the easiest one, using the actions prop of the Show component:

// in src/posts/PostShow.js
// ...
import CardActions from "@material-ui/core/CardActions";
import ChatBubbleIcon from "@material-ui/icons/ChatBubble";
import { Button } from "react-admin";

const AddNewCommentButton = ({ record }) => (
  <Button
    component={Link}
    to={{
      pathname: "/comments/create",
      search: `?post_id=${record.id}`,
    }}
    label="Add a comment"
  >
    <ChatBubbleIcon />
  </Button>
);

const PostShowActions = ({ basePath, data }) => (
  <CardActions>
    <ListButton basePath={basePath} />
    <RefreshButton />
    <AddNewCommentButton record={data} />
  </CardActions>
);

const PostShow = props => (
  <Show {...props} actions={<PostShowActions />}>
    ...
  </Show>
);

Note: If using the location state property:

// in src/posts/PostShow.js
// ...
import ChatBubbleIcon from "@material-ui/icons/ChatBubble";
import { Button } from "react-admin";

const AddNewCommentButton = ({ record }) => (
  <Button
    component={Link}
    to={{
      pathname: "/comments/create",
      state: { post_id: record.id },
    }}
    label="Add a comment"
  >
    <ChatBubbleIcon />
  </Button>
);

We now have a button to create a new comment for the current post on our show page. Great! However, this button is displayed on every tab. It would be better to show it only on the comment tab.

We know from the documentation that every field component receives a record prop containing the current record (here our post). Let's remove the actions prop and add the button below the comment datagrid:

// in src/posts/PostShow.js
// ...
const PostShow = props => (
  <Show {...props}>
    <TabbedShowLayout>
      ...
      <Tab label="Comments">
        <ReferenceManyField
          addLabel={false}
          reference="comments"
          target="post_id"
          sort={{ field: "created_at", order: "DESC" }}
        >
          <Datagrid>
            <DateField source="created_at" />
            <TextField source="body" />
            <EditButton />
          </Datagrid>
        </ReferenceManyField>
        <AddNewCommentButton />
      </Tab>
    </TabbedShowLayout>
  </Show>
);

Now our button is only displayed in the appropriate context which is better for our users. We just need to style it a bit so that it's not so close to the Datagrid.

// in src/posts/PostShow.js
// ...
import ChatBubbleIcon from "@material-ui/icons/ChatBubble";
import { withStyles } from "@material-ui/core/styles";
import { Button } from "react-admin";

const styles = {
  button: {
    marginTop: "1em",
  },
};

const AddNewCommentButton = withStyles(styles)(({ classes, record }) => (
  <Button
    className={classes.button}
    variant="raised"
    component={Link}
    to={{
      pathname: "/comments/create",
      search: `?post_id=${record.id}`,
    }}
    label="Add a comment"
  >
    <ChatBubbleIcon />
  </Button>
));

Hooray! We're done!

Conclusion

Hopefully this article proved you that although react-admin is really helping to build admin interfaces quickly, it does not prevent you to customize its features. Morever, It's just Reactâ„¢, It's just JavaScriptâ„¢ !

You might be wondering why the feature we added in this article isn't simply baked-in react-admin. This is because there are many ways to handle this. Here we redirected from the posts resource page to the comments resource page and vice versa. However it would be possible to open the comments creation form inside a modal, or a sliding panel. Would we implement a baked-in solution, our users would keep asking for more and more options and react-admin would become another unmaintanable bloated framework. Instead, we chose to focus on really core features while ensuring our users can extend or replace complete parts of them, thanks to React.

However, as the automatic population of a creation form from location state or search is a very common need, we decided to support it officialy! In react-admin 2.2, this will be done automatically for you. Adding a link to redirect the user will still be your responsability though.

By the way, having a form inside a modal or sliding panel will be the focus of the next article. Stay tuned !

You can explore the full code for this article in this codesandbox

Did you like this article? Share it!