Membuat Form Pada Aplikasi React – 3

Pada modul ini akan dibahas bagian backend untuk menerima data dari form dan menyimpan kedalam database. Dan sedikit perubahan pada frontend untuk menunjukan feedback status save.

Backend

Buka file cmd/api/animesHandlers.go.

Tambahkan struct pada awal file untuk informasi response proses insert atau update.

type jsonResp struct {
	OK  bool   `json:"ok"`
	Msg string `json:"msg"`
}

Tambahkan fungsi baru dan struct baru seperti berikut

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

Pada file cmd/api/animeModel.go tambahkan fungsi Insert dan Update

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

}

Pada file cmd/api/routes.go tambahkan route baru

router.HandlerFunc(http.MethodPost, "/v1/admin/addeditanime", app.addEditAnime)

Frontend

Buka file src/components/AddEditAnime.js, lalu lakukan perubahan berikut.

Tambahkan variable alert pada constructor.

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

Pada fungsi handleSubm tambahkan perintah untuk set nilai alert.

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.."}
            });
        }
    });
};

Pada form, tambahkan div baru untuk menampilkan feedback proses save berhasil atau gagal.

<Fragment>
<h2>Add/Edit Anime</h2>
<div className={`alert ${this.state.alert.type}`} role="alert">{this.state.alert.msg}</div>
<hr/>

Berikut isi lengkap src/components/AddEditAnime.js

import React, {Component, Fragment} from "react";
// import Input from "./FormComponents/Input";

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


    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>
                    </form>
                    <div className="mt-3">
                        <pre>{JSON.stringify(this.state, null, 3)}</pre>
                    </div>
                </Fragment>
            );
        }


    }
}
Contoh diatas masih ada kelemahan, jika tombol save ditekan berulang, maka anime baru akan selalu ditambahkan dengan data yang sama.

Silakan bereksperimen, misalnya dengan mengubah button disable jika proses save sudah dilakukan.

Sharing is caring:

Leave a Comment