s4na's blog

s4naのテックブログ

ファーストペンギン

フィヨルドブートキャンプの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-commentsdata-commentable-id, data-commentable-type, data-current-user-idを渡す
    • 疑問: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

/app/javascript/comments.vue

  • 「なんかたくさんあるけどなんだっけ?」と一瞬思ったけど、Vue.jsにはライフサイクルがあったことを思い出した
  • 変数とdate: () => {}の違いってなんだっけ?課題で、dateに書くか書かないかでとても違いが出たことしか覚えていない・・・
    • Vue.js入門を読んだら、date: () => {}はdataプロパティと書いてあった
      • UIの状態を置く場所
  • script以外に、scriptと一緒にtempleteが書かれた.vueファイルを読み書きしたことがないことに気づく😲‼️
    • Vue.js入門を再度読み返した

props

export default {
  props: ['commentableId', 'commentableType', 'currentUserId'],
  components: {
    'comment': Comment,
    'markdown-textarea': MarkdownTextarea
  },

data

  data: () => {
    return {
      currentUser: {},
      comments: [],
      description: '',
      tab: 'comment'
    }
  },
  • そういえば、props, dataの違いがわからないので、調べてみた
    • Vue.js/props
      • コンポーネントからデータを受け取るためにエクスポートされた属性のリスト/ハッシュです。

      • 疑問:親コンポーネントから一方的にデータを受け取るだけだから、データに変化がない?
    • Vue.js/data
      • Vue インスタンスのためのデータオブジェクトです。

      • Vue.js は再帰的にインスタンスのプロパティを getter/setter に変換し、”リアクティブ” にします。

      • オブジェクトはプレーンなものでなければなりません。 ブラウザの API オブジェクトのようなネイティブオブジェクトやプロトタイププロパティは無視されます。経験則としては、データはデータになるべきです。

      • 自身で状態を持つ振舞いによってオブジェクトを監視することは推奨されません。

      • Vue.js入門にあったように、UIの状態を置く場所

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.vuecomment.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 の仕様は jQuery.ajax() とは主に二つの点で異なっています。

        • fetch() から返される Promise は レスポンスが HTTP 404 や 500 を返して HTTP エラーステータスの場合でも拒否されません。代わりに (ok ステータスが false にセットされて) 正常に解決し、拒否されるのはネットワークのエラーや、何かがリクエストの完了を妨げた場合のみです。
        • 既定では、 fetch はサーバーとの間で cookies を送受信しないため、サイトがユーザーセッションの維持に頼っている場合は未認証のリクエストになります (cookie を送るには、認証情報の init オプションを設定しておく必要があります)。2017年8月25日に、既定の認証情報のポリシーが same-origin に変更になり、 Firefox は 61.0b13 から変更しました。
  • then
    • 初めてPromiseの実装コードを見た(MDNで軽く読んだことしかなかった)
    • 前から順番に流れていくので便利
    • Promiseを使う
  • /api/users/${this.currentUserId}.jsonから情報を取得している。
    • /api/users/:idは名前の通り、api
  • credentials: 'same-origin'とは?
  • 流れ
    • currentUserIdでユーザー情報取得
    • commentable_idcommentable_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の構成の確認
  • APIjbuilderで作成されている
  • コメント機能に関係がありそうなAPIの構成
    • /app
      • /controllers
        • /api
          • comments_controller.rb
          • users_controller.rb
      • /views
        • /api
          • /comments
            • _comment.json.jbuilder
            • create.json.jbuilder
            • index.json.jbuilder
          • /users
            • _user.json.jbuilder
            • show.json.jbuilder
/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素晴らしい
  • amatsuda/jbも良いという話を聞くので、今度触ってみたい
    • たしか「jbuilderと違い、DLSではなく生のRubyで書けるのが良い」みたいな話だったと記憶しています・・・
  • メモ: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

rails/jbuilder https://github.com/rails/jbuilder

mounted

  • Vue.js/mounted
  • 疑問:textareaAutoSize()謎のメソッドが呼ばれている
    • importされているmarkdown-textarea.vueが怪しい
      • import 'textarea-autosize/dist/jquery.textarea_autosize.min'5行目でそれらしいjQueryを呼んでいた
        • javierjulio/textarea-autosizeのUsageにtextareaAutoSize()について記述があったのでビンゴ
        • 概要
          • This is a jQuery plugin for vertically adjusting a textarea based on user input and controlling any presentation in CSS.

          • Google翻訳:ユーザー入力に基づいてテキストエリアを垂直方向に調整し、CSSのプレゼンテーションを制御するためのjQueryプラグインです。

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)
        })
    }
  },

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>
  • templeteタグの中はslim(RubyDSL)。
  • Vue.jsが処理した後、処理後の文字列をRailsに渡していて、slimとして処理されてます

投稿されたコメントの表示

      comment(v-for="(comment, index) in comments"
        :key="comment.id"
        :comment="comment",
        :currentUser="currentUser",
        @delete="deleteComment")
  • 投稿されたコメントの表示はcomment()内での処理が行なっています
  • comments自体のデータは、
    • data: () => { comments: [] }で定義され、
    • createdfetch/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"で空白だったら、computedvalidation: cunftion() {}が呼び出されて、投稿ボタンがdisabledされる

  computed: {
    // 略

    validation: function() {
      return this.description.length > 0
    }
  }

/app/javascript/comment.vue

/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.vuemarkdown-textarea.vueはコメント機能への追加機能なので、今回は追わないことにします

props

  props: ['comment', 'currentUser', 'availableEmojis'],
  components: {
    'reaction': Reaction,
    'markdown-textarea': MarkdownTextarea
  },
  • props
    • コメント、ユーザー、絵文字を保持している
  • components
    • メモ:Vueインスタンス?もpropsに入れることがあるのか・・・知らなかった。
      • もし、変化するVueインスタンス(存在するかはわからない)があったら、dataに入るのかも?🤔

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とは?

  • .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の時は何もしない
  • descriptioncommentの内容として保管

その他

  • あまり変わらなさそうなので、軽くだけ目を通した

おわりに

思ったこと

結構読むのに時間がかかりました。
途中、コードを書き始めながら実装しなかったので、issue消化がこの記事を投稿してからになります・・・(これは明確な失敗だ・・・)

アウトプットはしていきたいですが、進捗も出したいので、今後も工夫して上手く両立させていきたいです。