近々業務で本格的にvueを使うのでアウトプットとして、一通りの機能を持ったアプリを作ってみました。
vue-cliとRailsで作成したAPIを使ってブログアプリを作っていきます。
未熟なため用語の不適切な使い方など多数散見されると思いますが、あなたの熱い想いはコメント欄で吐き出しまくってください。めっちゃありがたく頂戴いたします。
主な機能
- 投稿
- 編集
- 削除
- vue-routerによるページ遷移
- vuexによる状態管理
- サクセスメッセージ 、validation
前提
- vueコマンドが使える。
- vueをかじったことがある(v-modelとかなんとなくわかる)。
詳しい説明は公式ドキュメントなどを参考にしていただけると🙇♂️
ちなみに自分はVue.js入門という本で網羅的に勉強しました。
技術書はリーダブルコード以来だったのですが、アホみたいにわかりやすかったです。
環境
Mac
Rails 5.2.4.2
ruby 2.5.5
npm 6.13.7
RailsでAPIを作る
まずはAPIから作っていきましょう。
apiモードでRailsアプリを作成
$ rails new blog-app --api
末尾に--api
とつけることでAPIモードでアプリを作成できます。
コントローラーとモデルを作る
今回必要なコントローラーとモデルを作っていきましょう。
$ rails g controller v1::blogs
$ rails g model Blog title:string body:text
v1::blogs
とすることでバージョンで名前空間を作成します。
blogsコントローラーを以下のように設定します。
class V1::BlogsController < ApplicationController
def index
blogs = Blog.all
render json: blogs
end
def update
blog = Blog.find(params[:id])
if blog.update(blog_params)
render json: blog
else
render json: blog.errors
end
end
def create
blog = Blog.new(blog_params)
if blog.save
render json: blog, status: :created
else
render json: blog.errors, status: :unprocessable_entity
end
end
def destroy
blog = Blog.find(params[:id])
if blog.destroy
render json: blog
end
end
private
def blog_params
params.require(:blog).permit(:title, :body)
end
end
indexアクション
は今回のロジック上は必要ないですが、ブラウザ上でデータを見やすくするために作成しました。
<% @blogs.each do |blog| %>
<%= blog.title %>
<%= blog.body %>
<% end %>
簡単にデータを表示できるように記述しておきます。
Rails.application.routes.draw do
namespace :v1 do
resources :blogs, only: [:create, :destroy, :index, :update]
end
end
ここもきっちり名前空間で区切っておきましょう。
クロスドメインによるエラーを解決
基本的な機能はできたのですが、このままだとAPI通信をした際にconsoleで以下のエラーが出てしまいます。
Access to XMLHttpRequest at 'https://... ' from origin 'http://... ' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource
このエラーを解決するために以下の作業をします。
gem 'rack-cors'
デフォルトではコメントアウトされているので外してあげてください。
$ bundle install
rack-cors
を入れると以下のファイルが現れるので同様に記述してください。
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*' #<=全てのオリジンからのリクエストを許可
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
ここまででAPIの作成は終了です。いよいよvueのプロジェクトに入りましょう。
vue-cliをインストール
まずはvue-cliをインストールして環境を作ります。以下の記事がわかりやすいです。
Vue.js を vue-cli を使ってシンプルにはじめてみる
インストールできたら以下のコマンドでプロジェクトを作成します。
$ vue create blog-app
Manuallyを選択。
? Please pick a preset:
default (babel, eslint)
❯ Manually select features
vuexとvue-routerを選択。
Vue CLI v3.4.0
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select,
<a> to toggle all, <i> to invert selection)
◉ Babel
◯ TypeScript
◯ Progressive Web App (PWA) Support
◉ Router
❯◉ Vuex
◯ CSS Pre-processors
◉ Linter / Formatter
◯ Unit Testing
◯ E2E Testing
この後いくつか質問が聞かれますが、全部EnterでOKです。
ターミナルで以下のようになったら成功です。
cd
でアプリのディレクトリに移動し、npm run serve
コマンドを実行してください。
Successfully created project blog-app.
👉 Get started with the following commands:
$ cd blog-app
$ npm run serve
ここまででvueアプリの雛形ができました。
一覧表示ページを作る
まずは保存したブログが一覧で見ることができるトップページのようなものを作っていきましょう。
今回、デザインはvuetify
というcssフレームワークにまかせますのでダウンロードしておきます。
$ vue add vuetify
? Choose a preset: Default
現状のフォルダを確認しておきましょう。
<template>
<div id="app">
<v-container>
<router-view/>
</v-container>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 50px;
}
</style>
デフォルトで入っているHelloWorld
は使わないので削除しておきます。
基本的に今回作っていくページは全てこの<router-view/>
の中に代入されていきます。
Railsアプリでいう<%= yield %>
みたいな感じですね?
次にルーティングを設定していきます。
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Blogs from '../views/Blogs.vue' // 追加
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/blogs', // 追加
name: 'blogs',
component: Blogs
}
]
const router = new VueRouter({
routes
})
export default router
こうすることでpath: '/blogs'
というルートにアクセスすると、views/Blogs.vue
ファイルが呼ばれるようになります。
このBlogs.vue
が一覧表示ページになります。
ただ今回はルートページはvue-routerの簡単な練習もかねてHome.vue
に設定してみました。
これはデフォルトでインストールされたフォルダです。
ちょっとだけいじっていきましょう。
<template>
<div class="home">
<h1>Welcome to Blog App !</h1>
<router-link to="/blogs">start</router-link>
</div>
</template>
<script>
export default {
components: {
}
}
</script>
router-link
にto=""
でルーティングパスを渡してあげることができます。
start
というリンクをクリックすると先ほど設定した/blogs
に飛ぶようになっています。
つまりBlogs.vue
が表示されるということです。
ではBlogs.vue
を作っていきます。
<template>
<div>
<h1>Blogs</h1>
<v-row>
<v-col cols="4">
<form>
<v-text-field
v-model="blog.title"
label="Title"
></v-text-field>
<v-textarea
v-model="blog.body"
label="Body"
></v-textarea>
<v-btn class="mr-4" @click="onSubmit">Create</v-btn>
</form>
</v-col>
<v-col cols="8">
<table>
<tr>
<th>Title</th>
<th>Description</th>
</tr>
<tr>
<td>aa</td>
<td>aa</td>
<td>show</td>
<td>Edit</td>
<td>Destroy</td>
</tr>
</table>
</v-col>
</v-row>
</div>
</template>
<script>
export default {
data() {
return {
blog: {}
}
},
methods: {
onSubmit() {
}
}
}
</script>
まだ何も投稿していないので適当にtableにデータを入れておきます。ここは後ほど自動化していきます。
現状だとボタンをクリックしても関数を設定していないので何もおきません。
では実際にAPI通信を挟んでデータを表示、追加してみましょう。
APIと通信してデータを表示する
まずはaxios
という非同期でHTTP通信をしたいときに、Vue.jsでよく使われるHTTPクライアントをインストールします。
$ npm install --save axios
api通信のベースとなるファイルを作ります。
apiフォルダとその下にindex.jsファイルを作成してください。
import axios from 'axios'
export default () => {
return axios.create({
baseURL: 'http://0.0.0.0:3000/v1'
})
}
先ほどインストールしたaxiosをインポートして使えるようにしています。
ここで上記のようにaxiosを関数化しておくと全てのファイルで簡単に呼び出すことができます。
さらにbaseURLとしてAPI側のURLを指定しておくと、リクエストの際にいちいち'http://0.0.0.0:3000/v1/blogs'
などのように書かなくてよくなります。
'/blogs'
だけで上記のURLと同じ通信することができるのです。
vuexで状態を管理できるようにする
src/store/index.jsを以下のように書き換えます。
- 先ほど作成したapiファイルをimport
- state,actions,mutationsを書き換える
- modulesは今回使わないので消す
import Vue from 'vue'
import Vuex from 'vuex'
import axios from '@/api/index'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
blogs: []//④
},
mutations: {
FETCH_BLOGS(state, blogs) {//③
state.blogs = blogs
}
},
actions: {
async fetchBlogs({ commit }) {//①
await axios().get('/blogs')
.then(res => {
commit('FETCH_BLOGS', res.data)//②
})
.catch(e => console.log(e))
}
}
})
流れを簡単に説明すると、
- ブラウザでクリックなどのイベントが起こる
- actionsでAPIとの通信を挟む
- mutationsにactionsで通信したデータをコミットする
- stateに最終状態として挿入する
みたいな感じです。
上のコードの番号①〜④の順で実行されます。
では、サーバーから取ってきた情報を実際に表示させてみましょう。
以前作ったRailsAPIアプリを起動します(いちいち起動するのがめんどくさい人はRailsアプリだけ先にデプロイしてしまってもOKです)。
$ rails s -b 0.0.0.0
このときポート番号が両方とも同じだと衝突してエラーになってしまうので注意です。
幸い、vue-cliはデフォルトで8080
、railsは3000
を使っているので今回は大丈夫です。
まだサーバーにデータが何も入っていない状態なのでconsoleから作成してみましょう。
- ターミナルで
rails c
コマンド -
Blog.create(title: "test1", body: "This is a test1")
で適当に何かデータを追加
$ rails c
Running via Spring preloader in process 2127
Loading development environment (Rails 5.2.4.2)
irb(main):001:0> Blog.create(title: "test1", body: "This is a test1")
(0.1ms) begin transaction
Blog Create (8.0ms) INSERT INTO "blogs" ("title", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["title", "test1"], ["body", "This is a test1"], ["created_at", "2020-04-17 08:13:47.969926"], ["updated_at", "2020-04-17 08:13:47.969926"]]
(3.4ms) commit transaction
=> #<Blog id: 1, title: "test1", body: "This is a test1", created_at: "2020-04-17 08:13:47", updated_at: "2020-04-17 08:13:47">
これで現在サーバーにはデータが一つだけ入っている状態になります。
API通信をしてBlogs.vue
ページ上に表示させましょう。
App.vue
に以下を追加します。
<script>
export default {
created() {
this.$store.dispatch('fetchBlogs')
},
}
</script>
ここでライフサイクルの中のcreated
というフックを実行します。
インスタンスが作成された後に同期的に呼ばれます。この段階では、インスタンスは、データ監視、算出プロパティ、メソッド、watch/event コールバックらのオプションのセットアップ処理が完了したことを意味します。
これで何かしらのアクションが起こるたびに先ほど定義したfetchBlogs
アクションを実行します。
これでサーバーからデータを取得できるようになりました。
Blogs.vue
に取得したデータを表示できるようにしていきましょう。
<template>
<div>
<h1>Blogs</h1>
<v-row>
<v-col cols="4">
<form>
<v-text-field
v-model="blog.title"
label="Title"
></v-text-field>
<v-textarea
v-model="blog.body"
label="Body"
></v-textarea>
<v-btn class="mr-4" @click="onSubmit">Create</v-btn>
</form>
</v-col>
<v-col cols="8">
<table>
<tr>
<th>Title</th>
<th>Description</th>
</tr>
<tr v-for="blog in blogs" :key="blog.id"> <!-- 変更 ③-->
<td>{{ blog.title }}</td> <!-- 変更 ④-->
<td>{{ blog.body }}</td> <!-- 変更 ④-->
<td>show</td>
<td>Edit</td>
<td>Destroy</td>
</tr>
</table>
</v-col>
</v-row>
</div>
</template>
<script><img width="940" alt="スクリーンショット 2020-04-17 17.29.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416999/7ce646f0-1161-06fe-4200-db60b9b4991f.png">
import { mapState } from 'vuex' //変更 ①
export default {
computed: { //変更 ②
...mapState(['blogs'])
},
data() {
return {
blog: {}
}
},
methods: {
onSubmit() {
}
}
}
</script>
①〜④の順にデータが移動します。
- vuexに簡単にアクセスできるようにする記述
-
src/store/index.js
の中のstate/blogs
を取得し、利用できるようにする -
v-for="blog in blogs"
で②で取得したblogs
のレコードを一つずつblog
として展開する -
blog.title
とすることでレコード上のtitleが一つずつ表示される
ここまでで http://0.0.0.0:3000/v1/blogs にアクセスすると以下の画像のような状態に(僕のは使いまわしているのでid: 66となってますが、id: 1があればOKです)。
http://localhost:8080/#/blogs にアクセスすると以下のようになります。
ここまででサーバーと通信してデータを取得し、vue側で表示できるようになりました。
次はブログを投稿できるようにしていきましょう。
ブログを投稿できるようにする
まずは投稿フォームを単一コンポーネントに切り出します。
<template>
<form>
<v-text-field
v-model="blog.title"
label="Title"
></v-text-field>
<v-textarea
v-model="blog.body"
label="Body"
></v-textarea>
<v-btn class="mr-4" @click="onSubmit">Create</v-btn>
</form>
</template>
<script>
export default {
data() {
return {
blog: {},
}
},
methods: {
onSubmit() {
}
}
}
</script>
Blogs.vue
にあったコードを切り取ってきます。
これをBlogs.vueにimportします。
<template>
<div>
<h1>Blogs</h1>
<v-row>
<v-col cols="4">
<AddBlog /> <!-- 追加 -->
</v-col>
<v-col cols="8">
<table>
<tr>
<th>Title</th>
<th>Description</th>
</tr>
<tr v-for="blog in blogs" :key="blog.id">
<td>{{ blog.title }}</td>
<td>{{ blog.body }}</td>
<td>show</td>
<td>Edit</td>
<td>Destroy</td>
</tr>
</table>
</v-col>
</v-row>
</div>
</template>
<script>
import { mapState } from 'vuex'
import AddBlog from './AddBlog' //追加
export default {
components: {
AddBlog //追加
},
computed: {
...mapState(['blogs'])
},
data() {
return {
blog: {}
}
},
methods: {
onSubmit() {
}
}
}
</script>
コンポーネントを分けただけなので http://localhost:8080/#/blogs にアクセスしても特に変わりはないかと思います。
ではvuexを使って投稿機能を作っていきましょう。
まずはcreate
ボタンを押したらvuexの
addBlogアクションにアクセスするように
AddBlog.vue`を書き換えていきます。
<template>
<form>
<v-text-field
v-model="blog.title"
label="Title"
></v-text-field>
<v-textarea
v-model="blog.body"
label="Body"
></v-textarea>
<v-btn class="mr-4" @click="onSubmit">Create</v-btn>
</form>
</template>
<script>
export default {
data() {
return {
blog: {},
}
},
methods: {
async onSubmit() {
await this.$store.dispatch('addBlog', this.blog) //追加
}
}
}
</script>
this.$store.dispatch
で第一引数(addBlog)に指定したvuexのアクションに第二引数(this.blog)の情報を渡します。
ここのthis.blog
はdataの中に入っているtitleとbodyです。
ではvuexにaddBlogアクションを作っていきます。
import Vue from 'vue'
import Vuex from 'vuex'
import axios from '@/api/index'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
blogs: []
},
mutations: {
FETCH_BLOGS(state, blogs) {
state.blogs = blogs
},
ADD_BLOG(state, blog) { // 追加 ③第一引数はstate、第二引数は②でコミットされたsavedBlogが渡ってくる
const blogs = state.blogs.concat(blog)
state.blogs = blogs
}
},
actions: {
async fetchBlogs({ commit }) {
await axios().get('/blogs')
.then(res => {
commit('FETCH_BLOGS', res.data)
})
.catch(e => console.log(e))
},
async addBlog({ commit }, blog) { // 追加 ① 、第二引数blogに先ほどのthis.blogが入る
const res = await axios().post('/blogs', blog)
const savedBlog = res.data
commit('ADD_BLOG', savedBlog) // ②
return savedBlog
},
}
})
-
AddBlog.vue
からアクセスされたactions addBlog
にデータを送信 -
ADD_BLOG mutations
にコミット -
state
のblogs
に渡されたデータを入れる
投稿したあともフォームにデータが残るのは嫌なので、以下のように追記します。
<script>
export default {
data() {
return {
blog: {},
}
},
methods: {
async onSubmit() {
await this.$store.dispatch('addBlog', this.blog)
this.blog.title = '' //追記
this.blog.body = '' //追記
}
}
}
</script>
これで投稿したらフォームの中身がリセットされるようになりました。
投稿時に投稿詳細ページに遷移する
現在までのアプリだと投稿したらその場でBlogs.vue
にデータが追加されてテーブルのレコードが増えていくという感じです。
正直、他のvueのサンプルアプリはこんなのばっかりだったので、今回は投稿時に個別の詳細ページに遷移させてみたいと思います。
以下の流れで作成します。
- ルーティング作成
- AddBlog.vueに投稿時に遷移するロジックを追記
- 投稿詳細ページを作成
ではルーティングから書いていきます。
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Blogs from '../views/Blogs.vue'
import Blog from '../views/Blog.vue' // 追加
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/blogs',
name: 'blogs',
component: Blogs
},
{
path: '/blogs/:id', // 追加
name: 'show-blog',
component: Blog,
params: true
}
]
const router = new VueRouter({
routes
})
export default router
params: true
とすることで投稿時に取得したidを埋め込み動的なルーティングになります。
次にAddBlog.vue
に追記していきます。
<script>
export default {
data() {
return {
blog: {},
}
},
methods: {
async onSubmit() {
const blog = await this.$store.dispatch('addBlog', this.blog) // 変更
this.$router.push({ name: 'show-blog', params: { id: blog.id }}) // 追記
}
}
}
</script>
this.$router.push
とすることで指定したルーティング先のページに飛びます。
これはちょっとややこしいですが、以下のようにreturn
することで作られたばかりのデータの情報を返してくれているので実行できています。
async addBlog({ commit }, blog) {
const res = await axios().post('/blogs', blog)
const savedBlog = res.data
commit('ADD_BLOG', savedBlog)
return savedBlog // この部分でpostされたデータを返している
}
では実際に詳細ページであるBlog.vue
を作成します。
<template>
<div>
<p>Title: {{ blog.title }}</p> <!-- ④ -->
<p>Body: {{ blog.body }}</p>
<router-link to="/blogs">Back</router-link>
</div>
</template>
<script>
import { mapState } from 'vuex' // ①
export default {
computed: {
...mapState(['blogs']), // ②
blog() { // ③
return this.blogs.find(blogId => blogId.id === this.$route.params.id) || {}
}
}
}
</script>
- storeの情報を簡潔にかけるように
mapState
をimport - state/blogsを展開(this.blogsとして使える)
-
path: '/blogs/:id'
にマッチする個別情報をstate/blogsの中から見つけてblog()
として関数化する - ③のblogを展開し、個別のtitle,bodyを表示
次は編集機能、削除機能をつけていきます。
編集機能を作る
投稿を編集できるようにします。
- 編集用のルーティングを作成
- 編集ページを作成
- vuex内で編集のロジックを追記
まずはルーティングから書いていきましょう。
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Blogs from '../views/Blogs.vue'
import Blog from '../views/Blog.vue'
import EditBlog from '../views/EditBlog.vue' // 追加
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/blogs',
name: 'blogs',
component: Blogs
},
{
path: '/blogs/:id',
name: 'show-blog',
component: Blog,
params: true
},
{
path: '/blogs/:id/edit', // 追加
name: 'edit-blog',
component: EditBlog,
params: true
}
]
const router = new VueRouter({
routes
})
export default router
詳細ページの時とほとんど同じですね。
次にBlog.vue
とBlogs.vue
にeditボタン
を書いていきます。
<template>
<div>
<Flash />
<p>Title: {{ blog.title }}</p>
<p>Body: {{ blog.body }}</p> <!-- 追加↓ -->
<router-link :to="{ name: 'edit-blog', params: { id: blog.id }}">Edit</router-link> | <router-link to="/blogs">Back</router-link>
</div>
</template>
<template>
<div>
<Flash />
<h1>Blogs</h1>
<v-row>
<v-col cols="4">
<AddBlog />
</v-col>
<v-col cols="8">
<table>
<tr>
<th>Title</th>
<th>Description</th>
</tr>
<tr v-for="blog in blogs" :key="blog.id">
<td>{{ blog.title }}</td>
<td>{{ blog.body }}</td> <!-- 追加↓ -->
<td><router-link :to="{ name: 'show-blog', params: { id: blog.id }}">show</router-link></td>
<td><router-link :to="{ name: 'edit-blog', params: { id: blog.id }}">edit</router-link></td>
<td>delete</td>
</tr>
</table>
</v-col>
</v-row>
</div>
</template>
ついでに詳細ページへのリンクも加えておきましょう。
続いて編集ページを作っていきます。
<template>
<div>
<h2>Editing Blog</h2>
<form>
<v-text-field
v-model="blog.title"
label="Title"
></v-text-field>
<v-textarea
v-model="blog.body"
label="Body"
></v-textarea>
<v-btn class="mr-4" @click="updateBlog">Update</v-btn>
</form>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState(['blogs']),
blog() {
return this.blogs.find(b => b.id == this.$route.params.id) || {}
}
},
methods: {
async updateBlog() {
const blog = await this.$store.dispatch('editBlog', this.blog) // ①
this.$router.push({ name: 'show-blog', params: { id: blog.id }})
}
}
}
</script>
この時点で気付いたのですが、formの部分とかは共通なのでコンポーネント分けした方が良いです笑
今回はこのままいかせてください。
ここでは①の部分でstoreの中のeditBlog
アクションに編集した内容でthis.blog
を送ってます。
ではそのeditBlog
アクションをstore/index.js
の中に作っていきましょう。
import Vue from 'vue'
import Vuex from 'vuex'
import axios from '@/api/index'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
blogs: []
},
mutations: {
FETCH_BLOGS(state, blogs) {
state.blogs = blogs
},
ADD_BLOG(state, blog) {
const blogs = state.blogs.concat(blog)
state.blogs = blogs
},
EDIT_BLOG(state, blog) { // 追記
state.blogs.forEach(b => {
if (b.id === blog.id) {
b = blog
}
})
}
},
actions: {
async fetchBlogs({ commit }) {
await axios().get('/blogs')
.then(res => {
commit('FETCH_BLOGS', res.data)
})
.catch(e => console.log(e))
},
async addBlog({ commit }, blog) {
const res = await axios().post('/blogs', blog)
const savedBlog = res.data
commit('ADD_BLOG', savedBlog)
return savedBlog
},
async editBlog({ commit }, blog) { // 追記
const res = await axios().put(`/blogs/${blog.id}`, blog)
const editedBlog = res.data
commit('EDIT_BLOG', editedBlog)
return editedBlog
}
}
})
axios().put
とすることでサーバーに対して更新の処理を行うリクエストを送ります。
そしてmutasions
ではstate.blogs
の中で、送られてきたIDと合致するもののblog
を書き換えるロジックを書いています。
詳細ページにもeditボタン
を追加しておきましょう。
<template>
<div>
<p>Title: {{ blog.title }}</p>
<p>Body: {{ blog.body }}</p>
<router-link :to="{ name: 'edit-blog', params: { id: blog.id }}">edit</router-link> | <router-link to="/blogs">Back</router-link> <!-- 追加 -->
</div>
</template>
次に削除機能を作っていきましょう。
投稿削除機能を作る
投稿したブログを削除する機能を作っていきます。
- 削除ボタンを作る
- vuexに削除のロジックをかく
削除ボタンはBlogs.vue
に書くことにします。
<template>
<div>
<h1>Blogs</h1>
<v-row>
<v-col cols="4">
<AddBlog />
</v-col>
<v-col cols="8">
<table>
<tr>
<th>Title</th>
<th>Description</th>
</tr>
<tr v-for="blog in blogs" :key="blog.id">
<td>{{ blog.title }}</td>
<td>{{ blog.body }}</td>
<td><router-link class="button_link" :to="{ name: 'show-blog', params: { id: blog.id }}">[ show ]</router-link></td>
<td><router-link class="button_link" :to="{ name: 'edit-blog', params: { id: blog.id }}">[ edit ]</router-link></td>
<td><span class="button_link" @click="deleteBlog(blog)">[ delete ]</span></td> <!-- 追加 -->
</tr>
</table>
</v-col>
</v-row>
</div>
</template>
<script>
import { mapState } from 'vuex'
import AddBlog from './AddBlog'
export default {
components: {
AddBlog
},
computed: {
...mapState(['blogs'])
},
data() {
return {
blog: {}
}
},
methods: {
deleteBlog(blog) { // 追加
this.$store.dispatch('deleteBlog', blog)
}
}
}
</script>
<style scoped>
.button_link {
cursor: pointer;
color: blue;
text-decoration: underline;
}
</style>
今回はアクションに引数blog
を渡してあげています。
v-for
で回したうちの一つの情報を持っています。
これをthis.$store.dispatch('deleteBlog', blog)
の状態でvuexにアクセスして渡しています。
import Vue from 'vue'
import Vuex from 'vuex'
import axios from '@/api/index'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
blogs: [],
flash_message: {
status: false,
message: ''
}
},
mutations: {
setMessage(state, payload) {
state.flash_message = payload
},
FETCH_BLOGS(state, blogs) {
state.blogs = blogs
},
ADD_BLOG(state, blog) {
const blogs = state.blogs.concat(blog)
state.blogs = blogs
},
EDIT_BLOG(state, blog) {
state.blogs.forEach(b => {
if (b.id === blog.id) {
b = blog
}
})
},
DELETE_BLOG(state, blogId) { // 追加 ②
const blogs = state.blogs.filter(b => b.id != blogId)
state.blogs = blogs
}
},
actions: {
async fetchBlogs({ commit }) {
await axios().get('/blogs')
.then(res => {
commit('FETCH_BLOGS', res.data)
})
.catch(e => console.log(e))
},
async addBlog({ commit }, blog) {
const res = await axios().post('/blogs', blog)
const savedBlog = res.data
commit('ADD_BLOG', savedBlog)
return savedBlog
},
async editBlog({ commit }, blog) {
const res = await axios().put(`/blogs/${blog.id}`, blog)
const editedBlog = res.data
commit('EDIT_BLOG', editedBlog)
return editedBlog
},
async deleteBlog({ commit }, blog) { // 追加 ①
await axios().delete(`/blogs/${blog.id}`, blog)
commit('DELETE_BLOG', blog.id)
}
}
})
axios().delete
で削除用のリクエストを飛ばします。
②ではstate.blogs.filter(b => b.id != blogId)
でstate.blogs`の中から、送られてきたid以外のもので新しい配列を作って返しています。
送っているidは削除ボタンを押した要素のidなので、それ以外で配列を作っているということです。
ここまでで削除機能ができました。
基本的な機能はこれで完成です。
あとはvalidationとアクション成功時のflashメッセージを作っていきます。
validationを作成する
新規投稿、編集時にフォームが空だった場合、送信できないようにします。
<template>
<v-form ref="checkForm">
<v-text-field
v-model="blog.title"
label="Title"
:rules="[required('Title')]"
></v-text-field>
<v-textarea
v-model="blog.body"
label="Body"
:rules="[required('Body')]"
></v-textarea>
<v-btn class="mr-4" @click="onSubmit">Create</v-btn>
</v-form>
</template>
<script>
export default {
data() {
return {
blog: {},
required(propertyType) {
return v => v && v.length > 0 || `You must input a ${propertyType}`
}
}
},
methods: {
async onSubmit() {
if (this.$refs.checkForm.validate()) {
const blog = await this.$store.dispatch('addBlog', this.blog)
this.$router.push({ name: 'show-blog', params: { id: blog.id }})
}
}
}
}
</script>
これはcssフレークーワークvuetify
の記述方法を使っています。
EditBlog.vue
も全く同じように記述します。
最後に投稿や編集、削除が成功した際にサクセスメッセージを出すようにしていきましょう。
Flashメッセージを作成する
- vuexでメッセージの状態を管理できるようにする
- Flash.vueを作成
- Flash.vueをメッセージを表示させたいページにimport(今回はBlog.vue、Blogs.vue)
state: {
blogs: [],
flash_message: { // 追加
status: false,
message: ''
}
},
mutations: {
setMessage(state, payload) { // 追加
state.flash_message = payload
},
<template>
<transition name="fade">
<div v-if="flash.status" class="success mb-3">{{ flash.message }}</div>
</transition>
</template>
<script>
export default {
computed: {
flash() {
return this.$store.state.flash_message
}
}
}
</script>
<style scoped>
.success {
color: green;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
</style>
まずは投稿時にサクセスメッセージをtrue
にし、メッセージをvuexにコミットしておきます。
methods: {
async onSubmit() {
if (this.$refs.checkForm.validate()) {
const blog = await this.$store.dispatch('addBlog', this.blog)
this.$store.commit('setMessage', {
status: true,
message: 'Blog was successfully created.'
})
setTimeout(() => {
this.$store.commit('setMessage', {})
}, 2000)
this.$router.push({ name: 'show-blog', params: { id: blog.id }})
}
}
}
次にAddBlog.vue
から投稿した際にBlog.vue
でサクセスメッセージを表示できるようにします。
<template>
<div>
<Flash /> // 追加
<p>Title: {{ blog.title }}</p>
<p>Body: {{ blog.body }}</p>
<router-link :to="{ name: 'edit-blog', params: { id: blog.id }}">edit</router-link> | <router-link to="/blogs">Back</router-link>
</div>
</template>
<script>
import { mapState } from 'vuex'
import Flash from '@/components/Flash.vue' // 追加
export default {
components: { // 追加
Flash
},
computed: {
...mapState(['blogs']),
blog() {
return this.blogs.find(blogId => blogId.id === this.$route.params.id) || {}
}
}
}
</script>
Blogs.vue
にも記述しておきましょう。
<template>
<div>
<Flash /> // 追加
<h1>Blogs</h1>
<v-row>
<v-col cols="4">
<AddBlog />
</v-col>
<v-col cols="8">
<table>
<tr>
<th>Title</th>
<th>Description</th>
</tr>
<tr v-for="blog in blogs" :key="blog.id">
<td>{{ blog.title }}</td>
<td>{{ blog.body }}</td>
<td><router-link class="button_link" :to="{ name: 'show-blog', params: { id: blog.id }}">[ show ]</router-link></td>
<td><router-link class="button_link" :to="{ name: 'edit-blog', params: { id: blog.id }}">[ edit ]</router-link></td>
<td><span class="button_link" @click="deleteBlog(blog)">[ delete ]</span></td>
</tr>
</table>
</v-col>
</v-row>
</div>
</template>
<script>
import { mapState } from 'vuex'
import AddBlog from './AddBlog'
import Flash from '@/components/Flash.vue' // 追加
export default {
components: {
AddBlog,
Flash // 追加
},
computed: {
...mapState(['blogs'])
},
data() {
return {
blog: {}
}
},
methods: {
deleteBlog(blog) { // 追加
this.$store.dispatch('deleteBlog', blog)
this.$store.commit('setMessage', {
status: true,
message: 'Blog was successfully destroyed.'
})
setTimeout(() => {
this.$store.commit('setMessage', {})
}, 2000)
}
}
}
</script>
<style scoped>
.button_link {
cursor: pointer;
color: blue;
text-decoration: underline;
}
</style>
以上で完成です。デプロイする気力はありませんでした。
お付き合いいただきありがとうございます、お疲れ様でした!!
今回のコード
Railsの方 → https://github.com/haga0531/BlogAppApi
Vueの方 → https://github.com/haga0531/blog-app-vue
(なんかアプリ作りながら記事書いていたらcommit数一回というゴミみたいな危機管理能力になっていたのは許してください)
まとめ
どの記事もtodoアプリばっかりだったので少しだけレベルアップしたアプリを作ってvuex、vue-routerの練習をしてみました。
コンポーネントの分け方は甘々だし、用語の使い方がゴチャゴチャなことも多くあると思います。
ドMなので厳し目にコメントくださると画面の前で土下座しながら修正させていただくので、ぜひご指摘のほどよろしくお願いします。
いや〜、しかし書いてて超楽しいですね、vue。
今後は業務でも使用しているNuxt.js
やFirebase
、Java
などにも取り組んでいきます。
次は認証ありのアプリを作ってみます。
参考にさせていただいた神記事たち
[入門]Rails API × Nuxt SPA × Firebase Authで作る Todo Appチュートリアル
インターン先の超尊敬している先輩の記事です。
この記事をみながらQiita独特の書き方的なのを学びました。」ちなみに上記チュートリアルは僕自身3周しました。神でした。僕の記事読むよりこっち読んでください。
Vue & Vuetifyでバリデーション付きのフォームを作ってみる
vuetifyを使ったvalidationの書き方を参考にさせていただきました。