20190530のvue.jsに関する記事は12件です。

Nuxt.jsでカレンダーを自作してみた

概要

チームで運用するスケジュール表を自作しようと思い、NuxtとAPIで作成できないか試行錯誤してみた内容を
記録しておく為の記事になります。CSSの説明は省きますが、完成イメージは :arrow_down: こんな感じの物を紹介していきます。
schedule.gif

※今回API通信の説明は行いません。また、Nuxt.jsの環境構築などはできている物と仮定します。

pages

index.vue
<template>
  <div>
    <Calendar />
  </div>
</template>

<script>
import Calendar from '~/components/schedule/Calendar.vue'

export default {
    components: {
      Calendar
    }
}
</script>

今回は機能の一部となるcomponents/schedule/Calendar.vueを読み込んでそれを表示させているだけです。

store

スケジュールを表示するための『日付情報』などを保持する「schedule.js」ファイルと
選択されているチームの情報を表示するための『メンバー情報』などを保持する「member.js」ファイルを作成しました。

schedule.jsの説明

schedule.js
export const state = () => ({
  year: new Date().getFullYear(),
  month: new Date().getMonth()+1,
  today: new Date().getDay()
})

export const mutations = {
  changeCalendar(state, add){
    if(state.month + add > 12){
      state.year += add
      state.month = 1
    }else if(state.month + add < 1){
      state.year += add
      state.month = 12
    }else{
      state.month += add
    }
  }
}

states

まず、「schedule.js」ファイルで保持するstatesは3つで、
『year:年』『month:月』『today:日付』です。そのままですね:sweat:
初期値は当日の「年・月・日」を取得するようにしてあります。

mutations

mutaionは、store内のstateを変更するための関数です。
changeCalendarでは引数で渡されている値と現在の『state.month』を足した値によって処理を分けています。
後程HTML部分で説明しますが、ここの引数は「-1」か「1」が渡されるようになっています。
『state.month』と『引数add』が「12」より多かった場合、「1」より小さかった場合は
『state.year』の方に『add』を足すことで、年数を変更するようにし、『state.month』を1月か12月にしています。
それ以外の場合は、『state.month』にそのまま『add』を足しています。

member.jsの説明

member.js
export const state = () => ({
  // チーム名とチームに所属するメンバーのkey
  teams:[
    { key:0, name:"チーム①", member:[0,1], flag:false },
    { key:1, name:"チーム②", member:[2,3], flag:false },
    { key:2, name:"チーム③", member:[4,5,6], flag:false }
  ],
  // メンバーのkeyと名前
  members:[
    { key: 0, name: 'テスト マン1'},
    { key: 1, name: 'テスト マン2'},
    { key: 2, name: 'テスト マン3'},
    { key: 3, name: "テスト マン4"},
    { key: 4, name: 'テスト マン5'},
    { key: 5, name: 'テスト マン6'},
    { key: 6, name: 'テスト マン7'}
  ],
  // 選択されているチームのメンバー
  selectMember:[]
})

export const mutations = {
  // チーム選択時呼ばれるmutation
  selectTeam(state, select){
    state.teams.map((val) => {
      val.flag = false
    })
    // フラグを立てる
    state.teams[select].flag = true
    // 選ばれているチームのメンバーのIDを格納
    state.selectMember = state.teams[select].member
  }
}

states

「member.js」で保持するstatesも3つです。それぞれの概要はコメントアウトを参照してください。

「state.teams」は『name:チーム名』『member:チームに所属しているメンバーのキー』
『flag:スケジュール表に表示しているか』という構造になっています。

「state.members」は『key:ユーザーキー』『name:ユーザーネーム』という構造です。

「state.selectMember」は選択されているチームメンバーのキーを格納する用のstateになります。

mutations

『selectTeam』では選択されたチームの
まず、map関数で「state.teams」の『flag』を全てfalseにします。
その後、選択されたチームの『flag』をtrueにしています。後程説明しますが、
この『flag』はclass名の出し分けで使われるものになります。

次に、『state.selectMember』に選ばれているチームのメンバーキーを格納します。こちらも後程説明しますが、
このキーの数だけループを回し、htmlを出力する処理を行います。

components

下記例で記載はしてありますが、CSSの説明については省かせていただきます。

schedule/Calendar.vue
<template>
  <div class="mainschedule">
    <div class="heading">
      <div class="schedule-ttl">schedule</div>
    </div>
    <div class="teams">
      <div class="teambox" v-for="team in teams" :key="team.key" :class="{active: team.flag}" @click="showMember(team)">
        {{ team.name }}
      </div>
    </div>
    <div class="schedule-month">
      <span class="arrow" @click="changeMonth(-1)">&lt;&lt;</span> {{ schedule.year }}{{ schedule.month }}<span class="arrow" @click="changeMonth(1)">&gt;&gt;</span>
    </div>
    <div class="calendar-box">
      <table>
        <tr>
          <th class="name-field"></th>
          <th v-for="day in lastday" :key="day">{{ day }}</th>
        </tr>
        <tr>
          <td class="name-field"></td>
          <td v-for="day in lastday" :key="day">{{ week[(firstweek + day) % 7] }}</td>
        </tr>
        <tr v-for="num in selectMember" :key="num">
          <td class="name-field">{{ members[num].name }}</td>
          <td v-for="day in lastday" :key="day"></td>
        </tr>
      </table>
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    teams () { return this.$store.state.member.teams },
    members () { return this.$store.state.member.members},
    selectMember () { return this.$store.state.member.selectMember},
    schedule () { return this.$store.state.schedule },

    // 月初曜日の取得
    firstweek () { return new Date(this.$store.state.schedule.year, this.$store.state.schedule.month-1, 1).getDay()-1 },
    // 月末の日数の取得
    lastday () { return new Date(this.$store.state.schedule.year, this.$store.state.schedule.month, 0).getDate() }
  },
  methods: {
    showMember(team) {
      this.$store.commit('member/selectTeam',team.key)
      this.selectMember = team.member
    },
    changeMonth(add) {
      this.$store.commit('schedule/changeCalendar', add)
    }
  },
  data() {
    return{
      week: ["日", "月", "火", "水", "木", "金", "土"]
    }
  }
}
</script>

<style lang="scss">
.mainschedule {
  padding: 10px;

  .heading{
    padding: 25px;

    .schedule-ttl {
      font-size: 32px;
      text-align: center;
      border-bottom: solid 2px #b2bec3;
    }
  }

  .teams {
    display: flex;
    justify-content: center;

    .teambox {
      display: flex;
      justify-content: center;
      border: solid 1px #dfe6e9;
      padding: 10px;
      margin: 10px;
      width: 300px;

      &.active {
        background: #dfe6e9;
        color: #2d3436;
      }
    }
  }

  .schedule-month {
    font-size: 24px;
    padding: 10px;
    text-align: center;

    .arrow {
      font-size: 20px;
      cursor: pointer;
    }
  }

  .calendar-box{
    display: flex;
    justify-content: center;
    align-items: center;
  }

  table {
    th,td {
      width: 35px;
      height: 35px;
      text-align: center;
      border: solid 1px;
    }

    .name-field {
      width: 150px;
    }
  }
}
</style>

script部分

script部分
<script>
export default {
  computed: {
    teams () { return this.$store.state.member.teams },
    members () { return this.$store.state.member.members},
    selectMember () { return this.$store.state.member.selectMember},
    schedule () { return this.$store.state.schedule },

    // 月初曜日の取得
    firstweek () { return new Date(this.$store.state.schedule.year, this.$store.state.schedule.month-1, 1).getDay()-1 },
    // 月末の日数の取得
    lastday () { return new Date(this.$store.state.schedule.year, this.$store.state.schedule.month, 0).getDate() }
  },
  methods: {
    showMember(team) {
      this.$store.commit('member/selectTeam',team.key)
      this.selectMember = team.member
    },
    changeMonth(add) {
      this.$store.commit('schedule/changeCalendar', add)
    }
  },
  data() {
    return{
      week: ["日", "月", "火", "水", "木", "金", "土"]
    }
  }
}
</script>

computed

ページロード時に実行される関数です。それぞれの関数で呼び出しているのは下記の様なものになります。

関数名 内容
teams Storeのmember.js内の『state:teams』を
呼び出しています。
members Storeのmember.js内の『state:members』を
呼び出しています。
selectMember Storeのmember.js内の『state:selectMember』を
呼び出しています。
schedule Storeのschedule内の『state』を
丸ごと呼び出しています。
firstweek Storeのschedule内の『state:year』と
『state:month』を元に表示する月の
最終日の曜日を取得しています。
lastday Storeのschedule内の『state:year』と
『state:month』を元に表示する月の
月末日を取得しています。

computedでStore内のstateを呼び出すことによって、html文内で毎回長々と呼び出し分を書く必要がなくなります。
firstweeklastdayの取得の仕方の詳細は省かせていただきます。

methods

イベントによって実行される関数を定期しています。

関数名 内容
showMember Storeのmember.js内の『mutations:selectTeam』
を呼び出しています。
引数には選択したチームのidが渡されています。
changeMonth Storeのchedule.js内の『mutations:changeCalendar』
を呼び出しています。
引数には『<<』を選択したら「-1」
『>>』を選択したら「1」を渡しています。

methods内ではStore内の情報を直接変更できなくなっています。
そのため、それぞれのmutationsにcommitを行い、stateを書き換える必要があります。

data

ページ内で使用できる変数を作成しています。曜日の配列はわざわざstateで管理する程でもないと
思ったのでこちらでweekという変数として設定しました。

html部分

    <div class="teams">
      <div class="teambox" v-for="team in teams" :key="team.key" :class="{active: team.flag}" @click="showMember(team)">
        {{ team.name }}
      </div>
    </div>
    <div class="schedule-month">
      <span class="arrow" @click="changeMonth(-1)">&lt;&lt;</span> {{ schedule.year }}年{{ schedule.month }}月 <span class="arrow" @click="changeMonth(1)">&gt;&gt;</span>
    </div>
    <div class="calendar-box">
      <table>
        <tr>
          <th class="name-field"></th>
          <th v-for="day in lastday" :key="day">{{ day }}</th>
        </tr>
        <tr>
          <td class="name-field"></td>
          <td v-for="day in lastday" :key="day">{{ week[(firstweek + day) % 7] }}</td>
        </tr>
        <tr v-for="num in selectMember" :key="num">
          <td class="name-field">{{ members[num].name }}</td>
          <td v-for="day in lastday" :key="day"></td>
        </tr>
      </table>
    </div>

チーム選択出力部分

まず、チーム選択部分出力の説明です。

<div class="teams">
  <div class="teambox" v-for="team in teams" :key="team.key" :class="{active: team.flag}" @click="showMember(team)">
    {{ team.name }}
  </div>
</div>

v-for="team in teams" :key="team.key"
v-forで『teams( computed内にある関数で取ってきたstate )』がなくなるまでループを回しています。
phpのforeach文で言うと、下記の様な事をしていると考えていただければ良いかと思います。

foreach(teams as team){
 echo "hoge";
}

v-forを使用する際にはkeyの設定が必要になります。今回は『teams.key』をkeyに設定しています。

:class="{active: team.flag}"
:classでclassを付けるか付けないかの条件分岐をする事ができます。今回の場合、『team.flag』が
trueだった場合には、「active」というクラスが付与されるようになっています。
このクラスでどのチームが選択されているか分かりやすいようCSS調整しています。

@click="showMember(team)"
この要素がクリックされた時に実行される『methods』の関数を指定しています。
この場合はshowMemberの関数が呼び出され、引数で選択された『team』の情報が渡されます。

{{ team.name }}
それぞれのチームの名前を表示しています。

月変更部分

<div class="schedule-month">
      <span class="arrow" @click="changeMonth(-1)">&lt;&lt;</span> {{ schedule.year }}年{{ schedule.month }}月 <span class="arrow" @click="changeMonth(1)">&gt;&gt;</span>
</div>

<span class="arrow" @click="changeMonth(-1)">&lt;&lt;</span>
『 << 』部分がクリックされた時に『methods』内のchangeMonth関数が呼ばれるように指定しています。
ここの場合は先月に戻りたいので、引数には「-1」を渡しています。

カレンダー部分

<table>
    //日付
    <tr>
      <th class="name-field"></th>
      <th v-for="day in lastday" :key="day">{{ day }}</th>
    </tr>

    //曜日
    <tr>
      <td class="name-field"></td>
      <td v-for="day in lastday" :key="day">{{ week[(firstweek + day) % 7] }}</td>
    </tr>

    //メンバー
    <tr v-for="num in selectMember" :key="num">
      <td class="name-field">{{ members[num].name }}</td>
      <td v-for="day in lastday" :key="day"></td>
    </tr>
</table>
日付・曜日

日付と曜日部分のv-forは全く同じで、computedで取得したlastday( 表示月の最終日 )
の回数分だけループを回しています。

<th class="name-field"></th>
選択されたチームの名前が曜日の下の行から入るので、空文字で設置してあります。

{{ week[(firstweek + day) % 7] }}
曜日の上記箇所については、script内のdata()で設定した『week』変数を使用しています。
firstweekはcomputedで取得した月初日の曜日の引数です。こちらと現在の日付を足して7で割ることで
現在の曜日に対応する配列番号を指定する事ができます。

メンバー

<tr v-for="num in selectMember" :key="num">
ここでは<tr>selectMemberの数だけループを回しています。

<td class="name-field">{{ members[num].name }}</td>
上記でselectMemberに対応するメンバーの名前を取得し表示しています。

<td v-for="day in lastday" :key="day"></td>
ここのループは日付・曜日部分のループと全く同じで、中身は空で渡しています。

以上で完成です:smile: !

おわりに

かなり長々となってしまいましたが、最後まで読んでくださった皆様ありがとうございました!
中々複雑な構造になってしまいましたが、もっとスマートなStoreの呼び出し方や、
v-forの回数を減らせる記述方法などがあれば教えていただきたいです。

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

Vue Routerのreload実装する

Vue Routerにはreload機能がたいため、自作スクリプト

methods: {
    reload() {
        this.$router.go({path: this.$router.currentRoute.path, force: true});
    },
    something() {
        // reloadを呼び出すことで画面リロード
        this.reload();
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Vue.js]filterをコンポーネントのscript内で使えるようにするメモ

やりたいこと

filterをコンポーネントのscript内で使えるようにしたい

ダメな例

<script>
export default {
  name: 'ThePieGraph',
  methods: {
    fillData() {
      this.datacollection = {
        datasets: [
          {
            data: [ 変数 | filter ], 
            // ↑★ここにfiletrを置きたいが、こういう書き方だとダメ
          },
        ],
      }
    },
  },
}

やったこと

いい例

src直下にpluginsディレクトリを切って、その中にfilter.jsを置く

prototypeを用いると、コンポーネントのscript内でも、関数としてfilterを呼び出すことができる

/**
 * 配列をオブジェクトから名前抽出
 * @param {String} key
 * @param {{}} obj
 * @returns {String}
 */
const objToName = (key, obj) => {
  if (obj[key] !== undefined) {
    return obj[key]
  }
  return key
}

export default {
  install(vue) {
    // 数値を小数点に変換する
    const decimalPointShaping = value => {
      // console.log('decimalPointShaping',value)
      if (!Number(value)) {
        return value
      }
      return Math.round(value)
    }
    vue.filter('decimalPointShaping', decimalPointShaping)

    // 全体から平均的なパーセントを割り出す
    const percentageCalculation = (value, sum) => {
      console.log('percentageCalculation', value, sum)
      return (value / sum) * 100
    }
    vue.filter('percentageCalculation', percentageCalculation)

    // APIの結果から人柄の結果を変換する
    const personalityConversion = value => objToName(value, big4)
    vue.filter('personalityConversion', personalityConversion)

    // APIの結果からbig5の結果を変換する
    const big5Conversion = value => objToName(value, big5)
    vue.filter('big5Conversion', big5Conversion)

    // 全体から平均的なパーセントを割り出し、かつ数値を整数に整形する
    const percentAndDecimal = (value, sum) =>
      decimalPointShaping(percentageCalculation(value,sum))


/********************** 対象の記述↓ **********************/
    // フィルターをコンポーネントスクリプト内で使えるようにする
    vue.prototype.$customFilter = {
      decimalPointShaping,
      personalityConversion,
      big5Conversion,
      percentageCalculation,
      percentAndDecimal,
    }
/********************** 対象の記述↑ **********************/

  },
}

使い方

<script>
export default {
  name: 'ThePieGraph',
  methods: {
    fillData() {
      this.datacollection = {
        datasets: [
          {
            data: [
              this.$customFilter.percentAndDecimal(this.big4_average,this.big4_sum),
              this.$customFilter.percentAndDecimal(this.big4_reserved,this.big4_sum),
              this.$customFilter.percentAndDecimal(this.big4_role_models,this.big4_sum),
              this.$customFilter.percentAndDecimal(this.big4_self_centered,this.big4_sum),
            ], //★↑ここ
          },
        ],
      }
    },
  },
}

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

はじめての Nuxt.js

Nuxt はずっと前から気にはなってて、でも「今回は SSR しないからいいや」とか「今回は静的サイトジェネレーターで作るからいいや」とか思って避けてきたけど、なんか最近 Nuxt 使うって話を 3 回くらい聞いたので、ちょっと使ってみて構成とか眺めてみようと思う

先に使ってみた感想を述べておくと

  • Nuxt の公式ドキュメントがちゃんと作られているので、これだけ見れば大体解決した
  • 後述してますが TypeScript とか Vuex とかとても簡単に導入できるので感動した
  • webpack 周りが隠蔽されてる
    • 前に VueCLI v2 使ってみた時はけっこうビルド周りがむき出しになってた気がする(VueCLI v3 ではこの辺解決されてるっぽい)
  • Vue で SSR する時に Nuxt を使うという印象を持っていたが、SSR モードか SPA モードか選べたので考えが違った
  • フロントエンドチームが多いチームであれば、Angular などのガッチリ系フレームワークも検討しようと思っていたが、Vue が好きなら Nuxt でもいいかもしれない
    • TypeScript のサポートも昔よりはかなり良くなってて、Vue v3 がリリース(今年リリースされる?)されたら Vue 自体が TypeScript で作り直されるので、さらに Vue + Nuxt + TypeScript 環境が良くなっていくのではないかと思う

今回作業したリポジトリは以下
https://github.com/kurosame/nuxt-boilerplate

インストール

create-nuxt-appを使うと良さそう
npx でいけるとかすばらしい

npx create-nuxt-app nuxt-boilerplate

プロジェクト名は GitHub のリポジトリ名にした

? Project name nuxt-boilerplate
? Project description My groundbreaking Nuxt.js project
? Use a custom server framework express
? Choose features to install Progressive Web App (PWA) Support, Linter / Formatter, Prettier, Axios
? Use a custom UI framework vuetify
? Use a custom test framework jest
? Choose rendering mode Universal
? Author name kurosame
? Choose a package manager yarn

とりあえず全部入れといたが、以下に該当する場合はインストールから除外してもいいと思う
(でも環境を CLI ツールで管理する時点で環境周りをメンテナンスすることは基本的に無いと思うので、全部入れてしまっても問題無いと思う)

  • SSR しない
? Use a custom server framework
None
? Choose rendering mode
SPA
  • モバイル対応しない
? Choose features to install
Progressive Web App (PWA) Supportを選択しない
  • HTTP リクエストしない(ほぼ無いと思うが)
? Choose features to install
Axiosを選択しない
  • デザインは全部自前
? Use a custom UI framework
None

サーバーの Node フレームワークはあまり詳しくないが、特に今使っているのが無ければ Express か Koa あたりを選んでおけば良いと思う
Vuetify は 1 年くらい使ってるが、かなり完成度高い UI フレームワークと思っているので、入れた
Linter や Prettier や Jest などのテストフレームワークは必須で入れておいた方が良い

実行

package.json を開くと、いくつかスクリプトが設定してあるが、とりあえず以下を実行

yarn dev

マシン環境にもよるが、まあまあ時間はかかる
スクリーンショット 2019-05-27 13.13.34.png

2 回目以降はnode_modules/.cacheの下にbabel-loaderのキャッシュを作ってる影響からか、少し速くなっている
スクリーンショット 2019-05-27 13.13.47.png

でもホットリローディングをサポートしているので、1 回起動すれば遅さは気にならない
nodemonを使って./serverディレクトリ内のファイルを監視してサーバの再起動を行っている

画面が表示されたら HTML ソースを見てみると、server-rendered="true"及び HTML の各要素がそのままレンダリングされていると思う
ちゃんと SSR されていることが分かる
ちなみに SPA モードであれば、JS が実行されてから画面がレンダリングされるので、読み込む JS ファイルのみが指定してあり、HTML はスタイルなどを除けばほとんど空である

また、実行すると.nuxtというディレクトリができており、サーバを立ち上げた時に読み込ませるコンテンツが格納されている
./server/index.jsを見てみると、Nuxt のインスタンスを作成して、Promise を返すnuxt.renderを Express に Middleware としてセットしている

しばらく開発で使うのは、dev スクリプトだけで良さそう

以下の他のスクリプトはある程度 Nuxt でアプリケーション作った後にちゃんと使ってみようと思う

  • build
    • minify して.nuxt/dist/に格納
  • start
    • production モードでサーバを立ち上げる
  • generate
    • Vue を静的コンテンツとしてビルドしてdist/に格納してくれる
    • Netlify や Firebase などでそのままホスティングできる(はず)
    • VuePress 使ったことがあれば、それ

使ってみる

ちょっとだけ使ってみてやったことや思ったことを書いてみようと思う

TypeScript を使う

yarn add -D @nuxt/typescript ts-node

tsconfig.jsonがあればnuxtコマンドで勝手にデフォルト値を書いてくれるらしい

touch tsconfig.json
npx nuxt

先程インストールしたts-nodeは Node で TS をトランスパイルせずに実行できるツールでこれが無いとnpx nuxtが動かない

これで導入は完了

後は、拡張子を.jsから.tsに変えて中身を書き換えていく
以下はserver/index.jsを TS に修正している例

server/index.ts
import NuxtConfiguration from '@nuxt/config'
...
// Import and Set Nuxt.js options
const config: NuxtConfiguration = require('../nuxt.config.js')
config.dev = !(process.env.NODE_ENV === 'production')
...

TS 導入前は上記のconfig.devが any 型だったが、導入後はちゃんと型定義の boolean 型を参照している

package.json も忘れずに修正

package.json
"scripts": {
-    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
+    "dev": "cross-env NODE_ENV=development nodemon server/index.ts --watch server",
-    "start": "cross-env NODE_ENV=production node server/index.js",
+    "start": "cross-env NODE_ENV=production ts-node server/index.ts",
}

Vue コンポーネントの TS 化についてはドキュメントではvue-property-decoratorの使用を薦めている

yarn add vue-property-decorator

layouts/default.vueを例に修正すると

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

@Component
class Default extends Vue {
  clipped: boolean = false
  drawer: boolean = false
  fixed: boolean = false
  items: { icon: string; title: string; to: string }[] = [
    {
      icon: 'apps',
      title: 'Welcome',
      to: '/'
    },
    {
      icon: 'bubble_chart',
      title: 'Inspire',
      to: '/inspire'
    }
  ]
  miniVariant: boolean = false
  right: boolean = true
  rightDrawer: boolean = false
  title: string = 'Vuetify.js'
}

export default Default
</script>

ちなみにvue-property-decoratorを使わなくても書ける

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  data(): {
    clipped: boolean
    drawer: boolean
    fixed: boolean
    ...
  } {
    return {
      clipped: false,
      drawer: false,
      fixed: false,
      ...
    }
  }
})
</script>

Vuex を使う

Nuxt をインストールした時にルート直下に store ディレクトリができており、既に Vuex 入ってる感があるが、中身は空である
Nuxt は store ディレクトリが存在すれば Vuex をインポートしてくれて、store をルートインスタンスに流してくれるらしい

なので

  • Vuex を手動で入れる必要がない
  • どのコンポーネントからでもthis.$storeで参照できる

また、モジュールモードとクラシックモードの 2 つのモードがあるが、クラシックモードは廃止予定らしい
肥大化してくるとモジュールごとに State が参照できた方が良いので、モジュールモードで良さそう

Nuxt 側でモジュールモードとクラシックモードのどちらで Vuex インスタンスが作られるかの判定は
store/index.(js|ts)が存在して、Store インスタンスをエクスポートしていれば、クラシックモード
それ以外はモジュールモード

モジュールモードでstore/index.(js|ts)という名前で作った場合、ルートモジュールという扱いで Store インスタンスの直下に State が存在する
store/モジュール名.(js|ts)という名前で作った場合、名前付きモジュールとして Store インスタンスの modules プロパティ配下に State が存在する

それぞれの State を参照する場合

  • ルートモジュールはthis.$store.state.counterで参照できる
  • ルート以外はthis.$store.state.todos.listで参照できる
    • 上記はstore/todos.(js|ts)というファイルを作った場合

ちなみに私は上記のルートモジュールと呼ばれてる Store インスタンスの直下に State を置くのは 1 度もやったことない

以下のファイルを store ディレクトリ配下に追加

store/sample.ts
interface IState {
  counter: number
}

export const state: IState = {
  counter: 123
}

簡単すぎる例だが、ファイルを追加するだけで Store に counter が保持できる

そして以下のように参照する

適当なVue
<script>
export default {
  mounted() {
    console.log(this.$store.state.sample.counter) // 123
  }
}
</script>

また、Vuex のヘルパー関数も使える

適当なVue
<script>
import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState({
      sample: 'sample'
    })
  },
  mounted() {
    console.log(this.sample.counter) // 123
  }
}
</script>

Linter とコードフォーマッターを使う

TS を使っているが、TSLint は非推奨となるらしいので、ESLint の TS パーサーを使う
.eslintrc.jsを見る限り ES(JS)だけの Linter で良ければ、Nuxt をインストールした時点で出来てるので、後は rules や extends でルールを追加するだけで良さそう

package.jsonを見るとeslint-plugin-prettiereslint-config-prettierが依存パッケージとしてインストールされており、.eslintrc.jsの extends に prettier が設定してあるので、ESLint と Prettier の競合は気にしなくて良さそう

TS 用にLinterを使う場合の修正箇所は以下

yarn add -D @typescript-eslint/eslint-plugin
.eslintrc.js
parserOptions: {
-    parser: 'babel-eslint'
+    parser: '@typescript-eslint/parser'
},

-  plugins: ['prettier'],
+  plugins: ['@typescript-eslint', 'prettier'],
package.json
"scripts": {
-    "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
+    "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore .",
}

また、ESLint のルールだと以下のような import は誤検知して、「no-unused-vars」を出してしまう

import NuxtConfiguration from '@nuxt/config'

この場合、以下のように ESLint のルールは握りつぶして、@typescript-eslintの方のルールを有効化する必要があるらしい
こちらの記事に書いてありますが、いくつかこのような誤検知するルールがあるらしいので、その都度同様の対応が必要

.eslintrc.js
rules: {
  'no-unused-vars': 'off',
  '@typescript-eslint/no-unused-vars': 'error'
}

Airbnb の ESLint ルールを入れてみる

yarn add -D eslint-config-airbnb-base
.eslintrc.js
extends: [
  'airbnb-base',
...
]

いい感じに動いているが、Vue とか Vuex は Nuxt に直接依存しており、こちらでpackage.jsonに書く必要が無いため、以下のような Lint エラーが出てる

.../nuxt-boilerplate/pages/index.vue
  63:1  error  'vuex' should be listed in the project's dependencies. Run 'npm i -S vuex' to add it  import/no-extraneous-dependencies

.../nuxt-boilerplate/plugins/vuetify.js
  1:1  error  'vue' should be listed in the project's dependencies. Run 'npm i -S vue' to add it  import/no-extraneous-dependencies

上記は以下のように対応可能

.eslintrc.js
settings: {
  'import/core-modules': ['vue', 'vuex']
},

他にも Nuxt と ESLint で競合するルールはありそう

vue-router を使う

Nuxt ではエントリーポイントとなる JS などで routes を書いてnew VueRouterをやる必要はない
pages ディレクトリ配下にルーターから直接呼ばれるコンポーネントを配置するだけで良い

以下の 2 つを配置

pages/sample/index.vue
<template>
  <span>サンプルインデックス!</span>
</template>
pages/sample/test.vue
<template>
  <span>サンプルテスト!</span>
</template>

それぞれ以下のようにしてルーティングさせる

適当なVue
<nuxt-link to="/sample">Go sample index</nuxt-link>
<nuxt-link to="/sample/test">Go sample test</nuxt-link>

ファイル名がindex.vueであれば、ディレクトリ名が path になり、それ以外のファイル名であれば、ディレクトリ名/ファイル名が path になる
先程の Vuex とルールが似ている

パラメータ付きの動的ルーティングもファイル名もしくはディレクトリ名の先頭にアンダースコアを付けることで実現できるらしい

nuxt-link というコンポーネントは router-link を extends したコンポーネントとなっており、nuxt-link を使わずに今まで通り<router-link>を使って実装することも可能
ただし、nuxt-link を使うと画面表示領域にリンクがあれば、そのリンク先のコンテンツを先読みしてくれるので、遷移が速くなる

試しに、以下のコードを適当な Vue の下の方に配置して、ファーストビューから見えないようにして、スクロールしてリンクを表示させるとindex.vueだけプリフェッチされることが分かる
Chrome DevTool の Network タブで確認すると分かりやすい

適当なVue
<nuxt-link to="/sample">Go sample index</nuxt-link>
<router-link to="/sample/test">Go sample test</router-link>

その他

選択した UI フレームワークでサンプルコードが作られている

私は Vuetify を選んだのだが、割としっかりサンプルコードが Vuetify で作られてて、全部の UI フレームワーク分作られてると思うとちゃんとしてるなって思った

テストのサンプルがほぼ無い

Logo コンポーネントの 1 ケースしかテストのサンプルコードが無い
E2E のサンプルコードは無い

でも最近は Vue のテスト周りのエコシステムが充実してきて、ドキュメントも豊富なので、問題ないと思う

Nuxt のアップデートは簡単にできるのだろうか

Angular みたいなマイグレーション機能はたぶん無いと思うが、Nuxt とか CLI 系のツールを使っているとアップデートが楽にできるとかなりありがたい
簡単にネットで調べた限りは Nuxt を最新にして出たエラーを潰してるっぽい

さいごに

主にツールの導入になってしまい、まだコードはほとんど書いてないのですが、これから ToDo アプリ程度のものは作ってみて、Nuxt を学んでみようかと思います
続きは別記事でまた書こうかなと思います

フロントエンドは全部 1 から作る派なのですが、たまに自動で環境作ってくれる系ツールを使うとそのお手本のような構成に勉強になることがあります
また、今後フロントエンドの環境を作る上で活かせることもあると思うので、試しに使ってみるのもありかなと思いました

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

Vuetify2.0のBeta版を使ってみる

Vuetify2.0を試してみたところVuetify1.5系のコードを持って行ったら全然動かなかったのでVuetify2.0を使う上で書き換えなければいけない部分を現時点で調査ができてる範囲でレポート

公式ドキュメント

まずこれが全然参考にならないのが大きな関門。サイトドメインはnext.vuetifyjs.comとなっているけれどもどうやらその内容の大半は1.x系の内容のコピーからアップデートされていない様子

githubのリリースノート

どうやらこれが正規の以降ガイドな模様

const opts = { ... }
-Vue.use(Vuetify, opts)
+Vue.use(Vuetify)
-new Vue(...).$mount('#app')
+new Vue({
+  vuetify: new Vuetify(opts)
+}).$mount('#app')

こんな風にオプション情報の指定方法を変えろとの事

またテーマについても

const opts = {
-  dark: true,
  theme: {
-    primary: '...',
+    dark: true,
+    themes: {
+      light: {
+        primary: '...',
+        ...
+      },
+      dark: {
+        primary: '...',
+        ...
+      }
+    }
  }
}

の様にprimaryなどの定義はthemes > light,darkの中に定義しろとの事

ただしこれでも上手くいかない
ブラウザコンソールでエラーを確認したところv-appが定義されてないみたいなエラー

githubのリリースノートにたどり着く以前に試した

  components: {
    VApp,
  },

をoptsに追記してみるがこれもダメ

というか実はこの書式はVue.use(Vuetify...に以下の様に配置した場合themeカラーは使えなかったものの
ボタン表示まではできていたので再度componentsだけこっちに移動

Vue.use(Vuetify, {
  components: {
    VApp,
  },
}

全体としては以下の様に書けば動作した

main.js
import Vue from 'vue'
import App from './App.vue'
import colors from 'vuetify/es5/util/colors'
import 'vuetify/dist/vuetify.min.css'

import Vuetify, {VApp,VBtn} from 'vuetify/lib'
const VuetifyComponents = {
  components: {
    VApp, VBtn,
  },
}

Vue.use(Vuetify, VuetifyComponents)

const opts = {
  theme: {
    dark: false,
    themes: {
      light: {
      },
      dark: {
      }
    }
  }
}

new Vue({
  vuetify: new Vuetify(opts),
  render: h => h(App),
}).$mount('#app')
App.vue
<template>
  <v-app>
    <div class="text-xs-center">
      <v-btn color="primary">Primary</v-btn>
      <v-btn color="secondary">Secondary</v-btn>
      <v-btn color="accent">Accent</v-btn>
    </div>
  </v-app>
</template>

opts.theme.dark===falseの場合
img_20190530_1.png

opts.theme.dark===trueの場合
img_20190530_2.png

opts.theme.dark===falseでthemesを以下の様に変えてみた場合

    themes: {
      light: {
        primary: colors.green.base,
        secondary: colors.green.darken4,
        accent: colors.purple.base,
      },
...

img_20190530_3.png

最初の触りとしてこれまでできれば移行は視野に入れられるのかなという感じ

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

Vuetifyのv-iconに自作SVGアイコンを(楽に)使いたい!!

TL;DR

SVGアイコンをWebfontにしてstyle.cssをインポートすると
<v-icon>{名前}</v-icon>で使えるようになるよ!

Vuetifyのリファレンスがかなりアレ

Vuetifyのv-iconってFontAwesomeとかのアイコンを利用できるじゃないですか。
でも自作のSVGアイコンも使いたいですよね。

一応リファレンスにやり方が載っています。

Installing Icons — Vuetify.js

簡単に訳すと、

SVGファイルを単一コンポーネントにしてVue.useの段階でセットすれば$vuetify.icons.{名前}で利用できるよ!

ということです。
追加したいアイコンが100個ぐらいあった場合、1個1個単一コンポーネントにしてVue.useで読み込ませるのは非常に不便ですしコードが長くなるしで割と最悪です。

そこで色々調べていたところ,このような投稿がありました。

Vuetify Custom Icons Documentation is Horrendous : vuetifyjs

一番最後の投稿に

Well, for anyone from the future who stumbles across this, I found a way to do it without using Vuetify at all.

So that site that I found, fontello.com, gives a css file that references the icons by class name, as I alluded to above. This class name is set on the site when you download the zip. 

So if you just import the css file as I did above, and then you can just reference the icon by name (with the prefix you used) in a v-icon and it should work.

If anyone sees this and is having trouble getting it to work, shoot me a message. 

I'm usually pretty good about answering within a couple days.

というコメントがあります。

fontello.comというサイトでcssファイルを使ってimportすれば読み込めるぞ!という内容です。

色々あって今回はfontello.comではなく同じ様なことをしてくれるicomoon.ioで試してみます。

サイトにアクセス

スクリーンショット 2019-05-30 9.33.52.png

Icon Font & SVG Icon Sets ❍ IcoMoon

アクセスしたら右上のIcoMoon Appをクリックしましょう

スクリーンショット 2019-05-30 9.34.00.png

でこんな感じになったら自作SVGアイコンをドラッグアンドドロップ

スクリーンショット 2019-05-30 9.41.45.png

アップロードしたアイコンが表示されたら、Webfontを生成したいアイコンをクリック(複数可能です)

クリックしたら右下のGenerate Fontをクリックして先に進みましょう

スクリーンショット 2019-05-30 9.42.10.png

この画面では アイコンの名前とコードを決められます。
コードは一意に定まる様にし、名前はわかりやすい名前をつけます。ここでの名前は実際に使用する際に使われます

設定が終わったら右下のDownloadボタンをクリックするとicomoon.zipファイルのダウンロードが始まります

Vueのプロジェクトに追加

解凍したicomoonフォルダをsrc/assetsに配置しましょう。
だいたいこんな感じになると思います。

スクリーンショット 2019-05-30 11.18.30.png

早速使ってみましょう

今回はVueのプロジェクトにvuetifyを入れたあと、HelloWorld.vueを編集し動作の確認を行います。

style.cssをインポートしての中に icon-{名前} を入れて、実行してみましょう。

スクリーンショット 2019-05-30 11.21.16.png

僕は先程 svgという名前とemoiという名前をアイコンに付けているので使用する際は上記の様になります。

ここで実行してみましょう

スクリーンショット 2019-05-30 11.20.57.png

うまくいってますね。 これでアイコンを表示させることが出来ました。

追加の設定

インポートしたstyle.cssを見ると、colorの設定があります。

v-icon側でcolorを設定したい場合、style.cssにあるcolorの設定を削除する必要があります。

実際の運用

親コンポーネントでimportするのはアレなのでv-iconをラッピングしたoreore-v-iconみたいな単一コンポーネントを作ってそこでインポートするのが運用としては正しいと思います。

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

element ui enterキーで フォーム送信

以下でフォームを送信せず、メソッドを動かせる。

onSubmit メソッドを動かす。

参考
http://imejin.biz/magazine/el-form-submit-on-enter/

<el-form  :rules="rules" ref="form" :model="form" @submit.native.prevent="onSubmit('form')">

                <el-form-item label="占いたい人の名前" prop="name">
                    <el-input v-model="form.name"></el-input>
                    <div class="t-c view-count"><span v-if="fortune.higawari">毎日変わるよ!</span></div>
                </el-form-item>

                <el-form-item style="text-align: center;">
                    <el-button type="success" native-type="submit">占う</el-button>
                </el-form-item>
            </el-form>

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

[GASで実装する] 同一ページ内で任意の場所までスクロールするやつ

GASは1プロジェクト1ページ

ページ内遷移も簡単ではない
移動したい先に<タグ id='xxx'>をおいて、そこに<a href="#xxx"></a>みたいなことはできない

情報量が増えてくると、何らかのナビゲーションは必要

ということで調べてみた

複数ページを作成して、doget.gsにパラメータを設定して分岐させる。という方法をわかりやすく説明されているのがこちら
GoogleAppsScript(GAS)でページ遷移を擬似的に実装する方法
同様の実装は複数のブログなどでも紹介されているので、割とポピュラーな実装方法なのかも

jQueryならなんかできるっぽい?

わからなかった(どうもjQueryのコードは見ても理解できない)

jQueryがダメならVueで何とかできる?

Watchは使えるか?

watch
watch: {
  '$route': function (val) {
    if (val.hash.match(/^#/)) {
      document.getElementById(val.hash.replace(/^#/, '')).scrollIntoView()
    }
  }
}

今回のケースでは使えなかった

smoothscrollってどうよ

続いて参考にさせていただいたサイト
意外と簡単!Vue.jsでスムーススクロールとトップへ戻るボタンを実装する方法

CDNから

CDNのリンクをhtmlに追加
<!-- vue.js -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- vue scroll 画面スクロール用 -->
<script src="https://cdn.jsdelivr.net/npm/vue-smooth-scroll@1.0.13/dist/vue-smooth-scroll.min.js"></script>
smoothscroll.html
<a href="#section1" v-smooth-scroll>go to Section1</a>
.
.
.
<div id="section1">セクション1</div>
vuedirective.js
Vue.directive('scroll', {
  inserted: function (el, binding) {
    let f = function (evt) {
      if (binding.value(evt, el)) {
        window.removeEventListener('scroll', f)
      }
    };
    window.addEventListener('scroll', f)
  }
});

上記をVueインスタンスの前に記述

以上でとりあえず動く

「トップへ戻る」的なボタンの実装がうまくいかないのが気になるので引き続き調べる

参考

vue-smooth-scroll
HTML Service: Create and Serve HTML

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

昔、作ったWebサービスをTypeScriptとVue.jsで作り直した話

はじめに

Vue.jsとTypeScriptでWebサービスを作ってみたいけどアイデア浮かばなかったので昔作ったアプリを作り直しました。

作ったもの

Qnow
Qiitaで新規投稿された記事のみを取得して表示するサービスです。
タグ名でフィルタリングして表示します。
app.gif
表示した記事のタグ名をクリックすることでクリックしたタグ名で再検索することができます。
ただ、今の仕様だと日付が変わってすぐだと記事が取得できないので後々変更しようと思います。

使用した技術

  • Vue.js
    • TypeScript
  • Github
    • ソースコードの管理
  • CircleCi
    • masterにプッシュされたらビルドとデプロイを実行
  • Firebase
    • 静的ファイルの配信

Vue.js

Vue Cli3を使用しました。
Vue Cli3ではTypeScriptが正式採用されているのでTypeScriptを使用しました。
でも、TypeScriptを全然書いたことなくてany型を使っているところがちょくちょくあるので修正をしたいと考えています。
Vueの単一コンポーネントはスタイルにスコープをあてることができるのでとても便利ですね。
CSSはSASSのSCSS記法で書いています。これも標準でサポートしてあるので楽でした。

Github

ソースコードを管理するのに使用しました。
英語が全然わからないのでGoogle翻訳で翻訳しながらコミットメッセージを書いたので変なコミットメッセージが多いかもです。

CircleCi

Githubにpushされたらビルドして自動的にデプロイするようにしました。

circleci/config.yml
version: 2
jobs:
  build:
    docker:
      - image: circleci/node:10.15.3
    working_directory: ~/repo
    steps:
      - checkout
      - restore_cache:
        # 復元するキャッシュのkey
          keys:
            - v1-dependencies-{{ checksum "package.json" }}
            - v1-dependencies-
      # 依存関係インストール
      - run:
          name: Install Project
          command: npm install

      # ビルド
      - run:
          name: Build
          command: npm run build

      # ビルドの確認  
      - run:
          name: Check dist
          command: ls -la dist

      # キャッシュの保存
      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}
      - run:
          name: Install Firebase-tools
          command: npm install --save-dev firebase-tools

      - run:
          name: Deploy to Firebase hosting
          command: ./node_modules/.bin/firebase deploy --project "$FIREBASE_PROJECT_ID" --token "$FIREBASE_DEPLOY_TOKEN"

設定手順

  1. CircleCiにログイン
  2. Githubアカウントでログインしていると思うのでプロジェクトは紐付けられる
  3. Githubのリポジトリに.circleci/config.ymlを追加する
  4. GithubにPushするごとにビルドが実行される
  5. firebaseでプロジェクトを作成する
  6. firebaseでfirebase-toolsをインストール
  7. firebaseにログインする
$ firebase login

画面にしたがってログインする
8. firebaseをCI上で使用するために以下のコマンドを打つ

$ firebase login --reauth
$ firebase login:ci

これでtokenを取得できる
9. .circleci/config.ymlにfirebaseでデプロイするコマンドを追加する
10. CircleCi上の設定でfirebaseのtokenとfirebaseのプロジェクトIDを追加する

おわりに

このサービスを開発するにあたって自分の技術のなさを改めて知ることができました。
これからも日々勉強をしようと思います。
よろしければフィードバック頂けると嬉しいです。
ソースコード: https://github.com/gba-3/qnow

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

ReactやVueなどのJavascriptフレームワークからRailsの多対多モデルに対して、関連付けも含めて一気に作ってみた

こんにちは!@hairgaiです。
突然ですが、とあるTeamを作成するときにMemberを一緒に所属させたい!と思うことって一日に一回くらいありませんか?
今回は、それを実装するときにJavascriptからRailsへのパラメータで若干悩んだので、それを共有しようかなと思います。

前提

よくある多対多です。

  • Teamモデル
    • has_many :team_members
    • has_many :members, through: :team_members
  • TeamMemberモデル
    • belongs_to :team
    • belongs_to :member
  • Memberモデル
    • has_many :team_members: :destroy
    • has_many :teams, through: :team_members

React 16.8.6、Rails 5.2.2

実装

Memberが数人いて何かTeamを作るときに、一度作成して後から所属させるのは面倒なので、当然ながら「Teamを作るときにMemberを所属させつつ作れたらいいのに」と思うと思います。
なので、フロントでTeamを作成するときに、既に登録済みのMemberを一覧で表示して、それを選択させることでパラメータを同時に送らせることにします。

image.png

こんな感じですね。

また、Railsでは、モデルを作成するときに、その多対多関係にあるモデルを一緒に関連付けることが可能です。
なので、上記のように「Teamを作るときにMemberを所属させつつ作れたらいいのに」と思ったら、

pry(main)> Member.create!(email: 'a@example.com')

=> #<Member:0x00007fb1498f04d8
  id: 1,
  email: "a@example.com",
  # ...

pry(main)> team = Team.create!(name: 'hoge', member_ids: [1])

=> #<Team:0x00007fb142722860
  id: 1,
  name: "hoge",
  # ...

pry(main)> team.members

=> [#<Member:0x00007fb149951af8
  id: 1,
  email: "a@example.com",
  # ...
  ]

のように、[モデル名]_idsというパラメータに配列を渡すと関連付けることが出来ます。
なので、フロント側でこのパラメータが渡ってくるようにURLパラメータを生成すれば良いということですね。

パラメータ送りたい

ググると色々な記事が出てくる通り、JavascriptからURLにパラメータ用のクエリを乗っけるときは、qsを使いました。
qsは、Javascriptの連想配列(Hash)からURLパラメータを生成してくれるライブラリで、例えば

import qs from 'qs'; // or var qs = require('qs');

const params = { team: { name: 'hoge' }};
qs.stringify(params) // => 'team%5Bname%5D=hoge'

という感じで使います。
なので(今回はパラメータの話なので解説しませんが)、フロント側でいい感じにパラメータを連想配列として生成します。

// 実際はフレームワーク上でユーザの入力に合わせて作りますが、下記のような連想配列が生成されます。
const params = {
  team: {
    name: 'hoge',
    member_ids: [2, 3]
  }
}

qs.stringify(params) // => 'team%5Bname%5D=hoge&team%5Bmember_ids%5D%5B0%5D=2&team%5Bmember_ids%5D%5B1%5D=3'

これをライブラリ等を用いてRailsに送ってみると…(axiosを使いました)

    15: def update
    16:   binding.pry
 => 17:   @team.update!(team_params)
    # ...

[1] pry(#<Api::V1::TeamsController>)> params
=> <ActionController::Parameters {"team"=>{"id"=>"1", "name"=>"hoge", "member_ids"=>{"0"=>"1", "1"=>"2", "2"=>"3"}}, "format"=>"json", "controller"=>"api/v1/teams", "action"=>"update", "id"=>"1"} permitted: false>

あれ?なんか変な形で送られてますね。

クエリの生成方法のオプションがあった

qsが生成したURLパラメータをデコードしてみると…

team[name]=hoge&team[member_ids][0]=2&team[member_ids][1]=3

ということで、team[member_ids][0]=2&team[member_ids][1]=3のように指定されています。これをRailsでは"member_ids"=>{"0"=>"2", "1"=>"3"}と解釈してしまうようですね。(確かにHashっぽいです)
では、Railsではどう記述すればArrayと解釈されるのか、ということで、逆にArrayからクエリを生成してみましょう。

pry(main)> { name: 'hoge', member_ids: [2, 3] }.to_query('team')
=> "team%5Bmember_ids%5D%5B%5D=2&team%5Bmember_ids%5D%5B%5D=3&team%5Bname%5D=hoge"

# => team[member_ids][]=2&team[member_ids][]=3&team[name]=hoge

順番は違いますが、若干指定の仕方が違いますね。Arrayのクエリでは、[]のように数字を入れずに指定すれば良いようです。
現在はその数字の処理はqs側にまかせています。qsのGitHubをよく読んでみると…

You may use the arrayFormat option to specify the format of the output array:

qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' })
// 'a[0]=b&a[1]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' })
// 'a[]=b&a[]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })
// 'a=b&a=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'comma' })
// 'a=b,c'

とのことです。
なので、フロント側でURLパラメータを生成する際に、

const params = {
  team: {
    name: 'hoge',
    member_ids: [2, 3]
  }
}

qs.stringify(params, { arrayFormat: 'brackets' }) // => 'team%5Bname%5D=hoge&team%5Bmember_ids%5D%5B%5D=2&team%5Bmember_ids%5D%5B%5D=3'

とオプションを追加してみると…

[1] pry(#<Api::V1::TeamsController>)> params
=> <ActionController::Parameters {"team"=>{"id"=>"1", "name"=>"hoge", "member_ids"=>["2", "3"]}, "format"=>"json", "controller"=>"api/v1/teams", "action"=>"update", "id"=>"1"} permitted: false>

出来ました!レコードも正常に作られます。

結論

Railsは、Railsが提供するWayに沿えば非常に強力なフレームワークですが、その分色々な決まりがあるので、こういった細かいところもキチンと調べていきたいですね。

この記事が誰かの参考になれば幸いです。

参考

ありがとうございました!

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

2019年に知っておくべきJava ScriptのフレームワークTOP5

こんにちは、ベイマックスのでかい人形を買おうかどうか迷って半年になるテノヒラです:hand_splayed_tone2:
最近チームでwikiを書く文化が芽生えつつあるんですよ。
わたしもビギナーに向けてトレンドとかをお伝えするものがかければと思って記事を漁っていたら、フレームワークについてこじんまりかついい感じにまとめてある素晴らしい記事を発見したので翻訳してご紹介します!
翻訳する記事はこちら?
Top Javascript frameworks you should know in 2019
世界のトレンドと日本のトレンドはちょっと違ったりするので要チェックです:eyes:

ところどころ意訳をしていますが、もし致命的な翻訳ミスがありましたら編集リクエストしてください:kissing_heart:

はじめに

JavaScriptは世界で一番人気のあるプログラミング言語です。
インターネットでほぼ全てのwebサイトのフォームに使われています。
JavaScriptまたはJavaScriptのフレームワーク、ライブラリ関係なくどこでもJavaScriptを見つけることができます。

JavaScriptはフロントエンドだけでなくサーバーサイドの言語として使われています。
いまではJavaScriptでwebサイトを完全に構築させることができるのです:open_mouth:

下記が2019年で最も人気なフレームワーク5選です。

1) Angular

2) React

3) NodeJS

4) Vue.js

5) Backbone.js

Angular

image.png

Angularは最も人気のあるJavaScriptのフレームワークで、Typescriptのオープンソースです。
最初は2016年9月14日にGoogleがリリースしました。
AngularはMVC (Model-View-Conrtoller)パターンをベースにしています。
これは完璧なフレームワークで、企業のwebアプリケーションを構築するすべてを提供しています。
なのでAngularはwebアプリケーションを構築する上で最も人気のあるJavaScriptなのです。
大企業では大抵採用されています。

React

image.png

Reactはユーザーインターフェイスを創るためのオープンソースのライブラリです。
2013年にFacebookによって開発されました。
Reactはコンポーネントベースで拡張性があり、webインターフェースを構築するのに速いフレームワークです。
Angularと異なっている部分は、Reactはwebインターフェースを構築する機能を提供しています。

最近はReactがwebインターフェースを構築する最も人気があるJavaScriptライブラリのひとつとなりました。
ただ、Reactは最も手のかかるJavaScriptのライブラリです。
需要高いのでやれるようになれば思わず笑みが溢れるよう給与がもらえます:smirk:

Node JS

image.png

Nodejs、Node.jsもしくはNodeはオープンソースで、Google ChromeのJavaScript V8 Engineに基づいたサーバーサイドのJavaScriptのフレームワークです。
主に速い構築、拡張性、サーバーサイドアプリケーションをベースとした信頼性の高いネットワークを主に設計しています。
Indeed.comには現在、8,905つの求人があります。
Node開発者の平均的な給与はだいたい 10万5千ドル(約1,148万円)です。

Vue.js

image.png

Vueもしくはvue.jsはユーザーインターフェースを構築するための革新的なフレームワークです。
ライブラリのコアは、表示のレイヤーだけに焦点を合わせている、簡単に習得できる、他のライブラリもしくは既存のプロジェクトとの統合といった部分です。
違う視点からみると、Vueはモダンなツールとサポートしているライブラリとを一緒に使った時に洗礼されたシングルページアプリケーションにすることができます。
webページの表示に焦点を当てたVueもフロントエンド界隈では人気のあるフレームワークとなります。

Indeed.comには現在、1,316つの求人があります。
平均的な給与はおよそ10万ドル(約1,093万円)もしくはそれ以上です。

Backbone.js

image.png

BackboneもしくはBackbone.jsはRESTful JSONインターフェースのフレームワークで、model–view–presenter (MVP)アプリケーションデザインパターンをベースとしているものです。

Backboneは2010年にJeremy Ashkenasによってつくられ、7年後には最も人気のあるフレームワークのひとつとなりました。

Indeed.comには現在、5,135つの求人があります。
backboneフロントエンドとbackboneフルスタックエンジニアの平均的な給与はそれぞれおよそ10万2千ドル(約1,115万円)と10万1千ドル(約1,104万円)です。

React Vs Angular

ReactとAngularはweb開発者によって最も討論されることです。
これらを学習したプログラマーと開発者にとってどちらを学ぶべきなのかは悩むところです。
webサイトを開発する人にとってはどちらを使うべきかを悩みます。
どちらを採用するかを決定するのはいつも課題となります。

Angularは完全なweb applicationの構築する機能を提供しており、Reactはインターフェースだけを構築するのに使われているライブラリです。
アーキテクチャーの概念によると、Angularはモデルと表示、制御の機能を処理しますが、Reactは表示のみを処理します。
AngularはTypescriptをベースとしていますが、ReactはJavascript上で動きます。
ReactはFacebookによって開発されましたが、AngularはGoogleによって開発されています。
AngularのIonicはモバイルアプリケーションの構築に使われますが、
Reactはモバイルアプリケーション開発のためにReact Nativeを提供します。

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

Vue.jsで昔懐かしのframeタグを実現する方法

前置き

HTML5以前の時代には、frameタグなるものが存在していました。
1つのWEBページの中に複数のWEBページを埋め込んで上下や左右に並べて表示させるタグです。
2000年代前半あたりにはframeタグを活用した個人サイトなどが数多く存在したような気がします。
(index.htmlの中にframesetを書いてフレームの左側にmenu.html、右側にtop.htmlとかやってたり)

HTML5が登場してからはframeタグはframesetタグと共に非推奨となり、今ではもうこのタグを使っているサイトはほぼ無いと思います。

ですがもしかするとframeタグが今でも好きな方がいらっしゃるのではと思い、代替案としてVue.jsを用いてframeタグらしきものを再現してみました。
(実際に需要があるかどうかは特に調査していませんので気にしないでください。)

デモ

vue-frame
(frameの境目をドラッグしてコンテンツの幅を変えられます)

動作イメージ

vue-frame.gif

ざっくり解説

構成

キャプチャ.JPG

左側

LeftFrame.vue
<template>
  <div class="left-frame" v-bind:style="{width:width + 'px'}">
    <div class="left-frame-content">ヒダリー</div>
    <div class="frame-border" @mousedown="$emit('startResize')"></div>
  </div>
</template>

<script>
export default {
  props: {
    width: Number
  }
};
</script>

<style scoped>
.left-frame {
  background-color: rgb(240, 240, 255);
  display: flex;
}

.left-frame-content {
  flex-grow: 1;
  padding-left: 10px;
}

.frame-border {
  width: 3px;
  background-color: rgb(208, 208, 208);
  border-left: solid 0.5px rgb(170, 170, 170);
  border-right: solid 0.5px black;
}

.frame-border:hover {
  cursor: col-resize;
}
</style>
  • コンテンツの横幅は可変にするため、変数で保持
  • フレームの境界線にはドラッグイベントを持たせるため、borderっぽいdiv要素(frame-border)を配置

右側

jRightFrame.vue
<template>
  <div class="right-frame">ミギー</div>
</template>

<style scoped>
.right-frame {
  background-color: white;
  flex-grow: 1;
  padding-left: 10px;
}
</style>
  • 画面右側に表示させたいコンテンツを配置するだけ

本体

Frame.vue
<template>
  <div
    class="frame"
    v-bind:class="{dragged: isDragged}"
    @mousemove="resizeFrame"
    @mouseup="endResizeFrame">
    <left-frame v-bind:width="leftWidth" @startResize="startResize"></left-frame>
    <right-frame></right-frame>
  </div>
</template>

<script>
import LeftFrame from "./LeftFrame.vue";
import RightFrame from "./RightFrame.vue";

const LEFT_FRAME_MIN_WIDTH = 45;
const FRAME_ADJUSTED_SETTING = 2;

export default {
  components: {
    LeftFrame,
    RightFrame
  },
  data() {
    return {
      isDragged: false,
      leftWidth: 200
    };
  },
  methods: {
    startResize() {
      this.isDragged = true;
    },
    resizeFrame(event) {
      if (event.buttons === 0) {
        this.endResizeFrame();
        return;
      }
      if (this.isDragged) {
        if (event.clientX + FRAME_ADJUSTED_SETTING < LEFT_FRAME_MIN_WIDTH) {
          this.leftWidth = LEFT_FRAME_MIN_WIDTH;
          return;
        }
        this.leftWidth = event.clientX + FRAME_ADJUSTED_SETTING;
      }
    },
    endResizeFrame() {
      this.isDragged = false;
    }
  }
};
</script>

<style scoped>
.frame {
  display: flex;
  flex-direction: row;
  height: 100vh;
}

.dragged * {
  cursor: col-resize;
}
</style>

  • ドラッグ開始(startResize())はLeftFrameから発火させる
  • mousemoveイベントでマウスの位置を監視する
  • event.buttonsの値でマウスの押下状態を判断(===0ならマウス押下無し)
  • マウスの位置(event.clientX)でleftFrameの幅を変更させる(フレームのstyleに合わせて適当に微調整)

ソースコード全体

vue-frame(GitHub)

参考資料

<frame> - HTML: HyperText Markup Language | MDN

jQueryを使った擬似フレーム(高機能版)

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