React Admin Advanced Recipes - Adding Related Records Inline With Custom Forms
This is the second article in a new series of advanced tutorials for React-admin. It assumes you already understand the basics of react-admin
and have at least completed the official tutorial.
In the last article, we saw how to add a button on a show or edit page to create a new resource related to the one displayed. Our use case was to create a new comment from the post show or edit pages. The New Comment button redirected to the comments create page with default values parsed from the url.
In this article, I'll explain how to have a create, show or edit view of a referenced resource inside a modal or a sliding side panel. This will allow end users to create a new post from the comment create page, or to preview the selected post. I will work on a fork of the codesandbox that was built in the last article.
Let's start with quick post creation from the comment create page.
Quick Create In a Modal
The comment create page currently looks like the following:
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) : "";
const redirect = post_id ? `/posts/${post_id}/show/comments` : false;
return (
<Create {...props}>
<SimpleForm
defaultValue={{ created_at: today, post_id }}
redirect={redirect}
>
<ReferenceInput
source="post_id"
reference="posts"
allowEmpty
validate={required()}
>
<SelectInput optionText="title" />
</ReferenceInput>
<DateInput source="created_at" />
<LongTextInput source="body" />
</SimpleForm>
</Create>
);
};
I'd like to add a New button next to the posts ReferenceInput
. Let's first figure out how we are going to retrieve the id of a newly created post, and pass it to our ReferenceInput
.
I'll need a custom ReferenceInput
, that I'll name PostReferenceInput
. Initially, I was tempted to add a handlePostCreated
method to it, which would be called by whatever component responsible for the post creation, and set the new post's id in the state. It would then apply the new post's id as the value
of the ReferenceInput
. However, the react-admin input components do not accept a value
prop. This is because react-admin takes care of binding them to the redux-form and passing them the appropriate props. Passing a value
won't work as it will be overridden by redux-form
Field
component.
Fortunately, redux-form
provides many redux action creators to manipulate our form, among which is change
, which sets the value of a specific field, and submit
, which submits the form. This also allows me to extract those manipulations in a custom PostQuickCreateButton
component which can handle the modal state too.
// in PostQuickCreateButton.js
import React, { Component, Fragment } from "react";
import { connect } from "react-redux";
import { change, submit, isSubmitting } from "redux-form";
import {
fetchEnd,
fetchStart,
required,
showNotification,
Button,
SaveButton,
SimpleForm,
TextInput,
LongTextInput,
CREATE,
REDUX_FORM_NAME,
} from "react-admin";
import IconContentAdd from "@material-ui/icons/Add";
import IconCancel from "@material-ui/icons/Cancel";
import Dialog from "@material-ui/core/Dialog";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogActions from "@material-ui/core/DialogActions";
import dataProvider from "../dataProvider";
class PostQuickCreateButton extends Component {
state = {
error: false,
showDialog: false,
};
handleClick = () => {
this.setState({ showDialog: true });
};
handleCloseClick = () => {
this.setState({ showDialog: false });
};
handleSaveClick = () => {
const { submit } = this.props;
// Trigger a submit of our custom quick create form
// This is needed because our modal action buttons are oustide the form
submit("post-quick-create");
};
handleSubmit = values => {
const { change, fetchStart, fetchEnd, showNotification } = this.props;
// Dispatch an action letting react-admin know a API call is ongoing
fetchStart();
// As we want to know when the new post has been created in order to close the modal, we use the
// dataProvider directly
dataProvider(CREATE, "posts", { data: values })
.then(({ data }) => {
// Update the main react-admin form (in this case, the comments creation form)
change(REDUX_FORM_NAME, "post_id", data.id);
this.setState({ showDialog: false });
})
.catch(error => {
showNotification(error.message, "error");
})
.finally(() => {
// Dispatch an action letting react-admin know a API call has ended
fetchEnd();
});
};
render() {
const { showDialog } = this.state;
const { isSubmitting } = this.props;
return (
<Fragment>
<Button onClick={this.handleClick} label="ra.action.create">
<IconContentAdd />
</Button>
<Dialog
fullWidth
open={showDialog}
onClose={this.handleCloseClick}
aria-label="Create post"
>
<DialogTitle>Create post</DialogTitle>
<DialogContent>
<SimpleForm
// We override the redux-form name to avoid collision with the react-admin main form
form="post-quick-create"
resource="posts"
// We override the redux-form onSubmit prop to handle the submission ourselves
onSubmit={this.handleSubmit}
// We want no toolbar at all as we have our modal actions
toolbar={null}
>
<TextInput source="title" validate={required()} />
<LongTextInput source="teaser" validate={required()} />
</SimpleForm>
</DialogContent>
<DialogActions>
<SaveButton saving={isSubmitting} onClick={this.handleSaveClick} />
<Button label="ra.action.cancel" onClick={this.handleCloseClick}>
<IconCancel />
</Button>
</DialogActions>
</Dialog>
</Fragment>
);
}
}
const mapStateToProps = state => ({
isSubmitting: isSubmitting("post-quick-create")(state),
});
const mapDispatchToProps = {
change,
fetchEnd,
fetchStart,
showNotification,
submit,
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(PostQuickCreateButton);
Now that I have a way to create the new post, let's see how to use it:
// in PostReferenceInput.js
import React, { Fragment } from "react";
import { ReferenceInput, SelectInput } from "react-admin";
import PostQuickCreateButton from "./PostQuickCreateButton";
const PostReferenceInput = () => (
<Fragment>
<ReferenceInput {...this.props}>
<SelectInput optionText="title" />
</ReferenceInput>
<PostQuickCreateButton />
</Fragment>
);
Voila!
Or not... If you followed along, there is still one remaining issue. After you create a new post, it does appear as the value of the SelectInput
. However, if you select another post afterwards, the new one disappears! This is because the ReferenceInput
has no clue about this new post. I must instruct it to refresh its data after the post creation:
// in PostQuickCreateButton.js
// ...
import {
// ...
crudGetMatching,
} from "react-admin";
// ...
class PostQuickCreateButton extends Component {
// ...
handleSubmit = values => {
const {
change,
crudGetMatching,
fetchStart,
fetchEnd,
showNotification,
} = this.props;
// Dispatch an action letting react-admin know a API call is ongoing
fetchStart();
// As we want to know when the new post has been created in order to close the modal, we use the
// dataProvider directly
dataProvider(CREATE, "posts", { data: values })
.then(({ data }) => {
// Refresh the choices of the ReferenceInput to ensure our newly created post
// always appear, even after selecting another post
crudGetMatching(
"posts",
"comments@post_id",
{ page: 1, perPage: 25 },
{ field: "id", order: "DESC" },
{}
);
// Update the main react-admin form (in this case, the comments creation form)
change(REDUX_FORM_NAME, "post_id", data.id);
this.setState({ showDialog: false });
})
.catch(error => {
showNotification(error.message, "error");
})
.finally(() => {
// Dispatch an action letting react-admin know a API call has ended
fetchEnd();
});
};
// ...
}
// ...
const mapDispatchToProps = {
// ...
crudGetMatching,
};
// ...
Quick Preview In a Sidepanel
Now, what about adding a quick preview of the selected post? Let's add a PostQuickPreviewButton
component showing more details about the selected post inside a Drawer.
// in PostQuickPreviewButton.js
import React, { Component, Fragment } from "react";
import { connect } from "react-redux";
import Drawer from "@material-ui/core/Drawer";
import { withStyles } from "@material-ui/core/styles";
import IconImageEye from "@material-ui/icons/RemoveRedEye";
import IconKeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight";
import { Button, SimpleShowLayout, TextField } from "react-admin";
const styles = theme => ({
field: {
// These styles will ensure our drawer don't fully cover our
// application when teaser or title are very long
"& span": {
display: "inline-block",
maxWidth: "20em",
},
},
});
const PostPreviewView = ({ classes, ...props }) => (
<SimpleShowLayout {...props}>
<TextField source="id" />
<TextField source="title" className={classes.field} />
<TextField source="teaser" className={classes.field} />
</SimpleShowLayout>
);
const mapStateToProps = (state, props) => ({
// Get the record by its id from the react-admin state.
record: state.admin.resources[props.resource]
? state.admin.resources[props.resource].data[props.id]
: null,
version: state.admin.ui.viewVersion,
});
const PostPreview = connect(
mapStateToProps,
{}
)(withStyles(styles)(PostPreviewView));
class PostQuickPreviewButton extends Component {
state = { showPanel: false };
handleClick = () => {
this.setState({ showPanel: true });
};
handleCloseClick = () => {
this.setState({ showPanel: false });
};
render() {
const { showPanel } = this.state;
const { id } = this.props;
return (
<Fragment>
<Button onClick={this.handleClick} label="ra.action.show">
<IconImageEye />
</Button>
<Drawer anchor="right" open={showPanel} onClose={this.handleCloseClick}>
<div>
<Button label="Close" onClick={this.handleCloseClick}>
<IconKeyboardArrowRight />
</Button>
</div>
<PostPreview id={id} basePath="/posts" resource="posts" />
</Drawer>
</Fragment>
);
}
}
export default connect()(PostQuickPreviewButton);
And this is how to use it inside our PostReferenceInput
component:
import React, { Fragment } from "react";
import { Field } from "redux-form";
import { ReferenceInput, SelectInput } from "react-admin";
import PostQuickCreateButton from "./PostQuickCreateButton";
import PostQuickPreviewButton from "./PostQuickPreviewButton";
const PostReferenceInput = props => (
<Fragment>
<ReferenceInput {...props}>
<SelectInput optionText="title" />
</ReferenceInput>
<PostQuickCreateButton />
// We use Field to get the current value of the `post_id` field
<Field
name="post_id"
component={({ input }) =>
input.value && <PostQuickPreviewButton id={input.value} />
}
/>
</Fragment>
);
export default PostReferenceInput;
Voila!
Conclusion
I've shown in this article that you're not constrained to use react-admin components such as Create
or Show
. Sometimes, a mix of classic React and some carefully picked react-admin components will allow you to create features not already baked-in. As we said in the previous article, It's just Reactâ„¢, It's just JavaScriptâ„¢ !
You can explore the full code for this article in this codesandbox