Masih melanjutkan form untuk add edit anime, kali ini kita tambahkan fungsi cancel dan delete.
Untuk cancel, tampilan akan dialihkan ke halaman /admin.
Buka file buka file src/components/AddEditAnime.js, tambahkan element <Link>. Jika IDE Anda tidak melakukan otomatis import library, jangan lupa tambahkan perintah import { Link } from “react-router-dom”;
<Link to="/admin" className="btn btn-warning ms-1">Cancel</Link>
Untuk fungsi delete, kita tambahkan link untuk delete dan fungsi konfirmasi delete.
Fungsi konfirmasi delete digunakan library third party. Sebenarnya tutorial kami lebih condong untuk pure aplikasi react. Digunakan library lain agar tujuan tutorial lebih singkat.
Install package dengan perintah
npm install react-confirm-alert --save
Kemudian pada file src/components/AddEditAnime.js tambahkan code berikut
Code untuk import library alert.
import { confirmAlert } from 'react-confirm-alert'; // Import
import 'react-confirm-alert/src/react-confirm-alert.css'; // Import css
Code untuk link button delete
{anime.id >0 && (
<a href="#!" onClick={()=>this.confirmDelete()} className="btn btn-danger ms-1">Delete </a>
)}
Lalu code untuk fungsi confrimDelete()
confirmDelete = (e)=>{
confirmAlert({
title: 'Confirm Delete',
message: 'Are you sure to do this?',
buttons: [
{
label: 'Yes',
onClick: () => {
fetch("http://localhost:4000/v1/admin/deleteanime/" + this.state.anime.id, {method:"GET"})
.then(response => response.json)
.then(data => {
if(data.error){
this.setState({
alert:{type:"alert-danger", msg: data.error.message}
})
}else{
this.props.history.push({
pathname:"/admin",
})
}
})
}
},
{
label: 'No',
onClick: () => {}}
]
});
}
Kemudian pada aplikasi go backend, buka file animeModel.go, lalu tambahkan fungsi berikut
func (m *AnimeModel) Delete(id int) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `DELETE FROM animes WHERE id=$1`
_, err := m.DB.ExecContext(ctx, query, id)
if err != nil {
return err
}
return nil
}
Buka file animesHandlers.go, lalu tambahkan fungsi delete handler.
func (app *application) deleteAnime(rw http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.Atoi(params.ByName("id"))
if err != nil {
app.errorJSON(rw, err)
}
err = app.models.Animes.Delete(id)
if err != nil {
app.errorJSON(rw, err)
}
ok := jsonResp{
OK: true,
}
err = app.writeJSON(rw, http.StatusOK, ok, "response")
if err != nil {
app.errorJSON(rw, err)
}
}
Tambahkan route pada file routes.go
router.HandlerFunc(http.MethodPost, "/v1/admin/deleteanime", app.deleteAnime)
Berikut screen capture hasil akhir dari form yang sudah ditambahkan fungsi delete.


Berikut isi code akhir pada file AddEditAnime.js
import React, {Component, Fragment} from "react";
import { Link } from "react-router-dom";
import { confirmAlert } from 'react-confirm-alert'; // Import
import 'react-confirm-alert/src/react-confirm-alert.css'; // Import css
export default class AddEditAnime extends Component{
constructor(props){
super(props);
this.state ={
anime:{
id:0,
title: "",
desc: "",
year: "",
},
isLoaded : false,
error : null,
errors : [],
alert:{
type: "d-none",
msg :"",
}
}
this.handleChg = this.handleChg.bind(this)
this.handleSubm = this.handleSubm.bind(this)
}
handleSubm = (evt) =>{
evt.preventDefault();
// validasi form
let errors = [];
if (this.state.anime.title === ''){
errors.push("title");
}
if (this.state.anime.desc === ''){
errors.push("desc");
}
if (this.state.anime.year === ''){
errors.push("year");
}
this.setState({errors:errors});
if (errors.length>0){
return false;
}
const data = new FormData(evt.target);
const payload = Object.fromEntries(data.entries());
const requestOpt = {
method: 'POST',
body: JSON.stringify(payload)
}
fetch('http://localhost:4000/v1/admin/addeditanime', requestOpt)
.then(response => response.json())
.then(data => {
if(data.error){
this.setState({
alert: {type: "alert-danger", msg: data.error.msg}
});
}else{
this.setState({
alert: {type: "alert-success", msg: "Save success.."}
});
}
});
};
handleChg = (evt)=>{
let value = evt.target.value;
let name = evt.target.name;
this.setState((prevstate)=>({
anime:{
...prevstate.anime,
[name] : value,
}
}));
}
hasError(key){
return this.state.errors.indexOf(key) !== -1;
}
componentDidMount(){
const id = this.props.match.params.id;
if (id > 0){
fetch("http://localhost:4000/v1/anime/" +id)
.then((response)=>{
if(response !== "200"){
let err = Error;
err.Message = "Invalid response code: " + response.status;
this.setState({error : err});
}
return response.json();
})
.then((json) => {
this.setState({
anime:{
id: id,
title: json.anime.title,
year: json.anime.year,
desc: json.anime.desc,
},
isLoaded : true,
},
(error) => {
this.setState({
isLoaded : true,
error,
})
}
)
})
}else{
this.setState({isLoaded:true});
}
}
confirmDelete = (e)=>{
confirmAlert({
title: 'Confirm Delete',
message: 'Are you sure to do this?',
buttons: [
{
label: 'Yes',
onClick: () => {
fetch("http://localhost:4000/v1/admin/deleteanime/" + this.state.anime.id, {method:"GET"})
.then(response => response.json)
.then(data => {
if(data.error){
this.setState({
alert:{type:"alert-danger", msg: data.error.message}
})
}else{
this.props.history.push({
pathname:"/admin",
})
}
})
}
},
{
label: 'No',
onClick: () => {}}
]
});
}
render(){
let {anime, isLoaded, error} = this.state;
if (error){
return <div>Error : {error.Message}</div>
}else if (!isLoaded){
return <p>Loading...</p>
}else{
return(
<Fragment>
<h2>Add/Edit Anime</h2>
<div className={`alert ${this.state.alert.type}`} role="alert">{this.state.alert.msg}</div>
<hr/>
<form onSubmit={this.handleSubm}>
<input type="hidden" name="id" id="id" value={anime.id} onChange={this.handleChg}/>
<div className="mb-3">
<label htmlFor="title" className="form-label">Title</label>
<input type="text" className={this.hasError("title")?"form-control is-invalid":"form-control"} id="title" name="title" value={anime.title} onChange={this.handleChg}/>
<div className={this.hasError("title")?"text-danger":"d-none"}>Please enter title...</div>
</div>
<div className="mb-3">
<label htmlFor="des" className="form-label">Description</label>
<textarea type="text" className={this.hasError("desc")?"form-control is-invalid":"form-control"} id="desc" name="desc" rows="3" onChange={this.handleChg} value={anime.desc}/>
<div className={this.hasError("desc")?"text-danger":"d-none"}>Please enter description...</div>
</div>
<div className="mb-3">
<label htmlFor="year" className="form-label">Year</label>
<input type="text" className={this.hasError("year")?"form-control is-invalid":"form-control"} id="year" name="year" value={anime.year} onChange={this.handleChg}/>
<div className={this.hasError("year")?"text-danger":"d-none"}>Please enter year...</div>
</div>
<hr/>
<button className="btn btn-primary">Save</button>
<Link to="/admin" className="btn btn-warning ms-1">Cancel</Link>
{anime.id >0 && (
<a href="#!" onClick={()=>this.confirmDelete()} className="btn btn-danger ms-1">Delete </a>
)}
</form>
</Fragment>
);
}
}
}
Untuk file backend, berikut isi code final.
File animeModel.go
package models
import (
"context"
"database/sql"
"fmt"
"time"
)
type AnimeModel struct {
DB *sql.DB
}
func (m *AnimeModel) Get(id int) (*Anime, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `SELECT id, title, description, year
FROM animes WHERE id = $1
`
row := m.DB.QueryRowContext(ctx, query, id)
var anime Anime
err := row.Scan(
&anime.Id,
&anime.Title,
&anime.Desc,
&anime.Year,
)
if err != nil {
return nil, err
}
query = `SELECT ag.id, ag.anime_id, ag.genre_id, g.genre_name
FROM animes_genres ag
LEFT JOIN genres g ON (g.id = ag.genre_id)
WHERE ag.anime_id = $1
`
rows, _ := m.DB.QueryContext(ctx, query, id)
genre := make(map[int]string)
for rows.Next() {
var ag animeGenre
err := rows.Scan(
&ag.Id,
&ag.AnimeId,
&ag.GenreId,
&ag.Genre.GenreName,
)
if err != nil {
return nil, err
}
genre[ag.Id] = ag.Genre.GenreName
}
anime.AnimeGenre = genre
return &anime, nil
}
func (m *AnimeModel) All(genre ...int) ([]*Anime, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
where := ""
if len(genre) > 0 {
where = fmt.Sprintf("WHERE id IN (SELECT anime_id FROM animes_genres WHERE genre_id= %d)", genre[0])
}
query := fmt.Sprintf(`SELECT id, title, description, year
FROM animes %s ORDER BY title`, where)
rows, err := m.DB.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var animes []*Anime
for rows.Next() {
var anime Anime
err = rows.Scan(
&anime.Id,
&anime.Title,
&anime.Desc,
&anime.Year,
)
if err != nil {
return nil, err
}
query2 := `SELECT ag.id, ag.anime_id, ag.genre_id, g.genre_name
FROM animes_genres ag
LEFT JOIN genres g ON (g.id = ag.genre_id)
WHERE ag.anime_id = $1
`
rows2, _ := m.DB.QueryContext(ctx, query2, anime.Id)
genre := make(map[int]string)
for rows2.Next() {
var ag animeGenre
err = rows2.Scan(
&ag.Id,
&ag.AnimeId,
&ag.GenreId,
&ag.Genre.GenreName,
)
if err != nil {
return nil, err
}
genre[ag.Id] = ag.Genre.GenreName
}
rows2.Close()
anime.AnimeGenre = genre
animes = append(animes, &anime)
}
return animes, nil
}
func (m *AnimeModel) Genres() ([]*genre, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `SELECT id, genre_name
FROM genres ORDER BY genre_name
`
rows, err := m.DB.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var genres []*genre
for rows.Next() {
var g genre
err = rows.Scan(
&g.Id,
&g.GenreName,
)
if err != nil {
return nil, err
}
genres = append(genres, &g)
}
return genres, nil
}
func (m *AnimeModel) Insert(anime Anime) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `INSERT INTO animes (title, description, year) VALUES ($1, $2, $3)`
_, err := m.DB.ExecContext(ctx, query,
anime.Title,
anime.Desc,
anime.Year,
)
if err != nil {
return err
}
return nil
}
func (m *AnimeModel) Update(anime Anime) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `UPDATE animes SET title=$1, description=$2, year=$3 WHERE id=$4`
_, err := m.DB.ExecContext(ctx, query,
anime.Title,
anime.Desc,
anime.Year,
anime.Id,
)
if err != nil {
return err
}
return nil
}
func (m *AnimeModel) Delete(id int) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `DELETE FROM animes WHERE id = $1`
_, err := m.DB.ExecContext(ctx, query, id)
if err != nil {
return err
}
return nil
}
file animesHandlers.go
package main
import (
"backend/models"
"encoding/json"
"net/http"
"strconv"
"github.com/julienschmidt/httprouter"
)
type jsonResp struct {
OK bool `json:"ok"`
Msg string `json:"msg"`
}
func (app *application) getAnime(rw http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.Atoi(params.ByName("id"))
if err != nil {
app.errorJSON(rw, err)
return
}
anime, err := app.models.Animes.Get(id)
if err != nil {
app.errorJSON(rw, err)
return
}
err = app.writeJSON(rw, http.StatusOK, anime, "anime")
if err != nil {
app.errorJSON(rw, err)
return
}
}
func (app *application) getAnimes(rw http.ResponseWriter, r *http.Request) {
animes, err := app.models.Animes.All()
if err != nil {
app.errorJSON(rw, err)
return
}
err = app.writeJSON(rw, http.StatusOK, animes, "animes")
if err != nil {
app.errorJSON(rw, err)
}
}
func (app *application) getAllGenres(rw http.ResponseWriter, r *http.Request) {
genres, err := app.models.Animes.Genres()
if err != nil {
app.errorJSON(rw, err)
return
}
err = app.writeJSON(rw, http.StatusOK, genres, "genres")
if err != nil {
app.errorJSON(rw, err)
}
}
func (app *application) getAnimeByGenre(rw http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
genreid, err := strconv.Atoi(params.ByName("genre_id"))
if err != nil {
app.errorJSON(rw, err)
return
}
animes, _ := app.models.Animes.All(genreid)
err = app.writeJSON(rw, http.StatusOK, animes, "animes")
if err != nil {
app.errorJSON(rw, err)
}
}
type animePayload struct {
Id string `json:"id"`
Title string `json:"title"`
Desc string `json:"desc"`
Year string `json:"year"`
}
func (app *application) addEditAnime(rw http.ResponseWriter, r *http.Request) {
var payload animePayload
err := json.NewDecoder(r.Body).Decode(&payload)
if err != nil {
app.logger.Println(err)
app.errorJSON(rw, err)
return
}
var anime models.Anime
anime.Id, _ = strconv.Atoi(payload.Id)
anime.Title = payload.Title
anime.Desc = payload.Desc
anime.Year, _ = strconv.Atoi(payload.Year)
if anime.Id == 0 {
err = app.models.Animes.Insert(anime)
if err != nil {
app.errorJSON(rw, err)
}
} else {
err = app.models.Animes.Update(anime)
if err != nil {
app.errorJSON(rw, err)
}
}
ok := jsonResp{
OK: true,
}
err = app.writeJSON(rw, http.StatusOK, ok, "response")
if err != nil {
app.errorJSON(rw, err)
}
}
func (app *application) deleteAnime(rw http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.Atoi(params.ByName("id"))
if err != nil {
app.errorJSON(rw, err)
}
err = app.models.Animes.Delete(id)
if err != nil {
app.errorJSON(rw, err)
}
ok := jsonResp{
OK: true,
}
err = app.writeJSON(rw, http.StatusOK, ok, "response")
if err != nil {
app.errorJSON(rw, err)
}
}
file routes.go
package main
import (
"net/http"
"github.com/julienschmidt/httprouter"
)
func (app *application) routes() http.Handler {
router := httprouter.New()
router.HandlerFunc(http.MethodGet, "/status", app.statusHandler)
router.HandlerFunc(http.MethodGet, "/v1/anime/:id", app.getAnime)
router.HandlerFunc(http.MethodGet, "/v1/animes", app.getAnimes)
router.HandlerFunc(http.MethodGet, "/v1/genres", app.getAllGenres)
router.HandlerFunc(http.MethodGet, "/v1/animes/:genre_id", app.getAnimeByGenre)
router.HandlerFunc(http.MethodPost, "/v1/admin/addeditanime", app.addEditAnime)
router.HandlerFunc(http.MethodGet, "/v1/admin/deleteanime/:id", app.deleteAnime)
return app.enableCORS(router)
}
Dengan berakhirnya modul ini, tutorial penggunaan form untuk aplikasi react sudah selesai. Pada modul selanjutnya akan dibahas penggunaan JSON Web Token untuk route security.