【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.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!'
【Vue.js】axiosを利用してAPIを使用する
- 実装したいこと
- 使用バージョン
- 実装の流れ
- そもそも、APIって?
- Tasks controller
- Routing
- CSRF対策無効化
- Vue.jsでAPIを使用する
- axiosインストール
- axios設定
- Vueファイル設定
- curl コマンド
実装したいこと
タスク管理アプリを作成しています。
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を作成できると。
今回はこっち。恥ずかしながらRailsでAPIを生成できるのを知らなかった
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] "/api/tasks"</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 tourl
unlessurl
is absolute. It can be convenient to setbaseURL
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
は不完全なurl
にurl
を補完してくれる機能。
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インスタンスで、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_controller
のindex
アクションへ飛びます。
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>
ルーターが使用可能になっているアプリケーションでユーザーのナビゲーションを有効にするためのコンポーネントです。
<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の検証画面でも正常にみえるのに…
= 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株式会社
【Rails】雑多なメモ
all_day
昨日作った記事だけ取得するscope
を作りたかったとき、Date.yesterday
やTime.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
というように、丸一日分を取ってきてくれる。
参考
Railsで特定の日の新規ユーザ数などを取るクエリを作る - Qiita
DateAndTime::Calculations
特定の条件を満たすレコード数を取得するには?
count
メソッドを使う。
癖でlength
を使ってしまうけど、count
だと単純な数だけじゃなくて、要素を指定した上の数も取得できる。
【Rails】世界で一番分りやすく詳しいcountメソッドの使い方 | Pikawaka - ピカ1わかりやすいプログラミング用語サイト
Action Mailer
Action Mailer関連で、簡単なはずなのにちょっと詰まったところ
送信メールのプレビューを見るには
送信したメールのプレビューは、letter_opener_web
を使えばいいのですが、送信するメールのプレビューを見たいときは?
study-diary.hatenadiary.jp
メイラーを生成するときに、自動生成される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
へアクセスするとプレビュー画面が確認できる。
参考
『パーフェクト Ruby on Rails』P235
メール送信の挙動を確かめるには
cronjob
で、毎日AM9時にメールを送信すると設定していたのですが、今すぐにメールを送信してみたいとき・・・
deliver_now
を呼び出す。
コンソールで、ArticleMailer.(メソッド名).deliver_now
を実行する。
letter_opner_web
で確認するとメールが送られている。
参考
【Rails】Active Storageを使って画像を複数枚アップロード&削除する
実装したいこと
以前、ActiveStorageを使って画像をアップロードする機能を実装しました。
今回は、画像を複数枚アップロード&削除機能を実装したいです。
参照する記事が少なく手こずってしまいました。。特に削除機能は、情報が少なくて苦労しました。
複数枚の画像を一度にアップロードする
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
だけだと、機能しません。
アップロードした画像を削除するには
これが悩んでしまいました。。この記事を参考にして、新しくアクションを作って…と実装しようとしたのですが、うまくルーティングが組めず撃沈しました。
新しく画像削除用のコントローラーを生成してシンプルに実装したらようやくうまくできました。
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' #追加