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>
);
}
}
}
Silakan bereksperimen, misalnya dengan mengubah button disable jika proses save sudah dilakukan.