Coding Artist
Published in

Coding Artist

Full-Stack React With Phoenix (Chapter 7 | CRUD Operations)

Table of Contents

Chapter 1 | Why Bother?
Chapter 2 | Learning the Basics of Elixir
Chapter 3 | Introduction to Phoenix
Chapter 4 | Implementing React
Chapter 5 | Working With PostgreSQL
Chapter 6 | Creating a PostgreSQL API Service

Scope of This Chapter

In the previous chapter, we were able to start the process of programmatically creating a PostgreSQL API service.

So far, we have things working so a GET request on /users returns a JSON object containing all the rows in our blogs table in our database. Our React frontend is making the GET request and is rendering a blog card using the data in each row:

This chapter will be a continuation of this. We will finish off our API service so we can do create, read, update, and delete operations on our database and update the UI accordingly.

Show and Create

In the previous chapter, we set up the following routes. We fully implemented the get all blogs interaction. However, we need to finish off the getting a blog by id (read) and create a blog entry (crate) interactions.

In web/router.ex in our phoenix_react_curated_list project, you will see the definition of these routes:

scope "/", PhoenixReactCuratedList do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
get "/blogs", BlogsController, :index
post "/blogs", BlogsController, :create
get "/blogs/:id", BlogsController, :show
end

The following line will call a function called show in BlogsController when there is a GET request on the /blogs/:id route:

get "/blogs/:id", BlogsController, :show

This line will call a function called create in BlogsController when there is a POST request on the /blogs route:

post "/blogs", BlogsController, :create

If we head over to /web/controllers/blogs_controller.ex, we can see that we’ve already added the shell of these functions:

def create(conn, %{"blogs" => blogs_params}) doenddef show(conn, %{"id" => id}) doend

Show

Let’s start by implementing the show function.

First, let’s add a line that uses Repo.get to query the Blogs table using the id parameter and return a blog:

def show(conn, %{"id" => id}) do
blog = Repo.get(Blogs, id)
end

We also want to invoke our view to render a JSON object called show.json on our connection using the blog we stored:

def show(conn, %{"id" => id}) do
blog = Repo.get(Blogs, id)
render conn, "show.json", blog: blog
end

Just like our index function, we will handle this render function in our view found in web/views/blogs_view.ex.

We already have a render function that enumerates through a collection of all our blog posts and calls blogs_json on each iteration to return an object for each blog post within an array called blogs:

{
"blogs": [
{
"title": "Full-Stack React With Phoenix...",
"subtitle": "Benefits of an Elixir Backend",
"link": "http://bit.ly/2w5aen8",
"image": "http://bit.ly/2tOjzTM",
"author": "Mike Mangialardi"
}
]
}

In this render function, we use the blogs parameter:

%{blogs: blogs}

To render a single JSON object for a single blog, we can write another render function and using %{blog: blog} as the parameter:

def render("index.json", %{blog: blog}) do

end

We want to return just one JSON object by passing this parameter to the blogs_json function like so:

# show single blog
def render("show.json", %{blog: blog}) do
%{
blog: blogs_json(blog)
}
end

Notice that I’ve added a comment to distinguish between the render functions.

Open up Postman test out a GET request on http://localhost:4000/blogs/1 :

Awesome!

We can see that we’ve successfully returned a single JSON object called blog where there is an id of 1 in our blogs table of our PostgreSQL database.

We won’t use this HTTP request in our React frontend but the process would be similar as our GET http://localhost:4000/blogs that we requested using axios:

axios.get('http://localhost:4000/blog/1')
.then(response => {
this.setState({ blogs: response.data.blog });
})
.catch(error => {
console.log(error);
});

The code above shows a GET request on http://localhost:4000/blog/1 and the returned blog object being stored in the local state.

The data in the local state could then be used for rendering something to the UI.

Create

Now that we’ve finished the interaction to get a blog post by ID, let’s add the interaction to create a blog post via a POST request on /blogs.

The way this will work is that parameters will be provided on the HTTP request from our React frontend. The parameters will be used to insert a new row in our blogs table.

In order to work with parameters, we have to make an update to our model found in web/models/blogs.ex.

We need to add a changeset function:

defmodule PhoenixReactCuratedList.Blogs do
use PhoenixReactCuratedList.Web, :model
schema "blogs" do
field :title, :string
field :subtitle, :string
field :image, :string
field :link, :string
field :author, :string
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:title, :subtitle, :image, :link, :author])
end
end

According to official documentation, changesets allow filtering, casting, validation and definition of constraints when manipulating structs.

In our case, the changeset function will take in an empty struct and the parameters. It then casts the parameters as data for the empty struct with the approriate types as specified in our schema.

Back in our controller (web/controllers/blogs_controller.ex), we can call the changeset in our create function passing in any empty struct and the parameters:

def create(conn, %{"blogs" => blogs_params}) do
changeset = Blogs.changeset(%Blogs{}, blogs_params)

end

We then use changeset to insert a row using Repo.insert and do something accordingly. In our case, we will render all the blogs in case of a successful insertion:

def create(conn, %{"blogs" => blogs_params}) do
changeset = Blogs.changeset(%Blogs{}, blogs_params)
case Repo.insert(changeset) do
{:ok, _blogs} ->
blogs = Repo.all(Blogs)
render conn, "index.json", blogs: blogs
end
end

Because we aren’t returning a JSON object like we did for our read operations, we are done on the server-side.

On the client-side, we need our React application to make this POST request with the parameters provided.

We can gather values to be used as parameters by making a form in React.

Currently, the top level of our React frontend is rendering the <Blogs /> component which ultimately causes all of our blog post cards to render:

class App extends React.Component {
render() {
return (
<Blogs />
)
}
}

Let’s implement React Router so that we can render a component called Form on the /create route:

npm install --save react-router-dom

Then, let’s import it in our web/static/js/app.js file along with the Form component which we will make in just a bit:

import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
import Form from './containers/Form';

In the render function, we can add our routes and the components to be rendered on them:

render() {
return (
<Router>
<div>
<Route exact path="/" component={Blogs}/>
<Route path="/create" component={Form}/>
</div>
</Router>
)
}

Under the containers folder, let’s create a file called Form.js which will contain the Form component which we previously imported.

First, let’s add the shell of the code:

import React from "react";
import axios from "axios";
class Form extends React.Component {
constructor() {
super();
}
render() {
return (

)
}
}
export default Form

Above render(), let’s add a event handling function for a submitted form:

handleSubmit (event) {

}

Next, we can add the shell of a form that calls this event handler on the submission of the form:

render() {
return (
<form onSumbit={this.handleSumbit.bind(this)}>

</form>
)
}

Within our form, we can use predefined code via Bulma.io for our fields:

<form onSumbit={this.handleSumbit.bind(this)}>
<div className="field">
<label className="label">Title</label>
<div className="control">
<input className="input" type="text"/>
</div>
</div>
<div className="field">
<label className="label">Subtitle</label>
<div className="control">
<input className="input" type="text"/>
</div>
</div>
<div className="field">
<label className="label">Image</label>
<div className="control">
<input
className="input"
type="text"
placeholder="Enter Image URL"
/>
</div>
</div>
<div className="field">
<label className="label">Link</label>
<div className="control">
<input className="input" type="text"/>
</div>
</div>
<div className="field">
<label className="label">Author</label>
<div className="control">
<input className="input" type="text"/>
</div>
</div>
</form>

Before we write the code to handle our form, let’s make sure that we can navigate to this Form component via a button (predefined by Bulma) in our home/default component (containers/Blogs.js):

import { Link } from 'react-router-dom';//class Blogs extends React.Component  //...render() {
const posts = this.state.blogs.map((blog, index) =>
<BlogCard
key = { index }
title = { blog.title }
subtitle = { blog.subtitle }
image = { blog.image }
link = { blog.link }
author = { blog.author }
/>
);
return (
<div>
<div className="is-primary is-large"
style = {{
position: "absolute",
top: "10px",
right: "10px",
padding: "10px 15px",
background: "#00D1B2",
color: "#FFFFFF"
}}
>
<Link
to="/create"
style = {{ color: "white" }}
>
Create Blog Post
</Link>
</div>
{posts}
</div>
)
}

Run brunch build and go to the local host:

We can now see our button which links to our form.

Let’s finish this up by having our form collect the field values and use them as parameters in a POST request.

To do this, let’s begin by adding a local state to our Form component that will track all the values in our form:

constructor() {
super();
this.state = {
title: '',
subtitle: '',
image: '',
link: '',
author: ''
};
}

Next, we can bind the values from our inputs to our state like so:

<form onSumbit={this.handleSumbit.bind(this)}>
<div className="field">
<label className="label">Title</label>
<div className="control">
<input
className="input"
type="text"
value = {this.state.title}
/>
</div>
</div>
<div className="field">
<label className="label">Subtitle</label>
<div className="control">
<input
className="input"
type="text"
value = {this.state.subtitle}
/>
</div>
</div>
<div className="field">
<label className="label">Image</label>
<div className="control">
<input
className="input"
type="text"
placeholder="Enter Image URL"
value = {this.state.image}
/>
</div>
</div>
<div className="field">
<label className="label">Link</label>
<div className="control">
<input
className="input"
type="text"
value = {this.state.link}
/>
</div>
</div>
<div className="field">
<label className="label">Author</label>
<div className="control">
<input
className="input"
type="text"
value = {this.state.author}
/>
</div>
</div>
</form>

In the code above, we have added value = {this.state.___} in each input.

Next, we want to have the values update whenever there is a change within the fields. We can do this by having an event handler for each input:

handleTitle(event) {
this.setState({ title: event.target.value })
}
handleSubtitle(event) {
this.setState({ subtitle: event.target.value })
}
handleImage(event) {
this.setState({ image: event.target.value })
}
handleLink(event) {
this.setState({ link: event.target.value })
}
handleAuthor(event) {
this.setState({ author: event.target.value })
}

These event handlers need to be called during an onChange event within each input:

<<form onSubmit={this.handleSubmit.bind(this)}>
<div className="field">
<label className="label">Title</label>
<div className="control">
<input
className="input"
type="text"
value = {this.state.title}
onChange = {this.handleTitle.bind(this)}
/>
</div>
</div>
<div className="field">
<label className="label">Subtitle</label>
<div className="control">
<input
className="input"
type="text"
value = {this.state.subtitle}
onChange = {this.handleSubtitle.bind(this)}
/>
</div>
</div>
<div className="field">
<label className="label">Image</label>
<div className="control">
<input
className="input"
type="text"
placeholder="Enter Image URL"
onChange = {this.handleImage.bind(this)}
/>
</div>
</div>
<div className="field">
<label className="label">Link</label>
<div className="control">
<input
className="input"
type="text"
value = {this.state.link}
onChange = {this.handleLink.bind(this)}
/>
</div>
</div>
<div className="field">
<label className="label">Author</label>
<div className="control">
<input
className="input"
type="text"
value = {this.state.author}
onChange = {this.handleAuthor.bind(this)}
/>
</div>
</div>
</form>

In the code above, we added onChange = {this.handle___.bind(this)} to every input.

Let’s test this form out by logging the local state in handleSubmit:

handleSubmit (event) {
event.preventDefault();
console.log(this.state);
}

We also need a button to submit this form:

<form onSumbit={this.handleSumbit.bind(this)}>//fields  <button
type="submit"
value="Submit"
className="button is-primary"
>
Submit
</button>
</form>

Run brunch build and insert the following information in the form:

Full-Stack React With Phoenix (Chapter 4 | Implementing React)
Phoenix Plus React
http://bit.ly/2udts8Z
http://bit.ly/2vmhleA
Mike Mangialardi

Hit Submit and check the console:

Sweet! All of the form values and being updated correctly.

Now, let’s update handleSubmit to make the POST request using these values as parameters:

handleSubmit (event) {
event.preventDefault();
axios.post('http://localhost:4000/api/blogs', {
headers: {"Content-Type": "application/json"},
data: {
blogs: {
title: this.state.title,
subtitle: this.state.subtitle,
image: this.state.image,
link: this.state.link,
author: this.state.author
}
}
});
\

There’s also a mistake which I recently caught which needs to be corrected.

In web/router.ex, we have all of our API routes passing through the browser pipeline and you’ll see there’s code commented out to have routes pass through an api pipeline:

scope "/", PhoenixReactCuratedList do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
get "/blogs", BlogsController, :index
post "/blogs", BlogsController, :create
get "/blogs/:id", BlogsController, :show
end
# Other scopes may use custom stacks.
# scope "/api", PhoenixReactCuratedList do
# pipe_through :api
# end

For our GET requests, this hasn’t caused an issue. However, it will cause issue with our POST requests.

Let’s comment that block of code out and move our API routes there:

scope "/api", PhoenixReactCuratedList do
pipe_through :api

get "/blogs", BlogsController, :index
post "/blogs", BlogsController, :create
get "/blogs/:id", BlogsController, :show
end

Now, we can make the calls on the /api/blogs router.

Quickly, let’s update the two HTTP requests that we have in our React application.

In web/static/js/containers/Blog.js, update the following line:

axios.get('http://localhost:4000/api/blogs')

In web/static.js/containers/Form.js, update the following line:

axios.post('http://localhost:4000/api/blogs', {

Run brunch build and let’s try submitting the form again using the following information:

Full-Stack React With Phoenix (Chapter 4 | Implementing React)
Phoenix Plus React
http://bit.ly/2udts8Z
http://bit.ly/2vmhleA
Mike Mangialardi

Once you have submitted the form, open pgAdmin and view all the rows in out blogs table:

Success! We have implemented dynamic blog post submissions using our Phoenix API and React.

Note: I have an id of 7 for the second row because I did multiple tests in between.

If you go to http://localhost:4000/, you will now see our second blog rendering to the UI:

Our show and create interactions are now complete!

Delete

Note: The routes have since been updated to /api/blogs.

Let’s move on to an example delete operation by implementing the interaction that will delete a blog post by id.

First, open web/router.ex and let’s add the route, request type, controller, and function to call:

scope "/api", PhoenixReactCuratedList do
pipe_through :api
get "/blogs", BlogsController, :index
post "/blogs", BlogsController, :create
get "/blogs/:id", BlogsController, :show
delete "/blogs/:id", BlogsController, :delete
end

Next, we can open web/controllers/blogs_controller.ex and write out our delete function:

def delete(conn, %{"id" => id}) do
blog = Repo.get(Blogs, id)
Repo.delete(blog)
blogs = Repo.all(Blogs)
render conn, "index.json", blogs: blogs
end

delete is similar to show in that it takes an id parameter to get a row from our database. However, it has an additional step to delete the retrieved row.

Once this occurs, all of the current blog posts will render. We should see the specified blog post removed.

Let’s test out a delete using Postman.

Send a DELETE request on http://localhost:4000/api/blogs/1.

If we check pgAdmin, we can see our first row removed:

Update

Note: The routes have since been updated to /api/blogs.

The final interaction to implement will be updating a blog by id.

This will be very similar to our create interaction with a few modifications.

First, open web/router.ex and let’s add the route, request type, controller, and function to call:

scope "/api", PhoenixReactCuratedList do
pipe_through :api
get "/blogs", BlogsController, :index
post "/blogs", BlogsController, :create
get "/blogs/:id", BlogsController, :show
delete "/blogs/:id", BlogsController, :delete
put "/blogs/:id", BlogsController, :update
end

Next, we can open web/controllers/blogs_controller.ex and write out our update function:

def update(conn, %{"id" => id, "blogs" => blogs_params}) do
blog = Repo.get(Blogs, id)
changeset = Blogs.changeset(blog, blogs_params)
case Repo.update(changeset) do
{:ok, _blog} ->
blog = Repo.get(Blog, id)
render conn, "show.json", blog: blog
end
end

In the code above, we use the id parameter to retrieve a blog post and then use it to create a changeset. Just like in our create interaction, we use that changeset for our operation and add some logic on how to handle a successful interaction. Instead of Repo.insert, however, we use Repo.update. On a successful interaction, a JSON object with the data for that blog post is rendered.

Open web/static/js/app.js and let’s add a client-side route for a form that will give us parameters for our update:

<Router>
<div>
<Route exact path="/" component={Blogs}/>
<Route path="/create" component={Form}/>
<Route path="/update" component={UpdateForm}/>
</div>
</Router>

We also import the UpdateForm component which we will create next:

import UpdateForm from './containers/UpdateForm';

Create a file called UpdateForm.js within the containers folder to match our import and add the shell of our code:

import React from "react";
import axios from "axios";
class UpdateForm extends React.Component {
render() {
}
}
export default UpdateForm

Within our class, we can paste all the contents of Form.js.

We then just need to tweak handleSubmit with the correct HTTP request method and the URL like so:

handleSubmit (event) {
event.preventDefault();
axios({
method: 'put',
headers: {"Content-Type": "application/json"},
url: 'http://localhost:4000/api/blogs/7',
data: {
blogs: {
title: this.state.title,
subtitle: this.state.subtitle,
image: this.state.image,
link: this.state.link,
author: this.state.author
}
}
});
}

Ideally, the id would be passed to this component via a React Router parameter based on some input (i.e. clicking an edit icon on a blog card from the home page) and a variable would be appended to our URL like so:

url: 'http://localhost:4000/api/blogs/' + this.state.id,

However, we just manually specified the URL to save time:

url: 'http://localhost:4000/api/blogs/7',

Note: I currently have one row in my blogs table with an id of 7. Update for your table.

Let’s add a button to our home/main component as defined in Blogs.js that will link to this UpdateForm component:

return (
<div>
<div className="is-primary is-large"
style = {{
position: "absolute",
top: "10px",
right: "10px",
padding: "10px 15px",
background: "#00D1B2"
}}
>
<Link
to="/create"
style = {{ color: "white" }}
>
Create Blog Post
</Link>
</div>
<div className="is-primary is-large"
style = {{
position: "absolute",
top: "80px",
right: "10px",
padding: "10px 15px",
background: "#00D1B2"
}}
>
<Link
to="/update"
style = {{ color: "white" }}
>
Edit Blog Post
</Link>
</div>
{posts}
</div>
)

Run brunch build, click on Edit Blog Post, and fill out the form with new information.

You can update with any information that you want. In my case, the update made the following changes:

Amazing! We have finished all of our CRUD operations for our API service!

Final Code

Available on GitHub.

Concluding Thoughts

That’s it! You now know the heart of server-side development which includes routing a single page React app on the frontend and creating an API service for the frontend to retrieve data.

There’s still one major use case for an API service that requires a bit more work and explanation, user authentication.

We will proceed to explain this major use case in the next chapter.

Chapter 8

Chapter 8 is now available.

Sign Up for Notifications

Get notified when each chapter is released.

Cheers,
Mike Mangialardi
Founder of Coding Artist

--

--

--

Providing journeys for developers who see the web as their canvas

Recommended from Medium

React Three Fiber

Service workers: the little heroes behind Progressive Web Apps

How you can be 100% PWA

Java vs JavaScript — What is the Difference?

Get ready for job (JavaScript) Interview : Part 1

Javascript Stuff

Best productivity boosters for VS Code in 2020(In Progress)

Why I moved from angular 1.x to React (not angular 2.x)

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Michael Mangialardi

Michael Mangialardi

UI Developer in Southwest Virginia. Soli Deo Gloria. https://coding-artist.teachable.com

More from Medium

NextAuth Part 10 —Auth0 provider

Revenge of the JavaScript: Moving from Hugo to Next.js

Add Google Analytics to a Next.js Application

How to setup Redux in NextJS