【Vue.js】タスク詳細ページをモーダルで表示する
はじめに
タスクの詳細ページをモーダルで表示する。
前提
前提として下記のtaskテーブルがあります。
class Task < ApplicationRecord validates :title, presence: true validates :description, length: { maximum: 1000 } end
create_table "tasks", force: :cascade do |t| t.string "title", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.text "description" end
詳細ページのコンポーネントを作成
まずは詳細ページのコンポーネントを作成します。
$ mkdir app/javascript/pages/task $ mkdir app/javascript/pages/task/components $ touch app/javascript/pages/task/components/TaskDetailModal.vue
モーダルはbootstrapのモーダルをベースにします。
<template> <div :id="'task-detail-modal-' + task.id"> <div class="modal" @click.self="handleCloseModal">// :1 <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">{{ task.title }}</h5> <button type="button" class="close" @click="handleCloseModal">// :2 <span>×</span> </button> </div> <div class="modal-body" v-if="task.description"> <p>{{ task.description }}</p> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" @click="handleCloseModal">閉じる</button>// :2 </div> </div> </div> </div> <div class="modal-backdrop show"></div> </div> </template> <script> export default { name: "TaskDetailModal", props: { // :3 task: { title: { type: String, required: true }, description: { type: String, required: true } } }, methods: { handleCloseModal() { this.$emit('close-modal') // :4 } } } </script> <style scoped> .modal { display: block; // モーダルの表示・非表示をvueでコントールするので最初から表示状態にする } </style>
:1
<div class="modal" @click.self="handleCloseModal">
モーダル以外の部分をクリックしたときにもモーダルを非表示にします。
:2
<button type="button" class="close" @click="handleCloseModal"> <button type="button" class="btn btn-secondary" @click="handleCloseModal">閉じる</button>
モーダルのxボタンと閉じるボタンをクリックした時にモーダルを非表示にします。
:3
props: { task: { title: { type: String, // データのタイプを指定 required: true // 必須かどうか }, description: { type: String, required: true } }
props
を使って親コンポーネントからデータを受け取ります。
:4
methods: { handleCloseModal() { this.$emit('close-modal') } }
$emit
メソッドを使って親コンポーネントのイベントを発火します。
詳細ページのモーダルを呼び出す
<template> <div> <div class="d-flex"> <div class="col-4 bg-light rounded shadow m-3 p-3"> <div class="h4">TODO</div> <div v-for="task in tasks" :key="task.id" :id="'task-' + task.id" class="bg-white border shadow-sm rounded my-2 p-4" @click="handleShowTaskDetailModal(task)">// :1 <span>{{ task.title }}</span> </div> </div> </div> <div class="text-center"> <router-link :to="{ name: 'TopIndex' }" class="btn btn-dark mt-5">戻る</router-link> </div> <transition name="fade"> // モーダルの表示非表示にtrannsitionをつける。 <TaskDetailModal v-if="isVisibleTaskDetailModal" @close-modal="handleCloseTaskDetailModal" :task="taskDetail" /> // :2 </transition> </div> </template> <script> import TaskDetailModal from './components/TaskDetailModal' // 詳細ページのコンポーネントをインポート export default { components: { TaskDetailModal }, name: "TaskIndex", data() { return { tasks: [], taskDetail: {}, isVisibleTaskDetailModal: false // モーダルをデフォルトで非表示に設定 } }, created() { this.fetchTasks(); }, methods: { fetchTasks() { this.$axios.get("tasks") .then(res => this.tasks = res.data) .catch(err => console.log(err.status)); }, handleCloseTaskDetailModal() { // 子コンポーネントから呼び出しているメソッド this.isVisibleTaskDetailModal = false; // モーダルを非表示にする this.taskDetail = {}; // 引数のタスクを空にする }, handleShowTaskDetailModal(task) { this.isVisibleTaskDetailModal = true; // モーダルを表示 this.taskDetail = task; // 引数のタスクを代入 } } } </script> // :4 <style scoped> .fade-enter-active, .fade-leave-active { transition: opacity .5s; } .fade-enter, .fade-leave-to { opacity: 0; } </style>
:1
@click="handleShowTaskDetailModal(task)">
クリックされたtaskのデータを引数で渡します。
:2
<TaskDetailModal v-if="isVisibleTaskDetailModal" @close-modal="handleCloseTaskDetailModal" :task="taskDetail" />
v-if
を使ってisVisibleTaskDetailModal
がtrueのときは表示、falseのときは非表示にします。
先程の子コンポーネントから呼んでるイベントが@close-modal
。
(v-bind):task="taskDetail"
でtaskのデータを子コンポーネントに渡しています。
そして子コンポーネントがprops
で受け取っています。
:3
下記4つのクラスはenter/leave
トランジションを適用するための特殊なクラスです。
fade
の部分はname属性です。
name属性(fadeなど) + 特殊クラス(-enter-activeなど)
<style scoped> .fade-enter-active, .fade-leave-active { transition: opacity .5s; } .fade-enter, .fade-leave-to { opacity: 0; } </style>
.fade-enter-active
はenterトランジションの活性状態に適用されます。
今回だとモーダルが表示されるトランジションの開始から終了まで。
.fade-leave-active
はleaveトランジションの活性状態に適用されます。
今回だとモーダルが非表示されるトランジションの開始から終了まで。
完成画面
【Vue.js】RailsApiからjson形式のデータを取得する
はじめに
Vue.jsでRailsApiからjson形式のデータを取得する方法について。
前提
下記のようなtaskモデルがあります。
class CreateTasks < ActiveRecord::Migration[6.0] def change create_table :tasks do |t| t.string :title, null: false t.timestamps end end end
class Task < ApplicationRecord validates :title, presence: true end
コントローラを作成
まずはコントローラを作成します。
$ 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
ルーティングを追加
続いてルーティングを追加します。
Rails.application.routes.draw do root to: 'home#index' namespace :api do # 追加 resources :tasks end get '*path', to: 'home#index' end
CSRF対策を無効化
railsに標準で搭載されているCSRF対策を無効化します。
これがあるとpostリクエストができなくなるということと、APIにおいてはこの機能は必要がないため。
class ApplicationController < ActionController::Base protect_from_forgery with: :null_session end
curlコマンドでタスクを追加
下記コマンドでタスクを3つ追加します。
curlコマンドを使うと、ローカルのターミナルからhttpリクエストを送ることができます。
$ curl -X POST -H "Content-Type: application/json" -d '{"title":"Rubyのサンプルコードを書く"}' localhost:3000/api/tasks $ curl -X POST -H "Content-Type: application/json" -d '{"title":"Dockerを勉強する"}' localhost:3000/api/tasks $ curl -X POST -H "Content-Type: application/json" -d '{"title":"JavaScriptのfor文を理解する"}' localhost:3000/api/tasks
axiosの設定
axiosはAPI通信をするためのhttpクライアントです。
まずはこれをインストールします。
$ yarn add axios
下記コマンドでaxiosの設定ファイルを作成します。
$ mkdir app/javascript/plugins $ touch app/javascript/plugins/axios.js
axiosのインポートと、baseURL
の設定をします。
import axios from 'axios' const instance = axios.create({ baseURL: 'api' }) export default instance
先程作ったaxiosインスタンスのインポートと、他のvueファイルでthis.$axios.get.....
のようにaxiosを使えるようにするための定義をします。
import Vue from 'vue' import App from '../app.vue' import router from '../router' import axios from '../plugins/axios' // 追加 import 'bootstrap/dist/css/bootstrap.css' Vue.config.productionTip = false Vue.prototype.$axios = axios // 追加 document.addEventListener('DOMContentLoaded', () => { const app = new Vue({ router, render: h => h(App) }).$mount() document.body.appendChild(app.$el) })
vueファイルでタスクの情報をAPI通信で取得します。
<template> <div> <div class="d-flex"> <div class="col-4 bg-light rounded shadow m-3 p-3"> <div class="h4">TODO</div> <div v-for="task in tasks" :key="task.id" class="bg-white border shadow-sm rounded my-2 p-4"> <span>{{ task.title }}</span> </div> </div> </div> <div class="text-center"> <router-link :to="{ name: 'TopIndex' }" class="btn btn-dark mt-5">戻る</router-link> </div> </div> </template> <script> export default { name: "TaskIndex", data() { return { tasks: [] // 空にする } }, created() { // ライフサイクルフックのcreatedを定義 this.fetchTasks(); // ライフサイクルフックはrailsでいうコールバック }, methods: { fetchTasks() { this.$axios.get("tasks") // baseURLを設定しているのでurlを省略できる .then(res => this.tasks = res.data) // 取得したtaskのデータをtasksプロパティに代入 .catch(err => console.log(err.status)); } } } </script> <style scoped> </style>
完成画面
【Vue.js】ヘッダーとフッターをコンポーネントに分ける
はじめに
ヘッダーとフッターをコンポーネントとして分ける。
前提
下記のヘッダーとフッターの部分をコンポーネントとして分けます。
<template> <div class="d-flex flex-column min-vh-100"> <header class="mb-auto"> <nav class="navbar navbar-dark bg-dark"> <span class="navbar-brand mb-0 h1">{{ title }}</span> </nav> </header> <div class="text-center"> <h3>タスクを管理しよう!</h3> <div class="mt-4">生活や仕事に関するタスクを見える化して抜け漏れを防ぎましょう。</div> <router-link :to="{ name: 'TaskIndex' }" class="btn btn-dark mt-5">はじめる</router-link> </div> <footer class="mt-auto text-center"> <small>Copyright © 2020</small> </footer> </div> </template> <script> export default { name: "TopIndex", data() { return { title: "タスク管理アプリ" } } } </script> <style scoped> </style>
<template> <div> <router-view /> </div> </template>
ヘッダーとフッターのファイルを作成
新しくcomponents
ディレクトリを作って、その配下にヘッダーとフッターのファイルを作成します。
$ mkdir app/javascript/components $ touch app/javascript/components/TheHeader.vue $ touch app/javascript/components/TheFooter.vue
コンポーネントを作成
続いて、先程作成したファイルにヘッダーとフッターをコンポーネントとして作成します。
<template> <header> <nav class="navbar navbar-dark bg-dark"> <span class="navbar-brand mb-0 h1">タスク管理アプリ</span> </nav> </header> </template> <script> export default { name: "TheHeader" } </script>
<template> <footer class="text-center"> <small>Copyright © 2020</small> </footer> </template> <script> export default { name: "TheFooter" } </script>
コンポーネントをローカル登録して呼び出す
javascript/app.vue
ファイルにヘッダーとフッターのコンポーネントをローカル登録して、テンプレート内で呼び出します。
<template> <div class="d-flex flex-column min-vh-100"> <TheHeader class="mb-auto" /> // コンポーネントを呼び出す <router-view /> <TheFooter class="mb-auto" /> // コンポーネントを呼び出す </div> </template> <script> import TheHeader from './components/TheHeader' import TheFooter from './components/TheFooter' // 先程作成したファイルからコンポーネントを読み込む export default { components: { // 読み込んだコンポーネントを定義 TheHeader, TheFooter } } </script>
javascript/top/index.vueファイルを修正
javascript/top/index.vue
ファイルからヘッダーとフッターの部分を削除します。
<template> <div class="text-center"> <h3>タスクを管理しよう!</h3> <div class="mt-4">生活や仕事に関するタスクを見える化して抜け漏れを防ぎましょう。</div> <router-link :to="{ name: 'TaskIndex' }" class="btn btn-dark mt-5">はじめる</router-link> </div> </template> <script> export default { name: "TopIndex", } </script> <style scoped> </style>
【Vue.js】VueRouterを使ってルーティングを作成
はじめに
vue.jsでVueRouterを使ってルーティングを作成する。
yarnを使ってVueRouterをインストール
$ yarn add vue-router
VueRouterの初期設定
import Vue from "vue"; import Router from "vue-router"; import TopIndex from "../pages/top/index"; import TaskIndex from "../pages/task/index"; Vue.use(Router) const router = new Router({ mode: "history", routes: [ { path: "/", component: TopIndex, name: "TopIndex", }, { path: "/tasks", component: TaskIndex, name: "TaskIndex", }, ], }) export default router
vueとVueRouterをインポートします。
import Vue from "vue"; import Router from "vue-router";
画面遷移先のファイルをコンポーネントとしてインポートします。
import TopIndex from "../pages/top/index"; import TaskIndex from "../pages/task/index";
モジュールシステムを使う際に、明示的にモジュールをインストールする必要があるため下記の記載をします。
Vue.use(Router)
VueRouterのインスタンスを作成します。
const router = new Router({ mode: "history", // HTML5 Historyモードを使用 routes: [ { path: "/", component: TopIndex, name: "TopIndex", }, { path: "/tasks", component: TaskIndex, name: "TaskIndex", }, ], })
VueRouterのインスタンスのrouterをエクスポートして、他のファイルでインポートできるようにします。
export default router
<router-view>コンポーネントを記載
<template>
<div>
<router-view /> // ルーティングにマッチしたコンポーネントを描画
</div>
</template>
hello_vue.jsにVueRouterのファイルを読み込む
import Vue from 'vue' import App from '../app.vue' import router from '../router' // 追加:先程のrouterを読み込む import 'bootstrap/dist/css/bootstrap.css' Vue.config.productionTip = false document.addEventListener('DOMContentLoaded', () => { const app = new Vue({ router, // vueインスタンス生成時のオプションに追加 render: h => h(App) }).$mount() document.body.appendChild(app.$el) })
各ページのファイルの設定
<template> <div class="d-flex flex-column min-vh-100"> <header class="mb-auto"> <nav class="navbar navbar-dark bg-dark"> <span class="navbar-brand mb-0 h1">{{ title }}</span> </nav> </header> <div class="text-center"> <h3>タスクを管理しよう!</h3> <div class="mt-4">生活や仕事に関するタスクを見える化して抜け漏れを防ぎましょう。</div> <router-link :to="{ name: 'TaskIndex' }" class="btn btn-dark mt-5">はじめる</router-link> // <router-link>はaタグみたいな感じ // ルーティングの設定でnameプロパティを設定したのでリンク先をnameで指定できる </div> <footer class="mt-auto text-center"> <small>Copyright © 2020</small> </footer> </div> </template> <script> export default { name: "TopIndex", data() { return { title: "タスク管理アプリ" } } } </script> <style scoped> </style>
<template> <div class="d-flex flex-column min-vh-100"> <header class="mb-auto"> <nav class="navbar navbar-dark bg-dark"> <span class="navbar-brand mb-0 h1">{{ title }}</span> </nav> </header> <div class="d-flex"> <div class="col-4 bg-light rounded shadow m-3 p-3"> <div class="h4">TODO</div> <div v-for="task in tasks" :key="task.id" class="bg-white border shadow-sm rounded my-2 p-4"> // v-for はrailsでいうeachメソッド <span>{{ task.title }}</span> </div> </div> </div> <div class="text-center"> <router-link :to="{ name: 'TopIndex' }" class="btn btn-dark mt-5">戻る</router-link> </div> <footer class="mt-auto text-center"> <small>Copyright © 2020</small> </footer> </div> </template> <script> export default { name: "TaskIndex", data() { return { title: "タスク管理アプリ", tasks: [ { id: 1, title: "スーパーに買い物に行く" }, { id: 2, title: "子供の迎えに行く" }, { id: 3, title: "新聞を解約する" }, { id: 4, title: "ゴミ出しをする" }, ] } } } </script> <style scoped> </style>
サーバーサイドのルーティングを集約
Rails.application.routes.draw do root to: 'home#index' get '*path', to: 'home#index' # 追加 end
完成
path : /
path : /tasks
【エラー】Your bundle is locked to mimemagic (0.3.4)....の対処法
はじめに
bundle install
実行時に下記のエラーに遭遇したのでその対処法。
Your bundle is locked to mimemagic (0.3.4), but that version could not be found in any of the sources listed in your Gemfile. If you haven't changed sources, that means the author of mimemagic (0.3.4) has removed it. You'll need to update your bundle to a version other than mimemagic (0.3.4) that hasn't been removed in order to install.
原因
mimemagic
はrailsのactivestorageが依存しているGemなのですが、どうやらライセンス問題(?)などで、一部使えなくなってしまったそうです。
解決法
解決法としてはRails 5.2.5 / 6.0.3.6 / 6.1.3.1
にアップデートすれば問題ないようです。
※mimemagic
に依存しなくなったため。
railsのバージョンを変えたくない場合
- 下記コマンドで
shared-mime-info
をインストール
$ brew install shared-mime-info
- gemfileに
gem "mimemagic", "~> 0.3.10"
を追加して、bundle update mimemagic
を実行すると解決します。
トークンを使ってAPIの認証機能をつくる【rails】
はじめに
構成
ApiKeyモデルを作成
まずはApiKeyモデルを作成します。
$ rails g model ApiKey user:references access_token:string expires_at:datetime
class CreateApiKeys < ActiveRecord::Migration[6.0] def change create_table :api_keys do |t| t.references :user, null: false, foreign_key: true t.string :access_token, null: false t.datetime :expires_at t.timestamps t.index :access_token, unique: true end end end
access_token
カラムにNOT NULL制約とインデックスを付与します。
$ rails db:migrate
モデルの関連付け
userモデルに関連付けを指定します。
has_many :api_keys, dependent: :destroy
initializeメソッドを上書き
ApiKeyの初期化時に期限つきのトークンを作成したいので、initializeメソッドを上書きします。
また、有効なトークンだけをとってくるscopeを追加します。
class ApiKey < ApplicationRecord belongs_to :user validates :access_token, presence: true, uniqueness: true scope :valid_expire, -> { where('expires_at > ?', Time.current) } def initialize(attributes = {}) super self.access_token = SecureRandom.urlsafe_base64 self.expires_at = 1.week.from_now end end
トークンに関するメソッドを作成
続いて、userモデルに有効なトークンを返すメソッドを追記します。
def grant_api_key return api_keys.valid_expire.first if api_keys.valid_expire.exists? # 有効なトークンがあればその中で一番古いトークンを返す。 # なければ新しいトークンを作成。 api_keys.create end
次は継承元のbase_controller
に、レスポンスヘッダーにトークンを入れる処理を書きます。
def set_token(user) api_key = user.grant_api_key response.headers['AccessToken'] = api_key.access_token end
ユーザー登録時とログイン時にトークンをセットするために、先程定義したset_token
メソッドを呼び出します。
module Api module V1 class RegistrationsController < BaseController def create user = User.new(user_params) if user.save json_string = UserSerializer.new(user).serialized_json set_token(user) # 追加 render json: json_string else render_400(nil, user.errors.full_messages) end end private def user_params params.require(:user).permit(:name, :email, :password) end end end end
module Api module V1 class AuthenticationsController < BaseController def create user = login(params[:email], params[:password]) raise ActiveRecord::RecordNotFound unless user json_string = UserSerializer.new(user).serialized_json set_token(user) # 追加 render json: json_string end private def form_authenticity_token; end end end end
これでユーザー新規作成時とログイン時に、有効なトークンがなければ発行し、レスポンスヘッダーに入れます。
そして有効なトークンがあれば、それをレスポンスヘッダーにいれる処理が完了しました。
ページアクセスする際の認証
記事一覧ページなどにアクセスする際に、先程のトークンを認証する処理をつくります。
bese_controller
にauthenticateメソッドを追加します。
module Api module V1 class BaseController < ApplicationController include Api::ExceptionHandler include ActionController::HttpAuthentication::Token::ControllerMethods # authenticate_or_request_with_http_tokenメソッドを使うために必要。 private def set_token(user) api_key = user.grant_api_key response.headers['AccessToken'] = api_key.access_token end def authenticate authenticate_or_request_with_http_token do |token, _options| @_current_user ||= ApiKey.valid_expire.find_by(access_token: token)&.user end end def current_user @_current_user # `_` は直接参照してほしくない変数につける。 end end end end
authenticate_or_request_with_http_tokenHTTP
メソッドでトークンの認証をおこないます。
falseの場合はToken: Access denied.
を返します。
current_user
を使えるようにするためにcurrent_user
メソッドも定義しています。
最後に、articles_controller
に先程定義したauthenticate
メソッドを、before_actionに設定します。
module Api module V1 class ArticlesController < BaseController before_action :authenticate # 追加 def index articles = Article.all json_string = ArticleSerializer.new(articles).serialized_json render json: json_string end def show article = Article.find(params[:id]) options = { include: %i[user 'user.name' 'user.email'] } json_string = ArticleSerializer.new(article, options).serialized_json render json: json_string end end end end
sorceryを使ってapiのユーザー認証をつくる【rails】
はじめに
sorceryを使ってユーザー認証機能のapiを作る準備。
前提
fast_jsonapiというserializerを使用
class UserSerializer include FastJsonapi::ObjectSerializer has_many :articles attributes :name, :email end
sorceryをインストール
gem 'sorcery'
Gemfileにsorcery
と記載してbundle install
。
下記コマンドでsorceryをインストールします。
$ rails g sorcery:install
続いて、authentication
コントローラを作成。
$ rails g controller authentications
ルーティングを追加。
resource :authentication, only: %i[create]
コントローラの中身を記述します。
module Api module V1 class AuthenticationsController < BaseController def create @user = login(params[:email], params[:password]) raise ActiveRecord::RecordNotFound unless user json_string = UserSerializer.new(user).serialized_json render json: json_string end private def form_authenticity_token; end end end end
sorceryのloginメソッドで、メールアドレスとパスワードを認証します。
該当するユーザーがいない場合はエラーを返します。
ユーザーがいる場合は、json形式でユーザーの情報を返します。
確認
postmanを使ってlocalhost:3000/api/v1/authentication
にemailとpasswordを指定してpostリクエストを送ります。
すると、下記のような値が帰ってきます。
{ "data": { "id": "1", "type": "user", "attributes": { "name": "Test1", "email": "test1@example.com" } } }