勉強履歴(と雑記)

プログラミング初心者のメモ書きです

Vue.js学習メモ(検索機能の実装)

Vue.jsを使って、リアルタイムで検索結果を表示する検索フォームを実装することを考えます。

検索フォームに打ち込んだ文字列をタイトルに含むタスクのみが画面上に表示されるようにします。

task/index.vue

<template>
<label for="search">絞り込み</label>
  <input
    id="search"
    v-model="searchTask"  // dataに定義したsearchTaskに入力した文字列を入れ込む
    type="text"
    placeholder="タスク名を入力してください"
    class="form-control"
  >
</template>

<script>
export default {
  data() {
    return {
      searchTask: '',
    }
  },
  computed: {
    todoTasks() {
      return this.filteredTasks.filter(task => {
        // 下に定義したfilteredTasksで絞り込んだtodoタスクのみ表示
        return task.status == "todo"
      })
    },
    filteredTasks() {
      return this.tasks.filter(task => {
        return task.title.indexOf(this.searchTask) != -1  
        // indexOf関数で-1にならない=searchTaskに代入されている文字列を含む
      })
    }
  },
  .
  .
  .
</script>

土日と平日の学習モチベーションに大きな差が生じている現状をどうにかしたい今日この頃です。

Vue.js学習メモ(画像アップロード)

ログイン機能を実装しているタスク管理アプリで、 個々のユーザーがプロフィール画像を登録できるように、画像アップロード機能を実装することを考えます。
RailsのActiveStorageを使って画像登録します。

①userモデルにプロフィール画像用のカラム(avatar)を設定

user.rb

class User< ApplicationRecord

  has_one_attached :avatar

end

②ユーザーに紐づくプロフィール画像のurlを返すメソッドをモデルに

class User< ApplicationRecord

  def avatar_url
    avatar.attached? ? Rails.application.routes.url_helpers.rails_blob_path(avatar, only_path: true) : 
nil
end

③プロフィールのコントローラーとルーティングを設定

api/profile_controller.rb

class Api::ProfileController < ApplicationController
  before_action :authenticate!

  def update
    user = User.find(current_user.id)
    if user.update(user_params)
      render json: user, methods: [:avatar_url]
    else
      render json: user.errors, status: :bad_request
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :avatar)
  end
end
Rails.application.routes.draw do
  namespace :api, format: 'json' do
    resource :profile
  end
end

④userコントローラーにapiのエンドポイントとストロングパラメータを追加

api/users_controller.rb

class Api::UsersController < ApplicationController

  def me
    render json: current_user, methods: [:avatar_url]
  end

  private

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation, :name, :avatar)
  end
end

⑤storeにユーザー更新のAPIリクエストを送る処理を記載

const actions = {
  .
  .
  .
  updateUser({ commit, state }, user) {
    return axios.patch(`profile/${state.authUser.id}`, user)
      .then(res => {
        commit('setUser', res.data)
      })
  }
}

⑥プロフィールページの作成

profile/index.vue

<template>
  <div id="profile-form">
    <ValidationObserver v-slot="{ handleSubmit }">
      <div class="form-group text-left">
        <ValidationProvider v-slot="{ errors }" rules="required">
          <label for="name">ユーザー名</label>
          <input 
            id="name" 
            v-model="user.name" 
            type="text" 
            class="form-control" 
            placeholder="username"
          >
          <span class="text-danger">{{ errors[0] }}</span>
        </ValidationProvider>
      </div>
      <div class="form-group text-left">
        <ValidationProvider v-slot="{ errors }" ref="provider" rules="image">
          <label for="avatar" class="d-block >プロフィール画像</label>
          <img :src="user.avatar_url" class="my-3" width="150px">
          <input
            id="avatar"
            type="file"
            accept="image/png,image/jpeg"
            class="form-control-file"
            @change="handleChange"
          >
          <span class="text-danger">{{ errors[0] }}</span>
        </ValidationProvider>
      </div>
      <button
        type="submit"
        class="btn btn-primary"
        @click="handleSubmit(update)"
      >
        更新
      </button>
    </ValidationObserver>
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'

export default {
  name: 'ProfileIndex',
  data() {
    return {
      user: {
        name: "",
        avatar_url: ""
      },
      uploadAvatar: ""
    }
  },
  computed: {
    ...mapGetters('users', ['authUser'])
  },
  created() {
    this.user = Object.assign({}, this.authUser)
  },
  methods: {
    ...mapActions('users', ['updateUser']),
    async handleChange(event) {
      const { valid } = await this.$refs.provider.validate(event)
      if (valid) this.uploadAvatar = event.target.files[0]
    },
    update() {
      const formData = new FormData()
      formData.append("user[name]", this.user.name)
      if (this.uploadAvatar) formData.append("user[avatar]", this.uploadAvatar)

      try {
        this.updateUser(formData)
        this.$router.push({ name: "TaskIndex" })
      } catch (error) {
        console.log(error);
      }
    },
  }
}
</script>

<style scoped>
</style>

⑦プロフィールページのルーティング

router/index.js

import ProfileIndex from "../pages/profile/index"

const router = new Router({
  routes: [
    .
    .
    .
    {
      path: '/profile',
      component: ProfileIndex,
      name: 'ProfileIndex',
      meta: { requiredAuth: true },
    }
  ]
})

⑧ヘッダーにプロフィール画像の表示+プロフィールページへのリンクを設定

components/TheHeader.vue

<li class="nav-item active avatar-image-wrapper">
  <img
    :src="authUser.avatar_url"
    class="rounded avatar-image"
  >
</li>
<li class="nav-item active">
  <router-link
    :to="{ name: 'ProfileIndex' }"
    class="nav-link"
  >
    プロフィール
  </router-link>
</li>

ハイライトが上手く表示されてないので、次の土日に直します(直した)。

Vue.js学習メモ(バリデーション)

VeeValidateを使って、タスク追加・編集フォームのバリデーションチェックを行っていきます。 フォームに入力されたデータに対して、リアルタイムでエラーを表示させることができるようになります。

①インストール

yarn add vee-validate
// もしくは
npm install vee-validate

②バリデーションルールを設定したいフォルダのscriptに書く。  長くなったり、複数のフォルダで設定したい場合は、専用のフォルダに書く。

import Vue from 'vue'

//  vee-validateのライブラリを呼び出し
import {
  ValidationProvider,
  ValidationObserver,
  extend
} from 'vee-validate'

//  バリデーションルール
import {
  email,
  required
} from 'vee-validate/dist/rules';

// Vue.componentに上記で取得したValidationProviderとValidationObserverを読み込ませる
Vue.component('ValidationObserver', ValidationObserver) 
//  各フィールドを監視するために使用
Vue.component('ValidationProvider', ValidationProvider) 
//  フォーム全体を監視するために使用

// 以下、extendでルールやメッセージの定義(emailなどはあらかじめ組み込まれている)
extend('email', {
  ...email,
  message: '{_field_}の形式で入力してください'
});

extend('required', {
  ...required,
  message: '{_field_}は必須項目です'
});

extend('min', {
  validate(value, { length }) {
    return value.length >= length;
  },
  params: ['length'],
  message: '{_field_}は{length}文字以上で入力してください'
});

③定義したルールをフォームで使う(例としてログインフォーム)。

<template>
  <div id="login-form">
    <ValidationObserver v-slot="{  handleSubmit  }">  
       //  ValidationObserverで全体を監視
      <div class="form-group text-left">
        <ValidationProvider  
          //  ValidationProviderで個別のフォームを監視
          v-slot="{ errors }"  
          // ValidationProvider内のエラーが全てそのまま配列に入ってる
          rules="required|email"  
          //  ここでは、必須とemailの形式になっているかのバリデーションルール
        >
          <label for="email">メールアドレス</label>
          <input
            id="email"
            v-model="user.email"
            name="メールアドレス"
            type="email"
            class="form-control"
            placeholder="test@example.com"
          >
          <span class="text-danger">{{ errors[0] }}</span>  
         //  ここでエラーの表示
        </ValidationProvider>
      </div>
      <div class="form-group text-left">
        <ValidationProvider
          v-slot="{ errors }"
          rules="required|min:3"  
          //  ここでは、必須チェック及び最低文字数が3文字以上か
        >
          <label for="password">パスワード</label>
          <input
            id="password"
            v-model="user.password"
            name="パスワード"
            type="password"
            class="form-control"
            placeholder="password"
          >
          <span class="text-danger">{{ errors[0] }}</span>
        </ValidationProvider>
      </div>
      <button
        type="submit"
        @click="handleSubmit(login)"
      >
        ログイン
      </button>
    </ValidationObserver>
  </div>
</template>

一回、最近書いた記事を見直した方がいいかなと思います(主に見やすさや出典の表記)。 そろそろ、vueを使ってアプリを作ろうかと思います。

Vue.js学習メモ(コンポーネントの分割)

Vue.jsとrailsでタスク管理アプリを作りながら勉強してます。 今回はタスクを3つ(TODO、DOING、DONE)に分けて、 なおかつタスクの一覧(TaskList)とタスク単体(TaskItem)を別のコンポーネントに分割していきたいと思います。 コンポーネント間の関係:index(親)→TaskList(子)→TaskItem(孫)

 変更前

index.vue

<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 d-flex align-items-center"
    @click="handleShowTaskDetailModal(task)"
  >
    <span>{{ task.title }}</span>
</div>

 変更後

index.vue(TODOのみ)

<TaskList
  :tasks="todoTasks"  
  // v-bindでTaskListにtasksプロパティを与える。todoTasksはcomputedで定義。
  taskListId="todo-list"
  @handleShowTaskDetailModal="handleShowTaskDetailModal"
>
  <template v-slot:header>  
   // slotでは親で差し替えたい部分の表記を書く
    <div class="h4">TODO</div> 
  </template>
</TaskList>

 computed: {
   todoTasks() {
     return this.tasks.filter(task => {
       return task.status == "todo"
     })
  },
}

TaskList.vue

<div :id="taskListId" class="bg-light rounded shadow m-3 p-3">
  <slot name="header">タスク区分</slot>  
  //子では<slot></slot>と書くだけ
  <template v-for="task in tasks">  
  //v-forでtasks内のtaskを表示していく
    <TaskItem :key="task.id" :task="task" @handleShowTaskDetailModal="$listeners['handleShowTaskDetailModal']" /> 
    // taskItemにtaskプロパティを渡す
    // $listenersで全てのtaskItem(孫)からのデータを受け取り、index(親)に渡す(Vue3では$attrsに統合)
  </template>
</div>

TaskItem.vue

<div
  :id="'task-' + task.id"
  class="bg-white border shadow-sm rounded my-2 p-4 d-flex align-items-center"
  @click="handleShowTaskDetailModal(task)"
>
  <span>{{ task.title }}</span>
</div>

methods: {
  handleShowTaskDetailModal(task) {
    this.$emit('handleShowTaskDetailModal', task)
  }
}

今回はなかなかどう分割したらいいのかがわからず、上手いこと行きませんでした。 まだまだvueの理解が不十分ですね。勉強を継続していきたいと思います。

土日も書くと言っておきながら結局やってませんでした。 いきなり週3でブログ更新は荷が重かったのかもしれないので、最低でも週1にハードル下げてやっていきます。

Vue.js学習メモ(子から親への値受け渡し)

昨日の続きです。

モーダルから新しくタスクを追加する場合を考えます。 子コンポーネント(TaskCreateModal.vue)→親コンポーネント(index.vue) 引き渡すデータ:タスクの情報(タイトル/title、詳細/description)

実装例

index.vue

<template> 
  <transition name="fade">
    <TaskCreateModal 
      v-if="isVisibleTaskCreateModal" 
      @close-modal="handleCloseTaskCreateModal" 
      @create-task="handleCreateTask"
    />
  </transition>
</template>

<script>
import TaskCreateModal from "./components/TaskDetailModal"
export default {
  name: "TaskIndex",
  components: {
    TaskCreateModal
  },
  methods: {
  handleCreateTask(task) {
  // 略
  }
}

TaskCreateModal.vue

<template>
  <div class="modal-body">
    <div class="form-group">
      <label for="title">タイトル</label>
      <input type="text" class="form-control" id="title" v-model="task.title">
    </div>
    <div class="form-group">
      <label for="description">説明文</label>
      <textarea rows="5" class="form-control" id="description" v-model="task.description"></textarea>
    </div>
    <div class="d-flex justify-content-between">
      <button @click="handleCreateTask" class="btn btn-success">追加</button>
    </div>
  </div>
</template>

<script>
export default {
  name: "TaskCreateModal",
  methods: {
    handleCreateTask() {
      this.$emit('create-task', this.task)
    },
  },
}
</script>

1)子コンポーネントをインポートし、componentsプロパティに登録(親から子のときと同じ)

import TaskCreateModal from "./components/TaskCreateModal"
export default {
  name: "TaskIndex",
  components: {
    TaskCreateModal
  },

2)v-modelディレクティブを使って、送信するデータを設定
タイトルに入力した値がtask.titleに、説明文に入力した値がtask.descriptionに代入される

<div class="form-group">
  <label for="title">タイトル</label>
  <input type="text" class="form-control" id="title" v-model="task.title">
</div>
<div class="form-group">
  <label for="description">説明文</label>
  <textarea rows="5" class="form-control" id="description" v-model="task.description"></textarea>
</div>

3)クリックイベントの設定
@click="処理名"とタグに記載することで、クリックした時にmethodsプロパティで定義した処理(今回はhandleCreateTask)が実行される

<template>
  <div class="d-flex justify-content-between">
    <button @click="handleCreateTask" class="btn btn-success">追加</button>
  </div>
</template>

<script>
export default {
   methods: {
    handleCreateTask() {
      this.$emit('create-task', this.task)
    }
}
</script>

4)子コンポーネントから親へのデータ送信
this.$emit('イベント名', 値)と記載することで、第二引数で値を渡しつつ親のイベントを引き起こす(今回は'create-task')

<script>
export default {
   methods: {
    handleCreateTask() {
      this.$emit('create-task', this.task)
    }
}
</script>

5)親コンポーネントでデータを受けとり(@create-task)、handleCreateTaskが実行される

<template>
  <transition name="fade">
    <TaskCreateModal 
      v-if="isVisibleTaskCreateModal" 
      @close-modal="handleCloseTaskCreateModal" 
      @create-task="handleCreateTask"
    />
  </transition>
</template>

<script>
export default {
  methods: {
    handleCreateTask(task) {
    // 略
  }
}
</script>

以上です。 あくまで自分用のメモではありますが、他の人が見てもわかりやすいブログに改善していきたいですね。

Vue.js学習メモ(親から子への値受け渡し)

Vue.jsの勉強では、タスクを管理するアプリを想定しています。 既にタスクのデータは追加されていて、タスク一覧に表示されているとします。 今回は、タスク一覧からタスクをクリックして、タスク詳細のモーダルを表示する実装を行いたいと思います。

コンポーネント(index.vue)→子コンポーネント(TaskDetailModal.vue) 引き渡すデータ:タスクの情報(タイトル/title、詳細/description)

実装例

index.vue

<template> 
<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)"
>
</template>

<script>
import TaskDetailModal from "./components/TaskDetailModal"
export default {
  name: "TaskIndex",
  components: {
    TaskDetailModal
  },
  data() {
    return {
      tasks: [],
      taskDetail: {},
      isVisibleTaskDetailModal: false
    }
  },

TaskDetailModal.vue

<template>
  <div class="modal-header">
    <h5 class="modal-title">{{ task.title }}</h5>
    <button type="button" class="close" @click="handleCloseModal">
      <span>&times;</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>
    </div>
</template>

<script>
export default {
  name: "TaskDetailModal",
  props: {
    task: {
      title: {
        type: String,
        required: true
      },
      description: {
        type: String,
        required: true
      }
    }
  },
}
</script>

コンポーネントから子コンポーネントにデータを渡す場合

1)子コンポーネントをインポートし、componentsプロパティに登録

import TaskDetailModal from "./components/TaskDetailModal"
export default {
  name: "TaskIndex",
  components: {
    TaskDetailModal
  },

2)子コンポーネントに渡すデータをdataプロパティで定義

data() {
    return {
      tasks: [],
      taskDetail: {},
      isVisibleTaskDetailModal: false
    }
  },

3)データを子コンポーネントタグにバインド
 今回は複数のタスクを表示するため、v-forを使う  (:keyはv-bind:keyの略記)

<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)"
>

4)子コンポーネントでpropsプロパティを使ってデータを受け取る

props: {
    task: {
      title: {
        type: String,
        required: true
      },
      description: {
        type: String,
        required: true
      }
    }
  },

5)プロパティで設定された値を表示

<div class="modal-header">
    <h5 class="modal-title">{{ task.title }}</h5>
    <button type="button" class="close" @click="handleCloseModal">
      <span>&times;</span>
    </button>
  </div>
  <div class="modal-body" v-if="task.description">
    <p>{{ task.description }}</p>
  </div>

長くなったので、逆の子から親への値受け渡しについては明日書きます。

どうしても時間がかかってしまうので、サクッとブログ書けるようになりたいですねー

今後のブログ更新について

こんな去年の年末から更新してないブログを見ている人はまずいないでしょうが、

改めてブログ書いていこうと思います。

 

以下、要約

  • このブログは、私が学んだプログラミング技術について雑にまとめていくものです。
  • アウトプット力を高めるために書いてます。
  • 更新日は、定時退社推奨日(私は基本定時退社ですが)である水曜日、土曜日、日曜日に何かしら書いていきます。
  • 技術以外の日々の感想も書こうと思いますが、折角Noteのアカウントも作っていたので、そっちで書こうかなと思います。

 

また、三日坊主になるかもしれませんが、1ヶ月後の私はブログを継続させていることでしょう(未来完了)。