こんにちは、KOUKIです。
GolangとReactでMovie Appの開発をしています。
今回は、Movie Formの処理を実装します。
尚、Udemyの「Working with React and Go (Golang)」を参考にしているので、よかったら受講してみてください。
前回
Movieデータの保存や更新を行うMovie Formの実装を行いました。
Movieデータの表示処理
EditMovieコンポーネントは、Movieデータの編集時にMovie IDに紐づくデータをAPIから取得し、画面に表示します。
そのための処理を実装しましょう。この辺りの処理は、実務でも実装する機会が多いです。
ルートの修正
画面遷移時に、Movie IDを渡せるように修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// src/App.tsx ... export default function App() { return ( <Router> ... <div className="row"> <div className="col-md-2"> <nav> <ul className="list-group"> ... <li className="list-group-item"> {/* <Link to="/admin/add">Add Movie</Link> */} <Link to="/admin/movie/0">Add Movie</Link> </li> ... </ul> </nav> </div> <div className="col-md-10"> <Switch> ... {/* <Route path="/admin/add" component={EditMovie} /> */} <Route path="/admin/movie/:id" component={EditMovie} /> ... </Switch> </div> </div> </div> </Router> ) } |
「/admin/movie/:id」の「:id」にて、URLからIDを抜き出す準備をしました。これは、Propsから取得することが可能です。
New/Editデータ処理
idが0の場合は新規画面、それ以外の場合は編集画面になるように、APIへMovieデータを取得する処理と画面制御処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
// components/EditMovie.tsx ... import axios from 'axios' const EditMovie = (props: any) => { ... useEffect(() => { ( async () => { setMpaaOptions([...]) // URLからIDを取得 setID(props.match.params.id) if (id > 0) { // Edit await axios.get(`movie/${id}`) .then((response) => { let movie = response.data.movie const releaseDate = new Date(movie.release_date) movie.release_date = releaseDate.toISOString().split("T")[0] setMovie(movie) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } else { // New setIsLoaded(true) } } )() }, [id]) ... if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> ... </Fragment> ) } export default EditMovie |
Props(props.match.params.id)からURLのIDを抜き出しました。IDが1以上か否かで、表示したFormが新規画面用なのか、編集用なのかを判定します。
検証
「docker-compose up」を叩いて、Dockerコンテナを起動します。
コンテナの起動を確認後、ブラウザから「http://localhost:3000/admin/movie/0」にアクセスして、新規画面を表示しましょう。

ブラウザから「http://localhost:3000/admin/movie/1」にアクセスして、編集画面を表示しましょう。

MPAA Ratingが初期値(Choose…)になってますね。
SelectタグのValue指定に誤りがあるので、修正しましょう。
1 2 3 4 5 6 7 8 9 10 |
// components/EditMovie.tsx <Select title={'MPAA Rating'} name={'mpaa_rating'} options={mpaaOptions} value={movie?.mpaa_rating} // values={movie?.mpaa_rating} handleChange={setMpaaRating} placeholder={'Choose...'} /> |

Movieデータの送信処理
MovieデータをAPIへ送信する処理を実装します。
Submit処理(React)
「Save」ボタンを押下した時の処理を実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// components/EditMovie.tsx ... const EditMovie = (props: any) => { ... const submit = async (e: any) => { e.preventDefault() const data = new FormData(e.target) const payload = Object.fromEntries(data.entries()) await axios.post('admin/editmovie', JSON.stringify(payload)) .then((response) => { console.log({response}) }) .catch((err) => { setError(err.message) }) } ... return (...) } export default EditMovie |
初めてFormDataオブジェクトを使用したのですが、Formのデータを簡単に収集することができました。しかし、useStateを使っているので後で消します。
1 2 3 4 5 6 7 8 9 |
// FormDataから取得した値 { description: "Two imprisoned men bond over a number of years" mpaa_rating: "R" rating: "5" release_date: "1994-10-14" runtime: "142" title: "The Shawshank Redemption" } |
リクエスト先のAPIはこれから実装します。
ルート(Golang)
ここからは、Golangの実装です。ルートを設定しましょう。
1 2 3 4 5 6 7 8 9 10 |
// cmd/api/routes.go ... func (app *application) routes() http.Handler { .... router.HandlerFunc(http.MethodPost, "/v1/admin/editmovie", app.editMovie) return app.enableCORS(router) } |
ハンドラー(Golang)
ハンドラーを設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// cmd/api/movie-handlers.go ... func (app *application) editMovie(w http.ResponseWriter, r *http.Request) { type jsonResp struct { OK bool `json:"ok"` } ok := jsonResp{ OK: true, } err := app.writeJSON(w, http.StatusOK, ok, "response") if err != nil { app.errorJSON(w, err) return } } |
具体的な処理は後ほど実装しますが、response「OK」を返却する簡易なAPIを実装しました。
Formの「Save」ボタンを押下すると下記の結果が得られます。

リクエストデータの取り出し(Golang)
APIへリクエストが届いていることが確認できたので、リクエストデータを取り出してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// cmd/api/movie-handlers.go type MoviePayload struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"Description"` Year string `json:"year"` ReleaseDate string `json:"release_date"` RunTime string `json:"runtime"` Rating string `json:"rating"` MPAARating string `json:"mpaa_rating"` } func (app *application) editMovie(w http.ResponseWriter, r *http.Request) { var payload MoviePayload err := json.NewDecoder(r.Body).Decode(&payload) if err != nil { log.Println(err) app.errorJSON(w, err) return } var movie models.Movie movie.ID, _ = strconv.Atoi(payload.ID) movie.Title = payload.Title movie.Description = payload.Description movie.ReleaseDate, _ = time.Parse("2006-01-02", payload.ReleaseDate) movie.Year = movie.ReleaseDate.Year() movie.Runtime, _ = strconv.Atoi(payload.RunTime) movie.Rating, _ = strconv.Atoi(payload.Rating) movie.MPAARating = payload.MPAARating movie.CreatedAt = time.Now() movie.UpdatedAt = time.Now() log.Println(movie) type jsonResp struct { OK bool `json:"ok"` } ok := jsonResp{ OK: true, } err = app.writeJSON(w, http.StatusOK, ok, "response") if err != nil { app.errorJSON(w, err) return } } |
json.NewDecoderメソッドで、リクエストからMovieでデータを取り出します。
また、Yearは文字列を指定しました。
React側のMovieモデルのyearの型もこれに合わせます。
1 2 3 4 5 6 7 8 9 10 11 12 |
// models/movie.ts export interface Movie { id: number title: string description: string year: string // int -> string release_date: string runtime: number rating: number mpaa_rating: string genres: Array<string> } |
この変更に伴い、OneMovieコンポーネントのテストデータのyearも修正してください。
1 2 3 4 5 6 7 8 9 10 |
// components/OneMovie.tsx ... const OneMovie = (props: any) => { const [movie, setMovie] = useState<Movie>({ id: 0, title: '', description: '', year: "2021", release_date: '', runtime: 0, rating: 0, mpaa_rating: '', genres: [] }) ... |
Formからリクエストを飛ばすとコンソール上でデータの確認ができます。

1 2 |
backend_1 | 2021/08/19 20:23:49 {0 Harry Potter Wonderful Movie! 2021 2021-08-20 00:00:00 +0000 UTC 160 10 PG 2021-08-19 20:23:49.531056542 +0000 UTC m=+13.329304786 2021-08-19 20:23:49.531056611 +0000 UTC m=+13.329304853 map[]} |
MovieデータのInsert処理(Golang)
MovieデータのInsert処理を実装しましょう。ちなみにDBはPostgreSQLをDocker上で動かしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// models/movies-db.go ... func (m *DBModel) InsertMovie(movie Movie) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() stmt := `insert into movies (title, description, year, release_date, runtime, rating, mpaa_rating, created_at, updated_at) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)` _, err := m.DB.ExecContext(ctx, stmt, movie.Title, movie.Description, movie.Year, movie.ReleaseDate, movie.Runtime, movie.Rating, movie.MPAARating, movie.CreatedAt, movie.UpdatedAt, ) if err != nil { return err } return nil } |
この処理をハンドラーから呼び出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// cmd/api/movie-handlers.go // jsonRespを外だし type jsonResp struct { OK bool `json:"ok"` Message string `json:"message"` } ... func (app *application) editMovie(w http.ResponseWriter, r *http.Request) { ... var movie models.Movie ... if movie.ID == 0 { // 新規の時だけ err = app.models.DB.InsertMovie(movie) if err != nil { app.errorJSON(w, err) return } } ok := jsonResp{ OK: true, } err = app.writeJSON(w, http.StatusOK, ok, "response") if err != nil { app.errorJSON(w, err) return } } |
先ほどと同じパラメータで、FormからAPIへリクエストを送ってください。
その後、「http://localhost:9232/」にアクセスし、データが登録されたか確認しましょう

管理画面からも確認できます。
「http://localhost:3000/movies/5」

バリデーション実装
フォームの入力値が想定のフォーマットや型であることをチェックするため、バリデーションを導入しましょう。
クライアント側では、Bootstrapを使うと楽です。
Inputコンポーネントの修正
BootStrap Validationのclassタグを渡せる様にコンポーネントを修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// components/form-components/Input.tsx const Input = (props: any) => { return ( <div className="mb-3"> <label htmlFor={props.name} className="form-label"> {props.title} </label> <input type={props.type} className={`form-control ${props.className}`} id={props.name} name={props.name} value={props.value} onChange={e => props.handleChange(e.target.value)} /> <div className={props.errorDiv}>{props.errorMsg}</div> </div> ) } export default Input |
呼び出し元も修正しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
// components/EditMovie.tsx ... const EditMovie = (props: any) => { ... const hasError = (key: string) => { return errors.indexOf(key) !== -1 ... return ( <Fragment> <h2>Add/Edit Movie</h2> <hr /> <form method="post" onSubmit={submit}> <Input title={"Title"} className={hasError("title") ? "is-invalid" : ""} type={'text'} name={'title'} value={movie?.title} handleChange={setTitle} errorDiv={hasError("title") ? "text-danger" : "d-none"} errorMsg={"Please enter a title"} /> <Input title={"Release Date"} className={hasError("release_date") ? "is-invalid" : ""} type={'date'} name={'release_date'} value={movie?.release_date} handleChange={setReleaseDate} errorDiv={hasError("release_date") ? "text-danger" : "d-none"} errorMsg={"Please enter a release_date"} /> <Input title={"Runtime"} className={hasError("runtime") ? "is-invalid" : ""} type={'text'} name={'runtime'} value={movie?.runtime} handleChange={setRuntime} errorDiv={hasError("runtime") ? "text-danger" : "d-none"} errorMsg={"Please enter a runtime"} /> ... <Input title={"utf-8">Rating"} className={hasError("utf-8">rating") ? "is-invalid" : ""} type={'text'} name={'rating'} value={movie?.rating} handleChange={setRating} errorDiv={hasError("rating") ? "text-danger" : "d-none"} errorMsg={"Please enter a rating"} /> ... <hr /> <button className="btn btn-primary" type="submit">Save</button> </form> </Fragment> ) } export default EditMovie |
空文字チェック
Inputフォームに文字が入力されていない場合、バリデーションエラーとしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
// components/EditMovie.tsx ... const EditMovie = (props: any) => { ... const [errors, setErrors] = useState<string[]>([]) // form data ... const submit = async (e: any) => { e.preventDefault() const movie: Movie = { id: id, title: title, description: description, year: "2021", release_date: releaseDate, runtime: runtime, rating: rating, mpaa_rating: mpaaRating, genres: [] } // validation let localErrors = [] console.log(movie) if (movie?.title === "") { localErrors.push("title") } if (movie?.release_date === "") { localErrors.push("release_date") } if (movie?.runtime === 0) { localErrors.push("runtime") } if (movie?.rating === 0) { localErrors.push("rating") } // Errorセット setErrors(localErrors) if (errors.length > 0) { return false } await axios.post('admin/editmovie', JSON.stringify(movie)) .then((response) => { console.log({response}) }) .catch((err) => { setError(err.message) }) } ... return (...) } export default EditMovie |
宣告通り、FormDataオブジェクトを消しました。useStateを使ってフォームの入力値を管理しているので不要でしたね。
検証
Inputフォームに文字を入力せずに「Save」ボタンを押下しましょう。

次回
次回も引き続き、Form処理を実装します。
記事まとめ
参考書籍
ソースコード
routes.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// cmd/api/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/movie/:id", app.getOneMovie) router.HandlerFunc(http.MethodGet, "/v1/movies", app.getAllMovie) router.HandlerFunc(http.MethodGet, "/v1/movies/:genre_id", app.getAllMoviesByGenres) router.HandlerFunc(http.MethodGet, "/v1/genres", app.getAllGenres) router.HandlerFunc(http.MethodPost, "/v1/admin/editmovie", app.editMovie) return app.enableCORS(router) } |
movie-handlers.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
// cmd/api/movie-handlers.go package main import ( "backend/models" "encoding/json" "errors" "log" "net/http" "strconv" "time" "github.com/julienschmidt/httprouter" ) func (app *application) getOneMovie(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) // urlからidを抜きだす id, err := strconv.Atoi(params.ByName("id")) if err != nil { app.logger.Print(errors.New("invalid id parameter")) app.errorJSON(w, err) return } movie, err := app.models.DB.Get(id) err = app.writeJSON(w, http.StatusOK, movie, "movie") if err != nil { app.errorJSON(w, err) return } } func (app *application) getAllMovie(w http.ResponseWriter, r *http.Request) { movies, err := app.models.DB.All() if err != nil { app.errorJSON(w, err) return } err = app.writeJSON(w, http.StatusOK, movies, "movies") if err != nil { app.errorJSON(w, err) return } } func (app *application) getAllGenres(w http.ResponseWriter, r *http.Request) { genres, err := app.models.DB.GenresAll() if err != nil { app.errorJSON(w, err) return } err = app.writeJSON(w, http.StatusOK, genres, "genres") if err != nil { app.errorJSON(w, err) return } } func (app *application) getAllMoviesByGenres(w http.ResponseWriter, r *http.Request) { params := httprouter.ParamsFromContext(r.Context()) genreID, err := strconv.Atoi(params.ByName("genre_id")) if err != nil { app.errorJSON(w, err) return } movies, err := app.models.DB.All(genreID) if err != nil { app.errorJSON(w, err) return } err = app.writeJSON(w, http.StatusOK, movies, "movies") if err != nil { app.errorJSON(w, err) return } } type MoviePayload struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"Description"` Year string `json:"year"` ReleaseDate string `json:"release_date"` RunTime string `json:"runtime"` Rating string `json:"rating"` MPAARating string `json:"mpaa_rating"` } type jsonResp struct { OK bool `json:"ok"` Message string `json:"message"` } func (app *application) editMovie(w http.ResponseWriter, r *http.Request) { var payload MoviePayload err := json.NewDecoder(r.Body).Decode(&payload) if err != nil { log.Println(err) app.errorJSON(w, err) return } var movie models.Movie movie.ID, _ = strconv.Atoi(payload.ID) movie.Title = payload.Title movie.Description = payload.Description movie.ReleaseDate, _ = time.Parse("2006-01-02", payload.ReleaseDate) movie.Year = movie.ReleaseDate.Year() movie.Runtime, _ = strconv.Atoi(payload.RunTime) movie.Rating, _ = strconv.Atoi(payload.Rating) movie.MPAARating = payload.MPAARating movie.CreatedAt = time.Now() movie.UpdatedAt = time.Now() if movie.ID == 0 { err = app.models.DB.InsertMovie(movie) if err != nil { app.errorJSON(w, err) return } } ok := jsonResp{ OK: true, } err = app.writeJSON(w, http.StatusOK, ok, "response") if err != nil { app.errorJSON(w, err) return } } |
movies-db.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
// models/movies-db.go package models import ( "context" "database/sql" "fmt" "time" ) type DBModel struct { DB *sql.DB } // Get retuns one movie and error, if any func (m *DBModel) Get(id int) (*Movie, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := `select id, title, description, year, release_date, rating, runtime, mpaa_rating, created_at, updated_at from movies where id = $1` row := m.DB.QueryRowContext(ctx, query, id) var movie Movie err := row.Scan( &movie.ID, &movie.Title, &movie.Description, &movie.Year, &movie.ReleaseDate, &movie.Rating, &movie.Runtime, &movie.MPAARating, &movie.CreatedAt, &movie.UpdatedAt, ) if err != nil { return nil, err } // get genres, if any query = `select mg.id, mg.movie_id, mg.genre_id, g.genre_name from movies_genres mg left join genres g on (g.id = mg.genre_id) where mg.movie_id = $1` rows, _ := m.DB.QueryContext(ctx, query, id) defer rows.Close() genres := make(map[int]string) for rows.Next() { var mg MovieGenre err := rows.Scan( &mg.ID, &mg.MovieID, &mg.GenreID, &mg.Genre.GenreName, ) if err != nil { return nil, err } genres[mg.ID] = mg.Genre.GenreName } movie.MovieGenre = genres return &movie, nil } // All retuns all movies and error, if any func (m *DBModel) All(genre ...int) ([]*Movie, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() where := "" if len(genre) > 0 { where = fmt.Sprintf( "where id in (select movie_id from movies_genres where genre_id = %d)", genre[0]) } query := fmt.Sprintf( `select id, title, description, year, release_date, rating, runtime, mpaa_rating, created_at, updated_at from movies %s order by title`, where) rows, err := m.DB.QueryContext(ctx, query) if err != nil { return nil, err } defer rows.Close() var movies []*Movie for rows.Next() { var movie Movie err := rows.Scan( &movie.ID, &movie.Title, &movie.Description, &movie.Year, &movie.ReleaseDate, &movie.Rating, &movie.Runtime, &movie.MPAARating, &movie.CreatedAt, &movie.UpdatedAt, ) if err != nil { return nil, err } genreQuery := `select mg.id, mg.movie_id, mg.genre_id, g.genre_name from movies_genres mg left join genres g on (g.id = mg.genre_id) where mg.movie_id = $1` genreRows, _ := m.DB.QueryContext(ctx, genreQuery, movie.ID) genres := make(map[int]string) for genreRows.Next() { var mg MovieGenre err := genreRows.Scan( &mg.ID, &mg.MovieID, &mg.GenreID, &mg.Genre.GenreName, ) if err != nil { return nil, err } genres[mg.ID] = mg.Genre.GenreName } genreRows.Close() movie.MovieGenre = genres movies = append(movies, &movie) } return movies, nil } func (m *DBModel) GenresAll() ([]*Genre, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() query := `select id, genre_name, created_at, updated_at 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, &g.CreatedAt, &g.UpdatedAt, ) if err != nil { return nil, err } genres = append(genres, &g) } return genres, nil } func (m *DBModel) InsertMovie(movie Movie) error { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() stmt := `insert into movies (title, description, year, release_date, runtime, rating, mpaa_rating, created_at, updated_at) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)` _, err := m.DB.ExecContext(ctx, stmt, movie.Title, movie.Description, movie.Year, movie.ReleaseDate, movie.Runtime, movie.Rating, movie.MPAARating, movie.CreatedAt, movie.UpdatedAt, ) if err != nil { return err } return nil } |
App.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
// src/App.tsx import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' import Home from './components/Home' import Movies from './components/Movies' import Admin from './components/Admin' import Genres from './components/Genres' import OneMovie from './components/OneMovie' import OneGenre from './components/OneGenre' import EditMovie from './components/EditMovie' export default function App() { return ( <Router> <div className="container"> <div className="row"> <h1 className="mt-3"> Go Watch a Movie! </h1> <hr className="mb-3" /> </div> <div className="row"> <div className="col-md-2"> <nav> <ul className="list-group"> <li className="list-group-item"> <Link to="/">Home</Link> </li> <li className="list-group-item"> <Link to="/movies">Movies</Link> </li> <li className="list-group-item"> <Link to="/genres">Genres</Link> </li> <li className="list-group-item"> <Link to="/admin/movie/0">Add Movie</Link> </li> <li className="list-group-item"> <Link to="/admin">Manage Catalog</Link> </li> </ul> </nav> </div> <div className="col-md-10"> <Switch> <Route path="/movies/:id" component={OneMovie} /> <Route path="/movies"> <Movies /> </Route> <Route path="/genre/:id" component={OneGenre} /> <Route exact path="/genres"> <Genres /> </Route> <Route path="/admin/movie/:id" component={EditMovie} /> <Route path="/admin"> <Admin /> </Route> <Route path="/"> <Home /> </Route> </Switch> </div> </div> </div> </Router> ) } |
EditMovie.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
// components/EditMovie.tsx import { Fragment, useEffect, useState } from "react" import { Movie } from '../models/movie' import Input from './form-components/Input' import TextArea from './form-components/TextArea' import Select from './form-components/Select' import './EditMovie.css' import axios from 'axios' const EditMovie = (props: any) => { const [movie, setMovie] = useState<Movie>() const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") const [errors, setErrors] = useState<string[]>([]) // form data const [id, setID] = useState(0) const [title, setTitle] = useState("") const [description, setDescription] = useState("") const [releaseDate, setReleaseDate] = useState("") const [runtime, setRuntime] = useState(0) const [rating, setRating] = useState(0) const [mpaaRating, setMpaaRating] = useState("") const [mpaaOptions, setMpaaOptions] = useState([{}]) useEffect(() => { ( async () => { setMpaaOptions([ {id: "G", value: "G"}, {id: "PG", value: "PG"}, {id: "PG13", value: "PG13"}, {id: "R", value: "R"}, {id: "NC17", value: "NC17"}, ]) // URLからIDを取得 setID(props.match.params.id) if (id > 0) { // Edit await axios.get(`movie/${id}`) .then((response) => { let movie = response.data.movie const releaseDate = new Date(movie.release_date) movie.release_date = releaseDate.toISOString().split("T")[0] setMovie(movie) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } else { // New setIsLoaded(true) } } )() }, [id]) const submit = async (e: any) => { e.preventDefault() const movie: Movie = { id: id, title: title, description: description, year: "2021", release_date: releaseDate, runtime: runtime, rating: rating, mpaa_rating: mpaaRating, genres: [] } // validation let localErrors: string[] = [] if (movie?.title === "") { localErrors.push("title") } if (movie?.release_date === "") { localErrors.push("release_date") } if (movie?.runtime === 0) { localErrors.push("runtime") } if (movie?.rating === 0) { localErrors.push("rating") } // Errorセット setErrors(localErrors) if (errors.length > 0) { return false } await axios.post('admin/editmovie', JSON.stringify(movie)) .then((response) => { console.log({response}) }) .catch((err) => { setError(err.message) }) } const hasError = (key: string) => { return errors.indexOf(key) !== -1 } if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>Add/Edit Movie</h2> <hr /> <form method="post" onSubmit={submit}> <Input title={"Title"} className={hasError("title") ? "is-invalid" : ""} type={'text'} name={'title'} value={movie?.title} handleChange={setTitle} errorDiv={hasError("title") ? "text-danger" : "d-none"} errorMsg={"Please enter a title"} /> <Input title={"Release Date"} className={hasError("release_date") ? "is-invalid" : ""} type={'date'} name={'release_date'} value={movie?.release_date} handleChange={setReleaseDate} errorDiv={hasError("release_date") ? "text-danger" : "d-none"} errorMsg={"Please enter a release_date"} /> <Input title={"Runtime"} className={hasError("runtime") ? "is-invalid" : ""} type={'text'} name={'runtime'} value={movie?.runtime} handleChange={setRuntime} errorDiv={hasError("runtime") ? "text-danger" : "d-none"} errorMsg={"Please enter a runtime"} /> <Select title={'MPAA Rating'} name={'mpaa_rating'} options={mpaaOptions} value={movie?.mpaa_rating} handleChange={setMpaaRating} placeholder={'Choose...'} /> <Input title={"Rating"} className={hasError("rating") ? "is-invalid" : ""} type={'text'} name={'rating'} value={movie?.rating} handleChange={setRating} errorDiv={hasError("rating") ? "text-danger" : "d-none"} errorMsg={"Please enter a rating"} /> <TextArea title={"Description"} name={'description'} value={movie?.description} handleChange={setDescription} rows={3} /> <hr /> <button className="btn btn-primary" type="submit">Save</button> </form> </Fragment> ) } export default EditMovie |
OneMovie.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
// components/OneMovie.tsx import { useEffect, useState, Fragment } from 'react' import { Movie } from '../models/movie' import axios from 'axios' const OneMovie = (props: any) => { const [movie, setMovie] = useState<Movie>({ id: 0, title: '', description: '', year: "2021", release_date: '', runtime: 0, rating: 0, mpaa_rating: '', genres: [] }) const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") useEffect(() => { ( async () => { await axios.get(`movie/${props.match.params.id}`) .then((response) => { setMovie(response.data.movie) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } )() }, []) // mapを使えるようにするために配列にする if (movie.genres) { movie.genres = Object.values(movie.genres) } else { movie.genres = [] } if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>Movie: {movie.title} ({movie.year})</h2> <div className="float-start"> <small>Raging: {movie.mpaa_rating}</small> </div> <div className="float-end"> {movie.genres.map((m, index) => ( <span key={index} className="badge bg-secondary me-1"> {m} </span> ))} </div> <div className="clearfix"></div> <hr /> <table className="table table-compact table-striped"> <thead></thead> <tbody> <tr> <td><strong>Title:</strong></td> <td>{movie.title}</td> </tr> <tr> <td><strong>Description: </strong></td> <td>{movie.description}</td> </tr> <tr> <td><strong>Run Time:</strong></td> <td>{movie.runtime} minutes</td> </tr> </tbody> </table> </Fragment> ) } export default OneMovie |
Input.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// components/form-components/Input.tsx const Input = (props: any) => { return ( <div className="mb-3"> <label htmlFor={props.name} className="form-label"> {props.title} </label> <input type={props.type} className={`form-control ${props.className}`} id={props.name} name={props.name} value={props.value} onChange={e => props.handleChange(e.target.value)} /> <div className={props.errorDiv}>{props.errorMsg}</div> </div> ) } export default Input |
movie.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// models/movie.ts export interface Movie { id: number title: string description: string year: string release_date: string runtime: number rating: number mpaa_rating: string genres: Array<string> } export interface Genre { id: number genre_name: string } |
コメントを残す
コメントを投稿するにはログインしてください。