Part 5
Repo | Starter Code | Finished Code |
---|---|---|
Rails API | 05_start | 05_end |
React Client | 05_start | 05_end |
If you’d like to code along with me here, clone down the repos to your machine and checkout the 05_start
branch in each. For rails, boot up your server like this: rails s -p 3001
. For react, yarn start
should do it.
Before we start, let’s fix a bug from last time
We’ve got a scenario where multiple copies of events will appear on the group show page when we return to a particular show route after we’ve gone back to the index. We can fix by updating our reducer:
case SUCCESSFULLY_LOADED_GROUP_EVENTS:
return {
groupsLoaded: {
...state.groupsLoaded,
[action.payload.group.id]: "successful",
},
list: state.list
.filter((event) => event.group_id !== action.payload.group.id)
.concat(action.payload.events),
};
The problem was caused by the fact that we were adding the events we got back from the API to the list again each time the action was dispatched. The adjustment we made here is that we add in the events from the API every time, but we filter out the events that are from the group we’re viewing before adding them back in from the API. This way we have the latest events from the API in the store, but we’re not storing the same events more than once.
Reduxifying the GroupFormContainer
For this one we’ll start with the GroupFormContainer
component. It currently looks like this:
import React, { Component } from 'react'
export default class GroupFormContainer extends Component {
state = {
name: '',
}
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value,
})
}
handleSubmit = (e) => {
e.preventDefault()
fetch('http://localhost:3001/groups', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ group: this.state }),
})
.then((res) => res.json())
.then((groupJson) => {
this.props.history.push('/')
})
}
render() {
return (
<form
onSubmit={this.handleSubmit}
className="max-w-6xl w-3/4 mx-auto mt-16 shadow-lg px-4 py-6"
>
<h1 className="text-center text-3xl font-semibold mb-2">New Group</h1>
<fieldset>
<input
type="text"
name="name"
onChange={this.handleChange}
value={this.state.name}
placeholder="Name your group"
className="w-full border p-4 my-4"
/>
</fieldset>
<button
className="w-full p-4 bg-blue-300 mt-4 hover:bg-blue-400 transition-all duration-200"
type="submit"
>
Add Group
</button>
</form>
)
}
}
For the form components, we won’t be changing quite as much as we did for the other containers. This is because for these components we won’t be accessing data from the store, we’ll only be adding the ability to dispatch upon submission. We could choose to store the form state in redux as well, in which case we’d have a more substantial change like the ones we made to the previous two container components. The benefit of doing that is that you’re able to maintain form state even if you hit the back button. So, for example, we could start filling in the new event form and then hit the back button and then come back to the form and we’d still have the fields from before. This could be especially useful if we had a multi-part form.
One potential issue with that, however, is that we’d also still have the form state if we navigated back home and then to another group show page and tried to add an event for that (different) group. Using local state for the form data, we don’t have to handle those cases.
So, we can start by importing connect
and moving our default export to the bottom and defining mapDispatchToProps
import React, { Component } from 'react'
import { connect } from 'react-redux'
class GroupFormContainer extends Component {
state = {
name: '',
}
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value,
})
}
handleSubmit = (e) => {
e.preventDefault()
fetch('http://localhost:3001/groups', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ group: this.state }),
})
.then((res) => res.json())
.then((groupJson) => {
this.props.history.push('/')
})
}
render() {
return (
<form
onSubmit={this.handleSubmit}
className="max-w-6xl w-3/4 mx-auto mt-16 shadow-lg px-4 py-6"
>
<h1 className="text-center text-3xl font-semibold mb-2">New Group</h1>
<fieldset>
<input
type="text"
name="name"
onChange={this.handleChange}
value={this.state.name}
placeholder="Name your group"
className="w-full border p-4 my-4"
/>
</fieldset>
<button
className="w-full p-4 bg-blue-300 mt-4 hover:bg-blue-400 transition-all duration-200"
type="submit"
>
Add Group
</button>
</form>
)
}
}
const mapDispatchToProps = (dispatch) => {
return {}
}
export default connect(null, mapDispatchToProps)(GroupFormContainer)
Now, for the mapDispatchToProps
we’ll want to have the ability to dispatch
a new action called createGroup()
which we need to go define. So, let’s hop over to our groups
actions and add it in:
// src/actions/groups.js
// ...
import {
//...
SUCCESSFULLY_CREATED_GROUP
} from '.'
// ...
export const createGroup = (formData) => {
return (dispatch) => {
return fetch('http://localhost:3001/groups', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ group: formData }),
})
.then((res) => res.json())
.then((groupJson) => {
dispatch({
type: SUCCESSFULLY_CREATED_GROUP,
payload: groupJson,
})
})
}
}
We’ve got an action type here that isn’t exported yet, so let’s go add it:
// src/actions/index.js
// ...
export const SUCCESSFULLY_CREATED_GROUP = 'SUCCESSFULLY_CREATED_GROUP'
Finally, we need to update our groupsReducer
so it knows how to respond to this new action type. In the process, we can clean out some of the old imports for constants that we’re not using or have renamed.
// src/reducers/groups.js
import {
START_LOADING_GROUPS,
SUCCESSFULLY_LOADED_GROUPS,
FAILED_LOADING_GROUPS,
SUCCESSFULLY_LOADED_GROUP_EVENTS,
SUCCESSFULLY_CREATED_GROUP,
} from '../actions'
const initialState = {
loadingState: 'notStarted',
list: []
}
export default function groupsReducer(state = initialState, action) {
switch (action.type) {
case START_LOADING_GROUPS:
return { ...state, loadingState: 'inProgress' }
case SUCCESSFULLY_LOADED_GROUPS:
return {
list: action.payload,
groupsLoadingState: 'successful',
}
case SUCCESSFULLY_LOADED_GROUP_EVENTS:
const foundGroup = state.list.find(
(group) => group.id == action.payload.group.id
)
if (foundGroup) {
return state
} else {
return {
...state,
list: state.list.concat(action.payload.group),
}
}
case SUCCESSFULLY_CREATED_GROUP:
return {
...state,
list: state.list.concat(action.payload),
}
default:
return state
}
}
Here, we’re making the following changes:
- Adding the
SUCCESSFULLY_CREATED_GROUP
action type to the import - Adding a reducer case to update the state upon receving the above action.
Wrapping the GroupFormContainer
in connect()
Now, to get this working in our component, we need to rework it to be using the mapDispatchToProps
to create a dispatchCreateGroup
prop. We’ll also need to import the createGroup
action creator and to replace our fetch in handleSubmit
with a call to dispatchCreateGroup
.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { createGroup } from '../actions/groups'
class GroupFormContainer extends Component {
state = {
name: '',
}
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value,
})
}
handleSubmit = (e) => {
e.preventDefault()
this.props.dispatchCreateGroup(this.state).then((groupJson) => {
this.props.history.push('/')
})
}
render() {
return (
<form
onSubmit={this.handleSubmit}
className="max-w-6xl w-3/4 mx-auto mt-16 shadow-lg px-4 py-6"
>
<h1 className="text-center text-3xl font-semibold mb-2">New Group</h1>
<fieldset>
<input
type="text"
name="name"
onChange={this.handleChange}
value={this.state.name}
placeholder="Name your group"
className="w-full border p-4 my-4"
/>
</fieldset>
<button
className="w-full p-4 bg-blue-300 mt-4 hover:bg-blue-400 transition-all duration-200"
type="submit"
>
Add Group
</button>
</form>
)
}
}
const mapDispatchToProps = (dispatch) => {
return {
dispatchCreateGroup: (formData) => dispatch(createGroup(formData)),
}
}
export default connect(null, mapDispatchToProps)(GroupFormContainer)
Finally, it’d be nice to be able to display some errors as well! So, to get that working, we need to be able to be in some situation where an error happens. Currently, the only way we could do that would be if our server wasn’t running. Let’s add a validation to make sure that a group has a name and that it’s unique so we can test this out. So, on the rails end, we’ll want to open up our Group
model.
# app/models/group.rb
class Group < ApplicationRecord
has_many :events
validates :name, presence: true, uniqueness: true
end
Now, we should be able to try out filling in the form without entering any content and see if we’re able to get a different result.
The response actually looks like this:
{
name: ["can't be blank"]
}
But, we’re still getting redirected to the groups index route and we don’t see an error. Checking the Redux devtools reveals this:
So, even though there’s an error server side, we’re still dispatching our success action. Why is that? Well, it has to do with the way that the fetch
API works. The only things that makes fetch
return a rejected promise is a network error. So, we need to intervene in our promise callback itself and check if the response is ok
. If it is, then we parse it as json and pass it along, otherwise we want to raise the parsed response as an error.
export const createGroup = (formData) => {
return (dispatch) => {
return fetch('http://localhost:3001/groups', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ group: formData }),
})
.then((res) => {
if (res.ok) {
return res.json()
} else {
return res.json().then((errors) => Promise.reject(errors))
}
})
.then((groupJson) => {
dispatch({
type: SUCCESSFULLY_CREATED_GROUP,
payload: groupJson,
})
})
}
}
If we do this and check the Redux devtools again, we can see that we’re no longer dispatching the SUCCESSFULLY_CREATED_GROUP
action. We’ll also find that we’re no longer doing the redirect to /
anymore. This is good, because we don’t want to do this redirect if there was an error, we want to show them the error and let them try again. Since we’re returning a rejected promise if there’s a validation failure–and we get a non ok response–the then
callback that we chain on within handleSubmit
is not invoked. In order to actually display errors to the user if we have them, we can introduce a new piece of local state to our component so we can trigger a re-render to display the errors. Since we get a rejected promise back if there are errors, we can catch
those errors with a callback attached after the then
method that trigger the react router redirect.
// src/containers/GroupFormContainer.js
// ...
state = {
name: "",
errors: {}
};
// ...
handleSubmit = (e) => {
e.preventDefault();
this.props.dispatchCreateGroup(this.state)
.then(groupJson => {
this.props.history.push('/')
})
.catch(errors => {
this.setState({
errors
})
})
}
Now, when we submit the form empty, it won’t redirect back to '/'
and we’ll continue to see the form. But, we’re not able to see an error yet. To do that, we’ll need to use our state within the render()
method to display them.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { createGroup } from '../actions/groups'
class GroupFormContainer extends Component {
state = {
name: '',
errors: {}
}
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value,
})
}
handleSubmit = (e) => {
e.preventDefault();
this.props.dispatchCreateGroup(this.state)
.then(groupJson => {
this.props.history.push('/')
})
.catch(errors => {
this.setState({
errors
})
})
}
render() {
return (
<form
onSubmit={this.handleSubmit}
className="max-w-6xl w-3/4 mx-auto mt-16 shadow-lg px-4 py-6"
>
<h1 className="text-center text-3xl font-semibold mb-2">New Group</h1>
<fieldset>
<p className="h-8 pl-2 text-red-400">{this.state.errors.name}</p>
<input
type="text"
name="name"
onChange={this.handleChange}
value={this.state.name}
placeholder="Name your group"
className={`w-full border-2 focus:outline-none focus:ring-2 p-4 mb-4 ${
this.state.errors.name && "focus:ring-red-400 border-red-400"
}`}
/>
</fieldset>
<button
className="w-full p-4 bg-blue-300 mt-4 hover:bg-blue-400 transition-all duration-200"
type="submit"
>
Add Group
</button>
</form>
)
}
}
const mapDispatchToProps = (dispatch) => {
return {
dispatchCreateGroup: (formData) => dispatch(createGroup(formData)),
}
}
export default connect(null, mapDispatchToProps)(GroupFormContainer)
Now we’ll see the input surrounded with a red border if we submit the form and there’s a validation error associated with that field. We also adjusted the margin on the inputs, removed the top margin and added a p
tag above each input
with a fixed height that will display the validation error for that field if there is one. We use a fixed height so that the inputs don’t move around when we display the error messages.
NewEventFormContainer
Okay, so for the NewEventFormContainer
we’re going to use connect
to allow it to dispatch
the action that will create a new event. So again, we’re going to be adding one new action type: SUCCESSFULLY_CREATED_EVENT
. We’ll have an action creator that will dispatch the action depending on how the form submission went. We need a reducer case to handle the new action type. We also need to rework the render()
method in the NewEventFormContainer
so it will add errors next to the field labels if they’re present. Finally, we’ll add validations to the Event
model so that we can test our component to make sure it displays errors properly when validations fail.
Adding action types and action creator
To start, we need to create the two constants we’ll need to support creating events.
// src/actions/index.js
// groups/:groupId/events/new
export const SUCCESSFULLY_CREATED_EVENT = "SUCCESSFULLY_CREATED_EVENT";
Next, we’ll add the action creator.
// src/actions/events.js
import { SUCCESSFULLY_CREATED_EVENT } from "."
export const createEvent = (formData) => {
return (dispatch) => {
return fetch("http://localhost:3001/events", {
method: "POST",
body: formData
})
.then(res => {
if (res.ok) {
return res.json()
} else {
return res.json().then(errors => Promise.reject(errors))
}
})
.then(eventJson => {
dispatch({
type: SUCCESSFULLY_CREATED_EVENT,
payload: eventJson
})
})
};
};
Adding a case to the eventsReducer
Now, we’ll want to add the cases to our reducer for creating events. We’ll want to import the SUCCESSFULLY_CREATED_EVENT
action type so we can use it in our case statement.
// src/reducers/events.js
import {
SUCCESSFULLY_LOADED_GROUP_EVENTS,
START_LOADING_GROUP,
SUCCESSFULLY_CREATED_EVENT,
} from "../actions";
const initialState = {
groupsLoaded: {},
list: [],
};
export default function eventReducer(state = initialState, action) {
switch (action.type) {
case START_LOADING_GROUP:
return {
...state,
groupsLoaded: { ...state.groupsLoaded, [action.payload]: "inProgress" },
};
case SUCCESSFULLY_LOADED_GROUP_EVENTS:
return {
groupsLoaded: {
...state.groupsLoaded,
[action.payload.group.id]: "successful",
},
list: state.list.concat(action.payload.events),
};
case SUCCESSFULLY_CREATED_EVENT:
return {
...state,
list: state.list.concat(action.payload),
}
default:
return state;
}
}
Connecting the NewEventFormContainer
In order to get the form hooked up to the store, we’ll want to use the connect
helper to add the ability to consume errors from the store and also to dispatch
the action that will trigger the fetch request. This is what the NewEventFormContainer
looks like currently.
// src/containers/NewEventFormContainer.js
import React, { Component } from 'react'
export default class NewEventContainer extends Component {
handleSubmit = (e) => {
e.preventDefault();
const form = e.target;
const body = new FormData();
// we need to add the data from the form into this body (FormData) object using the append method
// When we do this we want to be thinking about how the rails API is expecting the event_params to look.
body.append("event[name]", form.name.value);
body.append("event[description]", form.description.value);
body.append("event[start_time]", form.start_time.value);
body.append("event[end_time]", form.end_time.value);
body.append("event[location]", form.location.value);
body.append("event[poster]", form.poster.files[0], form.poster.value);
body.append("event[group_id]", this.props.match.params.groupId)
fetch("http://localhost:3001/events", {
method: "post",
body
})
.then(res => res.json())
.then(eventJson => {
this.props.history.push(`/groups/${this.props.match.params.groupId}`);
})
}
render() {
return (
<form
className="max-w-4xl w-11/12 mx-auto mt-16 shadow-lg px-8 py-6"
onSubmit={this.handleSubmit}
>
<h1 className="text-3xl text-center font-semibold mb-8">New Event</h1>
<fieldset className="">
<label htmlFor="name" className="block uppercase">
Name
</label>
<input
type="text"
name="name"
id="name"
className="w-full border-2 p-4 my-4"
/>
</fieldset>
<fieldset className="">
<label htmlFor="description" className="block uppercase">
Description
</label>
<textarea
className="w-full border-2 p-4 my-4"
name="description"
id="description"
></textarea>
</fieldset>
<fieldset className="">
<label htmlFor="start_time" className="block uppercase">
Start Time
</label>
<input
type="datetime-local"
name="start_time"
id="start_time"
className="w-full border-2 p-4 my-4"
/>
</fieldset>
<fieldset className="">
<label htmlFor="end_time" className="block uppercase">
End Time
</label>
<input
type="datetime-local"
name="end_time"
id="end_time"
className="w-full border-2 p-4 my-4"
/>
</fieldset>
<fieldset className="">
<label htmlFor="location" className="block uppercase">Location</label>
<input
type="text"
name="location"
id="location"
className="w-full border-2 p-4 my-4"
/>
</fieldset>
<fieldset className="">
<label htmlFor="poster" className="block uppercase">Poster</label>
<input
type="file"
className="w-full my-4"
name="poster"
id="poster"
/>
</fieldset>
<button
type="submit"
className="w-full p-4 bg-blue-300 mt-4 hover:bg-blue-400 transition-all duration-200"
>
Add Event
</button>
</form>
);
}
}
We’ll need to do a few things here:
- Move the default export to the bottom and use
connect
withmapDispatchToProps
. - add
errors
to state so we can update them if we get a rejected promise back from API - add
dispatchCreateEvent
to props to allowdispatch
of thecreateEvent
action creator usingmapDispatchToProps
- rework
handleSubmit
to usedispatchCreateEvent
and to setState with errors if something goes wrong - rework the
render()
method to pullerrors
out of the state and display them. We may also need to adjust some of the styles to make sure that the errors look okay.
Adding connect to NewEventContainer
We’re going to import connect
from react-redux
and change our export to be the connected component instead of NewEventContainer
directly.
import React, { Component } from "react";
import { connect } from "react-redux";
class NewEventContainer extends Component {
handleSubmit = (e) => {
e.preventDefault();
const form = e.target;
const body = new FormData();
// we need to add the data from the form into this body (FormData) object using the append method
// When we do this we want to be thinking about how the rails API is expecting the event_params to look.
body.append("event[name]", form.name.value);
body.append("event[description]", form.description.value);
body.append("event[start_time]", form.start_time.value);
body.append("event[end_time]", form.end_time.value);
body.append("event[location]", form.location.value);
body.append("event[poster]", form.poster.files[0], form.poster.value);
body.append("event[group_id]", this.props.match.params.groupId);
fetch("http://localhost:3001/events", {
method: "post",
body,
})
.then((res) => res.json())
.then((eventJson) => {
this.props.history.push(`/groups/${this.props.match.params.groupId}`);
});
};
render() {
return (
<form
className="max-w-4xl w-11/12 mx-auto mt-16 shadow-lg px-8 py-6"
onSubmit={this.handleSubmit}
>
<h1 className="text-3xl text-center font-semibold mb-8">New Event</h1>
<fieldset className="">
<label htmlFor="name" className="block uppercase">
Name
</label>
<input
type="text"
name="name"
id="name"
className="w-full border-2 p-4 my-4"
/>
</fieldset>
<fieldset className="">
<label htmlFor="description" className="block uppercase">
Description
</label>
<textarea
className="w-full border-2 p-4 my-4"
name="description"
id="description"
></textarea>
</fieldset>
<fieldset className="">
<label htmlFor="start_time" className="block uppercase">
Start Time
</label>
<input
type="datetime-local"
name="start_time"
id="start_time"
className="w-full border-2 p-4 my-4"
/>
</fieldset>
<fieldset className="">
<label htmlFor="end_time" className="block uppercase">
End Time
</label>
<input
type="datetime-local"
name="end_time"
id="end_time"
className="w-full border-2 p-4 my-4"
/>
</fieldset>
<fieldset className="">
<label htmlFor="location" className="block uppercase">
Location
</label>
<input
type="text"
name="location"
id="location"
className="w-full border-2 p-4 my-4"
/>
</fieldset>
<fieldset className="">
<label htmlFor="poster" className="block uppercase">
Poster
</label>
<input
type="file"
className="w-full my-4"
name="poster"
id="poster"
/>
</fieldset>
<button
type="submit"
className="w-full p-4 bg-blue-300 mt-4 hover:bg-blue-400 transition-all duration-200"
>
Add Event
</button>
</form>
);
}
}
const mapDispatchToProps = (dispatch) => {
return {
}
}
export default connect(null, mapDispatchToProps)(NewEventContainer);
Add dispatchCreateEvent
to props using mapDispatchToProps
// ...
import { createEvent } from "../actions/events";
// ...
const mapDispatchToProps = (dispatch) => {
return {
dispatchCreateEvent: (formData) => dispatch(createEvent(formData))
};
};
rework handleSubmit()
to use dispatchCreateEvent
While we do this, let’s rename body
to formData
to be consistent with what our action creator expects.
handleSubmit = (e) => {
e.preventDefault();
const form = e.target;
const formData = new FormData();
// we need to add the data from the form into this formData (FormData) object using the append method
// When we do this we want to be thinking about how the rails API is expecting the event_params to look.
formData.append("event[name]", form.name.value);
formData.append("event[description]", form.description.value);
formData.append("event[start_time]", form.start_time.value);
formData.append("event[end_time]", form.end_time.value);
formData.append("event[location]", form.location.value);
formData.append("event[poster]", form.poster.files[0], form.poster.value);
formData.append("event[group_id]", this.props.match.params.groupId);
this.props.dispatchCreateEvent(formData).then(() => {
this.props.history.push(`/groups/${this.props.match.params.groupId}`);
});
};
rework render()
method to display errors
Here, we’re going to add the errors (if they’re present) into the label tags above each input.
render() {
return (
<form
className="max-w-4xl w-11/12 mx-auto mt-16 shadow-lg px-8 py-6"
onSubmit={this.handleSubmit}
>
<h1 className="text-3xl text-center font-semibold mb-8">New Event</h1>
<fieldset className="">
<label htmlFor="name" className="block uppercase">
Name{" "}
<span className="text-red-400">{this.state.errors.name}</span>
</label>
<input
type="text"
name="name"
id="name"
className={`w-full border-2 p-4 my-4 focus:outline-none focus:ring-2 ${
this.state.errors.name && "focus:ring-red-400 border-red-400"
}`}
/>
</fieldset>
<fieldset className="">
<label htmlFor="description" className="block uppercase">
Description{" "}
<span className="text-red-400">
{this.state.errors.description}
</span>
</label>
<textarea
name="description"
id="description"
className={`w-full border-2 p-4 my-4 focus:outline-none focus:ring-2 ${
this.state.errors.description &&
"focus:ring-red-400 border-red-400"
}`}
></textarea>
</fieldset>
<fieldset className="">
<label htmlFor="start_time" className="block uppercase">
Start Time{" "}
<span className="text-red-400">{this.state.errors.start_time}</span>
</label>
<input
type="datetime-local"
name="start_time"
id="start_time"
className={`w-full border-2 p-4 my-4 focus:outline-none focus:ring-2 ${
this.state.errors.start_time &&
"focus:ring-red-400 border-red-400"
}`}
/>
</fieldset>
<fieldset className="">
<label htmlFor="end_time" className="block uppercase">
End Time{" "}
<span className="text-red-400">{this.state.errors.end_time}</span>
</label>
<input
type="datetime-local"
name="end_time"
id="end_time"
className={`w-full border-2 p-4 my-4 focus:outline-none focus:ring-2 ${
this.state.errors.end_time && "focus:ring-red-400 border-red-400"
}`}
/>
</fieldset>
<fieldset className="">
<label htmlFor="location" className="block uppercase">
Location{" "}
<span className="text-red-400">{this.state.errors.location}</span>
</label>
<input
type="text"
name="location"
id="location"
className={`w-full border-2 p-4 my-4 focus:outline-none focus:ring-2 ${
this.state.errors.location && "focus:ring-red-400 border-red-400"
}`}
/>
</fieldset>
<fieldset className="">
<label htmlFor="poster" className="block uppercase">
Poster{" "}
<span className="text-red-400">{this.state.errors.poster}</span>
</label>
<input
type="file"
className="w-full my-4"
name="poster"
id="poster"
/>
</fieldset>
<button
type="submit"
className="w-full p-4 bg-blue-300 mt-4 hover:bg-blue-400 transition-all duration-200"
>
Add Event
</button>
</form>
);
}
Adding validations so we can test this out in the browser
Now, we need to switch gears to rails and add validations to the Event
model so we’re able to see errors if they occur.
class Event < ApplicationRecord
# ...
validates :name,:location, :description, :start_time, :end_time, presence: true
validates :name, uniqueness: {scope: [:start_time, :location, :group_id], message: "Whoops! Did you already create this event?"}, if: Proc.new { |event| event.name.present? }
validate :validate_start_time, if: Proc.new { |event| event.start_time.present? && event.end_time.present? }
def validate_start_time
if start_time >= end_time
errors.add(:start_time, "must be before the end time")
end
end
# ...
end
Note that a couple of these validations are conditional. We only want them to run if a certain condition is met. This is done via a Proc object. While a Proc
may seem unfamiliar, you’ve probably seen them more than you think. For example, every time you pass a block
of code to an iterator, that block
is actually a Proc
object. The way the Proc
works in this context is it allows us access to the object being validated and we can return a boolean indicating whether we want this particular validation to run in this case. If our Proc
returns a truthy
value, the validation will run. If not, Rails will skip our validation. This is useful in our case because we don’t want validations to run on fields that aren’t present.
Before we move on, let’s try out submitting the form to see that we’re able to get validation errors appearing at this point. Also, let’s make sure that we’re able to navigate to this page, submit the form and see the result as expected.