Programming Journal

学習したことの整理用です。

【Vue.js】モーダルウィンドウ

実装したいこと

タスク一覧があり、それぞれのタスクタイトルをクリックすると、モーダルウィンドウでタスクの詳細が表示される。

データの流れ

細かいコード内容は省き、大まかな流れだけ追います。
データの流れの説明に必要な部分だけ切り取っているので、インデントぐちゃぐちゃです…

親側

<template>
<!-- 必要部分だけ抜粋 -->
<!-- ①各タスク名をクリックすると"handleOpenTaskDetailModal(task)"発火 -->
        <div
          v-for="task in tasks"
          :key="task.id"
          @click="handleOpenTaskDetailModal(task)"   
          >
            <span>{{ task.title }}</span>
        </div>
<!-- ③親から子に属性としてデータを渡す -->
<!-- isVisibleTaskDetailModalをtrue/falseに切り替えることで、子コンポーネントの表示・非表示をv-ifで実現する。-->
    <transition name="fade"> <!-- ⑨v-on(@に省略可)で子から$emitで渡された'close-modal'を受け取り、メソッド発火 -->
      <TaskDetailModal :task="taskDetail" v-if="isVisibleTaskDetailModal" @close-modal="handleCloseTaskDetailModal" /> 
    </transition>

</template>

<script>
import TaskDetailModal from '../../components/TaskDetailModal.vue' //2階層上なので、../../となる

export default {
  name: "TaskIndex",
  components: {
    TaskDetailModal
  },
  data() { //初期値を設定する(設定しないと警告がでる)
    return {
      tasks: [],
      taskDetail: {},
      isVisibleTaskDetailModal: false
    }
  },

  methods: {
// ②各タスククリックで発火される
    handleOpenTaskDetailModal(task) { 
      this.isVisibleTaskDetailModal = true; // trueにすることで表示
      this.taskDetail = task; // ループして引数で受けとったtaskを`taskDetail`に入れる※子側で使う
    },
// ⑩子から$emitで渡された'close-modal'により発火するメソッド
    handleCloseTaskDetailModal() {
      this.isVisibleTaskDetailModal = false; //非表示にするためにfalseにする
      this.taskDedail= {};
    }
  }
}
</script>

子側

templateはBootstrapからコピペして少々修正しただけです。
親から属性で渡されたデータをpropsで受け取ります。
このとき、受け取るデータ型を指定します。(指定しないと警告がでる)

<template>
<!-- 必要部分だけ抜粋 -->
<!-- ⑤親から受け取ったデータtask, descriptionを表示 -->
          <h5 class="modal-title">{{ task.title }}</h5> 

          <div class="modal-body" v-if="task.description">
            <p>{{ task.description }}</p>
          </div>

          <div class="modal-footer"> <!-- ⑥閉じるボタンをクリックすると、"handleCloseModal"メソッドが発火する -->
            <button @click="handleCloseModal" class="btn btn-secondary" data-dismiss="modal">閉じる</button>
          </div>

</template>

<script>
export default {
  name: 'TaskDetailModal',
  props: {  // ④親からデータ受け取り
    task: {
      title: {
        type: String, //データ型指定
        required: true
      },
      description: {
        type: String,
        required: true
      }
    }
  },
  methods: {
    handleCloseModal() {  // ⑦閉じるボタンクリックにより発火する。
      this.$emit('close-modal') // ⑧$emitで'close-modal'を親側に渡す。
    }
  }
}
</script>

<style scoped>
  .modal{
    display: block;
  }
</style>

データが行ったりきたりして混乱するので、自分で図を書いてみるとわかりやすかったです。

参考

【Vue.js】動的なモーダルウインドウの作り方を解説

モーダルウィンドウ | 基礎から学ぶ Vue.js

Modal - Bootstrap 4.2 - 日本語リファレンス

【Vue.js】エラーメモ

同じミスを数回繰り返してしまったのでメモ

f:id:Study-Diary:20201107145456p:plain
エラー画面

インスタンスで定義されていないのに、レンダーで参照されてるよ」とエラー

vue.runtime.esm.js:638
 [Vue warn]: Property or method "modalTask" is not defined on the instance but referenced during render. 
Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. 
See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.

丁寧に公式リンクが示されてるので確認。

Since Vue doesn’t allow dynamically adding root-level reactive properties, you have to initialize Vue instances by declaring all root-level reactive data properties upfront, even with an empty value:

If you don’t declare message in the data option, Vue will warn you that the render function is trying to access a property that doesn’t exist.

初期化してないのが原因だった。 空の値を宣言すること。

var vm = new Vue({
  data: {
    // declare message with an empty value
    message: ''
  },
  template: '<div>{{ message }}</div>'
})
// set `message` later
vm.message = 'Hello!'

Reactivity in Depth — Vue.js

【Vue.js】axiosを利用してAPIを使用する

実装したいこと

タスク管理アプリを作成しています。
APIからタスクのデータを取得して、表示させたいです。

基本的に公式の流れに沿って進めていきます。
axios を利用した API の使用 — Vue.js

また、ここまでは実装済です。
study-diary.hatenadiary.jp

使用バージョン

実装の流れ

大きな流れは、「タスク一覧を取得するAPIを生成→Vue.jsで取得」です。
Task modelは作成済です。

API作成

  • Tasks controllerを作成
  • routing設定
  • CSRF無効化

APIの呼び出し

  • axiosインストール
  • Vueファイルの設定

そもそも、APIって?

API is the acronym for Application Programming Interface, which is a software intermediary that allows two applications to talk to each other. Each time you use an app like Facebook, send an instant message, or check the weather on your phone, you’re using an API.
What is an API? (Application Programming Interface) | MuleSoft

例えば、Expediaみたいな航空券予約サービスはエアラインAPIを使っていて、顧客のリクエストを受けて各社の最新の情報を提供する。

RailsでもAPIモードがあるので、APIを作成できると。
今回はこっち。恥ずかしながらRailsAPIを生成できるのを知らなかった Railsで超簡単API - Qiita

Tasks controller

$ bundle exec rails g controller Api::Tasks index show create update destroy --skip-routes
class Api::TasksController < ApplicationController
  before_action :set_task, only: %i[show update destroy]

  def index
    @tasks = Task.all
    render json: @tasks
  end

  def show
    render json: @task
  end

  def create
    @task = Task.new(task_params)

    if @task.save
      render json: @task
    else
      render json: @task.errors, status: :bad_request
    end
  end

  def update
    if @task.update(task_params)
      render json: @task
    else
      render json: @task.errors, status: :bad_request
    end
  end

  def destroy
    @task.destroy!
    render json: @task
  end

  private

  def set_task
    @task = Task.find(params[:id])
  end

  def task_params
    params.require(:task).permit(:title)
  end
end

Routing

Rails.application.routes.draw do
  root to: 'home#index'

  namespace :api, format: 'json' do
    resources :tasks
  end

  get '*path', to: 'home#index'
end

namespace忘れでエラー発生

この後、curlコマンドでtaskを生成したのですが成功しませんでした。
大量のエラーメッセージが出て、VSCodeのターミナル画面ではカットされてしまったので、MacのTarminalでエラーメッセージを読むと、以下のようにrouting errorでした。
いつものように、resources :tasksとしていたのが原因でした。上記のようにname spaceが必要でした。

<header>
  <h1>Routing Error</h1>
</header>
<div id="container">
  <h2>No route matches [POST] &quot;/api/tasks&quot;</h2>

CSRF対策無効化

class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session # 追加
end

APIでは外部から叩かれるのでこの設定はオフにしておきます。

Vue.jsでAPIを使用する

ウェブアプリケーションを構築するとき、 API からデータを取得して表示することがよくあります。これを行うにはいくつかの方法があり、一般的なアプローチは Promise ベースの HTTP クライアントの axios を使うことです。 axios を利用した API の使用 — Vue.js

axiosインストール

$ yarn add axios

GitHub - axios/axios: Promise based HTTP client for the browser and node.js

APIから取得したデータを表示(基本構文)

axios
  .get('http://localhost:3000/api/tasks')
  .then(response => (this.tasks = response.data))
  .catch(error => console.log(error)) //エラー処理。console.logでエラー表示

axios設定

import axios from 'axios'

const axiosInstance = axios.create({ baseURL: 'api' });

export default axiosInstance //生成したインスタンスをexport

baseURL will be prepended to url unless url is absolute. It can be convenient to set baseURL for an instance of axios to pass relative URLs to methods of that instance. GitHub - axios/axios: Promise based HTTP client for the browser and node.js

baseURLは不完全なurlurlを補完してくれる機能。


import Vue from 'vue'
import App from '../app.vue'
import router from '../router'
// さっき作ったaxiosインスタンスをimportする
import axios from '../plugins/axios'
import 'bootstrap/dist/css/bootstrap.css'

Vue.config.productionTip = false
// importしたaxiosをprototypeに追加する
Vue.prototype.$axios = axios

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    router,
    render: h => h(App)
  }).$mount()
  document.body.appendChild(app.$el)
})

インスタンスプロパティの追加 — Vue.js

プロトタイプに追加すれば、すべてのVueインスタンスで、this.$axios.getのように簡単に使うことができる。

Vueファイル設定

<template>
  <div v-for="task in tasks" :key="task.id"> //配列に入ったtasksを1つずつ表示
    <span>{{ task.title }}</span>
  </div>
</template>

<script>
// (先述)/packs/hello_vue.jsにprototypeを設定しているので、importしなくてOK。
// (先述)各所でthis.$axios.get~~~のようにaxiosを使うことができるようになる。

export default {
  name: "TaskIndex",
  data() {
    return {
      tasks: [] //空の配列を用意する。受け取ったデータが入る
    }
  },
  created() { //インスタンス生成時にフック
    this.fetchTasks(); //fetchTasksメソッドを呼び出し
  },
  methods: {
    fetchTasks() {
      this.$axios.get("tasks") //baseURLオプションを登録しているのでこのように省略して書ける
      .then(response => (this.tasks = response.data)) //取得したデータをtasksに代入
      .catch(error => console.log(error.status));
    }
  }
}
</script>

<style scoped>
</style>

最初自分の実装では、このファイルにimport axios from 'axios'とか全部書いていたのですが、 hello_vue.js`に設定しておくことで一々呼び出さずに済みます。

curl コマンド

ターミナル上でタスクの作成〜削除まできます。

$ curl -X POST -H "Content-Type: application/json" -d '{"title":"タスクの作成"}' localhost:3000/api/tasks
$ curl -X DELETE  localhost:3000/api/tasks/(該当のID)
$ curl http://localhost:3000/api/tasks

【Vue.js】Vue Routerを活用して画面遷移する

実装したいこと

Vue Routerを使い、画面遷移機能を実装したいです。
タスク管理アプリで、トップ画面⇔タスク一覧画面と遷移できるように設定していきます。
Vue.jsは初めて使うので、データの流れが分からず実装に時間がかかってしまいました。復習していきます。

実装の流れ

使用バージョン

前提

データの流れ順に記載しています。
root /にアクセスすると、home#index home_controllerindexアクションへ飛びます。
indexアクションでは、home/index/html.erbレンダリングします。
home/index/html.erbでは、hello_vueを読み込みます。
ここからスタートしていきます!

Rails.application.routes.draw do
  root to: 'home#index'
  get '*path', to: 'home#index'
end
class HomeController < ApplicationController
  def index; end
end
<%= javascript_pack_tag 'hello_vue' %>

ルーターのインストール

インストール | Vue Router
インストール方法は直接ダウンロードかCDNです。

私はyarnでインストールしました。

$ yarn add vue-router

ルーターの設定

import Vue from 'vue';
import Router from "vue-router";

// ルート用のコンポーネントを読み込む
import TopIndex from "../pages/top/index.vue";
import TaskIndex from "../pages/task/index.vue";

// プラグインとして登録。これでRouterを使用できる。
Vue.use(Router)

// Routerインスタンスを生成
const router = new Router({
  mode: 'history', // URLにハッシュをつけない
  routes: [
    {
      path: '/',
      component: TopIndex,
      name: "TopIndex", // 名前付きルート
    },
    {
      path: '/tasks',
      component: TaskIndex,
      name: "TaskIndex",
     },
  ],
})

// 作ったRouterインスタンスをエクスポート
export default router

/にアクセスしたら、/pages/top/index.vueを読み込む、
/tasks にアクセスしたら、/pages/task/index.vueを読み込む、というように設定していきます。
importで他ファイルから読み込む設定をしています。

ルーターの初期化

上記で作成した、ルーターの設定をしたファイルrouter/index.jsを読み込み、rootとなるインスタンスを生成してマウントします。

import Vue from 'vue'
import App from '../app.vue' //App.vueを読み込む

import router from '../router'
import 'bootstrap/dist/css/bootstrap.css'

Vue.config.productionTip = false

// Vueアプリケーション起動
document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    router, // routerをVueインスタンスへ渡している
    render: h => h(App)
  }).$mount()
  document.body.appendChild(app.$el)
})

render: h => h(App)とは

以下のとおり、render関数を省略した形のよう
おまじないとして覚えておきます

Explanation for `render: h => h(App)` please · Issue #29 · vuejs-templates/webpack-simple · GitHub Vue.jsのrender: h => h(App)について調べた - Qiita

コンポーネント側の設定

<router-view>コンポーネントは与えられたパスに対してマッチしたコンポーネントを描画する関数型コンポーネントです。
<router-link>ルーターが使用可能になっているアプリケーションでユーザーのナビゲーションを有効にするためのコンポーネントです。

API リファレンス | Vue Router

<template>
  <router-view /> //ここにパスにマッチしたコンポーネントが描画される
</template>

さきほど、名前付きルートを登録したので toパラメーターに渡すことができる。

<template>
  <router-link :to="{ name: 'TaskIndex' }" class="btn btn-dark mt-5">タスク一覧へ</router-link>
</template>
<template>
  <router-link :to="{ name: 'TopIndex' }" class="btn btn-dark mt-5">戻る</router-link>
</template>

【Rails】【エラー】method: :deleteで指定しているのにshowアクションへ飛んでしまう

エラー発生

復習用に簡単なCRUD機能のついた記事投稿アプリを作成しています。
削除用のボタンを押しても削除されず、なぜかshow詳細ページへ遷移してしまう…。
method: :deleteで指定し、Cromeの検証画面でも正常にみえるのに…

f:id:Study-Diary:20201022191048p:plain
Crome検証画面

    = link_to 'delete',post_path(post),
                      class: "btn btn-danger",
                      method: :delete,
                      data: {confirm: 'Are you sure?' }
  def destroy
    @post.destroy!
    flash[:success] = 'You deleted your post!'
    redirect_to posts_path
  end
resources :posts

原因

(略)
//= require rails-ujs #これが抜けてた…

GitHub - rails/jquery-ujs: Ruby on Rails unobtrusive scripting adapter for jQuery

Unobtrusive scripting adapter for jQuery This unobtrusive scripting support file is developed for the Ruby on Rails framework, but is not strictly tied to any specific backend. You can drop this into any application to:

force confirmation dialogs for various actions; make non-GET requests from hyperlinks; make forms or hyperlinks submit data asynchronously with Ajax; have submit buttons become automatically disabled on form submit to prevent double-clicking. These features are achieved by adding certain "data" attributes to your HTML markup. In Rails, they are added by the framework's template helpers.

ハイパーリンクでGETリクエスト以外のものを生成してくれる。
軽く1時間は悩んでいたので解決してすっきり。

参考

[Rails 3] link_toのmethodが効かない!?|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

link_toで突然methodが効かなくなって困ってるあなたへ - Qiita

【Rails】雑多なメモ

all_day

昨日作った記事だけ取得するscopeを作りたかったとき、Date.yesterdayTime.current.yesterdayだと想定している挙動にならない…

all_dayを使う!

# NG
scope :yesterday_published, -> { where(published_at: Date.yesterday) }

# Good
scope :yesterday_published, -> { where(published_at: 1.day.ago.all_day) }

BETWEEN '2020-10-20 00:00:00' AND '2020-10-20 23:59:59というように、丸一日分を取ってきてくれる。

f:id:Study-Diary:20201021161834p:plain
all_day

参考

Railsで特定の日の新規ユーザ数などを取るクエリを作る - Qiita
DateAndTime::Calculations

特定の条件を満たすレコード数を取得するには?

countメソッドを使う。

f:id:Study-Diary:20201021165941p:plain
コンソール画面

癖でlengthを使ってしまうけど、countだと単純な数だけじゃなくて、要素を指定した上の数も取得できる。

【Rails】世界で一番分りやすく詳しいcountメソッドの使い方 | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

Action Mailer

Action Mailer関連で、簡単なはずなのにちょっと詰まったところ

送信メールのプレビューを見るには

送信したメールのプレビューは、letter_opener_webを使えばいいのですが、送信するメールのプレビューを見たいときは? study-diary.hatenadiary.jp

f:id:Study-Diary:20201021162726p:plain
rails g mailer UserMailer

メイラーを生成するときに、自動生成されるspec/mailers/previews/user_mailer_preview.rbを使う。

# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
  def report_summary #user.mailer.rbで作ったメソッド
    UserMailer.report_summary
  end
end

こんな感じで、メソッドを追加しておき、コメント部分のURLhttp://localhost:3000/rails/mailers/user_mailerへアクセスするとプレビュー画面が確認できる。

f:id:Study-Diary:20201021163718p:plain
preview画面

参考

『パーフェクト Ruby on Rails』P235

メール送信の挙動を確かめるには

cronjobで、毎日AM9時にメールを送信すると設定していたのですが、今すぐにメールを送信してみたいとき・・・ deliver_nowを呼び出す。
コンソールで、ArticleMailer.(メソッド名).deliver_nowを実行する。

f:id:Study-Diary:20201021164442p:plain
メールを送信するには

letter_opner_webで確認するとメールが送られている。

f:id:Study-Diary:20201021164707p:plain
letter_opener_web

参考

Action Mailer の基礎 - Railsガイド

【Rails】Active Storageを使って画像を複数枚アップロード&削除する

実装したいこと

以前、ActiveStorageを使って画像をアップロードする機能を実装しました。
今回は、画像を複数枚アップロード&削除機能を実装したいです。
参照する記事が少なく手こずってしまいました。。特に削除機能は、情報が少なくて苦労しました。

study-diary.hatenadiary.jp

複数枚の画像を一度にアップロードする

Model, Controller and Viewを設定していきます。

Model

複数枚の画像をアップロードしたいモデルにhas_many_attachedを追加します。

class Post < ApplicationRecord
  belongs_to :user
  has_one_attached :image
  has_many_attached :main_images #追加
(略)
end

Controller

main_images: []これで配列を渡しています。末尾に追加します。

(略) 
 private

def post_params
    params.require(:post).permit(:title, :body, :image, main_images: []) #最後追加
end

View

複数枚の画像が配列に入っているので、eachで回して表示します。

#入力フォーム部分(gem 'simple_form'使用)
= f.input :main_images, as: :file, hint: 'JPEG/PNG (1200x400)', input_html: { multiple: true }

#画像表示部分
- if @post.main_images.attached?
        - @post.main_images.each do |image|
          = image_tag image.variant(resize: '300x100').processed
躓いたところ

gem 'simple_form'を使っているときは、複数枚選択を設定するとき、input_html: { multiple: true }を使います。
multiple: trueだけだと、機能しません。

ruby on rails 4 - Using simple_forms simple_field_for with a multipart file uploader for Carrierwave - Stack Overflow

アップロードした画像を削除するには

これが悩んでしまいました。。この記事を参考にして、新しくアクションを作って…と実装しようとしたのですが、うまくルーティングが組めず撃沈しました。
新しく画像削除用のコントローラーを生成してシンプルに実装したらようやくうまくできました。

file upload - Rails 5.2 Active Storage purging/deleting attachments - Stack Overflow

Controller

class Post::AttachmentsController < ApplicationController

  def destroy
    image = ActiveStorage::Attachment.find(params[:id])
    image.purge
    redirect_to posts_path
  end
end

ちなみに、継承元を含めるコントローラーを生成するコマンド❯ rails g controller post::attachments

Rooting

(略)
    resource :post do
      member do
        resources :attachments, only: %i[destroy], controller: 'post/attachments'
      end
    end

View

      - if @post.main_images.attached?
        - @post.main_images.each do |image|
          = image_tag image.variant(resize: '300x100').processed
          = link_to '削除', post_attachment_path(image.id), method: :delete, class: 'btn btn-danger' #追加