Part 3
Repo | Starter Code | Finished Code |
---|---|---|
Rails API | 03_start | 03_end |
React Client | 03_start | 03_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.
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.
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.
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.
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.
Add links for to GroupShowContainer’s route from GroupListItem
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.