20190527のvue.jsに関する記事は11件です。

Laravel Mix の webpack.mix.js を Storybook でも利用する

はじめに

Atomic Design ~堅牢で使いやすいUIを効率良く設計する 、この本があまりに素晴らしくて、写経をしたいので、その準備として Storybook を利用できるようにしました。

Storybook を導入したのですが、Laravel Mix で設定した CSS Modules が適用されていませんでした。

Storybook へ Laravel Mix の webpack.mix.js を取り込む方法がわかったので、記録に残します。

Storybook の webpack.config.js を用意

デフォルトで Storybook から読み込まれる .storybook の下に webpack.config.js を作成します。

ディレクトリ構成は以下のようになります。

.storybook/
    addons.js
    config.js
    webpack.config.js

webpack.config.js は以下の内容です。
laravel-mix の webpack.config.js を require しているのがポイントです。

const path = require('path');
// your app's webpack.config.js
const custom = require('../node_modules/laravel-mix/setup/webpack.config.js');

module.exports = async ({ config, mode }) => {
  return { ...config, module: { ...config.module, rules: custom.module.rules } };
};

これで完了です。
これで、 webpack.mix.js の設定内容が Storybook にも反映されているはずです!!

さいごに

ドキュメントを読めばなんとかなるもんだ!

これで Atomic Design ~堅牢で使いやすいUIを効率良く設計する の写経ができるぞ。

参考

- https://storybook.js.org/docs/configurations/custom-webpack-config/#using-your-existing-config

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Vue.js] 別ウィンドウで開くリンクの作成

やりたいこと

別ウィンドウで開くリンクをつくりたかった

方法

メインのコンポーネント

<script>
import { ROUTER } from '@/libs/constant-router'
export default {
  name: 'Register',
  methods: {
    toTerms() {
      let routeData = this.$router.resolve({name: ROUTER.TERMS_NAME, query: {data: "someData"}});
      window.open(routeData.href, '_blank'); //←★ここ
    },
}
</script>

 設定ファイル群

constant-router.js

プロジェクトルート直下に「libs」と名付けたディレクトリを切って、その中に格納

export const ROUTER = {
  TERMS_PATH: '/terms',
  TERMS_NAME: 'terms',
}

router.js

import Terms from '@/views/Terms.vue'

Vue.use(Router)

const router = new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: ROUTER.TERMS_PATH,
      name: ROUTER.TERMS_NAME,
      component: Terms,
    },
  ],
})

router.toTerms = () => {
  router.push({ name: ROUTER.TERMS_NAME })
}


export default router

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue-chartjsでoptionsを動的に変更させる方法

optionsに関してはレスポンシブに変更されないみたいでthis.renderChart()で明示的に再描画する必要があるみたいです1
チャートのデータ変更はレスポンシブに変更されます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue.js 現在のパスに応じて表示したり隠したり

make ページでは メニュー要素を隠したい。
そんなときは以下のように書く。

vue.blade.php
    <el-footer id="footer" class="layout-footer" v-show="$route.path.indexOf('make') < 0">
こんな塩梅
    </el-footer>

これで現在のページが make にマッチしない時のみ メニューが表示されるようになる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt v2.7 で実行時に「TypeError: fsevents.watch is not a function」のエラーで起動できない

Nuxt v2.7.1にしたら、起動できなくなった

Nuxt v2.7.x で、yarn dev 実行時に以下のエラーが出た。

fsevents-handler.js
  const stop = fsevents.watch(path, callback);
                        ^

TypeError: fsevents.watch is not a function
    at createFSEventsInstance (/Users/masaakikakimoto/Documents/nuxt_firebase_sns/node_modules/@nuxt/builder/node_modules/chokidar/lib/fsevents-handler.js:57:25)
    at setFSEventsListener (/Users/masaakikakimoto/Documents/nuxt_firebase_sns/node_modules/@nuxt/builder/node_modules/chokidar/lib/fsevents-handler.js:111:16)
    at FsEventsHandler._watchWithFsEvents (/Users/masaakikakimoto/Documents/nuxt_firebase_sns/node_modules/@nuxt/builder/node_modules/chokidar/lib/fsevents-handler.js:296:18)
    at initWatch (/Users/masaakikakimoto/Documents/nuxt_firebase_sns/node_modules/@nuxt/builder/node_modules/chokidar/lib/fsevents-handler.js:433:27)
    at LOOP (fs.js:1787:14)
    at process._tickCallback (internal/process/next_tick.js:176:11)
npm ERR! code ELIFECYCLE
npm ERR! errno 1

調べると以下のIsuueがHit
https://github.com/nuxt/nuxt.js/issues/5725

nodejs のバージョンを下げることで解決した。

とりあえずnode v10.15.3でNuxtが正常起動することを確認しました。

(※v12系やv9系では、エラーが出るようだ。)

※追記 おまけ

気を良くして、いろいろとパッケージを追加していたら、またまたNuxtが起動しなくなった。

エラー内容

 ERROR  Failed to compile with 1 errors                       friendly-errors 17:07:19

This dependency was not found:                                friendly-errors 17:07:19
                                                              friendly-errors 17:07:19
* core-js/modules/es7.promise.finally in ./.nuxt/client.js    friendly-errors 17:07:19
                                                              friendly-errors 17:07:19
To install it, you can run: npm install --save core-js/modules/es7.promise.finally

corejs v2.6.5を入れ直すことで解決した。

yarn add core-js@latest2.6.5
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ニートのプログラミング未経験者がRailsとVueでTodoアプリを作ってみた

はじめに

Vuejs と Rails API を使って Todo アプリを作りました。
まずは、ローカル環境で動かし
最終的に Heroku へデプロイするところまで書きました。

最初に作ったものを載せておきます。

デモ

https://vue-rails-api-todo.herokuapp.com/

vue.gif

コード

https://github.com/youbeer/vue-rails-api-todo

ディレクトリ構成

frontend ディレクトリに Vue のファイルをまとめてあります

vue-rails-api-todo/
├── app
│   ├── channels
│   ├── controllers
│   ├── jobs
│   ├── mailers
│   ├── models
│   └── views
├── bin
├── config
├── db
├── docs
├── frontend
│   ├── dist
│   ├── node_modules
│   ├── public
│   └── src
├── lib
├── log
├── public
├── storage
├── test
├── tmp
└── vendor

対象読者(こんな方に読んでいただけたら)

Rails と Vue のチュートリアルを勉強してなにか作ってみたい方

事前準備

Rails と VueCLI3 のインストールを行なってください

自分の環境です

Mac MoJava
ruby 2.6.1
Rails 5.2.3
Vue 3.7.0

【Rails】サーバーサイドの作成

Rails プロジェクトを API モードで作る

terminal
rails new vue-rails-api-todo --api

Gemfile を修正

Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.1'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'puma', '~> 3.11'
gem 'rack-cors'
gem 'rails', '~> 5.2.3'
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]

group :development, :test do
  gem 'byebug', platforms: %i[mri mingw x64_mingw]
  gem 'sqlite3'
end

group :development do
  gem 'listen', '>= 3.0.5', '< 3.2'
  gem 'pry-byebug'
  gem 'pry-doc'
  gem 'pry-rails'
  gem 'pry-stack_explorer'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

group :production do
  gem 'pg'
end

Gem をインストール

terminal
bundle install

Model を作る

フィールドは2つだけです

  1. title: タスクの内容
  2. completed: 完了・未完了
terminal
rails g model Todo title:string completed:boolean

migration ファイルの修正

Not Null 制約 と デフォルト値を追記してます

db/migrate/20190525063511_create_tasks.rb
class CreateTodos < ActiveRecord::Migration[5.2]
  def change
    create_table :todos do |t|
      t.string :title, null: false
      t.boolean :completed, default: false, null: false

      t.timestamps
    end
  end
end

マイグレーション

terminal
rails g model Task title:string completed:boolean

Model にバリデーションを追加

app/models/todo.rb
class Todo < ApplicationRecord
  validates :title, presence: true
end

ルーティングの修正

resources :todos, except: :show
以外に2つルーティングを追加しました

  1. patch 'check_all', to: 'todos#check_all': タスクの完了・未完了
  2. delete 'delete_completed', to: 'todos#delete_completed': 完了タスクを全削除
config/routes.rb
Rails.application.routes.draw do
  root 'api/v1/todos#index'

  namespace :api do
    namespace :v1, format: :json do
      patch 'check_all', to: 'todos#check_all'
      delete 'delete_completed', to: 'todos#delete_completed'
      resources :todos, except: :show
    end
  end
end

ルーティングは詳細は、こんな感じです

terminal
                   Prefix Verb   URI Pattern                                                                              Controller#Action
                     root GET    /                                                                                        api/v1/todos#index
         api_v1_check_all PATCH  /api/v1/check_all(.:format)                                                              api/v1/todos#check_all
  api_v1_delete_completed DELETE /api/v1/delete_completed(.:format)                                                       api/v1/todos#delete_completed
             api_v1_todos GET    /api/v1/todos(.:format)                                                                  api/v1/todos#index
                          POST   /api/v1/todos(.:format)                                                                  api/v1/todos#create
              api_v1_todo PATCH  /api/v1/todos/:id(.:format)                                                              api/v1/todos#update
                          PUT    /api/v1/todos/:id(.:format)                                                              api/v1/todos#update
                          DELETE /api/v1/todos/:id(.:format)                                                              api/v1/todos#destroy
       rails_service_blob GET    /rails/active_storage/blobs/:signed_id/*filename(.:format)                               active_storage/blobs#show
rails_blob_representation GET    /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
       rails_disk_service GET    /rails/active_storage/disk/:encoded_key/*filename(.:format)                              active_storage/disk#show
update_rails_disk_service PUT    /rails/active_storage/disk/:encoded_token(.:format)                                      active_storage/disk#update
     rails_direct_uploads POST   /rails/active_storage/direct_uploads(.:format)                                           active_storage/direct_uploads#create

controller の用意

terminal
rails g controller api::v1::todos

controller を修正

app/controllers/api/v1/todos_controller.rb
class Api::V1::TodosController < ApplicationController
  before_action :set_todo, only: %i[show update destroy]

  # GET api/vi/todos/
  def index
    @todos = Todo.all.order(created_at: :asc)
    render json: @todos
  end

  # Post api/vi/todos
  def create
    @todo = Todo.new(todo_params)
    if @todo.save
      render json: @todo
    else
      render json: { status: 'error', data: @todo.errors }
    end
  end

  # Put api/vi/todos/:id
  def update
    if @todo.update(todo_params)
      render json: @todo
    else
      render json: { status: 'error', data: @todo.errors }
    end
  end

  # Delete api/vi/todos/:id
  def destroy
    @todo.destroy
    render json: @todo
  end

  # Delete api/vi/delete_completed
  def delete_completed
    todo = Todo.where(completed: true).delete_all
    render json: todo
  end

  # Put api/vi/check_all
  def check_all
    todo = Todo.update_all(completed: params['checked'])
    render json: todo
  end

  private

  def todo_params
    params.require(:todo).permit(:title, :completed)
  end

  def set_todo
    @todo = Todo.find(params[:id])
  end
end

cors の設定ファイルを修正

Vue 側からのアクセスを許可するため追記

origins 'http://localhost:8080'
config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:8080'

    resource '*',
             headers: :any,
             methods: %i[get post put patch delete options head]
  end
end

seed ファイルを修正

テストデータ作成用

db/seeds.rb
10.times do |i|
  Todo.create(title: "title No#{i + 1}", completed: i.even?)
end

postman で確認してみる

terminal
rails s
rails db:seed

Postman をインストールされていない方は、こちらからインストールしてください

  1. プルダウンから GET を選択し http://localhost:3000/api/v1/todos を入力
  2. Send をクリック
  3. テストデータの json が返ってくることを確認

時間がある方はその他のアクションも試してみてください
やり方は、ここでは割愛します

Screen Shot 2019-05-25 at 20.54.02.png

【Vue】フロントエンドの作成

プロジェクトを作成

Rails プロジェクトの直下に frontend という名前で Vue プロジェクトを作成します

terminal
vue create frontend

いくつか質問がでてくるので

  • Manually select features を選択肢し
  • Vuex を追加してください

その他はお好みでどうぞ

terminal
? Please pick a preset:
default (babel, eslint)
❯ Manually select features
terminal
? Check the features needed for your project:
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
❯◉ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

インストールが完了したら fronend へ移動しサーバを起動してみましょう

terminal
cd frontend && yarn serve

ブラウザから http://localhost:8080/ へアクセスし
こんな画面が表示されたら成功です。

Screen Shot 2019-05-25 at 16.03.28.png

Bootstrap と axios を追加

  • BootstrapVue: Vue 用の Bootstrap モジュール
  • axios: 今時の ajax モジュール
terminal
yarn add bootstrap-vue bootstrap axios

bootstrap の設定を追加

bootstrap を使うため main.js に追記

import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";

Vue.use(BootstrapVue);
frontend/src/main.js
import Vue from "vue";
import App from "./App.vue";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";

Vue.use(BootstrapVue);
Vue.config.productionTip = false;

new Vue({
  store,
  render: h => h(App)
}).$mount("#app");

store を編集

frontend/src/store.js
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";

Vue.use(Vuex);

const http = axios.create({
  baseURL:
    process.env.NODE_ENV === "development" ? "http://localhost:3000/" : "/",
  headers: {
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest"
  },
  responseType: "json"
});

export default new Vuex.Store({
  state: {
    filter: "all",
    todos: []
  },
  getters: {
    remaining(state) {
      return state.todos.filter(todo => !todo.completed).length;
    },
    completedAll(state, getters) {
      return getters.remaining === 0;
    },
    todosFiltered(state) {
      if (state.filter === "all") {
        return state.todos;
      } else if (state.filter === "active") {
        return state.todos.filter(todo => !todo.completed);
      } else if (state.filter === "completed") {
        return state.todos.filter(todo => todo.completed);
      }
      return state.todos;
    },
    showClearCompletedButton(state) {
      return state.todos.filter(todo => todo.completed).length > 0;
    }
  },
  mutations: {
    addTodo(state, todo) {
      state.todos.push({
        id: todo.id,
        title: todo.title,
        completed: false,
        editing: false
      });
    },
    clearCompleted(state) {
      state.todos = state.todos.filter(todo => !todo.completed);
    },
    updateFilter(state, filter) {
      state.filter = filter;
    },
    checkAll(state, checked) {
      state.todos.forEach(todo => {
        todo.completed = checked;
      });
    },
    deleteTodo(state, id) {
      const index = state.todos.findIndex(todo => todo.id === id);
      state.todos.splice(index, 1);
    },
    updateTodo(state, todo) {
      const index = state.todos.findIndex(item => item.id === todo.id);

      state.todos.splice(index, 1, {
        id: todo.id,
        title: todo.title,
        completed: todo.completed,
        editing: todo.editing
      });
    },
    retrieveTodos(state, todos) {
      state.todos = todos;
    }
  },
  actions: {
    retrieveTodos({ commit }) {
      http
        .get("/api/v1/todos")
        .then(response => {
          commit("retrieveTodos", response.data);
        })
        .catch(error => {
          console.log(error);
        });
    },
    addTodo({ commit }, todo) {
      http
        .post("/api/v1/todos", {
          title: todo.title,
          completed: false
        })
        .then(response => {
          commit("addTodo", response.data);
        })
        .catch(error => {
          console.log(error);
        });
    },
    clearCompleted({ commit }) {
      http
        .delete("/api/v1/delete_completed")
        .then(response => {
          commit("clearCompleted", response.data);
        })
        .catch(error => {
          console.log(error);
        });
    },
    checkAll({ commit }, checked) {
      http
        .patch("/api/v1/check_all", {
          checked
        })
        .then(() => {
          commit("checkAll", checked);
        })
        .catch(error => {
          console.log(error);
        });
    },
    deleteTodo({ commit }, id) {
      http
        .delete(`/api/v1/todos/${id}`)
        .then(response => {
          commit("deleteTodo", response.data.id);
        })
        .catch(error => {
          console.log(error);
        });
    },
    updateTodo({ commit }, todo) {
      http
        .patch(`/api/v1/todos/${todo.id}`, {
          title: todo.title,
          completed: todo.completed
        })
        .then(response => {
          commit("updateTodo", response.data);
        })
        .catch(error => {
          console.log(error);
        });
    }
  }
});

store は大きく5つのブロックに分かれています

axios のデフォルト通信設定

  • baseURL: API 取得のための URL
  • header:リクエスト時のヘッダの値
  • responseType:レスポンスの形式
import axios from "axios";

Vue.use(Vuex);

const http = axios.create({
  baseURL:
    process.env.NODE_ENV === "development" ? "http://localhost:3000/" : "/",
  headers: {
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest"
  },
  responseType: "json"
});

state

アプリの状態管理をするための単一オブジェクトです

ステート | Vuex

  • todo: タスクの配列
  • filter:全て ・ 完了 ・ 未完了 のフィルタ
state: {
  filter: "all",
  todos: []
},

getters

component でいう computed にあたります

ゲッター | Vuex

  • remaining: タスク完了の件数
  • showClearCompletedButton: クリアボタンを 表示 ・ 非表示 の切替用
getters: {
    remaining(state) {
      return state.todos.filter(todo => !todo.completed).length;
    },

    /*************** 省略 ***************/

    showClearCompletedButton(state) {
      return state.todos.filter(todo => todo.completed).length > 0;
    }
}

mutaition

state を変更するためのメソッド群です
action を経由して state を更新するために使っています

ミューテーション | Vuex

  • addTodo: 新しいタスクを追加しています
  • retrieveTodos: ページに最初にアクセスしたとき、タスク一覧を作成しています
mutations: {
  addTodo(state, todo) {
    state.todos.push({
      id: todo.id,
      title: todo.title,
      completed: false,
      editing: false
    });
  },

  /*************** 省略 ***************/

  retrieveTodos(state, todos) {
    state.todos = todos;
  }
},

actions

非同期処理を行うためのメソッド群です
axios を使って Rails API を取得するために使っています

アクション | Vuex

  • retrieveTodos: Rails の API からタスク一覧を取得しています
  • updateTodo: Rails の API からタスクの更新結果を取得しています
 actions: {
  retrieveTodos({ commit }) {
    http
      .get("/api/v1/todos")
      .then(response => {
        commit("retrieveTodos", response.data);
      })
      .catch(error => {
        console.log(error);
      });
  },

  /*************** 省略 ***************/

  updateTodo({ commit }, todo) {
    http
      .patch(`/api/v1/todos/${todo.id}`, {
        title: todo.title,
        completed: todo.completed
      })
      .then(response => {
        commit("updateTodo", response.data);
      })
      .catch(error => {
        console.log(error);
      });
  }
}

App を編集

frontend/src/App.vue
<template>
  <div id="app" class="container">
    <img alt="Vue logo" src="./assets/logo.png" class="logo" />
    <h1>VueTODO</h1>
    <todo-list></todo-list>
  </div>
</template>

<script>
import TodoList from "./components/TodoList.vue";

export default {
  name: "App",
  components: {
    TodoList
  }
};
</script>

<style lang="scss" scoped>
.logo {
  margin: 0 auto;
  display: block;
}
</style>

メインとなる TodoList コンポーネントを呼び出しています

import TodoList from "./components/TodoList.vue";

export default {
  name: "App",
  components: {
    TodoList
  }
};

TodoList コンポーネントを作成

新しいタスクの追加 と 子コンポーネントを束ねています

frontend/src/components/TodoList.vue
<template>
  <div>
    <b-container class="bv-example-row">
      <b-row>
        <b-col cols="12">
          <b-form @submit.prevent="addTodo">
            <b-form-group label="New todo" label-for="new-todo">
              <b-form-input
                id="new-todo"
                v-model="newTodo"
                placeholder="What needs to be done?"
              ></b-form-input>
            </b-form-group>
          </b-form>

          <b-list-group>
            <transition-group name="fade">
              <TodoItem
                v-for="(todo, index) in todosFiltered"
                :key="todo.id"
                :todo="todo"
                :index="index"
                class="todo-item"
                :check-all="completedAll"
              />
            </transition-group>
          </b-list-group>

          <b-list-group class="mt-4">
            <b-list-group-item
              class="flex-wrap d-flex justify-content-around align-items-center"
            >
              <TodoCheckAll />
              <TodoItemsRemaining />
            </b-list-group-item>
            <b-list-group-item
              class="flex-wrap d-flex justify-content-around align-items-center"
            >
              <TodoFiltered />
              <TodoClearCompleted />
            </b-list-group-item>
          </b-list-group>
        </b-col>
      </b-row>
    </b-container>
  </div>
</template>

<script>
import TodoItem from "@/components/TodoItem";
import TodoItemsRemaining from "@/components/TodoItemsRemaining";
import TodoCheckAll from "@/components/TodoCheckAll";
import TodoFiltered from "@/components/TodoFiltered";
import TodoClearCompleted from "@/components/TodoClearCompleted";
import { mapGetters } from "vuex";

export default {
  name: "TodoList",
  components: {
    TodoItem,
    TodoItemsRemaining,
    TodoCheckAll,
    TodoFiltered,
    TodoClearCompleted
  },
  data() {
    return {
      newTodo: ""
    };
  },
  computed: {
    ...mapGetters(["completedAll", "todosFiltered"])
  },
  created() {
    this.$store.dispatch("retrieveTodos");
  },
  methods: {
    addTodo() {
      if (this.newTodo.trim()) {
        this.$store.dispatch("addTodo", {
          id: this.idForTodo,
          title: this.newTodo
        });
      }
      this.newTodo = "";
    }
  }
};
</script>

<style lang="scss">
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
</style>

TodoItem コンポーネントを作成

親コンポーネントの TodoList から props を受け取り
個々のタスクの表示させています

frontend/src/components/TodoItem.vue
<template>
  <b-list-group-item
    class="flex-wrap d-flex justify-content-around align-items-center todo-item"
  >
    <b-col cols="2">
      <b-form-checkbox v-model="completed" @input="doneEdit"></b-form-checkbox>
    </b-col>

    <b-col cols="8">
      <label
        v-if="!editing"
        :class="{ completed: completed }"
        @dblclick="editing = true"
        >{{ title }}</label
      >
      <b-form-input
        v-else
        v-model="title"
        v-focus
        type="text"
        @blur="doneEdit"
        @keyup.enter="doneEdit"
        @keyup.escape="cancelEdit"
      />
    </b-col>

    <b-col cols="2">
      <button
        type="button"
        class="close"
        aria-label="Close"
        @click="deleteTodo(todo.id)"
      >
        <span aria-hidden="true">&times;</span>
      </button>
    </b-col>
  </b-list-group-item>
</template>
<script>
import { mapActions } from "vuex";
export default {
  name: "TodoItem",
  directives: {
    focus: {
      inserted: function(el) {
        el.focus();
      }
    }
  },
  props: {
    todo: {
      type: Object,
      required: true
    },
    index: {
      type: Number,
      required: true
    },
    checkAll: {
      type: Boolean,
      required: true
    }
  },
  data() {
    return {
      id: this.todo.id,
      title: this.todo.title,
      completed: this.todo.completed,
      editing: false
    };
  },
  watch: {
    checkAll() {
      this.completed = this.checkAll ? true : this.todo.completed;
    }
  },
  methods: {
    ...mapActions(["deleteTodo", "updateTodo"]),
    doneEdit() {
      this.editing = false;

      this.updateTodo({
        id: this.id,
        title: this.title,
        completed: this.completed,
        editing: this.editing
      });
    },
    cancelEdit() {
      this.title = this.todo.title;
      this.editing = false;
    }
  }
};
</script>

<style lang="scss" scoped>
.todo-item {
  animation-duration: 0.3s;
}

.completed {
  text-decoration: line-through;
  color: grey;
}
</style>

TodoItemsRemaining コンポーネントを作成

残りのタスク件数を表示させています

frontend/src/components/TodoItemsRemaining.vue
<template>
  <b-col cols="6">
    <span class="text-danger">{{ remaining }}</span>
    {{ remaining | pluralize("item") }} left
  </b-col>
</template>

<script>
import { mapGetters } from "vuex";
export default {
  name: "TodoItemsRemaining",
  filters: {
    pluralize: (n, w) => (n === 1 ? w : w + "s")
  },
  computed: {
    ...mapGetters(["remaining"])
  }
};
</script>

TodoFiltered コンポーネントを作成

All(全て) ・ Active(未完了) ・ Completed(完了)
の値によってタスクにフィルタをかけています

frontend/src/components/TodoFiltered.vue
<template>
  <b-col cols="6">
    <b-form-radio-group
      v-model="selected"
      :options="options"
      buttons
      button-variant="outline-primary"
      name="radio-btn-outline"
      @change="updateFilter"
    ></b-form-radio-group>
  </b-col>
</template>

<script>
import { mapState, mapMutations } from "vuex";
export default {
  name: "TodoFiltered",
  data() {
    return {
      selected: "all",
      options: [
        { text: "All", value: "all" },
        { text: "Active", value: "active" },
        { text: "Completed", value: "completed" }
      ]
    };
  },
  computed: {
    ...mapState(["filter"])
  },
  methods: {
    ...mapMutations(["updateFilter"])
  }
};
</script>

TodoClearCompleted コンポーネントを作成

完了したタスクの一括クリアボタンを表示させています

frontend/src/components/TodoClearCompleted.vue
<template>
  <b-col cols="6">
    <div>
      <b-button
        v-if="showClearCompletedButton"
        variant="outline-primary"
        @click="clearCompleted"
        >Clear Completed</b-button
      >
    </div>
  </b-col>
</template>

<script>
import { mapGetters, mapActions } from "vuex";
export default {
  name: "TodoClearCompleted",
  computed: {
    ...mapGetters(["showClearCompletedButton"])
  },
  methods: {
    ...mapActions(["clearCompleted"])
  }
};
</script>

TodoCheckAll コンポーネントを作成

タスクを一括で完了 ・ 未完了に切り替えるための
チェックボックスを表示させています

frontend/src/components/TodoCheckAll.vue
<template>
  <b-col cols="6">
    <b-form-checkbox :checked="completedAll" @change="checkAll"
      >Check All</b-form-checkbox
    >
  </b-col>
</template>

<script>
import { mapGetters, mapActions } from "vuex";
export default {
  name: "TodoCheckAll",
  computed: {
    ...mapGetters(["completedAll"])
  },
  methods: {
    ...mapActions(["checkAll"])
  }
};
</script>

ブラウザで確認

Rail のサーバを起動

terminal
rails s

Vue のサーバを起動

terminal
cd frontend
yarn serve

localhost:8080 にアクセスしてこんな画面が表示されたら成功です

Screen Shot 2019-05-27 at 11.00.41.png

Heroku へデプロイしてみる

事前準備

Heroku のアカウントがない場合はこちらから作成してください

デプロイには heroku toolbelt が必要なのでこちらからインストールしてください

mac の場合は Homebrew でインストール可能です

terminal
brew install heroku

プロジェクトを commit

プロジェクト直下へ移動し commit を行なってください

terminal
git init
git add .
git commit -m "init"

vue.config.js を作成

frontend ディレクトリの直下に vue.config.js ファイルを作成し
build ファイルの出力先をプロジェクト直下の public ディレクトリへ変更します

frontend/vue.config.js
module.exports = {
  outputDir: "../public"
};

Vue を build

terminal
yarn build

プロジェクト直下の public ディレクトリに
build されたファイルが作成されていることを確認してください

terminal
public/
├── css
│   ├── app.27d4506b.css
│   └── chunk-vendors.19588e8d.css
├── favicon.ico
├── img
│   └── logo.82b9c7a5.png
├── index.html
└── js
    ├── app.6635e2d3.js
    ├── app.6635e2d3.js.map
    ├── chunk-vendors.4ad97586.js
    └── chunk-vendors.4ad97586.js.map

Heroku にログイン

terminal
heroku login

上のコマンドを実行するとブラウザに切替わるのでボタンを押してログインしてください

Screen Shot 2019-05-27 at 11.15.45.png

Heroku にアプリを作成

アプリ名を入力すると URL にアプリ名が反映されます
https://アプリ名.herokuapp.com/
省略すると Heroku 側で自動的に割り振られます

terminal
heroku create アプリ名

Heroku のリポジトリへ push

terminal
git push heroku master

データベースの migration と テストデータを追加

terminal
heroku run rails db:migrate
heroku run rails db:seed

ブラウザで確認

terminal
heroku open

おわりに

最後まで読んでいただきありがとうございました。
おかしな部分がありましたら、ご指摘お願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VeeValidateを使用したコンポーネントのユニットテストをする方法

はじめに

VeeValidateVue Test Utils でテストができるようにしましたので備忘録として投稿します。

VeeValidateVue Test Utils でテストする方法

VeeValidateVue Test Utils でテストするとき下記のように xxx.spec.jp を作ります。

import Vuex from 'vuex'
import VeeValidate, { Validator } from 'vee-validate'
import flushPromises from 'flush-promises'
import Form from '@/components/Form.vue'

const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(VeeValidate)

describe('Form', () => {
  it('The name field is required', async () => {
    const wrapper = shallowMount(EntryForm, {
      localVue
    })

    wrapper.find('[name="name"]').setValue('')
    wrapper.vm.$validator.validateAll()
    await flushPromises()
    expect(wrapper.vm.errors.first('name')).to.equal('The name field is required.')
  })
})

おわりに

VeeValidateはフォームに入力してからエラーメッセージが表示されるまでタイムラグがあります。
初めは非同期でやっていたためにエラーメッセージが表示される前にテストが終わってエラーになってしまいました。
async , await flushPromises() を追記することで解決しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue Test Utils】複数の `it` を行うと`$validator` にデータがどんどん蓄積されてしまう問題を解決

はじめに

VeeValidateを使っているコンポーネントを対象に複数の it を使ってテストしています。
$validator が前の it でのデータを保有したまま次の it を実行するため、データがどんどん蓄積されています。
そのせいで、2つ目以降の it テストで期待した通りの結果が返ってこなくて FAIL になってしまいます。

wrapper.vm.$validator // これにデータが蓄積されてしまう

解決方法

$validator がリセットされれば良いので、 xx.spec.js の中に

  afterEach(() => {
    wrapper.vm.$validator.reset()
    wrapper.destroy()
  })

を入れてあげます。
これで it ごとに$validatorがリセットされるようになり、期待した通りの結果が返ってくるようになりました。

おわりに

VeeValidateを使っているコンポーネントを対象にしたテストを行うときは$validatorの状態に気をつけようと思いました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[vee-validate] 「Validating a non-existent field: "". Use "attach()" first.」エラー問題を解決

はじめに

VeeValidateを使っているコンポーネントをテストしたかったのですが、下記のようにエラーが吐き出されてしまいます。

[vee-validate] Validating a non-existent field: "#2". Use "attach()" first.

解決方法

テスト対象のコンポーネント側に下記の1行を追記します。

  beforeDestroy() { this.$validator.pause() }

おわりに

この答えにたどり着くまで丸二日間かかりました。
it を複数用意して非同期でテストを実行していたのが原因だったかなと思います。(よくわからないけど)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「[vee-validate] Validating a non-existent field: "". Use "attach()" first.」エラー問題を解決

はじめに

VeeValidateを使っているコンポーネントをテストしたかったのですが、下記のようにエラーが吐き出されてしまいます。

[vee-validate] Validating a non-existent field: "#2". Use "attach()" first.

解決方法

テスト対象のコンポーネント側に下記の1行を追記します。

  beforeDestroy() { this.$validator.pause() }

おわりに

この答えにたどり着くまで丸二日間かかりました。
it を複数用意して非同期でテストを実行していたのが原因だったかなと思います。(よくわからないけど)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画像処理100本ノックにJavaScriptで挑戦してみた 【画像処理100本ノックJS】

test.gif

概要

画像処理100本ノックをJavaScriptで挑戦してみました。
「ブラウザ上で完結させたい」 & 「デモを共有できたら面白い」という動機ではじめました。
まだ100問完了していませんが、ここまで解いてみた所感を書きます。

とりあえず、、、「100問は辛いです。」

画像処理100本ノックについて

画像処理が初めての人のための問題集をつくったりました。(完成!!) 研究室の後輩用に作ったものです。 自然言語処理100本ノックがあるのに、画像処理のがなかったので作ってみました。 画像処理の基本のアルゴリズム理解につながると思います。 https://qiita.com/yoyoyo_/items/2ef53f47f87dcf5d1e14

GitHub Qiita

この「画像処理100本ノック」にはPythonとC++のコードが解答例として用意されています。

デモの例 ※ → デモ はこちらから(Gasyori100KnockJS)

いくつかのデモの例を紹介します。


201904071958_njvf00.png
大津の2値化


20190518004109.png
プーリング (MAX値)


 2019-05-26 19.56.11.png
メディアンフィルタ



アフィン変換 (スキュー)


 2019-05-26 19.51.21.png
テンプレートマッチング


 2019-05-26 19.57.46.png
バイリニア補間


 2019-05-26 20.03.21.png
JPEG圧縮 (Step.2)DCT+量子化


 2019-05-26 19.53.02.png
Harrisのコーナー検出

実装の紹介

GitHub にソースを置いてます。

canvasを使った画像の表示と操作

画像の読み込み

画像の表示ピクセル値の操作にはcanvasのAPIを利用しています。

任意の画像を読み込む際には次のように実装しています。

<canvas id="canvas"></canvas>
// canvas関連のオブジェクト
const canvas = document.getElementById("canvas")
const ctx = canvas.getContext("2d")

// 任意の画像読み込み
let image = new Image()
image.src = "path/to/image.png"

// 読み込み完了時のイベント
image.onload = () => {
    canvas.width = image.width
    canvas.height = image.height
    ctx.drawImage(image, 0, 0, canvas.width, canvas.height)
    // canvas描画後、画像の処理を実行
}

ピクセル操作

html5-canvas-imageData-example.png

CREATE A PAINT BUCKET TOOL IN HTML5 AND JAVASCRIPT
http://www.williammalone.com/articles/html5-canvas-javascript-paint-bucket-tool/

より

getImageDataメソッドでImageDataオブジェクトを取得しています。
このオブジェクトにはr, g, b, a の順に画像情報が格納されています。
putImageDataを使って編集したImageDataオブジェクトをcanvasに描画しています。

これを用いることにより大概の画像の処理を行うことができます。

(canvasを用いず、Imageオブジェクトの画像情報に対して直接参照する方法があればいいんですけど... )

let src = ctx.getImageData(0, 0, canvas.width, canvas.height)
let dst = ctx.createImageData(canvas.width, canvas.height)
for (let i = 0; i < src.data.length; i += 4) {
    dst.data[i] = src.data[i]          // r
    dst.data[i + 1] = src.data[i + 1]  // g
    dst.data[i + 2] = src.data[i + 2]  // b
    dst.data[i + 3] = src.data[i + 3]  // a (透過度)
}

例えば、グレースケール画像であれば次のような処理になります。

const grayscale = (r, g, b) => 0.2126 * r + 0.7152 * g + 0.0722 * b
// 略
for (let i = 0; i < src.data.length; i += 4) {
    let gray = grayscale(src.data[i], src.data[i + 1], src.data[i + 2])
    dst.data[i] = gray[0]
    dst.data[i + 1] = gray[1]
    dst.data[i + 2] = gray[2]
    dst.data[i + 3] = src.data[i + 3]
}
ctx.putImageData(dst, 0, 0)

こんな風に表示されます。

201904071946_398cds.png

参考 : 画像をグレースケールに変換する JavaScript + canvas 【画像処理】

ヒストグラムの表示

 2019-05-26 1.46.55.png

このデモでは、ヒストグラムの表示にChart.jsを使っています。

実装については次のように行なっています。

ヒストグラム描画

import Chart from "chart.js"

export default class Histogram {
  /**
   * ヒストグラムを描画する
   * @param {Object} canvas 
   * @param {Object} data 
   */
  static renderHistogram(canvas, data) {
    let labels = new Array(data.length).fill('')
    new Chart(canvas, {
      type: 'bar',
      data:{
        labels,
        datasets: [
          {
            label: '画素値',
            data,
            backgroundColor: "rgba(80,80,80,0.5)"
          }
        ],
      },
      options: {
        title: {
          display: true,
          text: 'Histogram'
        },
        scales: {
          yAxes: [{
            ticks: {
              suggestedMin: 0,
            }
          }]
        },
        animation: {
          duration: 0
        }
      }
    })
  }
}
import Histogram from 'path/to/Histogram'

const grayscale = (r, g, b) => 0.2126 * r + 0.7152 * g + 0.0722 * b
// 略
let pixelValues = new Array(255).fill(0)
for (let i = 0; i < src.data.length; i += 4) {
    let gray = grayscale(src.data[i], src.data[i + 1], src.data[i + 2])
    gary = Math.floor(gray)
    pixelValues[gray]++
}
Histogram.renderHistogram(canvas, pixelValues)

参考 : 画像のヒストグラムを表示する Char.js JavaScript canvas

フレームワークにVueを使っています。
またSPAにも挑戦しました。

コンポーネントの制御が難しく、処理がバグっている箇所があると思います()

まとめ : JSで挑戦するメリット・デメリット

ブラウザ上で動かせるのがJSを使う最大のメリットだと思います。
加えて、チャート系のライブラリが豊富なので、matplotlibに比べ、グラフィカルな表現がしやすいのも良い点だと感じました。

一方で、行列演算に関してはJSではnumjsやmath.jsといったものはありますが、
Numpyほど簡潔に行列の処理を書くことはできません。

(※今回のデモではアフィン変換などの行列演算を多用する箇所で math.js を使いました。)

また、フーリエ変換のデモでは、実装に複素数を利用しますが、
Pythonは「j」が利用できるのに対し、JSの場合は実部と虚部に分けるような処理に実装する必要がありました。

改めてPython、Numpyの偉大さには感謝したいと思います。


画像処理100本ノックJS
https://s-yoshiki.github.io/Gasyori100knockJS/#/


画像処理100本ノックJS - GitHub
https://github.com/s-yoshiki/Gasyori100knockJS


JavaScriptで画像処理100本ノックに挑戦してみた
https://tech-blog.s-yoshiki.com/2019/03/1094/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む