20200322のvue.jsに関する記事は15件です。

Vue.jsでぐるなびAPIからデータを取得する

環境

macOS Catalina 10.15.3
vue 3.0.1

vue-cliでプロジェクトを作成

ターミナルで以下のコマンドを叩く

$ vue crate my-project

presetはBabel,Router,Vuex,Linter/Formatter,Unit Testingを選択しています

作成できたら、一度動作確認

$ cd my-project
$ npm run serve

これでlocalhost:8080に接続
以下の画面が表示されるはずです

ぐるなびAPIのアクセスキーを取得

ぐるなびAPIを使用するにはアクセスキーが必要になるので、以下のサイトから取得してください
ぐるなび Web Service

ぐるなびAPIからデータを取得

まず/srcの直下にapiというディレクトリを作成します。
この中にgnavi.jsというファイルを作成し、この中にぐるなびAPIからデータを取得する関数を定義します。

my-project
 └── src/
     └── api/
         └── gnavi.js

単一ファイルに直接メソッドを定義することもできますが、分けた方が保守性が高くなることやコードの重複を減らすことができます。

データの取得には、axiosを使用して取得するため、インストールしていない方は以下のコマンドを叩いてください。

$ npm install --save axios

では、ぐるなびAPIからデータを取得していきます。

gnavi.js
import axios from 'axios'

export default {
  searchShops(shopName) {
    // Promiseを返す
    return new Promise((resolve, reject) => {
      axios
        .get("https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=<ぐるなびAPIから取得したアクセスキー>", {
          // 店名検索
          params: {
            name: shopName
          })
          // 検索結果をresolve
          .then(shops => {
            resolve(shops.data.rest);
          })
          // エラーハンドリング
          catch(error => {
            const errorStatus = error.response.status;

            switch (errorStatus) {
              case 400:
                reject("不正なパラメータが指定されました");
                break;

              case 401:
                reject("不正なアクセスです");
                break;

              case 404:
                reject("お店が見つかりませんでした");
                break;

              case 405:
                reject("不正なアクセスです");
                break;

              case 429:
                reject("リクエスト回数上限超過");
                break;

              case 500:
                reject("処理中にエラーが発生しました");
                break;

              default:
                reject("不明なエラーです");
                break;
            }
        });
    })
  }
}

これでぐるなびAPIからデータを取得することができます。
これから、このデータを表示するためにsrc/components/HelloWorld.vueを書き直します。

HelloWorld.vue
<template>
  <v-container>
    <v-row>
      <v-col cols="12" xs="10" sm="8" md="6" lg="4">
        <v-text-field label="店名" v-model="shopName" />
      </v-col>
      <v-col cols="2">
        <v-btn @click="loadShops" :loading="loading">検索</v-btn>
      </v-col>
    </v-row>

    <v-alert v-if="error_msg" type="error">{{ error_msg }}</v-alert>

    <v-row v-if="shops">
      <v-col cols="12" xs="12" sm="6" md="4" lg="3" v-for="(shop, index) in shops" :key="index">
        <v-card>
          <v-img :src="shop.image_url.shop_image1" />
          <v-card-title>{{ shop.name }}</v-card-title>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
import gnavi from "@/api/gnavi.js";

export default {
  name: "HelloWorld",

  data() {
    return {
      shopName: null,
      shops: null,
      error_msg: null,
      loading: false
    };
  },

  methods: {
    loadShops() {
      this.shops = null;
      this.error_msg = null;
      this.loading = true;

      gnavi
        .searchShops(this.shopName)
        .then(res => {
          this.shops = res;
        })
        .catch(err => {
          this.error_msg = err;
        })
        .finally(() => {
          this.loading = false;
        });
    }
  }
};
</script>

デザインを簡単に整えるためにvuetifyを使用しています。
特に今回の内容とは、関係ないので導入などは省略します。

実行すると、こんな感じになります。

最初の画面
スクリーンショット 2020-03-22 22.55.40.png

やまやと入力して検索ボタンを押すと
スクリーンショット 2020-03-22 22.56.03.png

検索結果が0件の場合
スクリーンショット 2020-03-22 22.56.17.png

まとめ

今回は、ぐるなびAPIからデータを取得しました。
Vue.jsやPromiseなどの説明を省いたので、Vue.jsやJavaScriptに触れたことがない方は、わからないところがあったかもしれません。
ただ、今回のコードをコピペすれば、とりあえず動くと思うので(vuetifyをインストールすれば)、いろいろ試して見てください!

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

Vue-CLI3でBootstrap-Vue

背景

  • Bootstrapが完全にVue上コンポーネントにラッピングされたBootstrapVueとVue CLIの同時導入に関する記事です
    • VueをCDNで導入するとBootstrapに必要なjQueryと衝突することがあるので試してみた次第.
  • Vue CLI 3を使います.Vue CLIは2系の紹介サイトは多いのですが,3はまだ少ないように思うので,情報の足しになれば.

こんな人に

  • VueとBootstrapをCDNで使っていた
  • Vue Componentの知識はある
  • Vue CLIで初めてWebpack,Babel,ESlintに触れる

環境

  • node v8.16.0
  • Vue CLI 3.9.3
  • BootstrapVue 2.6.1

Vue CLI3導入

Vue CLIはnodeベースのVue webアプリケーションの開発ツール.
Vueは日本語情報あるが,Vue CLIは英語ページしかなく,またVue CLI 2系と扱いも異なるので注意.

導入は基本的には本家ページを参照

$ npm install -g @vue/cli
+ @vue/cli@3.9.3
$ vue --version
3.9.3

注意点として,node v8.9以上が必要なので,nodebrewなどでインストール・使えるようにしておくこと

プロジェクトの作成

$ vue create プロジェクト名

を実行して作成.質問はそのままEnterで問題なし.すると,現在のフォルダに "プロジェクト名" のフォルダとファイル類ができる.(以下ではproject-sampleとします)

フォルダ構成

project-sample/
 ├ package.json
 ├ vue.config.js : vueだけでなくwebpackに関するconfigもここ
 ├ babel.config.js : babelに関するconfig
 ├ src : ソースファイル
 |  ├ main.js : アプリのエントリ
 |  ├ App.vue : main.jsで読まれるメインページのコンポーネントファイル
 |  ├ components : App.vue以外のコンポーネントファイル
 |  ├ assets : コンポーネントで使う画像やcssなど
 |  ├ plugins : main以外のjsファイル
 ├ public : 静的ファイルとテンプレートデータ
 ├ dist : 後述のbuildするとここにできる

ローカルで実行

$ cd project-sample
$ npm run serve

ブラウザでlocalhost:8080にアクセスし,Hello World的なのが表示されればOK.

Vue CLIのコマンド(npm runの後にserve,build,lint)は現在のプロジェクトルート(project-sample)下で使います.

この状態でApp.vueなどのファイルを変更すると,リアルタイムで更新されます.

私の環境(Mac OS 10.12/Safari 12.1)では更新されたりされなかったりしたため,Google Chromeを使用している.ビルドエラーやESlintのエラーがあればブラウザにも表示されます.

ビルド

本番用ファイルにwebpackでバンドルする.

$ npm run build

distフォルダにできたファイルをサーバに入れ,

BootstrapVue導入

プロジェクトルートで以下コマンドでインストール

$ npm install vue bootstrap-vue bootstrap

main.jsを変更

main.js
import Vue from 'vue'
import {BootstrapVue, IconsPlugin} from 'bootstrap-vue' //追加
Vue.use(BootstrapVue) //追加
Vue.use(IconsPlugin) //追加
import 'bootstrap/dist/css/bootstrap.css' //追加
import 'bootstrap-vue/dist/bootstrap-vue.css' //追加
import App from './App.vue'

Vue.config.productionTip = false

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

使い方

Vue Componentになっているものは公式Docsで参照できますが,基本的にはxxのコンポーネントは<b-xx>という形のタグで使うことになります.例えば,

before
<button class="btn btn-primary">ボタン</button>

after
<b-button variant="primary">ボタン</b-button>

というようになります.色はvariant,サイズはsizeといったclassとは別のattributeが割り当てられています.

注意点

コンポーネントへのスタイル適用

VueのComponentはcssのタグセレクタでヒットしないので,以下の使い方ができません.

vue
<template>
 <div class="test">
  <b-button>1</b-button>
  <b-button>2</b-button>
 </div>
</template>
(中略)
<style>
.test b-button {
  background-color: gray;
}
</style>

実際のレンダリングに対応するセレクタを使うか,それぞれクラスで指定するなりが必要です.

.test button {
  background-color: gray;
}
</style>

他にも,コンポーネントによってはdiv下にbuttonがあったりとどこにスタイルが適用されるかわかりにくい場合もあるのが厄介でした.

その他設定

ビルド後ファイルをサーバのルートに置かない場合

ビルド後のファイル(js,css)のロードは,デフォルトではルートからのディレクトリで指定される.例えばexample.com/test/の下にdist内のファイルを配置するような場合は以下の設定が必要

vue.config.js
module.exports = {
  publicPath : './'
}

console.logを使う場合

何も設定しないとESLintに怒られるので設定.

package.json
(中略)
"eslintConfig": {
  "rules": {
    "no-console": "off"
  }
}

serveとbuildでimport先を変更

開発時はダミーで,実装時はDB読むようにしたい場合には,Webpackの際にprocess.env.NODE_ENVで切り替える必要があります.aliasで設定された名前がjsのimportで使えることを利用して切り替えてみました.

jsもしくはvueファイル
(中略)
import SomeModule from 'moduleName'
vue.config.js
const path = require('path')
let mod = 'src/plugins/dev.js'
if(process.env.NODE_ENV=='production'){
  mod = 'src/plugins/prod.js'
}
module.exports = {
  (中略)
  configureWebpack: {
    resolve: {
      alias: {
        'moduleName$': path.resolve(__dirname,mod)
      }
    }
  }
}

Sass利用+全コンポーネント共通ファイル

Sass導入は以下コマンドでインストール

$ npm install -D sass-loader sass

vueファイルのstyleでlangを指定すると使える(以下の場合はscssで)

some.vue
<style lang="scss">
</style>

共通ファイル(例えばsrc/assets/global.scss)はvue.config.jsに以下設定

vue.config.js
module.exports = {
  css: {
    loaderOptions: {
      prependData: '@import "src/assets/global.scss";'
    }
  }
}

変数,mixinの定義に便利だが,通常のセレクタを書いてしまうと二重,三重の定義になってしまうのであまりよろしくなさそう.

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

FullCalendar with Vue で、イベントの日の日付数字に色をつける(例:日本の祝日)

前提

google calendar apiを利用してイベントを取得してFullCalendarによって作られたカレンダーに表示します。
今回は、日本の祝日を取得して休みの日の日付は赤にしたいな〜、なんて思います。

FullCalendar についての情報

VueComponent
https://fullcalendar.io/docs/vue

events from Google Calendar
https://fullcalendar.io/docs/google-calendar

google calendar api

FullCalendarでgoogle calendar APIを使うので、クライアント用のAPI keyを取得
(上記のevents from Google Calendar参考)

コンポーネントを書く

  • 祝日の他にも予定が入っているカレンダーを取得したかったので、eventSourcesを使ってgoogle calendarから予定を取得します。

  • 祝日は、公開されているカレンダーに「日本の祝日(カレンダーID: japanese__ja@holiday.calendar.google.com)」があるので、それを利用。

  • 取得成功で、呼び出されるコールバックで、日にちが入っているDOMを読み込む
    参考:https://fullcalendar.io/docs/event-source-object

  • DOMにはdataset.dateに年月日がセットされているので、コールバック関数で取得できる祝日の日付とつけあわせて、一致していたら、DOMにクラスを追加。

あとは、cssで見た目をセットすればよいんじゃないでしょうか?

ということで、実際のコード↓

calendar
<template>
  <FullCalendar
    default-view="dayGridMonth"
    :plugins="calendarPlugins"
    :googleCalendarApiKey="googleCalendarApiKey"
    :eventSources="eventSources"
  />
</template>

<script>
import FullCalendar from '@fullcalendar/vue'
import dayGridPlugin from '@fullcalendar/daygrid'
import googleCalendarPlugin from '@fullcalendar/google-calendar';

export default {
  components: {
    FullCalendar
  },
  data() {
    return {
      calendarPlugins: [ 
        dayGridPlugin, googleCalendarPlugin
      ],
      googleCalendarApiKey: '取得したgoogle API keyを記載',
      eventSources: [
        {
          googleCalendarId: 'japanese__ja@holiday.calendar.google.com',
          success:function(e) {
            let days = document.getElementsByClassName('fc-day-top');

            e.forEach(el => {
              for(let i=0; i<days.length; i++) {
                let day = days[i]
                if (el.start === day.dataset.date) {
                  day.classList.add('is_holiday')
                }
              }

            });
          }
        },
      ],
    }
  }
}

</script>
<style>
   @import '~/node_modules/@fullcalendar/core/main.css';
   @import '~/node_modules/@fullcalendar/daygrid/main.css';
</style>

あとがき

apiで取得したデータで祝日リストを作って、dayRendarにわたそうとか、いろいろこころみたのですが、thisの中身がVueのコンポーネントにならないとか、いろいろあって、かなり手間取った結果、かなりシンプルに落ち着いたかと。

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

【Vue.js】テンプレートリテラル記法メモ

JSファイルのバッククォート内にHTMLタグごと記述する

下記のコードで「こんにちは」と表示される

index.html
<div id="app">

</div>
index.js
const vm = new Vue({
  el: '#app',
  template: `
    <div v-if="message">
    {{ message }}
    </div>
    <div v-else>
      メッセージがありません
    </div>
  `,
  data() {
    return {
      message: 'こんにちは'
    }
  }
})

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

Nuxt.js 存在しないページを一括でリダイレクトしたい時

/pages/*.vue
を下記の内容で作成する。

<template>
  <div>
    404 redirect
  </div>
</template>
<script>
export default {
  middleware({ redirect }) {
    return redirect('/')
  }
}
</script>

That's it.
Relax, man.

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

Nuxt.js 存在しないページへのアクセスを一括リダイレクトしたい時

/pages/*.vue
を下記の内容で作成する。

<template>
  <div>
    404 redirect
  </div>
</template>
<script>
export default {
  middleware({ redirect }) {
    return redirect('/')
  }
}
</script>

That's it.
Relax, man.

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

EC2上でvue+expressの環境構築からAPI通信実装まで

バックエンド

EC2インスタンス上でexpressを動かし、アプリケーションのbackend環境を構築する。
ここでは例としてアプリケーションappを作る

環境構築

AMI:Ubuntu18.04、IP:x.x.x.x の場合

$ sudo apt-get update
$ sudo apt-get install nodejs
$ sudo apt-get install npm
$ sudo update-alternatives --install /usr/bin/node node /usr/bin/nodejs 10
$ sudo npm install -g express
$ sudo npm install -g express-generator
$ express --view=pug app
$ cd app
$ npm install
$ npm start
> app@0.0.0 start /home/ubuntu/app
> node ./bin/www

AMI:AmazonLinux2、IP:x.x.x.x の場合

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
$ . ~/.nvm/nvm.sh
$ nvm install node
$ sudo npm install -g express
$ sudo npm install -g express-generator
$ express --view=pug app
$ cd app
$ npm install
$ npm start
> app@0.0.0 start /home/ubuntu/app
> node ./bin/www

app/bin/wwwファイルでポート番号を指定できる。デフォルトではポート3000なので、ブラウザからアクセスして確認できる。

APIの作成

app下にindex.jsを作成し、以下を記述。

GET
// expressモジュールを読み込む
const express = require('express');

// expressアプリを生成する
const app = express();

// ルート(http://x.x.x.x:3000/)にアクセスしてきたときに「Hello」を返す
app.get('/', (req, res) => res.send('Hello'));

// ポート3000でサーバを立てる
app.listen(3000, () => console.log('Listening on port 3000'));
POST
const express = require('express');
const bodyParser = require('body-parser');

const app = express();

// urlencodedとjsonは別々に初期化する
app.use(bodyParser.urlencoded({
    extended: true
}));
app.use(bodyParser.json());

app.post('/', function(req, res) {
    console.log(req.body);
    res.send('POST request to the homepage');
})

// ポート3000でサーバを立てる
app.listen(3000, () => console.log('Listening on port 3000'));
$ node index.js # 実行
Listening on port 3000

POSTメソッドでは、body-parserモジュールを用いてJSONで取得する。

テスト

APIを叩く。

ブラウザから(GET)

x.x.x.x:3000/にアクセス。

コンソールから(POST)

POST
$ curl -X POST http://x.x.x.x:3000/ -H "Accept: application/json" -H "Content-type: application/json" -d '{ "name" : "tanaka" }'

フロントエンド

環境構築

vueの環境構築はこちらの記事で書いた通りなので省略。

フロントエンドから外部APIを叩く

デフォルトのAboutページにボタンをつくり、押したら非同期で外部API(httpbin)を叩くようにしてみる。

必要なものをいれる。

$ npm install axios vue-axios
main.ts
import Vue from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import router from './router'
import store from './store'
import axios from 'axios' //追記
import VueAxios from 'vue-axios' //追記

Vue.config.productionTip = false
Vue.use(VueAxios, axios) //追記

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
About.vue
<template>
  <div class="about">
    <h1>This is an about page</h1>
    <button @click="clicked" >Call API</button>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';

@Component
export default class About extends Vue {
  private clicked():void {
      this.axios.get('https://httpbin.org/get')
        .then((response) => {
          console.log(response);
        })
        .catch((e) => {
          console.log(e);
        });
  }
}
</script>

consoleにデータが来てれば成功。

フロントエンドからexpressの内部APIを叩く

上のコードのURLをそのままhttp://x.x.x.x:3000とすると以下のCORSのエラーが出る。

Access to XMLHttpRequest at http://x.x.x.x:3000/ from origin http://x.x.x.x:8080 has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

CORS(Cross-Origin Resource Sharing)とは何か?

あるオリジンで動いている Web アプリケーションに対して、別のオリジンのサーバーへのアクセスをオリジン間 HTTP リクエストによって許可できる仕組み

引用) https://qiita.com/att55/items/2154a8aad8bf1409db2b

どうしたらいいか

index.js(CORS設定版)
const express = require('express');
const bodyParser = require('body-parser');
const app = express();

// CORSを許可する
app.use(function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");

    // OPTIONSでメソッドがバレないようにする。 (?)
    if ('OPTIONS' === req.method) {
        res.send(200)
    } else {
        next()
    }
});

app.use(bodyParser.urlencoded({
    extended: true
}));
app.use(bodyParser.json());

// POST
app.post('/', function(req, res) {
    console.log(req.body);
    res.send('Hello');
})
// GET
app.get('/', (req, res) => res.send('Hello'));

app.listen(3000, () => console.log('Listening on port 3000'));

上記のコードにしてフロントとバックを起動後、GETでコールしてみる。

$ cd app
$ node index.js #バックエンド起動
$ cd front
$ npm run serve #フロントエンド起動

コンソールに以下が得られたら成功。

{data: "Hello", status: 200, statusText: "OK", headers: {}, config: {}, …}

Appendix

EC2インスタンスに置いたvueファイルを更新するのにいちいちscpとかviとかしてるのはだるすぎるのでVSCodeから直接SSHでいじるための方法。

VSCode拡張機能であるremote developmentを入れるて以下に設定ファイルを書いておく。

.ssh/config
Host 任意の接続名   //configには何個も接続先とキーとかの情報を書けるのでそれらを区別する名前。
  HostName x.x.x.x
  User ec2-user
  Port 22
  IdentityFile /Users/xxx/.ssh/hoge.pem

参考

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

【vue.js】3桁ごとにカンマを入れる正規表現【ES6】

3000000→3,000,000を表示されるようにしたい

index.html
<body>
  <div id="app">
    <p>
      合計 {{ sum | numberWithDelimiter }}円  
    </p>
  </div>
  <script src="js/index.js"></script>
</body>
index.js
new Vue({
  el: '#app',
  data() {
    return {
      sum: 3000000
    }
  },
  filters: {
    numberWithDelimiter(value) {
      if(!value) return '0'
      return value.toString().replace(/(\d)(?=(\d{3})+$)/g, '$1, ')
    }
  }
})

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

Vue.js入門6(私的メモ)

Vue.jsの高度な機能

  • トランジションアニメーション
  • スロット
  • カスタムディレクティブ
  • 描画関数
  • ミックスイン

メモ書き程度に。

トランジションアニメーション

CSS3やWeb Animation APIといったウェブ標準の規格、Velocity.jsやAnime.jsなどに代表されるアニメーションライブラリなど、アニメーションの実装は以前より比較的容易になっている。

Vue.jsでは、上記の標準仕様やライブラリと連携してトランジションアニメーションを容易に実装できる。

transitionラッパーコンポーネント

transitionラッパーコンポーネントは、囲んでいるコンポーネントあるいは要素の表示状態に応じてアニメーションに関する処理を行うコンポーネント。

具体的には、アニメーションの開始や終了といった各ポイントで、v-enter、v-enter-activeなどの事前にVue.jsが定めたクラスを付け替える。

transitionコンポーネントは、自身が囲むコンポーネントあるいは要素が出入り(enter/leave)する際にトランジションを追加する。

具体的には、

  • v-ifの条件が変わる
  • v-showの条件が変わる
  • 動的コンポーネント(componentコンポーネント)のis属性値が変わる

といった場合に起こる。

トランジションクラス

アニメーションを実現するためのクラスが適切なタイミングで付与される。

クラス名 内容
v-enter 要素が挿入されるに付与され、アニメーション開始時に削除されるクラス。アニメーションの初期スタイルを適用するために使用。挿入される要素が最初にどういう状態でいてほしいか。
v-enter-to 挿入アニメーションの開始時に付与され、アニメーション終了時に削除されるクラス。アニメーション終了時のスタイルを適用するために使用。挿入される要素が最終的にどういう状態でいてほしいか。
v-enter-active 要素の挿入前からアニメーション終了まで付与されるクラス。トランジションの設定を書くために使用。挿入アニメーションはどのように変化するのか。
v-leave 削除のアニメーションの開始前に付与され、アニメーション開始時に削除されるクラス。削除時のアニメーションの初期スタイルを適用するために使用。削除される要素が最初にどういう状態でいてほしいか。
v-leave-to 削除のアニメーションの開始時に付与され、アニメーション終了時に削除されるクラス。アニメーション終了時のスタイルを適用するために使用。削除される要素が最終的にどういう状態でいてほしいか。
v-leave-active 削除のアニメーションの開始前から終了後まで付与されるクラス。トランジションの設定を書くために使用。削除アニメーションはどのように変化するのか。

デフォルトのクラス名はv-がプレフィックスとなっているが、transitionコンポーネントに対してname属性をつけることで変更することができる。

<transition name="slide">
...
</transition>
/*
v-enter {
  ...
}
*/

.slide-enter {
  ...
}

<button type="button" name="button" @click="fadeFlag=!fadeFlag">アニメーション</button>
<transition name="fade">
  <!-- この中の要素を対象にアニメーションを行う -->
  <p id="fade-block" v-show="fadeFlag">スライド要素</p>
</transition>
#fade-block {
  padding: 50px;
  text-align: center;
  background-color: indianred;
}

.fade-enter,
.fade-leave-to {
  opacity: 0;
}

.fade-enter-to,
.fade-leave {
  opacity: 1;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.2s;
}

ダウンロード.gif

ブレークポイントは

  1. 検証でDevToolsを開く
  2. Elementsタブでアニメーション要素を右クリック
  3. Break onのattribute modificationsを選択

として確認する。


Vue Routerでもトランジションが使用できる。

使用方法は、router-view要素をtransitionコンポーネントで囲うだけ。


カスタムトランジションクラスについては公式サイトで確認。

JavaScriptフック

アニメーションに要素の大きさや画面上での位置、コンポーネントの状態といった動的な値を用いる場合、JavaScriptを用いる必要がある。

このようなケースに対応するため、transitionコンポーネントはJavaScriptによる処理をアニメーションの過程でフックできる機能を持つ。

以下のイベントに対してフックできる。

イベント名 タイミング
before-enter 要素が挿入される前
enter 挿入されたのちにアニメーションが始まる前
after-enter 挿入アニメーション後
enter-cancelled 挿入キャンセル後
before-leave 削除アニメーションが実行される前
leave 削除アニメーションが実行される前でbefore-leaveの後
after-leave 要素が削除された後
leave-cancelled 削除キャンセル後

イベントリスナはv-on(@)で設定する。

フックの第一引数には、トランジション対象のDOM要素が渡される。

enterとleaveのフックは、第二引数にイベント完了をVue.jsに伝えるためのコールバックを受け取る。
Vue.jsはデフォルトでtransitionendやanimationendイベントでトランジション(アニメーション)の終了を検知するが、CSSを使用せずにJavaScriptのみで実現する場合は、コールバックを使用する必要がある。
この場合、デフォルトの検知を無効化する:css="false"を設定しておくのが良い。

詳しくは公式サイトを見る。

スロット

Vue.jsのコンポーネントは、基本的に間のコンテンツ(他のコンポーネント、要素、テキストノード)は無視されてレンダリングされる。

しかし、コンポーネントによっては状況に応じて外からコンテンツを流した方が再利用性が高まることがある。

この仕組みをスロットと呼ぶ。

(例)モーダルウィンドウ

モーダルウィンドウの内容は、それが開かれるタイミングによって異なる。
この場合、単純にテキストを表示するだけならば、プロパティで渡してしまえば良い。

しかし、メッセージ内で強調表現を行ったり、メッセージ内のURLをリンク化したりする必要があった場合、プロパティだけでは間に合わない。

このような時に外からコンテンツを受け付けるスロットを利用することで柔軟性が増す。

そのほかにも、

  • ページ全体のレイアウトを表現するコンポーネントに、ページのヘッダー・ボディ・フッターを挿入
  • アクションシート(選択機能を持った要素の群)のコンポーネントに対して、選択可能なアクションを挿入する
  •  スライダーの各アイテムの内容を指定する

などの使用場面がある。


スロットには単一スロット(名前なしスロット)と名前付きスロットの大きく分けて二種類がある。

単一スロット

スロットに名前をつけないバージョン。

const MyButton = {
  template: `<button :style="{ backgroundColor: bg, color: text, border: borderLine }">
               <slot>OK</slot>
             </button>`,
  data: function() {
    return {
    }
  },
  props: {
    bg: {
      type: String,
      default: "white",
    },
    text: {
      type: String,
      default: "black",
    },
    border: {
      type: Boolean,
      default: true,
    },
  },
  computed: {
    borderLine: function() {
      return this.border ? "1px solid gray" : "none";
    },
  }
};
<my-button></my-button> <!-- デフォルトのOKが適用 -->
<my-button>送信</my-button> <!-- 送信が適用 -->
<my-button :bg="'skyblue'" :text="'white'" :border="false">作成</my-button> <!-- 作成が適用 -->
<my-button :bg="'indianred'" :text="'white'" :border="false">削除</my-button> <!-- 削除が適用 -->

スクリーンショット 2020-03-20 15.54.58.png

名前付きスロット

名前で指定した特定の箇所にスロットを挿入することができる。

位置が決まっているようなコンポーネント、例えばページのレイアウトなどに利用できる。

//試しにモーダルウィンドウ?を作ってみた
//ウィンドウを開くボタンのテキストはプロパティで渡す
//modalコンポーネント下にはテキストとボタンを渡す
//再利用性低いな

const Modal = {
  template: `<div id="modal">
               <button @click="toggle" v-show="!openFlag">{{ btnText }}</button>
               <transition name="fade">
                 <div id="modal-outer" v-show="openFlag">
                   <button id="modal-close" @click="toggle" v-show="openFlag">×</button>
                   <div id="modal-inner">
                     <slot name="text"><span>ここに説明が入ります</span></slot>
                     <slot name="btn"><button type="button" name="button">OK</button></slot>
                   </div>
                 </div>
               </transition>
             </div>`,
  props: {
    btnText: {
      type: String,
      default: "開く",
    }
  },
  data: function() {
    return {
      openFlag: false,
    }
  },
  methods: {
    toggle: function(e) {
      this.openFlag = !this.openFlag;
    },
  },
};

ダウンロード (1).gif

スロットのスコープ

Vue.jsでは、親コンポーネントのテンプレートで行われるデータバインディングは、スロットとして挿入されるコンテンツであっても、親のコンポーネントのスコープが適用される。

●スコープ付きスロット

コンポーネントを使う側でコンポーネントの動作をコントロールしたい場合などに、子コンポーネントのデータにアクセスしたいことがある。

このような時、slot-scope/slot(現在は非推奨)またはv-slotを用いて親コンポーネントから渡されたデータをスコープとしてまとめて受け取ることにより、子コンポーネントのデータにアクセスすることができる。

下の順に理解した(理解するまで時間かかった)。

  1. スロットは親コンポーネントから子コンポーネントを使用する時、親側で子コンポーネントに渡すモノを決定できる機能
  2. 子コンポーネント側でデータをバインド(v-bind:xxx or :xxx)しておくと、そのデータ(スロットプロパティ)を親側で操ることができる
  3. 子コンポーネントのスロットに名前をつけると、親側でアクセスする時に、子側のどこに何を流し込むかが決定しやすくなる
  4. 親コンポーネントから子コンポーネントのデータにアクセスする時は、slot-scope/slot(非推奨)かv-slot(推奨)で名前を指定し、スロットプロパティの受け口を作る

詳しくは公式サイトや以下を確認。

カスタムディレクティブ

ディレクティブが行なっていることは、内部で与えられたデータに応じたDOM操作である。

これにより、複雑なDOM操作なしに動的なUIを実現できる。

(カスタムディレクティブとコンポーネント・ミックスインの違いは使いながら理解していく)

カスタムディレクティブの定義

ディレクティブにはローカルディレクティブとグローバルディレクティブが存在する。

●グローバルディレクティブ

グローバルに登録した場合、アプリケーション全体で利用できる。

Vue.directive("ディレクティブ名", {/* ディレクティブ定義オブジェクト */});

●ローカルディレクティブ

特定のコンポーネントに適用されるディレクティブ。

directives: {
  "ディレクティブ名": {
    // ディレクティブ定義
    bind: function (el) {
      //設定
    }
  }
}

ディレクティブ定義オブジェクト

ディレクティブ定義の第二引数に入るディレクティブ定義オブジェクトには、カスタムディレクティブがDOM要素に紐づいたタイミングにフックして実行する関数を指定できる。

オプション名 内容
bind ディレクティブが対象の要素に紐づいた一度だけ呼ばれる。初回のセットアップ処理(イベントリスナーの登録など)を実行する。
inserted 紐づいた要素が親要素に挿入された際に呼ばれる。
update ディレクティブの値が変化などに伴い、紐づいた要素を含むコンポーネントのVNodeが更新される度に呼ばれる。なお、ディレクティブの値が変化しなくても呼ばれる場合があるため、以前の値と比較することで不要な更新を回避する必要がある。
componentUpdate コンポーネントのVNodeと子コンポーネントのVNodeが更新された際に呼ばれる。
unbind ディレクティブが紐づいている要素から取り除かれた時、一度だけ呼ばれる。後処理(イベントリスナーの削除など)を実行する。

フック関数の引数

引数名 内容
el ディレクティブが紐づく要素。
binding ディレクティブに関する情報を持つプロパティを含んだオブジェクト

bindingのプロパティについては、公式サイトを見る。

●updateフックによる値の変更の検知

無駄なDOM操作を防ぐために、updateフックで値を比較して、変更があった場合のみDOM操作を行うようにしておくとよい。

{
  update: function(el, binding) {
    if (binding.value !== binding.oldValue) {
      //変更があった場合のDOM操作
    }
  }
}

描画関数

テンプレートのような宣言的な書き方ではなく、プログラムによる柔軟な記述をした方が簡潔になる場合がある。

このような時、renderオプションで描画関数を指定する。

本来、テンプレートの記述もコンパイラが描画関数に変換している。

createElement関数

描画関数は引数にcreateElement関数というVNodeを生成する関数を受け取る。

第一引数に生成される要素の名前、第二引数にオプションを含むデータオブジェクト(HTML属性、プロパティ、イベントリスナー、クラス、スタイルバインディングなど)、第三引数に子ノードまたは子ノードの配列を渡す。

第一引数は必須であり、第二引数、第三引数はオプションである。

データオブジェクトを省略した場合、第二引数として子ノードを指定できる。

描画関数はVueインスタンスとしてthisにアクセスできるため、これを利用してプロパティや状態(データ)、算出プロパティなどにアクセスして要素名を決定できる。

詳しくは公式サイトを見る。


//inputとtextareaの入力系の型を作成
//tag(inputとtextareaのみ)とtype(text, password, fileに限る)を外から指定できる

const MyInput = {
  data: function() {
    return {
      myInputTags: ["input", "textarea"],
      myInputTypes: ["text", "password", "file"],
      myTag: this.tag,
      myType: this.type,
    };
  },
  props: {
    tag: {
      type: String,
      default: "input",
    },
    type: {
      type: String,
      default: "text",
    },
  },
  render: function(createElement) {

    if (this.myTag !== "input" || this.myType !== "text") {

      const tagFlag = this.myInputTags.includes(this.myTag);

      if (!tagFlag) {

        this.myTag = "input";

      }

      const typeFlag = this.myInputTypes.includes(this.myType);

      if (!typeFlag) {

        this.myType = "text";

      }

    }

    const input = createElement(this.myTag, {
      attrs: {
        type: this.myType,
      }
    });

    let label = this.$slots.default || "";

    if (label) {

      label = createElement("label", {
        attrs: {
          class: "input-label",
        }
      }, this.$slots.default);

    }

    return createElement("div", {
      attrs: {
        class: "input-box",
      }
    }, [label, input]);

  },
};

let vm = new Vue({
...
...
...
  components: {
    "my-input": MyInput,
  },
});
<my-input></my-input> <!-- デフォルト(tagがinput、typeがtext) -->
<my-input>ユーザー名</my-input> <!-- ラベル指定 -->
<my-input type="password">パスワード</my-input> <!-- ラベルとタイプ指定 -->
<my-input type="file">プロフィール画像</my-input> <!-- ラベルとタイプ指定 -->
<my-input tag="textarea">メッセージ</my-input> <!-- ラベルとタグ指定 -->
<my-input tag="span">×××</my-input> <!-- このタグはアウト(inputに変換) -->
<my-input type="button">×××</my-input> <!-- このタイプはアウト(textに変換) -->

スクリーンショット 2020-03-22 14.55.03.png
スクリーンショット 2020-03-22 14.55.14.png

ミックスイン

ミックスインは、機能を再利用するための仕組みである。

オブジェクトとして定義し、各コンポーネントに渡すことで実現できる。

複数のコンポーネントで同じ機能を実装するケースなどに利用できる。

つまり、複数のコンポーネント間で共有できる汎用的な小さい機能を切り出し、重複した記述を無くすことがミックスインの利用目的である。

シェアボタンの例

ページによって見た目が異なる場合(テキストボタンかアイコンボタンかなど)があるシェアボタンを例に、ミックスインを適用してみる。

まずはミックスインを使わないで書くと。

//テキストタイプのシェアボタン
const TextShareButton = {
  template: `<button @click="share" :style="{ backgroundColor: mycolor }">
               <slot>共有</slot>
             </button>`,
  data: function() {
    return {
      _sharing: false,
      colors: new Map([
                        ["white", "#F4F5F7"],
                        ["red", "#ef5350"],
                        ["blue", "##3498db"],
                        ["yellow", "#FFEE58"],
                        ["green", "#66BB6A"],
                        ["orange", "#FFA726"],
                        ["purple", "#AB47BC"],
                        ["black", "#212121"],
                      ]),

    };
  },
  props: {
    color: {
      type: String,
      default: "white",
    }
  },
  methods: {
    share: function() {
      //連続押し制御
      if (this._sharing) {
        return;
      }
      //キャンセルされた場合は、以降の処理を中止
      if (!window.confirm("シェアしますか?")) {
        return;
      }
      //OKが押された場合
      this._sharing = true;
      //実際にシェアする処理(今は擬似的)
      setTimeout(() => {
        window.alert("シェアしました");
        this._sharing = false;
      }, 1000);
    }
  },
  computed: {
    mycolor: function() {
      let color = this.color;
      color = this.colors.get(color) || "#F4F5F7";
      return color;
    }
  }
};

//アイコンタイプのシェアボタン
const IconShareButton = {
  template: `<button @click="share" :style="{ backgroundColor: mycolor }">
               <slot><i class="fas fa-share-square"></i></slot>
             </button>`,
  data: function() {
    return {
      _sharing: false,
      colors: new Map([
                        ["white", "#F4F5F7"],
                        ["red", "#ef5350"],
                        ["blue", "##3498db"],
                        ["yellow", "#FFEE58"],
                        ["green", "#66BB6A"],
                        ["orange", "#FFA726"],
                        ["purple", "#AB47BC"],
                        ["black", "#212121"],
                      ]),

    };
  },
  props: {
    color: {
      type: String,
      default: "white",
    }
  },
  methods: {
    share: function() {
      //連続押し制御
      if (this._sharing) {
        return;
      }
      //キャンセルされた場合は、以降の処理を中止
      if (!window.confirm("シェアしますか?")) {
        return;
      }
      //OKが押された場合
      this._sharing = true;
      //実際にシェアする処理(今は擬似的)
      setTimeout(() => {
        window.alert("シェアしました");
        this._sharing = false;
      }, 1000);
    }
  },
  computed: {
    mycolor: function() {
      let color = this.color;
      color = this.colors.get(color) || "#F4F5F7";
      return color;
    }
  }
};
<text-share-button></text-share-button> <!-- デフォルト -->
<text-share-button color="red">シェアする</text-share-button> <!-- 色とテキスト指定 -->
<icon-share-button color="green"></icon-share-button> <!-- アイコンはデフォルト、色指定 -->
<icon-share-button color="blue"><i class="fas fa-share-alt-square"></i></icon-share-button> <!-- アイコンと色指定 -->

スクリーンショット 2020-03-22 13.41.19.png

ダウンロード (2).gif

テキストシェアボタン、アイコンシェアボタンのコードを見てみると、互いに共通しているコードが見られる。

このような部分をミックスインとして切り出すことにより、変更箇所があったとしても一つのミックスインさえ変えれば済む。

//ミックスイン
const Sharable = {
  data: function() {
    return {
      _sharing: false,
      colors: new Map([
                        ["white", "#F4F5F7"],
                        ["red", "#ef5350"],
                        ["blue", "##3498db"],
                        ["yellow", "#FFEE58"],
                        ["green", "#66BB6A"],
                        ["orange", "#FFA726"],
                        ["purple", "#AB47BC"],
                        ["black", "#212121"],
                      ]),

    };
  },
  props: {
    color: {
      type: String,
      default: "white",
    }
  },
  methods: {
    share: function() {
      //連続押し制御
      if (this._sharing) {
        return;
      }
      //キャンセルされた場合は、以降の処理を中止
      if (!window.confirm("シェアしますか?")) {
        return;
      }
      //OKが押された場合
      this._sharing = true;
      //実際にシェアする処理(今は擬似的)
      setTimeout(() => {
        window.alert("シェアしました");
        this._sharing = false;
      }, 1000);
    }
  },
  computed: {
    mycolor: function() {
      let color = this.color;
      color = this.colors.get(color) || "#F4F5F7";
      return color;
    }
  }
}

//テキストタイプのシェアボタン
const TextShareButton = {
  template: `<button @click="share" :style="{ backgroundColor: mycolor }">
               <slot>共有</slot>
             </button>`,
  mixins: [Sharable],
};

//アイコンタイプのシェアボタン
const IconShareButton = {
  template: `<button @click="share" :style="{ backgroundColor: mycolor }">
               <slot><i class="fas fa-share-square"></i></slot>
             </button>`,
  mixins: [Sharable],
};

また、例えば、テキストシェアボタンコンポーネント内のデータにボタンのボーダーを引くか否かのフラグを追加したとすると、ミックスインで導入される_sharingとcolorsにマージされた状態を持つことになる。

加えて、mountedやcreatedといったフック関数もミックスインと適用したコンポーネントがマージされる。

この場合、ミックスイン(配列で複数のミックスインを渡した場合は先頭から)→コンポーネントの順でフック関数が呼ばれる。

なお、ミックスインとコンポーネントで同名のmethodsやcomponents, directivesが定義されていた場合、コンポーネントのオプションが優先される。

グローバルコンポーネント

アプリケーション全体に適用されるミックスインのこと。

例えば、ログイン機能を追加した際に非ログイン状態では開かせたくないページなど、各箇所を変更するには範囲が大きすぎる場合に有効。

//参考書ほぼ丸パクリ
//authをtrueにすれば、認証必要ページ
//authを指定しない、もしくはfalseとすれば誰でも閲覧可能
Vue.mixin({
  data: function() {
    return {
      loggedInUser: null,
    };
  },
  created: function() {
    //$options・・・Vueインスタンスに関する色々な情報(各コンポーネントのプロパティやディレクティブなど)
    //             を含んだオブジェクト(インスタンスプロパティ)
    let auth = this.$options.auth;
    //文字列をJSONとして解析し、javascriptの値やオブジェクトに変換
    this.loggedInUser = JSON.parse(sessionStorage.getItem('loggedInUser')).name;
    //認証が必要で なおかつ ログインしていない場合
    if (auth && !this.loggedInUser) {
      //本来はログインページに飛ばす処理?
      window.alert("このページはログインが必要です");
    };
  },
});

const LoginRequiredPage = {
  //オプションにカスタムプロパティを追加
  auth: true,
  template: `
    <div>
      <p v-if="!loggedInUser">このページにはログインが必要です</p>
      <p v-else>{{ loggedInUser }}さんでログイン中</p>
    </div>
  `,
};

最後に、ミックスインとして切り出すのは可能な限り単一の小さい機能にすること、そして命名時はどのような機能がコンポーネントに取り込まれるのか一目で把握できるようなものにすることを意識すべきである。

命名時の一つの方針としては、xxx + able(xxxできるようになる)がある。

参考文献

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

【Vue.js】CompositionAPIでフォームを実装

はじめに

最近趣味で、Vue.js、CompositionAPIを試しています。

その一環として、フォームをできる限りライブラリを使わずに実装してみたので、記事に残したいと思います。

フォームの概要

今回作成したフォームは、簡単な「ユーザー作成フォーム」です。

入力フィールドは「ユーザー名」と「メールアドレス」の2つです。

スクリーンショット 2020-03-01 22.14.57.png

また、入力値に問題があれば、以下のようにエラーメッセージを表示します。

スクリーンショット 2020-03-01 22.15.19.png

作成したソースコード

今回作成したソースコードは、以下のGithubリポジトリで公開しています。

Ushinji/vue-form-sample

入力フィールド / useField

動作

ユーザー作成フォームコンポーネントの実装の中で、まず入力フィールドから説明します。

スクリーンショット 2020-03-01 22.24.37.png

入力フィールドは、input属性とエラーメッセージの2つからなります。

エラーメッセージは入力バリデーションがエラーの場合に表示されます。今回のバリデーションエラーの判定条件は「ユーザー名が空欄ではない」ことです。

しかし、入力フィールドの初期状態は空欄であるためエラー判定となります。ユーザーからすると、何も操作していないにも関わらずいきなりエラーメッセージを見せられるのは心象が良いとはいえません。

そのため、このフォームでは初期状態はバリデーションエラーとはせず、ユーザーが入力フィールドに操作が行われた後にエラー判定を始めるようにしました。具体的なエラー判定開始タイミングは、入力フィールドに対するfocusが外れたとき(Blur)です。

実装

以上を踏まえて、ここから実装について説明します。

まず、入力フィールドに関するロジックを定義したuseFieldです。

// useField.ts
import { ref, computed } from 'vue';

const useField = (
  initialValue: string,
  validate: (value: string) => boolean = () => false
) => {

  const value = ref(initialValue);
  const isTouched = ref(false);

  const error = computed(() => {
    return !validate(value.value);
  });

  const onInput = (event: InputEvent) => {
    if (event.target instanceof HTMLInputElement) {
      value.value = event.target.value;
    }
  };

  const onBlur = () => {
    isTouched.value = true;
  };

  return {
    props: { value, onInput, onBlur },
    meta: {
      isTouched,
      error,
    },
  };
};

export default useField;

useFiledは「入力フィールドの初期値」と「入力バリデーション判定式」の2つを渡すことで入力フィールドのロジックを返します。

useFieldが管理する状態は入力値(value)、ユーザーの入力フィールドの操作判定(isTouched)、バリデーションエラー判定(error)の3つです。これらはrefとcomputedを利用し、リアクティブな値として定義しています。

入力値のvalueはonInput関数によって値が更新されます。onInput関数は該当の入力フィールドの@inputに指定することで、入力値の変更をトリガーにonIput関数が発火しvalueが更新されます。

またisTouchedはonBlur関数によって値が更新されます。onBlurは該当の入力フィールドの@blurに指定し、入力フィールドに対するfocusが外れたタイミングでisTouchedを更新します。

<!-- useFieldのロジック指定方法 -->
<input :value="value" @input="onInput" @blur="onBlur" />

また、useFieldの戻り値はpropsmetaという名前で構造化しました。propsについては、該当の入力フィールドにinputに指定する属性を集約しています。また、metaはエラーといったいわゆるmeta情報を集めています。

useFieldを使ったForm実装

先ほど説明したuseFieldを使ってFormの入力フィールドを実装すると、以下のようなコードになります。

<!-- UserCreateForm.vue -->
<template>
<div class="field">
  <label class="label" for="user-name">ユーザー名</label>
  <div class="control">
    <input
      id="user-name"
      name="user-name"
      type="text"
      :value="userNameField.props.value.value"
      @input="userNameField.props.onInput"
      @blur="userNameField.props.onBlur"
    />
    <p
      class="help is-danger"
      v-show="
        userNameField.meta.isTouched.value && userNameField.meta.error.value
      "
    >
     ユーザー名を入力してください。
    </p>
  </div>
</div>
</template>

<script lang="ts">
import { createComponent } from '@vue/composition-api';
import useField from './useField';

export default createComponent({
  setup() {
    // ユーザー名フィールドの定義
    const userNameField = useField('', (value: string) => {
      return value.length > 0;
    });
    return { userNameField };
  },
});
</script>

<script>内でユーザー名のフィールド情報(userNameField)を初期化します。そして目的のinput属性にフィールド情報を設定します。

また、エラーメッセージについてはInputにフォーカスが離れた後、バリデーションエラーの場合に表示されるように定義しました。

Submit処理の実装

まず、Formの<script>部分のコードです。フォームで利用するSubmitメソッド(onSubmit)とフォームエラー判定を定義します。

<script lang="ts">
import { defineComponent } from 'vue';
import useUserCreateForm from './composition';

export default defineComponent({
  setup() {
    // 各フィールドの定義(バリデーションメソッドの詳細は後述する)
    const userNameField = useField('', presenceValidator);
    const emailField = useField('', emailValidator);

    // フォームのエラー判定。各フィールドにエラー情報を元に判定する。
    const error = computed(() => {
      return userNameField.meta.error.value || emailField.meta.error.value;
    });

    // submitメソッド。各フィールドの値を使い、サーバーにPOSTリクエストを送信する。
    const onSubmit = async () => {
      if (error.value) {
        return;
      }
      // 今回はサーバーリクエストは行っていない
      console.log(userNameField.props.value.value, emailField.props.value.value);
    };

    // 各フィールド情報とフォーム情報をtemplate層に渡す
    return {
      userNameField,
      emailField,
      onSubmit,
      meta: {
        error,
      },
    };
  },
});
</script>

上述で利用した各フィールドのバリデーションメソッドは以下の通りです。

export const presenceValidator = (value: string) => {
  return value.length > 0;
};

export const emailValidator = (value: string) => {
  const re = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
  return re.test(value);
};

次にフォームの<template>層の実装は以下の通りです。<script>層で定義したonSubmitを送信ボタンクリック時に実行されるように定義します。

また、フォーム全体のエラー判定を送信ボタンの:disabledに指定することで、エラー時にボタンクリックができないようにします(disalbed状態)。

<template>
  <div class="container">
    <form @submit.prevent>

      <!-- 各フィールドは省略 -->

      <div class="field">
        <p class="control">
          <button
            class="button is-success"
            type="submit"
            @click="onSubmit"
            :disabled="meta.error.value"
          >
            作成する
          </button>
        </p>
      </div>
    </form>
  </div>
</template>

終わりに

今回はVue.jsのComposition APIを使ったフォーム実装について説明しました。

今回の実装は、以下の2つのライブラリを参考にして実装しました。

自分が普段はReact & hooksでフロントエンドコードを書いているので、Reactライクなコーディングになっている気がします。

何はともあれ、読んだ方に少しでも参考になれば幸いです。

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

vueの書き方でつまづいた所。v-select : itemにajaxで取得したdataの一部を表示させたい!

最近は全くQiitaを書く余裕がありません!
それくらい毎日全力で必死に生きております笑

ただ今慣れないvue.jsでとあるプロダクトを作っています。
毎日いちいちつまづきまくっていますが、その1つを今日は記載します。

実施していること

ベタですが、vue.js(vuetify導入)とLaravelで簡単なtodo管理アプリ的なのを作っていました。
ajax(axios)とAPIでデータの受け渡しをしています。

困ったこと

ajaxで受け取れたデータをv-selectの :itemsに上手く表示できない!

該当コード

   <v-select
     v-model="todo"
     :items="todos"
     prepend-icon="mdi-clipboard-check-multiple-outline"
     label="to doを選択"
      >
   </v-select>

<script>
  export default {
    data: () => ({
      todo: [],
      todos: [],
      }),

     methods: {


    fetchTodos: function(){
      axios.get('/api/get')
      .then(res=>{
        console.log(res)
        this.todos=res.data 
      })
    },
    },

    created() {
    this.fetchTodos()
  },
  }
</script>

非常にシンプルでよくあるコードです。
resの中身はデベロッパーツールで確認済み。

console.log(res)
[{…}, {…}]
  0: {…}
    id: (1)
    user_id: (1)
    name: (hoge)
    created_at: (...)
    updated_at: (...)
  1: {…}
     id: (2)
    user_id: (2)
    name: (hogehoge)
    created_at: (...)
    updated_at: (...)

このnameを :items="todos" に追加したい!
ここまでデータとれてるから絶対出来るじゃん!
と確信しつつもなかなか答えに辿りつかず。

上記のように this.todos=res.dataと記載するとitemsの中身は[object Object]、
配列のままそらぁそうだ、
だって配列の中のnameだけ下さいって指示書いてないもーん。
でもその指示の書き方がわかりません・・・

出来たコードがこちら

 <v-select
   v-model="todo"
   item-value="id"
   item-text="name"
   :items="todos"
   prepend-icon="mdi-clipboard-check-multiple-outline"
   label="to doを選択"
   >
  </v-select>

item-value と item-textを利用すると表示できました。

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

Vueのイベントハンドリングを理解する

どんな記事?

Vue.jsのイベントハンドリングを理解しよう!

イベントハンドラとは?

イベントハンドラとは、イベントの通知を受け取るように登録されている関数またはオブジェクトのことを指します。

その要素がどのようにイベントに反応するか、を管理します。

イベントには、クリック・押されたキーの検出・フォーカスの取得等々があります。

Vueにおけるイベントの購読

v-onディレクティブを使うことで、DOMイベントの購読イベント発火時のJavaScriptの実行が可能になります。

<div id="example">
  <!-- クリックされると変数counterに1ずつ足すボタン -->
  <button v-on:click="counter += 1">1足す</button>
</div>
var example = new Vue({
  el: 'example',
  data: {
    counter: 0
  }
});

メソッドを呼び出す

v-onは呼び出したいメソッドの名前も受け付けます。

<div id="example">
  <!-- クリックされたら挨拶するメソッドを実行 -->
  <button v-on:click="greet">挨拶する</button>
</div>
var example = new Vue ({
  el: 'example',
  data: {
    name: 'Vue.js'
  },
  //メソッド定義
  methods: {
    greet: function (event) {
      //`this`はVueインスタンスを参照
      alert('Hello ' + this.name + '!')
      //`event`はDOMイベントを指す
      if (event) {
        alert(event.target.tagName)
      }
    }
  }
})
実行結果(ボタンを押すとアラートに表示)
Hello Vue.js!

v-onの利点

なぜVueではv-onを使ってHTMLにリスナーを記述するのか。

  1. HTMLテンプレートからJSコード内のハンドラ関数を簡単に探せるようにする
  2. JS内のイベントリスナーを手作業で付け加える必要がないのでViewModelをDOM依存のない純粋なロジックにできる。これはテストを実施しやすくする。
  3. ViewModelが消去される時に全てのイベントリスナーは自動で削除される。

上記の利点により、Vueではv-onを使ってリスナーを記述する。

省略記法

v-onv-bind同様に省略記法がある。

完全な構文
<a v-on:click="doSomething"></a>
省略記法
<a @click="doSomething"></a>

参考

DOM on-イベントハンドラ
イベントハンドリング

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

Railsプロジェクト作成手順(Vue版)

環境

  • macOS Mojave 10.14.6
  • rbenv 1.1.2
  • Homebrew 2.2.10
  • ruby 2.7.0
  • Rails 6.0.2.2

方法

1. ディレクトリを作成する。

$ mkdir practice_project

2. 作ったディレクトリに移動する。

$ cd practice_project

3. Gemfileを生成する。

$ bundle init

4. Gemfileを編集する。

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "rails" ←コメントアウトを外す

5. Gemをインストールする.

bundle install --path vendor/bundle

6. Railsプロジェクトを生成する。

$ bundle exec rails new . -d mysql --skip-test --webpack=vue

Overwrite /Users/koji/practice/vue_practice/Gemfile? (enter "h" for help) [Ynaqdhm] Y

おわりに

『もっと簡単にできる方法あるよーーー!』

『ここわかりにくいよーーー!』

『ここ間違っているよーーー!』

等あればコメントいただけるとめちゃくちゃ嬉しいです!!!

twiiter → https://twitter.com/jiko797torayo

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

Railsプロジェクト作成手順

環境

  • macOS Mojave 10.14.6
  • rbenv 1.1.2
  • Homebrew 2.2.10
  • ruby 2.7.0
  • Rails 6.0.2.2

方法

1. ディレクトリを作成する。

$ mkdir practice_project

2. 作ったディレクトリに移動する。

$ cd practice_project

3. Gemfileを生成する。

$ bundle init

4. Gemfileを編集する。

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "rails" ←コメントアウトを外す

5. Gemをインストールする.

bundle install --path vendor/bundle

6. Railsプロジェクトを生成する。

$ bundle exec rails new . -d mysql --skip-test

Overwrite /Users/koji/practice/vue_practice/Gemfile? (enter "h" for help) [Ynaqdhm] Y

(Vueを使う場合)

$ bundle exec rails new . -d mysql --skip-test --webpack=vue

Overwrite /Users/koji/practice/vue_practice/Gemfile? (enter "h" for help) [Ynaqdhm] Y

おわりに

『もっと簡単にできる方法あるよーーー!』

『ここわかりにくいよーーー!』

『ここ間違っているよーーー!』

等あればコメントいただけるとめちゃくちゃ嬉しいです!!!

twiiter → https://twitter.com/jiko797torayo

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

Vueのデータバインディングを理解する

どんな記事?

Vue.jsでのデータバインディングを理解しよう!

データバインディングとは

データバインディングとは、データと描画を同期する仕組み。

テキストのバインディング

"Mustache"構文(二重中かっこ)を利用した形が基本です。

<span> Message: {{ msg }}</span>

こうすると{{ msg }}の部分が、対応するオブジェクトのmsgプロパティの値に置き換えられます。

HTMLのバインディング

v-htmlディレクティブを使用すると、HTML文をバインディングできます。

(ディレクティブとは、DOM要素に対して何かを実行することをライブラリに伝達する、マークアップ中の特別なトークン)

<p v-html="text"></p>
data: function() {
  retrun {
    text: 'Hello!'
  }
}
出力
Hello!

pのコンテンツはtextプロパティの値に置き換えられ、プレーンなHTMLとして解釈されます。

属性値のバインディング

v-bindを使用すると、HTML属性値をバインディングすることができます。

<div v-bind:class="{ active: isActive }"></div>
data: {
  isActive: true
}

以上のようにすると、HTMLはこのように描画されます。

描画結果
<div class="active"></div>

isActivefalseになると、active属性値は描画されなくなります。

テンプレートをより見やすくする為に、直接オブジェクトを指定してバインディングする方法もあります。

<div v-bind:style="styleObject"></div>
data: {
  styleObject: {
    color: 'red',
    fontSize: '13px'
  }
}
描画結果
<div style="color: red; fontSize: 13px;"></div>

「:」の意味

テンプレート内のv-bindの後ろについている:は、引数を意味しています。

<a v-bind:href="url"></a>

上記の例でhrefは、v-bindディレクティブに、urlの値と要素のhref属性をバインドするように伝える引数です。

省略記法

v-bindに対しては省略記法が提供されています。
v-bindを書かずに:のみにしても大丈夫なんですね。

完全な構文
<a v-bind:href="url"></a>
省略記法
<a :href="url"></a>

参考

Vue.js公式
Vue.js データバインディングとはなにか説明します
データをHTMLコードとして出力するには v-html ディレクティブを使う

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