Membuat Form Pada Aplikasi React – 4

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.

Sharing is caring:

Leave a Comment