- 投稿日:2021-10-25T19:59:19+09:00
【Vue.js】算出プロパティのキャッシュ
はじめに こんにちは! 今回は【Vue.js】算出プロパティのキャッシュについてアウトプットしていきます! キャッシュとは ・cpmputed・・・キャッシュされる(依存関係に元付きキャッシュされる) ・methods・・・キャッシュされない 書き方・解説 ランダムの数字を返す処理をcpmputedとmethodsで記述。 HTML <div id="app"> <h2> Computed </h2> <ol> <li>{{ computedNumber }}</li> <li>{{ computedNumber }}</li> <li>{{ computedNumber }}</li> <li>{{ computedNumber }}</li> </ol> <h2> Methods </h2> <ol> <li>{{ methodsNumber() }}</li> <li>{{ methodsNumber() }}</li> <li>{{ methodsNumber() }}</li> </ol> </div> Vue.js var app = new Vue({ el: '#app', computed: { computedNumber: function() { console.log('computed!') //⏫動作確認 return Math.random() //⏫ランダムな数字を生成できる } }, methods: { methodsNumber: function() { console.log('methods!') return Math.random() } } }) cpmputedとmethodsを記述します。処理内容は全く一緒です。 上記のようにcpmputedは全く同じ数字(キャッシュされている)、methodsは全て異なる数字(キャッシュされない)という風な特性があるからこうなります。 算出プロパティのキャッシュの再構築のトリガーとなるのはリアクティブなデータのみです。Math.random()関数はリアクティブではないので、複数回よんでもキャッシュされたデータか使われて同じ値が返される。 まとめ cpmputedはキャッシュされ全く同じ値が出力される。console表示は1回のみ。 methodsはキャッシュされないので全て異なる値が出力される。console表示は表示された値の数と一緒。 最後に 今回は算出プロパティのキャッシュについてアウトプットしました。 今回の記事を読んで質問、間違い等あればコメント等頂けると幸いです。 今後ともQiitaにてアウトプットしていきます! 最後までご愛読ありがとうございました!
- 投稿日:2021-10-25T14:14:51+09:00
Reactでお絵かき掲示板(の表示部分)を作る話
この文章は、決意表明をすることにより、自分を追い詰めるものである。あと、zennに書いたもののコピーである。 実は昔こんなの書いてるんですよ。 なぜReactにしたのか Vue VS React じつはどっちでやるか決めるのにかなりの時間がかかりました。VueもさわったしReactもさわったし、なんとなくそれなりに触りだけ弄ったりしました。結果、Reactで行こうと決めました。以下理由。 Vueは世界上に2と3の記事が混在していて結構煩雑 あっこれ2の記事だ、ってなったりする。だいぶ書き方違うのでなんで動かないのか調べたら文法だったり、Amazonで本買ったらVue2の本だったり(悔しい) Vueはすぐそれっぽいものができる故に、逆に理解しにくい とりあえず形ができるので構造を深く考えるまでに至らない。Reactは公式も言ってる通り、見た目難しい。見た目難しいということは、調べればわかるよ、と公式が言っているということである。 Reactのほうが食えそう 知り合いにVueで食ってる人は見たことないけどReactで食ってる人は何人かいたため。 ビジョン 現状の俺作お絵かき掲示板 ROIS をベースにしようと思うので、こうなる。 レンタルサーバーに設置できるようにしたいので、通信まわりはPHP なかなかサーバーサイドでnode.jsが走ってるレンタルサーバーはなかったのだ。「ロリポップが対応してるよ!」って記事が出てきてビビったことがあるけどよく読んだらマネージドクラウドでした。 データベースはSQLite これも手軽にレンタルサーバーに設置するため。テキストのログより検索性が高いし、ローカルのXAMPPで動いてるのをそのままFTPでアップロードしてオッケーな軽い感じなのが良い。SQLiteの情報を調べているとPHPのバージョンが4時代のものだったりするのが非常に難点。いまPHPはバージョン8、なんと倍ですよ! Reactを使うのはスレッド表示や検索表示のみ 「仮想DOMが他のDOMと排他的」って感じだから。要するにお絵描きのhtml5+javascriptに干渉しないようにするため。絵がバグる原因がReactとかイヤでしょ。 サーバーのPHPはjsonを返す 妥当。 Reactでやる意味あるの? うん、たぶん実装めんどくさいしPHPで直接書くほうが現段階では色んな意味で速い。プロジェクトの規模としてもそんなにメリットはないと思うけど、俺の学習でやるわけなので、生暖かく見守りながらアドバイスください。
- 投稿日:2021-10-25T14:06:37+09:00
「こんなはずじゃなかった」どこで間違えた俺の個人開発
この記事では賢い知識は得られません。息抜き程度に見ましょう。 こんなはずではなかった 「なぜだ、なぜこんなに伸びないんだ」 そう思いました。アプリを初めて開発した時には。 自分で作った初めての個人開発アプリを半年ほどかけて開発し、 公開しましたが引くほど使われていません。 「半年もかけたのに」 「もぅマヂ無理、ちょぉ頑張ったのに」 「ゥチのことゎもぅどぉでもぃぃんだって」 「docker-compose downしょ」 なぜこうなりましたか? 一緒に見ていきましょう。 開発とマーケティングはどっちも大事 自分には個人開発に取り組む動機に少し問題があったような気がします。 ここで少し、自分の経歴についてお付き合いください 私の経歴 (頑張って読んで) 私は元々、新卒で某ITベンチャーでバックエンドエンジニアとして働いていました。 そこではプロジェクトのサブリーダーとして働いていましたが、 打ち合わせやMTGが多くコードを存分に書ける時間はそれほど多くなかったのかなぁと思います。 幸いなことに、なぜかサブリーダーの私よりも そのメンバーとして働いている方々の方が優秀だったので、 楽させてもらっている方だったのですが ワガママ言うともっとコードを書きたいと思っていました。 詳しくはこちらの記事でも書いているので、よかったら見て下さい。 そんな折、転職をきっかけにポートフォリを作るようになりました。 楽しかったです。自分で全部決めて書けることが嬉しく思えました。 元々は技術力を伸ばすための転職を考えていたのですが ポートフォリを作る内、次第にこう思うようになりました。 「あれ、既にもうなんでも作れるんじゃね?」 「実は、技術力はそれなりにあって、もう独立できるんじゃね?」 皆さんもお気づきかもしれませんが、これは甚だしい勘違いです。 「殴ってやりたい」と思いましたか? それは正しい感情です! ですが筆者は豆腐メンタルなので鋭い批判は避け、意見するときは赤ちゃん言葉でコメントしましょう。(ex. 勘違いがすぎるでちゅ) そんな経緯で私は個人開発を始め、これで生計を立てることを目標にしました。 開発10割 マーケティング0割 僕の「個人開発」という列車は 使用技術を自分で決めれる 設計を全部自分でできる デザインも自分で全部決めれる という欲求を満たすことを動機に発進してしまいました。 ユーザーのニーズ調査などもせずにドンドン進んでいきます。 「楽しい!楽しい!(脳死)」 そしてアプリの開発が進んできてふと思います。 「これってちゃんと使ってもらえるのかな?」 「どうやって宣伝しよう」 「...まぁモノさえ良ければ使ってもらえるだろう!」 そして私は考えるのをやめました。 この時点で半年が経過していました。 ここまで見てきて皆さんは何が問題だと思いますか? たくさんありますよね。ほんと、どうしましょう。 自分が思うに問題点は以下かなと思っております。 ニーズ調査・フィードバック集めを行わない 作り込みすぎる 自分のアプリを客観視できていない 要するに独りよがりで、マーケティングの部分を疎かにし過ぎたのだと思います。 ではどうすべきだったのでしょう? これから上記の問題への対応を考えます。 ニーズ調査・フィードバック集めを行わない どれだけ手を込めて作っても使われなければ意味がありません。 自分の作ろうとしているアプリを必要としている人がいるのか? 自分自身本当に必要なアプリだと思えるのか? これを考える必要が大切でした。そのためにもQiitaやTwitterなどのSNSを日頃から頑張っておいて、フィードバックを返してくれる人々を集める必要があったのかなと思います。 私は高校生時代からTwitterなどといったSNSに乗り遅れて以来、 謎のプライドでSNSをやらないという姿勢を貫いてきました。 大間違いでした。最近始めました。フォローお願いします。。。 筆者のTwitterのプロフィール また、個人開発向けのアプリなども存在しているので、そのアプリが活発になればいいなぁと思っています。 一応、皆に知ってもらうためにGoogle広告などを打ってみましたが、全然うまくいきませんでした。 付け焼き刃でやっても、ユーザーが必要としていないならあまり意味がない気がします。 作り込みすぎる 作り込み、ダメ、ぜったい 私も元々はアプリを開発するために、「なるべく作り込みすぎないである程度作ったら公開するという姿勢を取ろう。その姿勢でいくつかアプリを作ってから反応の大きかったものだけ伸ばそう」と考えていました。 ですがいつの間にか沼に入って作り込みすぎちゃいました。 「中途半端な作り込みで公開しても誰も触ってくれないだろう」 そういった考えで作り込みすぎちゃいました。 また、これは誰が使うの...?というちょっとでも批判的な意見を受けるのを恐れていた部分があります。 しかし、そうは言っても使われないアプリを自分で作り続けても報われません。 勉強目的で個人開発をされている方はそれで全く問題ないのですが、 これで食ってきたいと考えている自分として全くの無意味です。 過去の自分を説き伏せたい気持ちでいっぱいです。 今の自分「やめて!それ以上作り込みすぎないで!」 過去の自分「うるせぇ、作りこめば注目されて使ってもらえるんだ!」 過去の自分「iPhoneだって売り出すまでは駄作と言われていたけど」 過去の自分「そのクオリティに惹かれて今では当たり前になっているんだ」 今の自分「落ち着いて、あなはビルゲイツではないわ!」 自分のアプリを客観視できていない 自分だけかもしれませんが、ついつい自分で作ったアプリが最高だと思っちゃいます。 なぜでしょうか。教えてください。 ゆえに引き時が分かりません。 このためにもフィードバックを受けておく必要があります。 自分だけで考え、作っていると追加機能がどんどん思いついて、 創作意欲が刺激され、いずれ、とんでもないキメラが生まれてしまうということを 忘れてはいけないということを学びました。 僕が冷酷無比な錬金術師であれば、それでいいのかもしれませんが、 残念ながら僕は心優しいエンジニアもどきなので、キメラは作らない方がいいでしょう。 (伝われ) 自分が正しいことばかりをしているのではないということを学び、 開発を続けていくことで、適切に戦略的撤退を行うべきだと思います。 まとめ 個人開発には冷静さが必要で、以下を意識して開発する必要があることを書きました。 ニーズ調査・フィードバック集めを行う 作り込みすぎない 自分のアプリを客観視する 失敗して学ぶことも重要と言われますが、 失敗しなくても分かることは経験しない方がいいと思います。 この記事が参考になるかは謎ですが、もしそうなれば幸いです。 ところで私は新しく個人開発するアプリに 「Progateみたいなゲーム感覚でSQLを学べる学習サイト」 を作ろうとしています。これはどうですか? ProgateでもSQLも学べますが、 よりSQLに特化したアプリを作ろうと思います。 いけると思いますか?無理そうですか? 皆さんの意見を聞かせて下さい。 必要となれば僕がキメラを作る前に止めて下さい(厨二) 最後に 僕はやめろって言ったんですけど、この記事で失敗例として取り上げている アプリのリンクを貼っておけって、僕の中の「まだ耐えられる勢」が言ってくるので リンクを貼っておきます。僕はやめろって言ったんですけどね。 僕はやめろって言ったんですけど、 僕の中の「自己顕示欲」もTwitterのフォローを もう一回お願いしておけって言ってくるので貼っておきます。 僕はしつこくなるからやめたほうがいいって止めたんですけどね。すいませんね。 筆者のTwitterのプロフィール
- 投稿日:2021-10-25T09:10:38+09:00
なに?マクドナルドから学ぶ、優れたcomponentsディレクトリ構造?!
ある日のパイセン「このコンポーネントのスコープはどこだい?」 CMSのついたイベント特設サイト制作案件だな あれは一部だけやったから詳しくないんだよ! 仕方ない、調べるか 確かAtomic Designベースで ディレクトリ構造をわけているんだよな! どれどれ… パイセンは、 ArticleSearchFieldコンポーネントって言ってたな… ArticleSearchて書いてあるんだから、 記事を探すところで使われているんだろう? ってことは、記事一覧あたりのはずだ src ┗ components ┣ atoms ┃ (略) ┣ molecules ┃ ┣ ArticleSearchField.vue // このコンポーネント ┃ (略) ┣ organisms ┃ (略) ┣ templates ┃ (略) ┗ pages ┣ ArticleSummary.vue // このあたりで使われているんだろうな! ┣ TopPage.vue // おそらくここもか…?? (略) TopPageでも使われているかもしれないな わからないからソースコードを見に行くか TopPage.vue <script> import xxxx from 'yyyy' // なさそうだな! // 略 </script> TopPageにはなかった! でもまだわからないから、ArticleSearchFieldで検索かけるか… このあと、結局ArticleSearchField.vueは ArticleSummaryという記事一覧ページでしか使われていないことがわかった! よかったじゃないか!だって? いいや、よくないね! Atomic Designベースで ディレクトリを分けているせいで コンポーネントのスコープが、わかりにくくなっているんだ! マクドナルドのメニューから考える、componentsディレクトリ構造 誰しもが1度はマクドナルドを食べたことがあるだろう? どれもバンズで挟んである具材が違うから、 それぞれの個性があって美味しい! (あとで注文しよう!) キミは知っていたかい? 実はマクドナルドは、 コンポーネントのディレクトリ構造を 考える機会をくれる 素晴らしいフードショップなんだ! ついに気が狂った、と思ったかい? そう思ったら騙されたと思って ハンバーガーを、表示されるページ 具材を、components として考えてみてくれ! ダブルチーズバーガーは当然、こうなるだろう? ハンバーガーは、いろいろな具材が重なってできているな! 例えばダブルチーズバーガーを見てみよう 主要原料は - バンズ - ビーフパティ ... 2枚 - チェダースライスチーズ ... 2枚 - ピクルス - オニオン みたいだ。 つまり、ダブルチーズバーガーというページがあったら こうなるよな? いいや、異論は受け付けないぜ! ダブルチーズバーガー.vue <template> <バンズ> <オニオン /> <ピクルス /> <チーズ /> <パティ /> <チーズ /> <パティ /> </バンズ> </template> <script> import バンズ from 'バンズcomponent' import パティ from 'ビーフパティcomponent' import チーズ from 'チェダースライスチーズcomponent' import ピクルス from 'ピクルスcomponent' import オニオン from 'オニオンcomponent' </script> ダブルチーズバーガーにあるcomponent(具材)たちは どれも汎用的そうだ! 汎用的そうなものは、当然どのバーガー(ページ)からも アクセスできたほうがいいから わかりやすいところに置いておこう src ┗ components ┣ burgerParts ┃ ┣ ビーフパティ.vue ┃ ┣ チェダースライスチーズ.vue ┃ ┣ ピクルス.vue ┃ ┗ オニオン.vue ┣ templates ┃ ┗ バンズ.vue ┗ pages ┗ ダブルチーズバーガー.vue ということは、フィレオフィッシュならこうなるさ! 次はダイエットの味方フィレオフィッシュだ! 主要原料は - バンズ - フィッシュポーション - チェダースライスチーズ だな! ということは、フィレオフィッシュというページがあったら 当然こうなるさ! フィレオフィッシュ.vue <template> <バンズ> <フィッシュ /> <チーズ /> </バンズ> </template> <script> import バンズ from 'バンズcomponent' import フィッシュ from 'フィッシュポーションcomponent' import チーズ from 'チェダースライスチーズcomponent' </script> ほらみろ! チェダースライスチーズcomponentがまたでてきた! 使い回せるぞ! 足りないものを加えていこう! src ┗ components ┣ burgerParts ┃ ┣ ビーフパティ.vue ┃ ┣ チェダースライスチーズ.vue ┃ ┣ ピクルス.vue ┃ ┣ オニオン.vue ┃ ┗ フィッシュポーション.vue // 追加 ┣ templates ┃ ┗ バンズ.vue ┗ pages ┣ ダブルチーズバーガー.vue ┗ フィレオフィッシュ.vue // 追加 フィッシュポーションはどのバーガーで使う? ちょっと待ってくれ! (ハンバーガーにかぶりつくのも止めてくれ!) (そのポテトは箱に戻して、一度手を拭いて!) フィッシュポーションは本当にburgerParts配下でいいのかい? このburgerPartsというディレクトリの役割は 汎用的に使えるcomponents(具材) をまとめることだったはずだ! キミはフィッシュポーションが フィレオフィッシュ以外で使われているを 見たことがあるかい? ボクはない! (もしキミがあったとしても、無いものとして考えてくれ。) (それが大人の対応ってものさ!) フィッシュポーションは、 フィレオフィッシュにしか使われていないのだから burgerPartsに配置するのはおかしい! パイセンにまた、 「フィッシュポーションのスコープはどこだい?」 なんて言われてしまうよ! 例えば、こうしてみたらどうだい? src ┗ components ┣ burgerParts ┃ ┣ ビーフパティ.vue ┃ ┣ チェダースライスチーズ.vue ┃ ┣ ピクルス.vue ┃ ┣ オニオン.vue ┣ templates ┃ ┗ バンズ.vue ┗ pages ┣ ダブルチーズバーガー.vue ┣ フィレオフィッシュ.vue ┗ フィレオフィッシュParts ┗ フィッシュポーション.vue これならフィッシュポーションは フィレオフィッシュでしか使われない ということがディレクトリ構造から明らかになった! ディレクトリ構造は、きっと、自由でいい。大切なのはわかりやすい意味をもたせることさ! モダン技術構成でのフロントエンド開発経験が まだまだ浅いボクには 「Atomic Designなディレクトリ構造最強だ!」 「もう13日の金曜日も、ミルクでビチャビチャのシリアルも怖くない!」 そう、思っていた時期があった でも、程なくして componentsのスコープという点では Atomic Designなディレクトリ構造は優れていない と感じてきてしまったんだ! 最近、またゼロからフロントエンドの開発をすることになって 優れたディレクトリ構造とはなんなのか? そう考えていたときに出会ったのが、この記事だった! Atomic Designをやめてディレクトリ構造を見直した話 (食べログ フロントエンドエンジニアブログ) そのプロジェクトでは この記事を参考にしたディレクトリ構造を考え 今のところAtomic Designなディレクトリ構造よりも安心して開発ができている! もちろん、キミが今読んでいるコレも、あの記事に大きな影響を受けているよ! 記事を読んで、プロジェクトで実践してみて、思うのは ディレクトリ構造は、きっと、自由でいい。大切なのはわかりやすい意味をもたせることさ! ということだな! 優れたディレクトリ構造は、安心感を生み出してくれる! ここまで読んでくれてありがとう! 優れたディレクトリ構造は、 配下のファイルの責務やスコープ そんなものを明らかにしてくれる、ボクはそう信じているよ さて、この記事も終わりだ。 マクドナルドに行かないとね! Follow ME !!! I'm sure to follow you back! twitter: @marty_ojiya
- 投稿日:2021-10-25T01:03:49+09:00
Vue.js+vuetifyとflaskで作ったRest APIサーバー間で通信し、CRUDを行うアプリケーションを作る
Vue.jsでRest APIサーバーと通信し、CRUDの各画面を作る flaskを使ってRestAPIサーバを作ってみるで作成したAPIサーバーを呼び出すフロントエンドを作成します。APIサーバーはリクエストをJSON形式で送信することでアカウントを作成、検索、更新、削除ができます。フロントエンドも作成、検索、更新、削除が実現できるUIを実装してみましょう。 本ブログの内容を試すにあたって、python, pip, pipenv, mysql, git, dockerがインストールされている必要があります。Flask+MySQL on dockerを始める準備でインストール方法またはインストール方法が載っているサイトを紹介しています。 また、DBが必要です。Flask+MySQL on dockerで説明しているdockerを参考にMySQLのdockerコンテナを起動してください。 Webアプリケーションで躓くポイント 筆者の様に主にバックエンド開発をやってきた者にとってフロント開発で躓くポイントがいくつかあります。 許されていない人が使えない様にするためのセキュリティのルールがある インフラ等の問題は解決しているのにエラーになる ブラウザのコードを動的に書き換えることによって動くプログラムであること コードが書き変わる順番などがあり、最終的に何が何に変更したかがわからないと正しい表示がされない。 独自なデザインのルールがある レスポンシブルデザインや、入出力の方法が複数ある。データの型があっているだけではアプリケーションとして成り立たない。 今回、セキュリティのルールの中でなかなか解消するのに手間がかかったオリジン間リソース共有を説明します。 CORS (オリジン間リソース共有) Wiki Cross-origin resource sharing の説明 Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the first resource was served. の通り、 オリジン間リソース共有は、最初に提供したリソースからドメインの外へ、制限されたWebページのリソースへ、他のドメインからリクエストを許す仕組みのことです。 つまり、ドメインが異なるアプリケーションからのリクエストは受け付けられない。これは同じホストでもポート番号が異なると異なるドメインということになり、vueが動くWebサーバーとflaskサーバーと異なるドメインとなるため、vueのアプリケーションからAPIのリクエストが失敗します。 これを回避するためにヘッダに許されるドメインを指定したり、特定のドメインからのアクセスを許すことができるCORS対応のパッケージをインストールするなどする必要があります。Flaskもflask-corsというパッケージでその制御を行なっている様です。 $ pipenv install flask_cors githubのサンプルコードは既にflask_corsインストールした状態でPipfileが作られているので上記コマンドは実行しなくても大丈夫です。 それではフロントの開発に入っていきます。 1. 画面の作成 今回、例としてアカウントの一覧、登録、編集、削除ができるWebアプリケーションを作ります。 フロントフレームワークであるVue.js+Vuetify導入 フロントフレームワークであるVue.js+Vuetify導入 2章 で作成したフロントアプリを拡張してアプリケーションを作っていきます。 1-1 フロント環境の準備 1. リポジトリをcloneします。 $ git clone git@github.com:kaorunix/flask_sv.git 本リポジトリは複数のqiita記事とかで題材に使っているので本記事では次のtagをチェックアウトしてください。 git checkout menu-template 2. flaskサーバーアプリ起動 Flask+MySQL on dockerを読み、mysqlの立ち上げ、flaskで作ったバックエンドを起動してください。 $ cd backend/src $ pipenv run python main.py * Serving Flask app 'main' (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 895-939-291 3. vue.jsフロントアプリ起動 プロジェクトのディレクトリから次の様に入力。 $ cd frontend $ npm install 私が安定したモジュールを選んでいないためかワーニングが色々出てしまいます? もし、再実行して過去にインストールしたモジュールと競合してエラーになる時は次のディレクトリを削除してから再度実行してください。 flask_sv/frontend配下で実行します。 $ rm -rf node_modules 次のコマンドでフロントアプリの実行です。 $ npm run serve 次のメッセージが出ると起動完了です。 You may use special comments to disable some warnings. Use // eslint-disable-next-line to ignore the next line. Use /* eslint-disable */ to ignore all warnings in a file. App running at: - Local: http://localhost:8080/ - Network: http://192.168.1.6:8080/ ブラウザで http://localhost:8080/にアクセスしてください。 次の画面が見れたらフロント部分の起動が確認できました。 サブメニューはまだ何もありません。ここにメニューを足していきましょう。 1-2 メニュー作成 ブラウザから見えるメニューは、frontend/src/App.vue で定義しています。 <template>タグの中の<v-navigation-drawer> フロントフレームワークであるVue.js+Vuetify導入#Vuetifyの開発 のVuetifyでメニューを作るで解説したメニューを記述するv-list-itemタグに記載します。 navigation-drawerは画面左側からスライドしてくることで表示できるメニューになります。 frontend/src/App.vue <template> ... <v-navigation-drawer v-model="drawer" app cliped > <v-container> <v-list-item> メニュー </v-list-item> <v-divider/> <v-list dense nav=false> <v-list-group v-for="main_menu_item in main_menu" :key="main_menu_item.name" :prepend-icon="main_menu_item.icon" no-action :append-icon="main_menu_item.lists ? undefined : ''"> <template v-slot:activator> <v-list-item-content> <v-list-item-title> {{ main_menu_item.name }} </v-list-item-title> </v-list-item-content> </template> <v-list-item v-for="sub_menu in main_menu_item.lists" :key="sub_menu.name" :to="sub_menu.link" > <v-list-item-content> <v-list-item-title>{{ sub_menu.name }}</v-list-item-title> </v-list-item-content> </v-list-item> </v-list-group> </v-list> </v-container> </v-navigation-drawer> ... </template> ... <script> export default { data () { return { drawer: null, company_menu: [ { name: '参照', link: '/' }, { name: '編集', link: '/' }, { name: '削除', link: '`mailto:s@a`' } ] } } } </script> company_menuと同じ階層にアカウントのメニューを追加していきます。次のコードを追加してください。 frontend/src/App.vue <template> ... </template> <script> ... company_menu: [ { name: '参照', link: '/' }, { name: '編集', link: '/' }, { name: '削除', link: '`mailto:s@a`' } ], // ここから追加 main_menu: [ { name: 'アカウント', icon: 'mdi-account', lists: [ { name: '一覧', link: '/account' }, { name: '作成', link: '/account/create' } ] }, ] // ここまで } } } </script> <v-list-item-content>で埋め込まれている main_menu_itemとsub_menuをjavascriptで定義します。 これを埋め込むとアカウントメニューが表示されます。 メニューを開くと、追加したメニューの通り一覧と作成のサブメニューも表示されました。 しかし、まだサブメニューをクリックしても画面は表示されません。 そのパスがどのコンポーネントに紐づいているかを記述します。 次のindex.jsにアカウントの一覧表示と作成画面のパスを追加しましょう。 frontend/src/router/index.js import Vue from 'vue' import VueRouter from 'vue-router' import Home from '../views/Home.vue' Vue.use(VueRouter) const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/about', name: 'About', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') } ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) export default router 上記のコードに以下のアカウント一覧とアカウント作成の設定を追加します。 frontend/src/router/index.js // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') ... { path: '/account', name: 'アカウント一覧', component: () => import('../views/account/List.vue') }, { path: '/account/create', name: 'アカウント作成', component: () => import('../views/account/Create.vue') }, ... } ] 同時に、次のvueも作りましょう。一旦空で作ります。 views/account/List.vue views/account/Create.vue コーディングを飛ばしたい方は次のtagをチェックアウトしてください。 $ git checkout account-menu 2. 検索画面 レコードを表示する必要があるため基本となる機能は検索です。 一覧ページに表示する内容は、検索の結果です。なので一覧画面を作りながら検索のAPIを実行することにしましょう。 2-1 テーブルの作成 Listはtableにしましょう。vuetifyに<v-simple-table>というタグがありました。 詳しくはv-simple-tableを参照してください。 frontend/src/views/account/List.vue <template> <div class="about"> <p>{{ message }}</p> <h3>アカウント一覧</h3> <v-simple-table> <template v-slot;default> <thead> <tr> <th>アカウントID</th> <th>アカウント名称</th> <th>有効開始日</th> <th>有効終了日</th> <th>作成者</th> <th>作成日時</th> <th>更新者</th> <th>更新日時</th> <th>ステータス</th> </tr> </thead> <tbody> <tr> <td>アカウントID(id)</td> <td>アカウント名称(account_name)</td> <td>有効開始日</td> <td>有効終了日</td> <td>作成者</td> <td>作成日時</td> <td>更新者</td> <td>更新日時</td> <td>ステータス</td> </tr> </tbody> </template> </v-simple-table> </div> </template> <script> export default { name: 'List', data () { return { message: '出力メッセージ' } } } </script> http://localhost:8080/accountにアクセスしてみましょう。 2-2 vueでデータを表示 vueの機能を使ってtableにデータを埋め込みましょう。 List.vue を次の様に変更してください。 frontend/src/views/account/List.vue <template> ... <tbody> <tr v-for="account in accounts" v-bind:key="account"> <td>{{ account.id }}</td> <td>{{ account.account_name }}</td> <td>{{ account.start_on }}</td> <td>{{ account.end_on }}</td> <td>{{ account.created_by }}</td> <td>{{ account.created_at }}</td> <td>{{ account.updated_by }}</td> <td>{{ account.updated_at }}</td> <td>{{ account.status }}</td> </tr> </tbody> ... </template> </v-simple-table> </div> </template> <script> var accounts = [ { id: 123, account_name: 'macbeth', start_on: '2021/02/01 10:00:00', end_in: '2021/11/30 18:00:00', created_by: 10, created_at: '2021/08/25 12:00:00', updated_by: 10, updated_at: '2021/08/25 12:00:00', status: 0 }, { id: 124, account_name: 'duncan', start_on: '2021/05/01 10:00:00', end_in: '2021/10/30 18:00:00', created_by: 12, created_at: '2021/08/25 12:00:00', updated_by: 10, updated_at: '2021/08/25 12:00:00', status: 1 } ] export default { name: 'List', data () { return { accounts: accounts, message: '出力メッセージ' } } } </script> 変更したらブラウザ再読み込みか、http://localhost:8080/accountにアクセスしてみましょう。 一覧画面が表示されます。 コーディングを飛ばしたい方は次のtagをチェックアウトしてください。 $ git checkout account-menu 2-3 APIを呼び出して検索結果を表示する 検索画面を検索を行なって表示するため、flaskを使ってRestAPIサーバを作ってみる(検索編) で作成したhttp://localhost:5000/api/account/searchのAPIを呼び出します。 検索APIは次のフォーマットです。 request { "account_name":"account", "start_on":"2021-05-05 00:00:00", "end_on":"2030-12-31 00:00:00", "created_by":100, "updated_by":150 } response { "body": [ { "account_name":"account", "start_on":"2021-05-05 00:00:00", "end_on":"2030-12-31 00:00:00", "created_by:100, "created_at:"2021-05-05 00:00:00", "updated_by:150, "updated_at:"2021-09-01 00:00:00", } ], "status": { "code" : "I0001", "message" : "Found (4) records.", "detail" : "" } } それでは実際にAPIを呼び出すため、List.vue を次の様に変更してください。 frontend/src/views/account/List.vue <template> <div class="about"> <p>{{ message }}</p> <h3>アカウント一覧</h3> <v-simple-table> <template v-slot;default> <thead> <tr> <th>アカウントID</th> <th>アカウント名称</th> <th>有効開始日</th> <th>有効終了日</th> <th>作成者</th> <th>作成日時</th> <th>更新者</th> <th>更新日時</th> <th>ステータス</th> </tr> </thead> ... <tbody> <tr v-for="account in accounts" v-bind:key="account"> <td>{{ account.id }}</td> <td>{{ account.account_name }}</td> <td>{{ account.start_on }}</td> <td>{{ account.end_on }}</td> <td>{{ account.created_by }}</td> <td>{{ account.created_at }}</td> <td>{{ account.updated_by }}</td> <td>{{ account.updated_at }}</td> <td>{{ account.status }}</td> </tr> </tbody> ... </template> </v-simple-table> </div> </template> <script> var request = { operation_account_id: 100 } var url = 'http://localhost:5000/api/account/search' const config = { headers: { 'Content-Type': 'application/json' } } export default { name: 'List', data () { return { accounts: [], message: null } }, mounted () { var self = this this.axios .post(url, request, config) .then(function (response) { console.log('List axios response %o', response.data.body) self.accounts = response.data.body self.message = response.data.status.message }) .catch(err => { console.log('List axios error') console.log(err) }) } } </script> 画面を確認しましょう。 DBのaccountテーブルに入っているレコードが表示されていれば成功です。 もし、本ブログから読み始めてレコードが無い人はインサートしておいてください。 $ mysql -u creist -p -h 127.0.0.1 Enter password: Welcome to the MySQL monitor. ... mysql> use flask_sv mysql> insert into account (account_name, start_on, end_on, created_by, created_at, updated_by, updated_at, status) values ('accountA', '2021-09-01 00:00:00', '2021-12-31 00:00:00', 999, now(), 999, now(), 0); mysql> select * from account; ... 話をfrontend/src/views/account/List.vue戻し、 <script>の中を説明します。 APIのリクエストになるjsonとAPIの呼び出しに必要な情報です。 特にheaderにContent-Typeを正しく設定しないとCORSでエラーとなってしまいます。 frontend/src/views/account/List.vue <script> var request = { operation_account_id: 100 } var url = 'http://localhost:5000/api/account/search' const config = { headers: { 'Content-Type': 'application/json' } } ここからvueの実装です。 Vueオブジェクトのdataプロパティのaccountsにresponseとして返却されたjson形式のaccount配列を代入します。 サーバーからのresponseは、jsonが入っているbody以外にもステータスコードなど通信内容が含まれたオブジェクトです。 frontend/src/views/account/List.vue <script> ... export default { name: 'List', data () { return { accounts: [], message: null } }, mounted () { var self = this this.axios .post(url, request, config) .then(function (response) { console.log('List axios response %o', response.data.body) self.accounts = response.data.body self.message = response.data.status.message }) .catch(err => { console.log('List axios error') console.log(err) }) } } </script> 一覧画面はAPIを呼び出して検索ができるようになったので一度手を離しましょう。 $ git checkout account-search 3. 作成画面 3-1 フォームの作成 まずはテキスト入力できるフォームを作りましょう。 次の内容のCreate.vueを作成します。 frontend/src/views/account/Create.vue <template> <div class="about"> <p>{{ message }}</p> <h1>アカウント</h1> <v-form ref="form"> <v-simple-table> <thead></thead> <tbody> <tr> <th>アカウント名称</th> <td> <v-text-field v-model="account_name" :counter="64" :rules="nameRules" :value="account_name" label="アカウント名称" required ></v-text-field> </td> </tr> <tr> <th>有効開始日</th> <td> <v-text-field slot="activator" v-model="start_on" :value="start_on" label="有効開始日" readonly v-on="on" ></v-text-field> </td> </tr> <tr> <th>有効終了日</th> <td> <v-text-field slot="activator" v-model="end_on" :value="end_on" label="有効終了日" readonly v-on="on" ></v-text-field> </td> </tr> <tr> <th>作成者</th> <td> <v-text-field v-model="created_by" :value="created_by" :counter="10" :rules="nameRules" label="作成者" required ></v-text-field> </td> </tr> </tbody> </v-simple-table> <v-btn class="mr-4" >保存</v-btn> <v-btn >リセット</v-btn> </v-form> </div> </template> VuetifyのText fieldsに入力フォームのデザインや機能など載っています。 しかしこのままでは日付項目に日付のフォーマットで文字を入れなければなりません。 コードの次箇所を編集しましょう。 frontend/src/views/account/Create.vue <template> ... <tr> <th>有効開始日</th> <td> <v-menu v-model="menu" max-width="290px" min-width="290px"> <!-- ポップアップを追加する要素にv-on="on" --> <template v-slot:activator="{ on }"> <v-text-field slot="activator" v-model="start_on" :value="start_on" label="有効開始日" readonly v-on="on" ></v-text-field> </template> <v-date-picker v-model="start_on"></v-date-picker> </v-menu> </td> </tr> <tr> <th>有効終了日</th> <td> <v-menu v-model="menu" max-width="290px" min-width="290px"> <!-- ポップアップを追加する要素にv-on="on" --> <template v-slot:activator="{ on }"> <v-text-field slot="activator" v-model="end_on" :value="end_on" label="有効終了日" readonly v-on="on" ></v-text-field> </template> <v-date-picker v-model="end_on"></v-date-picker> </v-menu> </td> </tr> ... </template> 有効開始日、有効終了日にカーソルを持っていくとカレンダーから選択できる様になります。 VuetifyのDate pickersに日付選択するコンポーネントの説明が載っています。 3-2 vueのアプリケーションの実装 次にアプリケーションの実装をしましょう。 vuetifyのFormsを参考にリセットボタンを実装します。 <v-btn>で作成されたリセットボタンにclickされた時の関数を定義します。 frontend/src/views/account/Create.vue <template> ... </v-simple-table> <v-btn class="mr-4" >保存</v-btn> <v-btn @click="reset">リセット</v-btn> </v-form> </div> </template> <script> export default { name: 'account_create', data: function () { return { account_name: '', start_on: '', end_on: '', created_by: '', message: '出力メッセージ' } }, methods: { reset () { this.$refs.form.reset() }, clear () { this.$v.$reset() this.account_name = '' this.start_on = '' this.end_on = '' this.created_by = '' } } } </script> 変更したら、作成画面を開いてフォームに入力し、リセットボタンを押下してみてください。 フォームに入力された文字が消されて空になればOKです。 3-3 フォームのデータをアカウント作成APIに送信する flaskを使ってRestAPIサーバを作ってみる(作成編)で作ったアカウント作成APIのフォーマットは次の様になります。 request { "account_name": "account123", "start_on": "2021-01-01 00:00:00", "end_on": "2021-12-31 23:59:59", "opration_account_id": 123 } response { "body": "", "status": { "code" : "I0001", "message" : "Created Account Succesfuly.", "detail" : "" } } Create.vueは次の様に修正します。 frontend/src/views/account/Create.vue <template> ... </v-simple-table> <v-btn class="mr-4" @click="submit">保存</v-btn> <v-btn @click="reset">リセット</v-btn> </v-form> </div> </template> <script> var contentType = 'application/json' var url = 'http://localhost:5000/api/account/create' const config = { headers: { 'Content-Type': contentType, 'Access-Control-Allow-Origin': 'http://localhost:5000' } } export default { name: 'account_create', data: function () { return { account_name: '', start_on: '', end_on: '', created_by: '', message: '出力メッセージ' } }, computed: { form () { return { account_name: this.account_name, start_on: this.start_on.concat(' 00:00:00'), end_on: this.end_on.concat(' 00:00:00'), operation_account_id: parseInt(this.created_by) } } }, methods: { validate () { this.$refs.form.validate() }, reset () { this.$refs.form.reset() }, resetValidation () { this.$refs.form.resetValidation() }, submit () { console.log(this.form) this.axios .post(url, this.form, config) .then(function (response) { console.log('Create axios response') console.log(response) document.location = 'http://localhost:8080/account' }) .catch(err => { console.log('Create axios error') console.log(err) }) }, clear () { this.$v.$reset() this.account_name = '' this.start_on = '' this.end_on = '' this.created_by = '' } } } </script> コードの説明をします。 searchと同じくAPIのリクエストになるjsonとAPIの呼び出しに必要な情報です。 frontend/src/views/account/Create.vue <script> var contentType = 'application/json' var url = 'http://localhost:5000/api/account/create' const config = { headers: { 'Content-Type': contentType } } ... </script> dataはフォームに必要な項目を定義します。 frontend/src/views/account/Create.vue <script> ... export default { name: 'account_create', data: function () { return { account_name: '', start_on: '', end_on: '', created_by: '', message: '出力メッセージ' } }, ... </script> form関数でAPIへ送信するJsonを作る。 frontend/src/views/account/Create.vue <script> ... computed: { form () { return { account_name: this.account_name, start_on: this.start_on.concat(' 00:00:00'), end_on: this.end_on.concat(' 00:00:00'), operation_account_id: parseInt(this.created_by) } } }, ... </script> フォームに入力された値が仕様に合っているかチェックするバリデーション、リセットなどの関数を定義している。 vuetifyのFormsを真似して作成します。 frontend/src/views/account/Create.vue <script> ... methods: { validate () { this.$refs.form.validate() }, reset () { this.$refs.form.reset() }, resetValidation () { this.$refs.form.resetValidation() }, ... </script> 保存ボタンが押されるときに呼び出される関数です。HTTP通信を行うaxiosライブラリでAPIを呼び出します。正常に終わったらアカウント一覧画面にリダイレクトしています。 frontend/src/views/account/Create.vue <script> ... submit () { console.log(this.form) this.axios .post(url, this.form, config) .then(function (response) { console.log('Create axios response') console.log(response) document.location = 'http://localhost:8080/account' }) .catch(err => { console.log('Create axios error') console.log(err) }) }, ... </script> リセットボタンが押された時に呼ばれる関数です。 frontend/src/views/account/Create.vue <script> ... clear () { this.$v.$reset() this.account_name = '' this.start_on = '' this.end_on = '' this.created_by = '' } } } </script> コーディングを飛ばしたい方は次のtagをチェックアウトしてください。 $ git checkout account-create 4. 編集 アカウントレコードを編集するためには、アカウントIDを特定しなければならない。その変更対象のアカウントを選ぶのはそのアカウント情報を参照できるよう様な画面である必要がある。なので一覧画面などで該当のアカウントを選択して変種モードに移るのが良いでしょう。 4-1 一覧画面に編集ボタンを追加 アカウント一覧画面に戻って次の修正をしましょう。 frontend/src/views/account/List.vue <template> ... <tbody> <tr v-for="account in accounts" v-bind:key="account"> <td> <v-btn fab x-small class="mx-2" slot="activator" color="cyan" dark @click=" getAccount(); dialog = true; " > <v-icon dark> mdi-pencil </v-icon> </v-btn> </td> <td>{{ account.id }}</td> ... </template> <script> ... </script> 編集ボタンが表示される様になりました。 4-2 一覧画面の編集ボタンからモーダル編集画面を起動 アイコンボタンからListの中にモーダルウィンドウを開くことにしましょう。モーダルはHTMLでは一つのコードとして作りますが、vueを使っているのでコンポーネントとして埋め込みましょう。 一覧画面は次の様に変更します。 コンポーネントのタグに置き換えます。埋め込むコンポーネントはvueファイルをimportして、componentsに追加します。 frontend/src/views/account/List.vue <template> ... <tbody> <tr v-for="account in accounts" v-bind:key="account"> <td><Update /></td> <td>{{ account.id }}</td> ... </template> <script> import Update from '@/views/account/Update.vue' ... export default { ... components: { Update } } </script> 次に、モーダルウィンドウで表示するフォームを Update.vue として作成します。 frontend/src/views/account/Update.vue <template> <v-dialog v-model="dialog" width="800" > <v-btn fab x-small class="mx-2" slot="activator" color="cyan" dark @click="dialog = true; " > <v-icon dark> mdi-pencil </v-icon> </v-btn> <v-card> <v-card-title class="headline grey lighten-2" primary-title > アカウント更新 </v-card-title> <v-card-text> <v-container grid-list-md> <v-layout wrap> <v-flex xs12> <v-text-field label="アカウントid" disabled required v-model="account.id"></v-text-field> </v-flex> <v-flex xs12> <v-text-field label="アカウント名称" required v-model="account.account_name"></v-text-field> </v-flex> <v-flex xs12> <v-menu max-width="290px" min-width="290px"> <!-- ポップアップを追加する要素にv-on="on" --> <template v-slot:activator="{ on }"> <v-text-field slot="activator" v-model="account.start_on" :value="account.start_on" label="有効開始日" readonly v-on="on" ></v-text-field> </template> <v-date-picker v-model="account.start_on"></v-date-picker> </v-menu> </v-flex> <v-flex xs12> <v-menu max-width="290px" min-width="290px"> <!-- ポップアップを追加する要素にv-on="on" --> <template v-slot:activator="{ on }"> <v-text-field slot="activator" v-model="account.end_on" :value="account.end_on" label="有効終了日" readonly v-on="on" ></v-text-field> </template> <v-date-picker v-model="account.end_on"></v-date-picker> </v-menu> </v-flex> <v-flex xs12> <v-text-field label="作成者" required v-model="operation_account_id"></v-text-field> </v-flex> </v-layout> </v-container> </v-card-text> <v-divider></v-divider> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" text @click="dialog = false" > 更新 </v-btn> <v-btn color="primary" text @click="dialog = false" > キャンセル </v-btn> </v-card-actions> </v-card> </v-dialog> </template> <script> export default { name: 'Update', data () { return { dialog: false, account: { id: 1234, account_name: '', start_on: '', end_on: '' }, operation_account_id: 4321 } } } </script> 更新ボタンをクリックするとモーダル小画面が開きます。 抜粋で、コードの解説をします。 <v-dialog>でモーダルウィンドウやダイアログを作ることができます。 vuetifyのDialogsに載っているコードをいろいろ試して利用します。 frontend/src/views/account/Update.vue <template> <v-dialog v-model="dialog" width="800" > <v-btn fab x-small class="mx-2" slot="activator" color="cyan" dark @click="dialog = true; " > <v-icon dark> mdi-pencil </v-icon> </v-btn> フォームはアカウント作成画面で使いました<v-text-field>で作っています。VuetifyのText fieldsを参照してください。 <v-layout>は、グリッドシステムを実現するタグです。グリッドシステムはvuetifyのことではないですが 絶対抑えておきたいWebデザインのグリッドシステムとフレームワーク #グリッドシステムが判りやすいかと思います。wrapで縦に分割する様です。 <v-flex>がグリッドの分割の単位になります。xsは、600px未満で12分割。ブラウザの幅によってグリッドの分割を変えることができるのでレスポンシブウェブデザインを実現できる様です。 <v-card>は1つにまとめるために使う様です。VuetifyのCardsの説明はあまり良くわからなく、v-cardを理解するためVuetify.js を使ってマテリアルデザインに挑戦しよう!を参考にしました。 frontend/src/views/account/Update.vue <template> ... <v-card> <v-card-title class="headline grey lighten-2" primary-title > アカウント更新 </v-card-title> <v-card-text> <v-container grid-list-md> <v-layout wrap> <v-flex xs12> <v-text-field label="アカウントid" disabled required v-model="account.id"></v-text-field> </v-flex> <v-flex xs12> <v-text-field label="アカウント名称" required v-model="account.account_name"></v-text-field> </v-flex> <v-flex xs12> <v-menu max-width="290px" min-width="290px"> <!-- ポップアップを追加する要素にv-on="on" --> <template v-slot:activator="{ on }"> <v-text-field slot="activator" v-model="account.start_on" :value="account.start_on" label="有効開始日" readonly v-on="on" ></v-text-field> </template> <v-date-picker v-model="account.start_on"></v-date-picker> </v-menu> </v-flex> <v-flex xs12> <v-menu max-width="290px" min-width="290px"> <!-- ポップアップを追加する要素にv-on="on" --> <template v-slot:activator="{ on }"> <v-text-field slot="activator" v-model="account.end_on" :value="account.end_on" label="有効終了日" readonly v-on="on" ></v-text-field> </template> <v-date-picker v-model="account.end_on"></v-date-picker> </v-menu> </v-flex> <v-flex xs12> <v-text-field label="作成者" required v-model="operation_account_id"></v-text-field> </v-flex> </v-layout> </v-container> </v-card-text> <v-divider></v-divider> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" text @click="dialog = false" > 更新 </v-btn> <v-btn color="primary" text @click="dialog = false" > キャンセル </v-btn> </v-card-actions> </v-card> </v-dialog> </template> dataにフォームに埋め込む初期値を設定します。編集画面なのでこれからAPIを呼び出しでDBから取得した値を埋め込むように作っていきましょう。 frontend/src/views/account/Update.vue <script> export default { name: 'Update', data () { return { dialog: false, account: { id: 1234, account_name: '', start_on: '', end_on: '' }, operation_account_id: 4321 } } } </script> 4-3 編集画面に検索した結果を表示 一覧画面から各行のアカウントIDを連携する必要があります。一覧画面のUpdateタグを次の様に変更してください。 frontend/src/views/account/List.vue <template> ... <td> <Update v-bind:account-id="account.id"></Update> </td> ... </template> <script> ... </script> 一覧画面でv-bindで渡されたaccount-idという変数はvueの中では、propsプロパティでaccountIdと定義すると値を渡すことができます。 accountIdを使ってflaskを使ってRestAPIサーバを作ってみる(編集編)の課題 4.1で作成したロックを行うAPIを呼び出します。 このAPIは、アカウントIDで検索されたアカウント情報を返却するので返却されたアカウント情報をフォームに埋め込みます。 request { account_id : int, operation_account_id : int } response { "body": { "name": "account", "id": <account_id>, "account_name": <account_name>, "start_on": "2021-01-01 10:00:00", "end_on": "2025-12-31 21:00:00" }, "status": { "code" : "I0001", "message" : "Account locked Succesfuly.", "detail" : "" } } Update.vueのコードを次の様に修正しましょう。 frontend/src/views/account/Update.vue <template> ... <v-btn fab x-small class="mx-2" slot="activator" color="cyan" dark @click="getAccount(); dialog = true; " > <v-icon dark> mdi-pencil </v-icon> </v-btn> ... </template> <script> var url = 'http://localhost:5000/api/account/' const config = { headers: { 'Content-Type': 'application/json' } } function applyDateFormat (datestring) { if (datestring.search(/^\d{4}[-/]\d{2}[-/]\d{2} \d{2}:\d{2}:\d{2}$/) === 0) { return datestring } else if (datestring.search(/^\d{4}[-/]\d{2}[-/]\d{2}$/) === 0) { return datestring.concat(' 00:00:00') } } function setAccount (account) { var a = { id: account.id, account_name: account.account_name, start_on: applyDateFormat(account.start_on), end_on: applyDateFormat(account.end_on) } return a } export default { name: 'Update', props: { accountId: { type: Number, required: true } }, data () { return { dialog: false, account: { id: '', account_name: '', start_on: '', end_on: '' }, operation_account_id: 50 } }, methods: { getAccount () { var request = { id: this.accountId, operation_account_id: parseInt(this.operation_account_id) } console.log('getAccount was called') const self = this this.axios .post(url + 'lock', request, config) .then(function (response) { console.log('Get axios response') console.log(response) self.account = setAccount(response.data.body) self.meesage = response.status.message }) .catch(err => { console.log('Get axios error') console.log(err) }) }, } } </script> コードの説明をします。 編集アイコンを押下すると、検索メソッド getAccount を呼び出しdialogがオンになりモーダルウィンドウを開きます。 frontend/src/views/account/Update.vue <template> ... <v-btn fab x-small class="mx-2" slot="activator" color="cyan" dark @click="getAccount(); dialog = true; " > <v-icon dark> mdi-pencil </v-icon> </v-btn> ... </template> アカウント一覧画面、アカウント作成画面同様、apiを呼ぶための情報を変数に設定しています。 frontend/src/views/account/Update.vue <script> var url = 'http://localhost:5000/api/account/' const config = { headers: { 'Content-Type': 'application/json' } } フロントで扱う日付情報を変換する関数を作りました。 date-picker で作った日付は時間が入っていにのですが、DB上の対応する項目はdatetimeなのでフォーマットを合わせます。もし、日付だけであった場合00:00:00を付加します。 frontend/src/views/account/Update.vue <script> ... function applyDateFormat (datestring) { if (datestring.search(/^\d{4}[-/]\d{2}[-/]\d{2} \d{2}:\d{2}:\d{2}$/) === 0) { return datestring } else if (datestring.search(/^\d{4}[-/]\d{2}[-/]\d{2}$/) === 0) { return datestring.concat(' 00:00:00') } } ... </script> setAccount は、日付情報を変換してaccountオブジェクトに変換する関数です。 frontend/src/views/account/Update.vue <script> ... function setAccount (account) { var a = { id: account.id, account_name: account.account_name, start_on: applyDateFormat(account.start_on), end_on: applyDateFormat(account.end_on) } return a } ... </script> props を使ってアカウント一覧画面からアカウントIDを受け取れます。 frontend/src/views/account/Update.vue <script> ... export default { name: 'Update', props: { accountId: { type: Number, required: true } }, data () { return { dialog: false, account: { id: '', account_name: '', start_on: '', end_on: '' }, operation_account_id: 50 } }, ... </script> methods はVueインスタンスのメソッドとして機能します。データの変更や、サーバーにHTTPリクエストを送る時はここに記述する必要があります。 getAccount は、アカウントIDと操作アカウントIDを使ってAPIを呼び出しています。そして返ってきたアカウント情報をvueオブジェクトのaccountに入れ直すことでフォームを再描画します。 frontend/src/views/account/Update.vue <script> ... methods: { getAccount () { var request = { id: this.accountId, operation_account_id: parseInt(this.operation_account_id) } console.log('getAccount was called') const self = this this.axios .post(url + 'lock', request, config) .then(function (response) { console.log('Get axios response') console.log(response) self.account = setAccount(response.data.body) self.meesage = response.status.message }) .catch(err => { console.log('Get axios error') console.log(err) }) }, } } </script> thisをselfに代入(bind)していますが、axiosのthenの中ではthisはaxiosオブジェクトのthisになってしまう様です。vueオブジェクトのaccountメンバにアクセスできませんでした。vueオブジェクトのaccountにアクセスするために、別変数selfに代入しておきます。 4-4 編集画面に入力した内容を更新 フォームで入力したアカウント情報でDBを更新しましょう。 flaskを使ってRestAPIサーバを作ってみる(編集編)の4.5. 排他編集で課題としていたロックを使った更新APIは次のフォーマットです。 request { "account_name": "start_on": "end_on": "operation_account_id": } response { "body": "", "status": { "code" : "I0001", "message" : "Updated Account Succesfuly.", "detail" : "" } } 更新APIを呼び出すためUpdate.vueを次の様に変更してください。 frontend/src/views/account/Update.vue <template> ... <v-btn color="primary" text @click=" submit(); dialog = false; " > 更新 </v-btn> ... </template> <script> export default { name: "Update", props: { ... data() { ... computed: { form() { return { id: this.account.id, account_name: this.account.account_name, start_on: applyDateFormat(this.account.start_on), end_on: applyDateFormat(this.account.end_on), operation_account_id: parseInt(this.operation_account_id) }; } }, methods: { getAccount() { ... validate() { this.$refs.form.validate(); }, submit () { console.log(this.form) this.axios .post(url + 'update_for_lock', this.form, config) .then(function (response) { console.log('Update axios response') console.log(response) document.location = 'http://localhost:8080/account' }) .catch(err => { console.log('Update axios error') console.log(err) }) } } } </script> コードの説明をします。 アカウント変更画面の更新ボタンを押した時の実行されるVueのメソッド名を書きます。 frontend/src/views/account/Update.vue <template> ... <v-btn color="primary" text @click=" submit(); dialog = false; " > 更新 </v-btn> ... </template> ... form関数は、入っている値をフォーマットしてからAPIに渡せるリクエストの形式にする関数です。 frontend/src/views/account/Update.vue ... <script> export default { name: "Update", props: { ... data() { ... computed: { form() { return { id: this.account.id, account_name: this.account.account_name, start_on: applyDateFormat(this.account.start_on), end_on: applyDateFormat(this.account.end_on), operation_account_id: parseInt(this.operation_account_id) }; } }, ... </script> submitメソッドはアカウント作成画面と同じ様にformの結果をaxiosでAPIに送り、一覧画面に遷移します。 frontend/src/views/account/Update.vue ... <script> ... methods: { getAccount() { ... validate() { this.$refs.form.validate(); }, submit () { console.log(this.form) this.axios .post(url + 'update_for_lock', this.form, config) .then(function (response) { console.log('Update axios response') console.log(response) document.location = 'http://localhost:8080/account' }) .catch(err => { console.log('Update axios error') console.log(err) }) } } } </script> コーディングを飛ばしたい方は次のtagをチェックアウトしてください。 $ git checkout account-update 5. 削除 5-1 削除アイコンの追加とダイアログ表示 まずは、一覧画面にアイコンを追加し、フォームでないダイアログを表示してみます。 次の修正をしてください。 frontend/src/views/account/List.vue <template> <div class="account_list"> <p>{{ message }}</p> <h3>アカウント一覧</h3> <v-simple-table> <template v-slot;default> <thead> <tr> <th>編集</th> <th>削除</th> ... <tbody> <tr v-for="account in accounts" v-bind:key="account.id"> <td> <Update v-bind:account-id="account.id"></Update> </td> <td> <Delete /> </td> ... </template> <script> import Update from '@/views/account/Update.vue' import Delete from '@/views/account/Delete.vue' ... components: { Update, Delete } } </script> 次の内容でDelete.vueを作りましょう。 frontend/src/views/account/Delete.vue <template> <v-dialog v-model="dialog" width="500"> <v-btn fab x-small class="mx-2" slot="activator" color="red lighten-2" dark @click="dialog = true" > <v-icon dark> mdi-delete </v-icon> </v-btn> <v-card> <v-card-title class="headline grey lighten-2" primary-title> アカウント削除 </v-card-title> <v-card-text> アカウント xxx を削除します。 </v-card-text> <v-divider></v-divider> <v-card-actions > <v-spacer></v-spacer> <v-btn color="primary" flat @click="dialog = false"> キャンセル </v-btn> <v-btn color="primary" flat @click="dialog = false"> 削除 </v-btn> </v-card-actions> </v-card> </v-dialog> </template> <script> export default { name: 'Delete', data () { return { dialog: false } } } </script> コードの説明をします。 一覧画面に表示するアイコンと、アイコンを押下したときに開くダイアログを定義しています。 frontend/src/views/account/Delete.vue <template> <v-dialog v-model="dialog" width="500"> <v-btn fab x-small class="mx-2" slot="activator" color="red lighten-2" dark @click="dialog = true" > <v-icon dark> mdi-delete </v-icon> </v-btn> ダイアログに表示する内容です。メッセージと削除を決断したときに押されるボタンを作成します。 frontend/src/views/account/Delete.vue <template> ... <v-card> <v-card-title class="headline grey lighten-2" primary-title> アカウント削除 </v-card-title> <v-card-text> アカウント xxx を削除します。 </v-card-text> <v-divider></v-divider> <v-card-actions >Ï <v-spacer></v-spacer> <v-btn color="primary" flat @click="dialog = false"> キャンセル </v-btn> <v-btn color="primary" flat @click="dialog = false"> 削除 </v-btn> </v-card-actions> </v-card> </v-dialog> </template> Vueオブジェクトはダイアログを開くか閉じるかのdialogを定義しています。 frontend/src/views/account/Delete.vue <script> export default { name: 'Delete', data () { return { dialog: false } } } </script> 実行し、削除アイコンをクリックすると次の画面の様になります。 5-2 ダイアログに削除対象のアカウント名称を表示 アカウント編集画面でやったように、アカウント一覧からアカウント情報を受け渡し、削除ダイアログに表示します。 変種画面と違うのは渡したい項目が複数あるのでオブジェクト形式で受け渡しています。 frontend/src/views/account/List.vue <template> ... <tbody> <tr v-for="account in accounts" v-bind:key="account.id"> <td> <Update v-bind:account-id="account.id"></Update> </td> <td> <Delete v-bind:account="account"></Delete> </td> ... </template> ... VueのpropsプロパティではtypeにObjectを指定します。 frontend/src/views/account/Delete.vue <script> export default { name: 'Delete', props: { account: { type: Object, required: true } }, ... data () { ... </script> 5-3 削除APIの呼び出し flaskを使ってRestAPIサーバを作ってみる(削除編)で作成した削除APIは次のフォーマットです。 request http://localhost:5000/account/delete/<account_id> へのGETリクエストです。 アカウントIDを指定するだけです。 response { "body": "", "status": { "code" : "I0001", "message" : "deleted Account Succesfuly.", "detail" : "" } } アカウント編集画面とほぼ同じですが次の修正で削除できるようになります。 削除ボタンからsubmitメソッドを呼び出しアカウント削除APIを呼び出しています。削除APIはGETのAPIとして作成したのでURLにアカウントID足すだけです。 frontend/src/views/account/Delete.vue <template> ... <v-btn color="primary" text @click="submit(); dialog = false"> 削除 </v-btn> ... </template> <script> var url = 'http://localhost:5000/api/account/' const config = { headers: { 'Content-Type': 'application/json' } } ... methods: { submit () { console.log(this.account.id) this.axios .get(url + 'delete/' + this.account.id, config) .then(function (response) { console.log('Delete axios response') console.log(response) document.location = 'http://localhost:8080/account' }) .catch(err => { console.log('Update axios error') console.log(err) }) } } } </script> 課題 5.1 削除の時に他のユーザーによって対象アカウント情報が編集・削除されるのを防ぐためユーザーを選ぶ時点でlockのAPIを呼び出しロックをかけるのが良いですが、 GETのAPIはoperation_account_idを取れないため実行者を特定できません。 flaskを使ってRestAPIサーバを作ってみる(削除編)の課題 5.1を行なった上で、アカウント一覧画面から削除アイコンをクリックしたら該当アカウントをロックし、削除ボタン押下時に、ロックした操作ユーザーであれば削除できるよう様変更してください。 6. 最後に CRUDができるAPIサーバーと通信してCRUDを行うフロントアプリを実装してみました。 まだこの状態では本番稼働できる状態とは程遠いですが、まだWebアプリを作ったことがない方の参考になればと思います。