DLM Tutorials

Part 5

RepoStarter CodeFinished Code
Rails API05_start05_end
React Client05_start05_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.

422 error in chrome network tab 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:

redux devtools SUCCESSFULLY_CREATED_GROUP happening on validation failure

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 with mapDispatchToProps.
  • add errors to state so we can update them if we get a rejected promise back from API
  • add dispatchCreateEvent to props to allow dispatch of the createEvent action creator using mapDispatchToProps
  • rework handleSubmit to use dispatchCreateEvent and to setState with errors if something goes wrong
  • rework the render() method to pull errors 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.

New Event Form Validation Error Messages

Keep working in the woodshed until your skills catch up to your taste.
If you'd like to get in touch, reach out on LinkedIn.