フィヨルドブートキャンプのWebアプリで使っている、Vue.jsのコードリーディング
はじめに
今回はVue.jsで書かれたコードを読んでいきたいと思います!
Vue.jsは少ししか触ったことがないので、読み間違えているところなどあったら申し訳ないです😥
また、私は初学者なので、わからないことは「わからない」であるとか「?(疑問符)」を付けて書いています。
教えてくださる方がいらっしゃれば嬉しいです🙇♂️
わかったことがあれば随時追記していきます!
本記事にはrequestの結果などが記載されていますが、全てテスト環境のテストデータが元になっています。
今回読むコードについて
今回読んでいくソースは、フィヨルドブートキャンプ本体のWebアプリケーションです。
使っているフレームワークはRails + Vue.jsです。また、Railsのテンプレートエンジンはslimを使用しています。そのため今回はslimで書かれたコードが多く登場します。
コード本体がこちら(fjord/bootcamp)で、OSSになっており誰でも読めます。
ライセンスについて
ライセンスはまだついておりませんが、今後付ける予定です。また、フィヨルドブートキャンプへの許可は取得しております。
今回読む箇所
今回はコメントをVue.js化 #1017を読んでいきます。
Vue.jsのコードリーディング
まずは日報(report)機能から読んでいきます。
読みたいのはコメント(comment)機能なのですが、コメント機能は独立で存在していないので、私としてはなじみのある日報のコメントから読んでいこうと思います。
/app/views/reports/show.html.slim
# 略 #js-comments(data-commentable-id="#{@report.id}" data-commentable-type="Report" data-current-user-id="#{current_user.id}") = render "footprints/footprints", footprints: @footprints
#js-comments
にdata-commentable-id
,data-commentable-type
,data-current-user-id
を渡す- 疑問:Vue.jsのjsがなければ
#〜
はそのまま表示される?
- 疑問:Vue.jsのjsがなければ
/app/javascript/packes/application.js
/* eslint no-console:0 */ // This file is automatically compiled by Webpack, along with any other files // present in this directory. You're encouraged to place your actual application logic in // a relevant structure within app/javascript and only use these pack files to reference // that code so it'll be compiled. // // To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate // layout file, like app/views/layouts/application.html.erb require('./markdown.js') require('./markdown-it.js') require('./autosize.js') require('../shortcut.js') require('../learning.js') require('../learning-status.js') require('../check.js') require('../check-stamp.js') require('../comments.js') require('../category-select.js') require('../grass.js') // 略
- Rails5.2のため、webpacker(
Sprockets)でJavaScriptが管理されている require('../comments.js')
により、/app/javascript/comments.js
にアクセスが可能となる
/app/javascript/comments.js
import Vue from 'vue' import Comments from './comments.vue' document.addEventListener('DOMContentLoaded', () => { const comments = document.getElementById('js-comments') if (comments) { const commentableId = comments.getAttribute('data-commentable-id') const commentableType = comments.getAttribute('data-commentable-type') const currentUserId = comments.getAttribute('data-current-user-id') new Vue({ render: h => h(Comments, { props: { commentableId: commentableId, commentableType: commentableType, currentUserId: currentUserId } }) }).$mount('#js-comments') } })
- jsでhtmlの情報を取得してvueをレンダリング
- 疑問:そういえばこれは、サーバー側で処理している?
- もしそうなら、htmlの出力データをテンプレートエンジン代わりにしている?
- 疑問:そういえばこれは、サーバー側で処理している?
- element.getAttribute
- 疑問:current-userのidをフロントのjsで判定したら、好きなユーザーとしてコメントを投稿できるのでは?
- この疑問は、初めレンダリングをフロントで行なっていると想定していたため発生しました。実際はサーバーサイドで行なっているので、書き換えはできない状態でした。
- 下記部分で
comment.vue
を呼び出している
new Vue({ render: h => h(Comments, { props: { commentableId: commentableId, commentableType: commentableType, currentUserId: currentUserId } }) }).$mount('#js-comments')
/app/javascript/comments.vue
- 「なんかたくさんあるけどなんだっけ?」と一瞬思ったけど、Vue.jsにはライフサイクルがあったことを思い出した
- 変数と
date: () => {}
の違いってなんだっけ?課題で、dateに書くか書かないかでとても違いが出たことしか覚えていない・・・- Vue.js入門を読んだら、
date: () => {}
はdataプロパティと書いてあった- UIの状態を置く場所
- Vue.js入門を読んだら、
- script以外に、scriptと一緒にtempleteが書かれた
.vue
ファイルを読み書きしたことがないことに気づく😲‼️- Vue.js入門を再度読み返した
- 3章のコンポーネントまで理解できれば良さそうなので、読んでみた
- Vue.js入門を再度読み返した
props
export default { props: ['commentableId', 'commentableType', 'currentUserId'], components: { 'comment': Comment, 'markdown-textarea': MarkdownTextarea },
- props
- 親コンポーネントからデータを受け取るところ
data
data: () => { return { currentUser: {}, comments: [], description: '', tab: 'comment' } },
- そういえば、props, dataの違いがわからないので、調べてみた
import
import Comment from './comment.vue' import MarkdownTextarea from './markdown-textarea.vue' import MarkdownIt from 'markdown-it' import MarkdownItEmoji from 'markdown-it-emoji' import MarkdownItMention from './packs/markdown-it-mention'
- 疑問:なぜ
comments.vue
の中で、自分自身をCommentにimportしている?comments.vue
とcomment.vue
が別ファイルでした(この時点では気づかず、だいぶ後にになって気づく・・・)
created
created: function() { fetch(`/api/users/${this.currentUserId}.json`, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest', }, credentials: 'same-origin', redirect: 'manual' }) .then(response => { return response.json() }) .then(json => { for(var key in json){ this.$set(this.currentUser, key, json[key]) } }) .catch(error => { console.warn('Failed to parsing', error) }) fetch(`/api/comments.json?commentable_type=${this.commentableType}&commentable_id=${this.commentableId}`, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest', }, credentials: 'same-origin', redirect: 'manual' }) .then(response => { return response.json() }) .then(json => { json.forEach(c => { this.comments.push(c) }); }) .catch(error => { console.warn('Failed to parsing', error) }) },
fetch
- MDN/Fetch API
- request, responseするためのAPI
- MDN/Fetch を使う
従来、このような機能は XMLHttpRequest を使用して実現されてきました。 Fetch はそれのより良い代替となるもので、サービスワーカーのような他の技術から簡単に利用することができます。 Fetch は CORS や HTTP 拡張のような HTTP に関連する概念をまとめて定義する場所でもあります。
- つまり、XMLHttpRequestとかAJaxは違う?🤔
- fetch() から返される Promise は レスポンスが HTTP 404 や 500 を返して HTTP エラーステータスの場合でも拒否されません。代わりに (ok ステータスが false にセットされて) 正常に解決し、拒否されるのはネットワークのエラーや、何かがリクエストの完了を妨げた場合のみです。
- MDN/Fetch API
then
- 初めてPromiseの実装コードを見た(MDNで軽く読んだことしかなかった)
- 前から順番に流れていくので便利
- Promiseを使う
/api/users/${this.currentUserId}.json
から情報を取得している。/api/users/:id
は名前の通り、api
- credentials: 'same-origin'とは?
- Request.credentials
same-origin:URL が呼び出し元のスクリプトと同一オリジンだった場合のみ、クッキーを送信する。
- 流れ
currentUserId
でユーザー情報取得commentable_id
、commentable_type
でコメントの親クラスについているコメントの情報を取得している
createdで呼び出しているAPIで受信する内容
http://localhost:3000/api/users/459775584.json
の結果
{ id: 459775584, login_name: "komagata", url: "http://localhost:3000/users/459775584", role: "admin", avatar_url: "http://localhost:3000/rails/active_storage/disk/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDRG9JYTJWNVNTSWRibEJ0UlhsV2VVSkdObXBRUjBRemFsRjZaWE5VVjBaakJqb0dSVlE2RUdScGMzQnZjMmwwYVc5dVNTSkRhVzVzYVc1bE95Qm1hV3hsYm1GdFpUMGlhMjl0WVdkaGRHRXVhbkJuSWpzZ1ptbHNaVzVoYldVcVBWVlVSaTA0SnlkcmIyMWhaMkYwWVM1cWNHY0dPd1pHT2hGamIyNTBaVzUwWDNSNWNHVkpJZzVwYldGblpTOXdibWNHT3daVSIsImV4cCI6IjIwMTktMTAtMzFUMTM6Mjk6NTQuNzc2WiIsInB1ciI6ImJsb2Jfa2V5In19--def46e9c9ea6a11afef8c3c0234562fbbf2d54f1/komagata.jpg?content_type=image%2Fpng&disposition=inline%3B+filename%3D%22komagata.jpg%22%3B+filename%2A%3DUTF-8%27%27komagata.jpg" }
http://localhost:3000/api/comments.json?commentable_type=Report&commentable_id=1017786020
の結果- コメントだけじゃなくて、reactionのデータも持っていた!発見
[ { id: 1064126206, description: "te", created_at: "2019-10-31T15:25:28.271+09:00", updated_at: "2019-10-31T15:25:28.271+09:00", commentable_type: "Report", commentable_id: 1017786020, commentable: { created_at: "2019-10-31T14:56:12.076+09:00" }, user: { id: 459775584, login_name: "komagata", url: "http://localhost:3000/users/459775584", role: "admin", avatar_url: "http://localhost:3000/rails/active_storage/disk/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDRG9JYTJWNVNTSWRibEJ0UlhsV2VVSkdObXBRUjBRemFsRjZaWE5VVjBaakJqb0dSVlE2RUdScGMzQnZjMmwwYVc5dVNTSkRhVzVzYVc1bE95Qm1hV3hsYm1GdFpUMGlhMjl0WVdkaGRHRXVhbkJuSWpzZ1ptbHNaVzVoYldVcVBWVlVSaTA0SnlkcmIyMWhaMkYwWVM1cWNHY0dPd1pHT2hGamIyNTBaVzUwWDNSNWNHVkpJZzVwYldGblpTOXdibWNHT3daVSIsImV4cCI6IjIwMTktMTAtMzFUMTM6MzM6MjAuODc0WiIsInB1ciI6ImJsb2Jfa2V5In19--894cdfc20c4bedc6ba2c9f3276eef453b212bccb/komagata.jpg?content_type=image%2Fpng&disposition=inline%3B+filename%3D%22komagata.jpg%22%3B+filename%2A%3DUTF-8%27%27komagata.jpg" }, reaction: [ ], reaction_count: [ { kind: "thumbsup", value: "👍", count: 0, login_names: [ ] }, // 略 { id: 1064126207, description: "a", created_at: "2019-10-31T22:27:13.414+09:00", updated_at: "2019-10-31T22:27:13.414+09:00", commentable_type: "Report", commentable_id: 1017786020, commentable: { created_at: "2019-10-31T14:56:12.076+09:00" }, user: { id: 459775584, login_name: "komagata", url: "http://localhost:3000/users/459775584", role: "admin", avatar_url: "http://localhost:3000/rails/active_storage/disk/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDRG9JYTJWNVNTSWRibEJ0UlhsV2VVSkdObXBRUjBRemFsRjZaWE5VVjBaakJqb0dSVlE2RUdScGMzQnZjMmwwYVc5dVNTSkRhVzVzYVc1bE95Qm1hV3hsYm1GdFpUMGlhMjl0WVdkaGRHRXVhbkJuSWpzZ1ptbHNaVzVoYldVcVBWVlVSaTA0SnlkcmIyMWhaMkYwWVM1cWNHY0dPd1pHT2hGamIyNTBaVzUwWDNSNWNHVkpJZzVwYldGblpTOXdibWNHT3daVSIsImV4cCI6IjIwMTktMTAtMzFUMTM6MzM6MjAuODgyWiIsInB1ciI6ImJsb2Jfa2V5In19--a45376882bdb1078327ba7d83abfc021f2ada912/komagata.jpg?content_type=image%2Fpng&disposition=inline%3B+filename%3D%22komagata.jpg%22%3B+filename%2A%3DUTF-8%27%27komagata.jpg" }, // 略
createdで呼び出しているAPIの送信コード
APIの構成の確認
- APIはjbuilderで作成されている
- Rails6.0からはRailsを新規作成したら付いてくるので馴染みがある
- Action Viewの概要/jbuilder
- コメント機能に関係がありそうなAPIの構成
/app/controllers/api/users_controller.rb
# frozen_string_literal: true class API::UsersController < API::BaseController def index users = User.select(:login_name, :first_name, :last_name) .order(updated_at: :desc) .as_json(except: :id) render json: users end def show @user = User.find(params[:id]) end end
- index, showが来ると、それぞれ
_users.json.jbuilder
,show.json.jbuilder
にリクエストを飛ばしている- 疑問:なぜindexに飛ばさない?
render json: XXX
- jsonでrenderに渡す
/api/views/api/users/_user.json.jbuilder
json.(user, :id, :login_name, :url, :role) json.avatar_url user.avatar_url
- APIが簡単に書けるjbuilder素晴らしい
- rails/jbuilderでStarを押しました✌️
- amatsuda/jbも良いという話を聞くので、今度触ってみたい
- メモ:
avatar
のように、画像の場合はurl
だけを渡す
/api/views/api/users/show.json.jbuilder
json.partial! "api/users/user", user: @user
- 疑問:
json.partial!
とは?- パーシャルが展開できる
- この場合、user毎に
/api/views/api/users/_user.json.jbuilder
を呼び出している
- 疑問:コントローラーの
users
が@user
で呼び出せているのはなぜ?どこで展開している?
You can use partials as well. The following will render the file views/comments/_comments.json.jbuilder, and set a local variable comments with all this message's comments, which you can use inside the partial.
ruby json.partial! 'comments/comments', comments: @message.comments
mounted
- Vue.js/mounted
- Vue.js/ライフサイクルダイアグラム
- mountedはVue.jsがエレメントを置き換えるにあたり、エレメントを表示するまでの最後に行う処理が書ける(ということだと思います)
- 疑問:
textareaAutoSize()
謎のメソッドが呼ばれている- importされている
markdown-textarea.vue
が怪しいimport 'textarea-autosize/dist/jquery.textarea_autosize.min'
5行目でそれらしいjQueryを呼んでいた
- importされている
methods
methods: { token () { const meta = document.querySelector('meta[name="csrf-token"]') return meta ? meta.getAttribute('content') : '' }, isActive: function(tab) { return this.tab == tab }, changeActiveTab: function(tab) { this.tab = tab }, createComment: function(event) { if (this.description.length < 1) { return null } let params = { 'comment': { 'description': this.description }, 'commentable_type': this.commentableType, 'commentable_id': this.commentableId } fetch(`/api/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-Token': this.token() }, credentials: 'same-origin', redirect: 'manual', body: JSON.stringify(params) }) .then(response => { return response.json() }) .then(json=> { this.comments.push(json); this.description = ''; }) .catch(error => { console.warn('Failed to parsing', error) }) }, deleteComment: function(id) { fetch(`/api/comments/${id}.json`, { method: 'DELETE', headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-Token': this.token() }, credentials: 'same-origin', redirect: 'manual' }) .then(response => { this.comments.forEach((comment, i) => { if (comment.id == id) { this.comments.splice(i, 1); } }); }) .catch(error => { console.warn('Failed to parsing', error) }) } },
- Vueインスタンスのメソッド
createComment
createComment: function(event) { if (this.description.length < 1) { return null } let params = { 'comment': { 'description': this.description }, 'commentable_type': this.commentableType, 'commentable_id': this.commentableId } fetch(`/api/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-Token': this.token() }, credentials: 'same-origin', redirect: 'manual', body: JSON.stringify(params) }) .then(response => { return response.json() }) .then(json=> { this.comments.push(json); this.description = ''; }) .catch(error => { console.warn('Failed to parsing', error) }) },
- プログラムの内容
null
なら何もしない- paramsでデータ受け取り
- fetchで
comments
のレスポンス取得 - レスポンスjson化
- commentsにデータ保管
- エラーが発生したらキャッチし、consoleに出力
computed
computed: { markdownDescription: function() { const md = new MarkdownIt({ html: true, breaks: true, linkify: true, langPrefix: 'language-' }); md.use(MarkdownItEmoji).use(MarkdownItMention) return md.render(this.description); }, validation: function() { return this.description.length > 0 } }
- Vue.js/computed
- Vueインスタンスの算出プロパティ
- 型
{ [key: string]: Function | { get: Function, set: Function } }
markdownDescription
- 疑問:markdownが使えるようにしていそうだけど、どうやって?
- 絵文字を使えるようにしている
templete
<template lang="pug"> .thread-comments-container h2.thread-comments-container__title コメント .thread-comments comment(v-for="(comment, index) in comments" :key="comment.id" :comment="comment", :currentUser="currentUser", @delete="deleteComment") .thread-comment-form .thread-comment__author img.thread-comment__author-icon(:src="currentUser.avatar_url") .thread-comment-form__form.a-card .thread-comment-form__tabs.js-tabs .thread-comment-form__tab.js-tabs__tab(:class="{'is-active': isActive('comment')}" @click="changeActiveTab('comment')") | コメント .thread-comment-form__tab.js-tabs__tab(:class="{'is-active': isActive('preview')}" @click="changeActiveTab('preview')") | プレビュー .thread-comment-form__markdown-parent.js-markdown-parent .thread-comment-form__markdown.js-tabs__content(:class="{'is-active': isActive('comment')}") markdown-textarea(v-model="description" id="js-new-comment" class="a-text-input js-warning-form thread-comment-form__textarea js-markdown" name="comment[description]") .thread-comment-form__markdown.js-tabs__content(:class="{'is-active': isActive('preview')}") .js-preview.is-long-text.thread-comment-form__preview(v-html="markdownDescription") .thread-comment-form__actions .thread-comment-form__action button#js-shortcut-post-comment.a-button.is-lg.is-warning.is-block(@click="createComment" :disabled="!validation") | コメントする </template>
投稿されたコメントの表示
comment(v-for="(comment, index) in comments" :key="comment.id" :comment="comment", :currentUser="currentUser", @delete="deleteComment")
- 投稿されたコメントの表示は
comment()
内での処理が行なっています comments
自体のデータは、data: () => { comments: [] }
で定義され、created
のfetch
で/api/comments.json
からの返りを受け取ったものをthis.comments.push(c)
しています
- 実際の表示自体は
comment.vue
が行なっています
コメント新規投稿用フォーム
.thread-comment-form .thread-comment__author img.thread-comment__author-icon(:src="currentUser.avatar_url") .thread-comment-form__form.a-card .thread-comment-form__tabs.js-tabs .thread-comment-form__tab.js-tabs__tab(:class="{'is-active': isActive('comment')}" @click="changeActiveTab('comment')") | コメント .thread-comment-form__tab.js-tabs__tab(:class="{'is-active': isActive('preview')}" @click="changeActiveTab('preview')") | プレビュー .thread-comment-form__markdown-parent.js-markdown-parent .thread-comment-form__markdown.js-tabs__content(:class="{'is-active': isActive('comment')}") markdown-textarea(v-model="description" id="js-new-comment" class="a-text-input js-warning-form thread-comment-form__textarea js-markdown" name="comment[description]") .thread-comment-form__markdown.js-tabs__content(:class="{'is-active': isActive('preview')}") .js-preview.is-long-text.thread-comment-form__preview(v-html="markdownDescription") .thread-comment-form__actions .thread-comment-form__action button#js-shortcut-post-comment.a-button.is-lg.is-warning.is-block(@click="createComment" :disabled="!validation") | コメントする
.thread-comment-form__tab.js-tabs__tab
.thread-comment-form__tabs.js-tabs .thread-comment-form__tab.js-tabs__tab(:class="{'is-active': isActive('comment')}" @click="changeActiveTab('comment')") | コメント .thread-comment-form__tab.js-tabs__tab(:class="{'is-active': isActive('preview')}" @click="changeActiveTab('preview')") | プレビュー
- 「コメント」と「プレビュー」の表示を制御しています
- アクティブな方に
is-active
を付与して表示を制御しているみたいです
- アクティブな方に
.thread-comment-form__markdown-parent.js-markdown-parent
.thread-comment-form__markdown-parent.js-markdown-parent .thread-comment-form__markdown.js-tabs__content(:class="{'is-active': isActive('comment')}") markdown-textarea(v-model="description" id="js-new-comment" class="a-text-input js-warning-form thread-comment-form__textarea js-markdown" name="comment[description]") .thread-comment-form__markdown.js-tabs__content(:class="{'is-active': isActive('preview')}") .js-preview.is-long-text.thread-comment-form__preview(v-html="markdownDescription")
- 「コメント」「プレビュー」の表示を行なっています
- 両方ともデータは常に持っていて、アクティブな方に
is-active
をつけて、表示しているみたいです
.thread-comment-form__actions
.thread-comment-form__actions
.thread-comment-form__action
button#js-shortcut-post-comment.a-button.is-lg.is-warning.is-block(@click="createComment" :disabled="!validation")
| コメントする
- コメント投稿機能です
#js-shortcut-post-comment
- もしかしたら、
comment.vue
をimportしていたのはこのためかも?
- もしかしたら、
:disabled="!validation"
で空白だったら、computed
のvalidation: cunftion() {}
が呼び出されて、投稿ボタンがdisabled
される
computed: { // 略 validation: function() { return this.description.length > 0 } }
/app/javascript/comment.vue
import
import Reaction from './reaction.vue' import MarkdownTextarea from './markdown-textarea.vue' import MarkdownIt from 'markdown-it' import MarkdownItEmoji from 'markdown-it-emoji' import MarkdownItMention from './packs/markdown-it-mention' import Tribute from 'tributejs' import TextareaAutocomplteEmoji from 'classes/textarea-autocomplte-emoji' import TextareaAutocomplteMention from 'classes/textarea-autocomplte-mention' import moment from 'moment'
reaction.vue
とmarkdown-textarea.vue
はコメント機能への追加機能なので、今回は追わないことにします
props
props: ['comment', 'currentUser', 'availableEmojis'], components: { 'reaction': Reaction, 'markdown-textarea': MarkdownTextarea },
props
- コメント、ユーザー、絵文字を保持している
components
data
data: () => { return { description: '', editing: false, tab: 'comment' } },
templete
<template lang="pug"> .thread-comment .thread-comment__author a.thread-comment__author-link(:href="comment.user.url" itempro="url") img.thread-comment__author-icon(:src="comment.user.avatar_url" v-bind:class="userRole") .thread-comment__body.a-card(v-if="!editing") header.thread-comment__body-header h2.thread-comment__title a.thread-comment__title-link(:href="comment.user.url" itempro="url") | {{ comment.user.login_name }} time.thread-comment__created-at(:datetime="commentableCreatedAt" pubdate="pubdate") | {{ updatedAt }} .thread-comment__description.js-target-blank.is-long-text(v-html="markdownDescription") reaction( v-bind:reactionable="comment", v-bind:currentUser="currentUser") footer.card-footer(v-if="comment.user.id == currentUser.id") .card-footer-actions ul.card-footer-actions__items li.card-footer-actions__item button.card-footer-actions__action.a-button.is-md.is-primary.is-block(@click="editComment") i.fas.fa-pen | 編集 li.card-footer-actions__item button.card-footer-actions__action.a-button.is-md.is-danger.is-block(@click="deleteComment") i.fas.fa-trash-alt | 削除 .thread-comment-form__form.a-card(v-show="editing") .thread-comment-form__tabs.js-tabs .thread-comment-form__tab.js-tabs__tab(v-bind:class="{'is-active': isActive('comment')}" @click="changeActiveTab('comment')") | コメント .thread-comment-form__tab.js-tabs__tab(v-bind:class="{'is-active': isActive('preview')}" @click="changeActiveTab('preview')") | プレビュー .thread-comment-form__markdown-parent.js-markdown-parent .thread-comment-form__markdown.js-tabs__content(v-bind:class="{'is-active': isActive('comment')}") markdown-textarea(v-model="description" :class="classCommentId" class="a-text-input js-warning-form thread-comment-form__textarea js-comment-markdown" name="comment[description]") .thread-comment-form__markdown.js-tabs__content(v-bind:class="{'is-active': isActive('preview')}") .js-preview.is-long-text.thread-comment-form__preview(v-html="markdownDescription") .thread-comment-form__actions .thread-comment-form__action button.a-button.is-md.is-warning.is-block(@click="updateComment" v-bind:disabled="!validation") | 保存する .thread-comment-form__action button.a-button.is-md.is-secondary.is-block(@click="cancel") | キャンセル </template>
.thread-comment__author
.thread-comment__author a.thread-comment__author-link(:href="comment.user.url" itempro="url") img.thread-comment__author-icon(:src="comment.user.avatar_url" v-bind:class="userRole")
- 投稿者アイコン(avatar)の表示
.thread-comment__body.a-card(v-if="!editing")
comments.vue
とほぼ変わらず、コメント表示機能
.thread-comment__body.a-card(v-if="!editing") header.thread-comment__body-header h2.thread-comment__title a.thread-comment__title-link(:href="comment.user.url" itempro="url") | {{ comment.user.login_name }} time.thread-comment__created-at(:datetime="commentableCreatedAt" pubdate="pubdate") | {{ updatedAt }} .thread-comment__description.js-target-blank.is-long-text(v-html="markdownDescription") reaction( v-bind:reactionable="comment", v-bind:currentUser="currentUser") footer.card-footer(v-if="comment.user.id == currentUser.id") .card-footer-actions ul.card-footer-actions__items li.card-footer-actions__item button.card-footer-actions__action.a-button.is-md.is-primary.is-block(@click="editComment") i.fas.fa-pen | 編集 li.card-footer-actions__item button.card-footer-actions__action.a-button.is-md.is-danger.is-block(@click="deleteComment") i.fas.fa-trash-alt | 削除
v-bind
とは?- Vue.js/v-bind
1つ以上の属性またはコンポーネントのプロパティと式を動的に束縛します。
- Vue.jsで属性かv-bindかの応用
- 公式の文章では正直意味がわからなかったが、
templete
で変数や式を使うことで、動的な文字列を作成できるということらしい
- 公式の文章では正直意味がわからなかったが、
- Vue.js/v-bind
.thread-comment-form__form.a-card(v-show="editing")
comments.vue
とほぼ変わらず、コメント投稿機能
li.card-footer-actions__item
- 「編集ボタン」、「削除ボタン」を提供しています
- ボタンをクリックすると、
editComment
,deleteComment
が呼び出されます
methods
updateComment
updateComment: function() { if (this.description.length < 1) { return null } let params = { 'comment': { 'description': this.description } } fetch(`/api/comments/${this.comment.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json; charset=utf-8', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-Token': this.token() }, credentials: 'same-origin', redirect: 'manual', body: JSON.stringify(params) }) .then(response => { this.editing = false; }) .catch(error => { console.warn('Failed to parsing', error) }) },
null
の時は何もしないdescription
をcomment
の内容として保管
その他
- あまり変わらなさそうなので、軽くだけ目を通した
おわりに
思ったこと
結構読むのに時間がかかりました。
途中、コードを書き始めながら実装しなかったので、issue消化がこの記事を投稿してからになります・・・(これは明確な失敗だ・・・)
アウトプットはしていきたいですが、進捗も出したいので、今後も工夫して上手く両立させていきたいです。