DLM

Part 3

RepoStarter CodeFinished Code
Rails API03_start03_end
React Client03_start03_end

If you’d like to code along with me here, clone down the repos to your machine and checkout the 03_start branch in each. For rails, boot up your server like this: rails s -p 3001. For react, yarn start should do it.

Adding Events, File Uploads and Serializers

For this section, we’ll be adding Events to our application. These are the tasks we’re going to need to accomplish:

• Add a New container called NewEventContainer with a form that will create a new event.

• Add a client side route /groups/:groupId/events/new that will render our new container component

• Build out our form inputs in the NewEventContainer render() method

• Add a handleSubmit event handler that will create a FormData object and post it to the API.

• Check in Postico that we’ve got data in our database after submitting the form.

• Add a new container component called GroupShowContainer

• Add a client side route /groups/:id that points to the GroupShowContainer

• Add links to the /groups/:id route from the GroupListItem component

• Add a componentDidMount to GroupShowContainer which will fetch from /groups/:id and get the group and its events. To start, we’ll console.log the API response.

• Add a GroupSerializer to our Rails API that we can use in our Groups#show action to render json for the group and all of its events (including a poster_url for the uploaded poster image)

• Display the events in a grid within GroupShowContainer after they load

• Connect the two components by adding a link from GroupShowContainer to NewEventContainer and then adding a redirect after submitting the form to create a new event that points back to the appropriate GroupShowContainer.

Adding the NewEventContainer

We’ll start by adding a new container called NewEventContainer with a form that will create a new event.

touch src/containers/NewEventContainer.js
import React, { Component } from 'react'

export default class NewEventContainer extends Component {
  render() {
    return <form></form>
  }
}

Our form needs to have inputs matching our database schema.

create_table "events", force: :cascade do |t|
  t.string "name"
  t.datetime "start_time"
  t.datetime "end_time"
  t.string "location"
  t.bigint "group_id", null: false
  t.bigint "users_id", null: false
  t.datetime "created_at", precision: 6, null: false
  t.datetime "updated_at", precision: 6, null: false
  t.index ["group_id"], name: "index_events_on_group_id"
  t.index ["users_id"], name: "index_events_on_users_id"
end

We’ll need inputs for name, start_time, end_time, location and we’ll also need to be able to accept a poster file input as well. We’re set up on the backend to support this because we have has_one_attached :poster in our Event model.

class Event < ApplicationRecord
  has_one_attached :poster
  #...
end

So, it would be useful to have a description for events as well. So let’s add that before we proceed. In the terminal with your rails API in focus, we’ll want to run:

rails g migration addDescriptionToEvents description:text

Check it to make sure it looks all right and then run it. You should see something like this, after you run rails db:migrate

== 20210225231454 AddDescriptionToEvents: migrating ===========================
-- add_column(:events, :description, :text)
   -> 0.0442s
== 20210225231454 AddDescriptionToEvents: migrated (0.0443s) ==================

Now if we check schema we should see

create_table "events", force: :cascade do |t|
  t.string "name"
  t.datetime "start_time"
  t.datetime "end_time"
  t.string "location"
  t.bigint "group_id", null: false
  t.bigint "users_id", null: false
  t.datetime "created_at", precision: 6, null: false
  t.datetime "updated_at", precision: 6, null: false
  t.text "description"
  t.index ["group_id"], name: "index_events_on_group_id"
  t.index ["users_id"], name: "index_events_on_users_id"
end

So, our set of fields for the form will be:

  • name (text input)
  • description (textarea)
  • start_time (datetime-local)
  • end_time (datetime-local)
  • location (text input)
  • poster (file input)

Before we build out the form let’s create a client side route so we can watch our progress in the browser.

Add a client side route for /groups/:groupId/events/new

To add the route, we’ll hop over to App.js and import the new component we created.

import NewEventContainer from './containers/NewEventContainer'
<Switch>
  <Route exact path="/">
    <GroupsIndexContainer />
  </Route>
  <Route path="/groups/new" component={GroupFormContainer} />
  <Route path="/groups/:groupId/events/new" component={NewEventContainer} />
</Switch>

So, to test this we’ll visit http://localhost:3000/groups/1/events/new in our browser and we should see the form there. Now we can move on to actually building out our inputs.

Build out our form inputs in the render() method

  • name (text input)
  • description (textarea)
  • start_time (datetime-local)
  • end_time (datetime-local)
  • location (text input)
  • poster (file input)
import React, { Component } from "react";

export default class NewEventContainer extends Component {
  render() {
    return (
      <form
        className="max-w-4xl w-11/12 mx-auto mt-16 shadow-lg px-8 py-6
      >
        <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>
    );
  }
}

Add a handleSubmit function that will create formData and post it to the API

We now need to add a handleSubmit to hit our API with the formData. Instead of using a controlled form here, we’re going to pull the data out of the target of our submit event and attach it to a formData object we’re building so we can send that as the body of our post request to create the new event. We ened to be thinking about what the rails API expects to see for the event_params

def event_params
  params.require(:event).permit(:name, :start_time, :end_time, :location, :poster)
end

So our formdata needs to include an event key and all other data should be nested underneath it. This means that when we append, all of the keys should start with event[

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)

  fetch('http://localhost:3001/events', {
    method: 'post',
    body,
  })
    .then((res) => res.json())
    .then((eventJson) => {
      console.log(eventJson)
    })
}

When I first did this, I got an error regarding the file input.

Cannot Read Property files of undefined

If you hit an error like this, make sure you keep an eye on the other code as well. In this case, the line that triggers this error is below a lot of lines of code that are very similar. So, the fix to the problem came from comparing the other form inputs to the file input. When I did that, I noticed that I’d forgotten to add the name attribute to the <input type="file" />. Adding the name to the input tag is what gives you access to a method on the HTMLFormElement object to access that input and its value. You can read more about how this works on MDN’s documentation for the HTMLFormElement object.

After fixing the error above, when we fill in the form and submit it we get an error in the console, so let’s check the network tab and the chrome console output.

Network Tab 422 Output on post ‘/events’

So we need to pass that information along. Another place we should check here is our rails server logs. If we check there, we’ll see something like this:

Started POST "/events" for ::1 at 2021-02-25 16:08:01 -0800
Processing by EventsController#create as */*
  Parameters: {"event"=>{"name"=>"Robots", "description"=>"yes", "start_time"=>"2021-02-25T16:07", "end_time"=>"2021-02-25T18:07", "location"=>"My Room", "poster"=>#<ActionDispatch::Http::UploadedFile:0x00007fd5e0cacf20 @tempfile=#<Tempfile:/var/folders/cs/6b2xd6z12dx3g0slv99vm92m0000gn/T/RackMultipart20210225-76245-1awfdqt.jpg>, @original_filename="robot-fi-m-festival-poster-template-fcbda66ecd661bce14afe3289ed4d35c_screen.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"event[poster]\"; filename=\"C:\\fakepath\\robot-fi-m-festival-poster-template-fcbda66ecd661bce14afe3289ed4d35c_screen.jpg\"\r\nContent-Type: image/jpeg\r\n">}}
Unpermitted parameter: :description
Completed 422 Unprocessable Entity in 2123ms (Views: 28.3ms | ActiveRecord: 114.4ms | Allocations: 60749)

Notice we’re getting a warning: unpermitted parameter: :description. So, we need to check our strong parameters and add :description there. Also, because the event is missing a group and a user, we’ll need to allow the :group_id through event_params as well. But, we can handle the :user_id by creating the event using our mocked current_user method in our application_controller. Because a user has_many :events, we can do current_user.events.build in our controller to have the foreign key, :user_id, assigned automatically. We’ll build out JWT auth later and then we can remove this method but for now let’s add this:

class ApplicationController < ActionController::API
  def current_user
    User.first_or_create(email: 'test@test.com', password: 'password')
  end
end

Now, we can use this method in EventsController#create to add that foreign key automatically.

# POST /events
def create
  @event = current_user.events.build(event_params)

  if @event.save
    render json: @event, status: :created, location: @event
  else
    render json: @event.errors, status: :unprocessable_entity
  end
end
# ...
def event_params
  params.require(:event).permit(:name, :description, :start_time, :end_time, :location, :group_id, :poster)
end

We need to be able to pass the :group_id into the controller through params from our react form component, so we’ll need to add that to our FormData object as well. To figure out how we’re going to do that, it would be nice to use some dev tools designed to show us what we have access to within the NewEventContainer component. So, we want to check our React developer tools and look at the component to see what we can do.

react developer tools info for NewEventContainer

Once we see the shape, we can pull out the data from the match prop added by react router (because this component is hooked up to a client side route). We’ll add the following line before we send the body through our fetch request in handleSubmit

body.append('event[group_id]', this.props.match.params.groupId)

Let’s give it a shot!

We got a server error :(

Completed 500 Internal Server Error in 750ms (ActiveRecord: 98.1ms | Allocations: 29170)



ActiveModel::UnknownAttributeError (unknown attribute 'user_id' for Event.):

app/controllers/events_controller.rb:18:in `create'

Checking the schema it actually looks like this:

create_table "events", force: :cascade do |t|
  t.string "name"
  t.datetime "start_time"
  t.datetime "end_time"
  t.string "location"
  t.bigint "group_id", null: false
  t.bigint "users_id", null: false
  t.datetime "created_at", precision: 6, null: false
  t.datetime "updated_at", precision: 6, null: false
  t.text "description"
  t.index ["group_id"], name: "index_events_on_group_id"
  t.index ["users_id"], name: "index_events_on_users_id"
end

We need to fix the references here.

rails g migration fixUserReferencesInEvents

Then fill in the migration with this:

class FixUserReferencesInEvents < ActiveRecord::Migration[6.1]
  def change
    rename_column :events, :users_id, :user_id
  end
end

As of Rails 4, renaming the column using rename_column will automatically update the corresponding index as well.

create_table "events", force: :cascade do |t|
  t.string "name"
  t.datetime "start_time"
  t.datetime "end_time"
  t.string "location"
  t.bigint "group_id", null: false
  t.bigint "user_id", null: false
  t.datetime "created_at", precision: 6, null: false
  t.datetime "updated_at", precision: 6, null: false
  t.text "description"
  t.index ["group_id"], name: "index_events_on_group_id"
  t.index ["user_id"], name: "index_events_on_user_id"
end

Now, let’s try that again! Didn’t work, maybe restart the server? Woohoo!

{id: 1, name: "Robots", start_time: "2021-02-25T16:25:00.000Z", end_time: "2021-02-25T18:25:00.000Z", location: "My Room", }created_at: "2021-02-26T00:32:12.255Z"description: "yes"end_time: "2021-02-25T18:25:00.000Z"group_id: 1id: 1location: "My Room"name: "Robots"start_time: "2021-02-25T16:25:00.000Z"updated_at: "2021-02-26T00:32:12.368Z"user_id: 1__proto__: Object

Now we can check Postico to see if the blob is there too.

Postico output confirming upload completed successfully Got it!!!

Add a New Container component GroupShowContainer.

Now that we’ve got our form working and we can add events, let’s add another container where we’ll be able to display the events that have been added to a group.

touch src/containers/GroupShowContainer.js

And inside the file:

// src/containers/GroupShowContainer.js
import React, { Component } from 'react'

export default class GroupShowContainer extends Component {
  render() {
    return <section>GroupShowContainer</section>
  }
}

Add a new client side route /groups/:id which points to the GroupShowContainer

So we can test this out in the browser, we’ll need to have a client side route that will render this component. To add that, hop into App.js and import the component:

import GroupShowContainer from './containers/GroupShowContainer'

Then add the route to the switch statement

<Switch>
  <Route exact path="/">
    <GroupsIndexContainer />
  </Route>
  <Route path="/groups/new" component={GroupFormContainer} />
  <Route path="/groups/:groupId/events/new" component={NewEventContainer} />
  <Route path="/groups/:groupId" component={GroupShowContainer} />
</Switch>

Note that the order matters here. If we put this route before the route to /groups/new, we’d never hit the /groups/new route because it would match /groups/:groupId. Since the Switch component only allows a single matching route, we want to make sure that routes that are specific cases of more general patterns are defined first. This truth about Switch is why we had to introduce the exact prop before our <Route path="/" />. Without it, we’ll only ever render the GroupsIndexContainer because any url will match the first route if we don’t specify an exact match.

Okay, so we’ve got the route, let’s add links to the index view so we can navigate to our new route. In order to do this, however, note that we need to actually import the link to our GroupListItem component. This is because the GroupListItem component is where we’re actually going to display the name of the group that we want to convert to a link to the route for our new GroupShowContainer component.

import React from 'react'
import { Link } from 'react-router-dom'

const GroupListItem = ({ group }) => {
  return (
    <li className="" key={group.id}>
      <Link to={`/groups/${group.id}`}>{group.name}</Link>
    </li>
  )
}

export default GroupListItem

Check it out in the browser and see if we can get to our new component via the groups index. If you see ‘GroupShowContainer’ in the browser after clicking on one of the links in the list, we’re good to go!

Add a componentDidMount to GroupShowContainer to fetch data from the API

We’ll want to add the componentDidMount so we can trigger a fetch to /groups/:id and get the group and its events. To start, we’ll console.log the API response.

In order to get the data for a particular group, we’re going to need to know the id of that group. Thankfully, because we’ve configured a route to point to our container componeont, React Router will provide us a match prop that will contain the route parameters, including the :groupId from the route we defined. We can access this by going through props: this.props.match.params.groupId.

componentDidMount() {
  const groupId = this.props.match.params.groupId
  fetch(`http://localhost:3001/groups/${groupId}`)
    .then(res => res.json())
    .then(groupJson => {
      console.log(groupJson);
    })
}

We’ll see something like this:

{
  "id": 1,
  "name": "software-engineering-052620",
  "created_at": "2021-02-24T00:36:06.684Z",
  "updated_at": "2021-02-24T00:36:06.684Z"
}

Looks good! But, we need to see the events for that group as well, so for that we need to introduce a serializer.

Add a GroupSerializer to our Rails API that will include the events in the json response.

In our rails terminal, let’s run

rails g serializer Group id name

Then we’ll open the file and add has_many :events

class GroupSerializer
  include JSONAPI::Serializer
  attributes :id, :name
  has_many :events
end

To check that this is working we need to visit http://localhost:3001/groups/1. If we do that now we’ll see no change, because we haven’t actually used the serializer yet. We need to create a new instance of the serializer in the show action in our GroupsController.

# GET /groups/1
def show
  render json: GroupSerializer.new(@group, include: [:events])
end

Now let’s revisit the route in the browser. Now we see this:

NameError in GroupsController#show
GroupSerializer cannot resolve a serializer class for 'event'. Attempted to find 'EventSerializer'. Consider specifying the serializer directly through options[:serializer].

So, we need an EventSerializer as well.

rails g serializer Event id name description start_time end_time location poster

This will give us:

class EventSerializer
  include JSONAPI::Serializer
  attributes :id, :name, :description, :start_time, :end_time, :location, :poster
end

Now, let’s try the endpoint again. Now we see this:

{
  "data": {
    "id": "1",
    "type": "group",
    "attributes": {
      "id": 1,
      "name": "software-engineering-052620"
    },
    "relationships": {
      "events": {
        "data": [
          {
            "id": "1",
            "type": "event"
          }
        ]
      }
    }
  },
  "included": [
    {
      "id": "1",
      "type": "event",
      "attributes": {
        "id": 1,
        "name": "Robots",
        "description": "yes",
        "start_time": "2021-02-25T16:25:00.000Z",
        "end_time": "2021-02-25T18:25:00.000Z",
        "location": "My Room",
        "poster": {
          "name": "poster",
          "record": {
            "id": 1,
            "name": "Robots",
            "start_time": "2021-02-25T16:25:00.000Z",
            "end_time": "2021-02-25T18:25:00.000Z",
            "location": "My Room",
            "group_id": 1,
            "user_id": 1,
            "created_at": "2021-02-26T00:32:12.255Z",
            "updated_at": "2021-02-26T00:32:12.368Z",
            "description": "yes"
          }
        }
      }
    }
  ]
}

We got a lot of stuff! As of now, this is more than we want and a little less than we need. We have a bunch of information about the poster, but none of it will actually help us display the image (a poster_url is missing).

We can make adjustments in our controller to return only the information we want. In our case, we can start by focusing on the poster_url and only returning that as opposed to all of the additional information we’re currently getting that isn’t necessary.

We want to add a method to the Event model to allow us to fetch the poster_url

def poster_url
  Rails.application.routes.url_helpers.url_for(poster) if poster.attached?
end

We can then adjust the serializer to use that as an attribute instead of poster

class EventSerializer
  include JSONAPI::Serializer
  attributes :id, :name, :description, :start_time, :end_time, :location, :poster_url
end

Let’s try this out in the browser again. Now, we get an error message

ArgumentError in GroupsController#show
Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true

To fix this, we need to add some code to the bottom of config/environments/development.rb

Rails.application.routes.default_url_options = {
  host: 'http://localhost:3001'
}

Next, since we changed a file in the config directory, let’s kill our rails server and restart it.

Now, we’ve got this:

{
  "data": {
    "id": "1",
    "type": "group",
    "attributes": {
      "id": 1,
      "name": "software-engineering-052620"
    },
    "relationships": {
      "events": {
        "data": [
          {
            "id": "1",
            "type": "event"
          }
        ]
      }
    }
  },
  "included": [
    {
      "id": "1",
      "type": "event",
      "attributes": {
        "id": 1,
        "name": "Robots",
        "description": "yes",
        "start_time": "2021-02-25T16:25:00.000Z",
        "end_time": "2021-02-25T18:25:00.000Z",
        "location": "My Room",
        "poster_url": "http://localhost:3001/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--7f41832ac530f42fe3f49057de8fdda3744ca3e6/robot-fi-m-festival-poster-template-fcbda66ecd661bce14afe3289ed4d35c_screen.jpg"
      }
    }
  ]
}

This is better, but it’s going to require a bunch of destructuring client side to get at the data that we actually want. So we can go a bit further in the controller to streamline what it is we actually want.

This is a good moment to add in a byebug so you can play around. In the GroupsController, we can use the serializable_hash method from the jsonapi serializer gem to pull out only the info we want. We want to have the group name and id and then an array of all of the events.

If we do this

hash = GroupSerializer.new(@group, include: [:events]).serializable_hash
hash[:data][:attributes] # will give us the id and name of the group
hash[:included] #looks like the following
[
  {
    :id=>"1",
    :type=>:event,
    :attributes=>{
      :id=>1,
      :name=>"Robots",
      :description=>"yes",
      :start_time=>Thu, 25 Feb 2021 16:25:00.000000000 UTC +00:00,
      :end_time=>Thu, 25 Feb 2021 18:25:00.000000000 UTC +00:00,
      :location=>"My Room",
      :poster_url=>"http://localhost:3001/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik1RPT0iLCJleHAiOm51bGwsInB1ciI6ImFjdGl2ZV9zdG9yYWdlL2Jsb2IifX0=--ecbc1ed90ea21daec6b9fb54800eec6b5e8e6cb21418935b32f58fb8752299de/robot-fi-m-festival-poster-template-fcbda66ecd661bce14afe3289ed4d35c_screen.jpg"
    }
  }
]

If we want to return something that looks like this:

{
  :group=> {
    :id=>1,
    :name=>"software-engineering-052620"
  },
  :events=>[
    {
      :id=>1,
      :name=>"Robots",
      :description=>"yes",
      :start_time=>Thu, 25 Feb 2021 16:25:00.000000000 UTC +00:00,
      :end_time=>Thu, 25 Feb 2021 18:25:00.000000000 UTC +00:00,
      :location=>"My Room",
      :poster_url=>"http://localhost:3001/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik1RPT0iLCJleHAiOm51bGwsInB1ciI6ImFjdGl2ZV9zdG9yYWdlL2Jsb2IifX0=--ecbc1ed90ea21daec6b9fb54800eec6b5e8e6cb21418935b32f58fb8752299de/robot-fi-m-festival-poster-template-fcbda66ecd661bce14afe3289ed4d35c_screen.jpg"
    }
  ]
}

Then, we’d need to map over the hash[:included] and pull out the attributes for each.

hash[:included].map{|event| event[:attributes]}

Finally, we’d have something like this:

# GET /groups/1
def show
  hash = GroupSerializer.new(@group, include: [:events]).serializable_hash
  render json: {
    group: hash[:data][:attributes],
    events: hash[:included].map{|event| event[:attributes]}
  }
end

Now, we’ve got this:

{
  "group": {
    "id": 1,
    "name": "software-engineering-052620"
  },
  "events": [
    {
      "id": 1,
      "name": "Robots",
      "description": "yes",
      "start_time": "2021-02-25T16:25:00.000Z",
      "end_time": "2021-02-25T18:25:00.000Z",
      "location": "My Room",
      "poster_url": "http://localhost:3001/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--7f41832ac530f42fe3f49057de8fdda3744ca3e6/robot-fi-m-festival-poster-template-fcbda66ecd661bce14afe3289ed4d35c_screen.jpg"
    }
  ]
}

Awesome! Now that we’ve got control over what the API returning and we’ve cut out unnecessary complexity in the data structure, we can set up our client side code to consume the data from this endpoint.

Displaying the Group and its events in a Grid within GroupShowContainer

Let’s update our GroupShowContainer component to have state that updates after componentDidMount fires to display the group and its events.

import React, { Component } from 'react'

export default class GroupShowContainer extends Component {
  state = {
    group: {},
    events: [],
    loading: true,
  }

  componentDidMount() {
    const groupId = this.props.match.params.groupId
    fetch(`http://localhost:3001/groups/${groupId}`)
      .then((res) => res.json())
      .then((groupJson) => {
        console.log(groupJson)
      })
  }

  render() {
    return <section>GroupShowContainer</section>
  }
}

When we load this in the browser, we should see our data from the api logged to the console.

Next, let’s add the centering logic to render and also a conditional statement that will render a spinner if loading is true and the content if not. We’ll also want to setState after we get the data from the API, setting loading to false in the process, so we can trigger the re-render that will replace the spinner with our content.

import React, { Component } from 'react'

export default class GroupShowContainer extends Component {
  state = {
    group: {},
    events: [],
    loading: true,
  }

  componentDidMount() {
    const groupId = this.props.match.params.groupId
    fetch(`http://localhost:3001/groups/${groupId}`)
      .then((res) => res.json())
      .then(({ group, events }) => {
        this.setState({
          group,
          events,
          loading: false,
        })
      })
  }

  render() {
    if (this.state.loading) {
      return <div>Loading Spinner</div>
    }
    return <section>GroupShowContainer</section>
  }
}

We won’t render the component contents until we have the data from the API, so we don’t need to add conditional logic to our second return statement’s JSX.

Now, let’s display the data and make it a tiny bit prettier.

import React, { Component } from 'react'

export default class GroupShowContainer extends Component {
  state = {
    group: {},
    events: [],
    loading: true,
  }

  componentDidMount() {
    const groupId = this.props.match.params.groupId
    fetch(`http://localhost:3001/groups/${groupId}`)
      .then((res) => res.json())
      .then(({ group, events }) => {
        this.setState({
          group,
          events,
          loading: false,
        })
      })
  }

  render() {
    if (this.state.loading) {
      return <div>Loading Spinner</div>
    }
    return (
      <section className="max-w-6xl w-11/12 mx-auto mt-16">
        <h1 className="text-3xl font-bold text-center">
          {this.state.group.name}
        </h1>
        <div className="grid grid-cols-3">
          {this.state.events.map((event) => (
            <figure>
              <img className="" alt={event.name} src={event.poster_url} />
              <h2>{event.name}</h2>
              <p>{event.start_time}</p>
              <p>{event.end_time}</p>
              <p>{event.location}</p>
              {/* Later we'll add a spoiler here to show the description */}
            </figure>
          ))}
        </div>
      </section>
    )
  }
}

This doesn’t look super good now, but we’ll come back to it and tighten it up a bit later.

Connecting the two components

Our final task for this part, will be to add a link from the GroupShowContainer to the route where we can add an event to the group. And then, when we submit the form to add a new group, add a redirect back to this /groups/:id route.

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>
    )
  }
}

Add a link to the new event route from the /groups/:id route:

<p className="my-2">
  <Link to={`/groups/${this.state.group.id}/events/new`}>Add an Event</Link>
</p>

Now, we can click the link from /groups/:id, fill in the form to add a new event on /groups/:id/events/new and get redirected back to /groups/:id after submission where we’ll see the event we added in the grid.

Resources

Keep working in the woodshed until your skills catch up to your taste.