もふもふ技術部

IT技術系mofmofメディア

Rails + Vue.jsでチャットUIっぽい吹き出しをコンポーネントにする

前回は「Rails + Vue.js + Ajaxでチャット機能っぽいサンプルを実装する」というのをやってみましたが、これに加えて吹き出しのようなチャットUIを実装し、コンポーネント化してみます。

このあたりが使いこなせるようになると、いままでバラバラのファイルに書かれていたjs,html,cssを小さい単一のコンポーネントに分割して管理できるのでVue.jsのありがたみがヒシヒシと感じられるようになってきます。

最終的にこんな感じの画面になります。

チャットUI

実装

まずは、views/messages/index.html.erbtableタグで実装していたメッセージ部分を、ulタグに変更し、li部分をballoonとしてコンポーネントに置き換えます。

<script>
    rails = window.rails = {};
    rails.messages = <%= raw @messages.to_json %>;
</script>

<p id="notice"><%= notice %></p>

<h1>メッセージ</h1>

<ul id="message-table">
  <balloon v-for="m in messages" :username="m.username" :body="m.body"></balloon>
</ul>
<br>


<%= form_for @message, html: { id: 'message-form' } do |f| %>
  <div class="field">
    <%= f.label :username %>
    <%= @message.username %>
    <%= f.hidden_field :username, 'v-model': 'message.username' %>
  </div>
  <div class="field">
    <%= f.text_area :body, cols: 100, rows: 10, 'v-model': 'message.body' %>
  </div>
  <button type="button" v-on:click="addNewMessage">送信</button><br />
  <span>username: {{ message.username }}</span><br />
  <span>body: {{ message.body }}</span><br />
<% end %>

<%= javascript_pack_tag 'messages/index' %>

app/javascript/packs/messages/index.jsコンポーネントをインポートして、Vueのインスタンス作成時にcomponentsを指定します。

import Vue from 'vue/dist/vue.esm.js'
import App from '../app.vue'
import axios from 'axios';
import Balloon from '../components/balloon.vue'


document.addEventListener("DOMContentLoaded", () => {
  new Vue({
    el: '#message-table',
    components: {
      Balloon,
    },
    data: {
      messages: rails.messages
    },
  })

  new Vue({
    el: '#message-form',
    data: {
      message: {
        username: document.querySelector("[v-model='message.username']").value,
        body: document.querySelector("[v-model='message.body']").value,
      },
      messages: rails.messages
    },
    methods: {
      addNewMessage: function() {
        console.log('addNewMessage')
        axios.post('/messages.json', {
          message: this.message
        })
        .then(res => {
          // console.log(res.data)
          this.messages.push({
            username: this.message.username,
            body: this.message.body
          })
        })

      },
    }
  })

})

app/javascript/packs/components/balloon.vueに実際のコンポーネントを実装をします。

<template>
  <li>
    <p class="username">
      {{ username }}
    </p>
    <div class="balloon1-left">
      <p>
        {{ body }}
      </p>
    </div>
  </li>
</template>

<script>
export default {
    props: ['username', 'body']
}
</script>

<style scoped>
li {
  list-style: none;
}

.username {
  float: left;
}

.balloon1-left {
  position: relative;
  margin: 1.5em 0 1.5em 15px;
  padding: 7px 10px;
  min-width: 120px;
  max-width: 500px;
  color: #555;
  font-size: 16px;
  background: #e0edff;
  margin-left: 70px;
}

.balloon1-left:before{
  content: "";
  position: absolute;
  top: 50%;
  left: -30px;
  margin-top: -15px;
  border: 15px solid transparent;
  border-right: 15px solid #e0edff;
}

.balloon1-left p {
  margin: 0;
  padding: 0;
}
</style>

Railsでは単一ファイルコンポーネントをしたいときにcssだけ別ファイルで生成されてしまうらしいので、設定を追加する。

config/webpack/environment.js

const { environment } = require('@rails/webpacker')

module.exports = environment
environment.loaders.get('vue').options.extractCSS = false

はい、これで出来上がり!

なんと簡単。素敵ですね、単一ファイルコンポーネント