20210314のvue.jsに関する記事は18件です。

rails&vueでherokuにデプロイする際に参考になったこと

はじめに

Rails 6.0.3.5
ruby 2.6.5
yarn 1.22.10
にてデプロイを実行した。

アプリ概要

投稿サイト
・jwtによるログインログアウト機能
・投稿機能(ログイン後投稿可、投稿者のみ削除編集可)
・プロフィール編集機能

エラー内容

デプロイは成功し、
ログイン機能は正常に動いたのだが、
POST機能、EDIT機能を行う際に、
検証ツールのconsoleにて、

401 Unauthorized

エラーが出現、

なぜ、ログインしているにも関わらず401が出現するのか、

protect_from_forgery with: :null_session

になっていることを確認し、

skip_before_action :verify_authenticity_token

を追加。

すると、今度は、 

500 (Internal Server Error)

が出現、

$ heroku logs

にてエラー内容を確認したところ、

undefined method `post' for nil:NilClass

というエラー内容、
また、プロフィール編集時のエラーは、

undefined method `update' for nil:NilClass

となりました、、、

原因検索1

ローカル環境で動いているため、
[post]や[update]が定義されていないということは考えにくい。

def create
    post = current_user.posts.create!(post_params) #ここのcurrent_user
    render json: post, serializer: postSerializer
  end

postする前のcurrent_userが渡っていなと推測。

なぜcurrent_userが取れていないのか、
local環境とproduction環境の違いをチェック。

config/enviroments/development.rbとproduction.rbの違いを確認した。
認証に関わりそうな部分はなく、
特にここが原因ではないみたい。

原因検索2

jwtが送信できているかを、ブラウザ検証ツールのnetworkタブで確認する.
Authorization: Bearer にて、トークンが到達していることを確認。

原因検索3

ローカル環境で、問題なく動いているため、
再度、herokuにて問題がないことを確認、
herokuのsettings/Reveal Config Varsにて、、、、、

master_keyの設定をしていませんでした。。。

secret_key_baseを設定し安心し切っていましたが、
思わぬところに原因がありました。

補足

rails & vueのデプロイ
(herokuにpushした後)につきましては、
ログアウト → ログインをしてから、挙動を確かめるようにしましょう。

最後に

初心者のうちは思いもよらないところにミスがある可能性が高いので、
深読みしすぎず、やるべきことをしっかりやってあるかの確認を、
怠らないように気をつけたいと思います。

ご視聴いただきありがとうございます。
誰かの役に立てば幸いです。

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

vue.jsに触れてみる

前書き

vue.jsをする機会ができたので軽く触れていこうと思います。
とりあえずプロジェクト作成から始めていきます。

プロジェクト作成

vue-cliのインストールから始めます。
その前にvue-cliとは

Vue CLIとは CLIはCommnad Line Interfaceの略で、コマンドラインを使ってvue.jsで開発を行うための前準備を支援してくれるツールです。

らしいです。

早速書きコマンドでインストール

npm install -g vue-cli

エラーが発生しました。
WS000000.JPG

どうやらC:\Users\Admin\AppData\Roaming\npm\node_modules\vue-cli\bin\vueってファイルが既にあるらしく怒られています。
一度対象ファイルを削除してもう一度インストールコマンド実行
でもまた同じエラー...

npm install -g vue-cli --force

上記コマンドで無理やりインストールできるそうなのでこちらでインストール

次にプロジェクトを下記コマンドで作成します。

vue init webpack [project名]

WS000001.JPG

作成できたみたいです。
下記コマンドでVue.jsのWebサーバが起動する。その後http://localhost:8080を開きます。

npm run dev

WS000003.JPG

上記画面が表示され無事動作確認ができました。

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

Google Books APIsでブックカバー検索

一年前に作ったブックレビューアプリ、検索の精度を上げてポートフォリオを強化しようと思ったら2021年現在あまりにも情報が溢れているためポートフォリオとしては価値が無いと思ったので途中だけど供養します。

Nuxt-bookreview

↑本のタイトルを検索し表紙を表示するところまで作ってます。

表示された画像はボタンになっていて、押すとその画像だけが表示されます。
特に意味はないのですが、これから色々と遊んでみようと思います。

スクリーンショット 2021-03-14 19.05.16.png

見た目

<template>
  <div class="">
    <div class="text-center">
      <input class="ex-form" v-model="bookTitle" />
      <button class="ex-btn_default" @click="search">検索</button>
    </div>

    <img
      style="width:200px"
      class="m-auto mt-48 justify-center"
      v-if="buttonImage"
      v-bind:src="buttonImage"
    />

    <div v-else>
      <div style="flex-wrap" class="flex-wrap justify-center">
        <div class="m-5 inline-flex" v-for="(booksArray, key) in booksArray" :key="key">
          <img
            class="inline-block test"
            style="width: 150px; min-width: 150px"
            @click="doAction(booksArray)"
            v-bind:src="booksArray"
          />
        </div>
      </div>
    </div>
  </div>
</template>

スクリプト

<script>
const axios = require("axios");
let url = "https://www.googleapis.com/books/v1/volumes?&maxResults=30&q=";
export default {
  data: function() {
    return {
      json_data: {},
      items: [],
      bookTitle: "",
      booksArray: [],
      buttonImage: ""
    };
  },
  methods: {
    doAction: function(booksArray) {
      this.buttonImage = booksArray;
    },
    list: function() {
      let booksArray = [];
      let book = [];
      for (let i = 0; i < 30; i++) {
        if (!this.items[i].volumeInfo.imageLinks) {
          continue;
        }
        book = this.items[i].volumeInfo.imageLinks.thumbnail;
        booksArray[i] = book;
        this.booksArray.push(booksArray[i]);
        console.log(booksArray);
      }
    },
    search: function() {
      this.booksArray = [];
      this.buttonImage = "";
      axios
        .get(url + this.bookTitle)
        .then(response => ((this.items = response.data.items), this.list()));
    }
  }
};
</script>
  1. 入力フォームにタイトルを入力
  2. 検索ボタンを押すとsearchメソッドが発火
  3. axios.getでjson取得
  4. レスポンスを変数itemsに格納
  5. listメソッドを発火

listメソッドについて

items以下に複数のvolumeinfoカラムがある。for文でそれぞれ展開し、配列booksArrayに格納する。
imageLinks以下を持たないカラムもあるので、その場合はスキップする。

listメソッドの他の書き方

whileを使う。

    list: function() {
      this.booksArray = [];
      let booksArray = [];
      let book = [];
      let i = -1;
      while (true) {
        i += 1;
        if (!this.items[i].volumeInfo.imageLinks) {
          continue;
        }
        book = this.items[i].volumeInfo.imageLinks.thumbnail;
        booksArray[i] = book;
        this.booksArray.push(booksArray[i]);

        if (i == 30) {
          break;
        }
      }
    },

CSS

基本はTailwindcssだが、ボタンやフォームなどのパーツはcssで書く。

<style>
.t-book-w {
  max-width: 150%;
}

.test {
  display: flex;
  flex-wrap: wrap;
}

/* 入力フォーム */
.ex-form {
  height: 36px;
  background-color: #efefed;
  border-radius: 5px;
  font-family: "Noto Sans JP", sans-serif;
  line-height: 1.5;
  letter-spacing: 0.05em;
  padding: 8px 16px;
  outline: none;
  font-size: 14pt;
  color: #444444 !important;
  margin-top: 50px;

  width: 50% !important;
}
.ex-form:focus {
  box-shadow: 0 0 0 2px #ebd800;
  background-color: #ffffff;
}

/* ボタン */
.ex-btn_default {
  border-radius: 5px;
  background-color: #98f0ff;
  min-height: 40px;
  min-width: 160px;
  width: 160px;
  font-family: "M PLUS Rounded 1c", sans-serif;
  font-weight: Bold;
  font-size: 18px;
  letter-spacing: 0.05em;
  filter: drop-shadow(1px 3px 5px rgba(68, 68, 68, 0.1));
  outline: none;
  border-radius: 30px;
  color: white;
  padding: 1px 30px 1px 30px;
}

.ex-btn_default:hover {
  background-color: #77e6fa;
  filter: drop-shadow(1px 3px 5px rgba(68, 68, 68, 0.2));
}

.ex-btn_default:focus {
  outline: none;
}
</style>

感想

JavaScript書くの楽しい。
アニメーションとか入れて遊んでみよう、という気持ちになってきました。

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

Google Books APIsでブックカバー検索 / Nuxt

一年前に作ったブックレビューアプリ、検索の精度を上げてポートフォリオを強化しようと思ったら2021年現在あまりにも情報が溢れているためポートフォリオとしては価値が無いと思ったので途中だけど供養します。

Nuxt-bookreview

↑本のタイトルを検索し表紙を表示するところまで作ってます。

表示された画像はボタンになっていて、押すとその画像だけが表示されます。
特に意味はないのですが、これから色々と遊んでみようと思います。

スクリーンショット 2021-03-14 19.05.16.png

見た目

<template>
  <div class="">
    <div class="text-center">
      <input class="ex-form" v-model="bookTitle" />
      <button class="ex-btn_default" @click="search">検索</button>
    </div>

    <img
      style="width:200px"
      class="m-auto mt-48 justify-center"
      v-if="buttonImage"
      v-bind:src="buttonImage"
    />

    <div v-else>
      <div style="flex-wrap" class="flex-wrap justify-center">
        <div class="m-5 inline-flex" v-for="(booksArray, key) in booksArray" :key="key">
          <img
            class="inline-block test"
            style="width: 150px; min-width: 150px"
            @click="doAction(booksArray)"
            v-bind:src="booksArray"
          />
        </div>
      </div>
    </div>
  </div>
</template>

スクリプト

<script>
const axios = require("axios");
let url = "https://www.googleapis.com/books/v1/volumes?&maxResults=30&q=";
export default {
  data: function() {
    return {
      json_data: {},
      items: [],
      bookTitle: "",
      booksArray: [],
      buttonImage: ""
    };
  },
  methods: {
    doAction: function(booksArray) {
      this.buttonImage = booksArray;
    },
    list: function() {
      let booksArray = [];
      let book = [];
      for (let i = 0; i < 30; i++) {
        if (!this.items[i].volumeInfo.imageLinks) {
          continue;
        }
        book = this.items[i].volumeInfo.imageLinks.thumbnail;
        booksArray[i] = book;
        this.booksArray.push(booksArray[i]);
        console.log(booksArray);
      }
    },
    search: function() {
      this.booksArray = [];
      this.buttonImage = "";
      axios
        .get(url + this.bookTitle)
        .then(response => ((this.items = response.data.items), this.list()));
    }
  }
};
</script>
  1. 入力フォームにタイトルを入力
  2. 検索ボタンを押すとsearchメソッドが発火
  3. axios.getでjson取得
  4. レスポンスを変数itemsに格納
  5. listメソッドを発火

listメソッドについて

items以下に複数のvolumeinfoカラムがある。for文でそれぞれ展開し、配列booksArrayに格納する。
imageLinks以下を持たないカラムもあるので、その場合はスキップする。

listメソッドの他の書き方

whileを使う。

    list: function() {
      this.booksArray = [];
      let booksArray = [];
      let book = [];
      let i = -1;
      while (true) {
        i += 1;
        if (!this.items[i].volumeInfo.imageLinks) {
          continue;
        }
        book = this.items[i].volumeInfo.imageLinks.thumbnail;
        booksArray[i] = book;
        this.booksArray.push(booksArray[i]);

        if (i == 30) {
          break;
        }
      }
    },

CSS

基本はTailwindcssだが、ボタンやフォームなどのパーツはcssで書く。

<style>
.t-book-w {
  max-width: 150%;
}

.test {
  display: flex;
  flex-wrap: wrap;
}

/* 入力フォーム */
.ex-form {
  height: 36px;
  background-color: #efefed;
  border-radius: 5px;
  font-family: "Noto Sans JP", sans-serif;
  line-height: 1.5;
  letter-spacing: 0.05em;
  padding: 8px 16px;
  outline: none;
  font-size: 14pt;
  color: #444444 !important;
  margin-top: 50px;

  width: 50% !important;
}
.ex-form:focus {
  box-shadow: 0 0 0 2px #ebd800;
  background-color: #ffffff;
}

/* ボタン */
.ex-btn_default {
  border-radius: 5px;
  background-color: #98f0ff;
  min-height: 40px;
  min-width: 160px;
  width: 160px;
  font-family: "M PLUS Rounded 1c", sans-serif;
  font-weight: Bold;
  font-size: 18px;
  letter-spacing: 0.05em;
  filter: drop-shadow(1px 3px 5px rgba(68, 68, 68, 0.1));
  outline: none;
  border-radius: 30px;
  color: white;
  padding: 1px 30px 1px 30px;
}

.ex-btn_default:hover {
  background-color: #77e6fa;
  filter: drop-shadow(1px 3px 5px rgba(68, 68, 68, 0.2));
}

.ex-btn_default:focus {
  outline: none;
}
</style>

感想

JavaScript書くの楽しい。
アニメーションとか入れて遊んでみよう、という気持ちになってきました。

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

Google Books APIsで本や漫画の表紙画像を検索 / Nuxt

一年前に作ったブックレビューアプリの検索の精度を上げてポートフォリオを強化しようと思ったら2021年現在あまりにも関連情報が溢れているため、完成させてもポートフォリオとしては価値が無いと思ったので途中だけど供養します。

Nuxt-bookreview

↑本のタイトルを検索し表紙を表示するところまで作ってます。

表示された画像はボタンになっていて、押すとその画像だけが表示されます。
特に意味はないのですが、これから色々と遊んでみようと思います。

スクリーンショット 2021-03-14 19.05.16.png

見た目

<template>
  <div class="">
    <div class="text-center">
      <input class="ex-form" v-model="bookTitle" />
      <button class="ex-btn_default" @click="search">検索</button>
    </div>

    <img
      style="width:200px"
      class="m-auto mt-48 justify-center"
      v-if="buttonImage"
      v-bind:src="buttonImage"
    />

    <div v-else>
      <div class="flex-wrap justify-center">
        <div class="m-5 inline-flex" v-for="(booksArray, key) in booksArray" :key="key">
          <img
            class="inline-block ex-test"
            style="width: 150px; min-width: 150px"
            @click="doAction(booksArray)"
            v-bind:src="booksArray"
          />
        </div>
      </div>
    </div>
  </div>
</template>

スクリプト

<script>
const axios = require("axios");
let url = "https://www.googleapis.com/books/v1/volumes?&maxResults=30&q=";
export default {
  data: function() {
    return {
      json_data: {},
      items: [],
      bookTitle: "",
      booksArray: [],
      buttonImage: ""
    };
  },
  methods: {
    doAction: function(booksArray) {
      this.buttonImage = booksArray;
    },
    list: function() {
      let booksArray = [];
      let book = [];
      for (let i = 0; i < 30; i++) {
        if (!this.items[i].volumeInfo.imageLinks) {
          continue;
        }
        book = this.items[i].volumeInfo.imageLinks.thumbnail;
        booksArray[i] = book;
        this.booksArray.push(booksArray[i]);
        console.log(booksArray);
      }
    },
    search: function() {
      this.booksArray = [];
      this.buttonImage = "";
      axios
        .get(url + this.bookTitle)
        .then(response => ((this.items = response.data.items), this.list()));
    }
  }
};
</script>
  1. 入力フォームにタイトルを入力
  2. 検索ボタンを押すとsearchメソッドが発火
  3. axios.getでjson取得
  4. レスポンスを変数itemsに格納
  5. listメソッドを発火

listメソッドについて

items以下に複数のvolumeinfoカラムがある。for文でそれぞれ展開し、配列booksArrayに格納する。
imageLinks以下を持たないカラムもあるので、その場合はスキップする。

listメソッドの他の書き方

whileを使う。

    list: function() {
      this.booksArray = [];
      let booksArray = [];
      let book = [];
      let i = -1;
      while (true) {
        i += 1;
        if (!this.items[i].volumeInfo.imageLinks) {
          continue;
        }
        book = this.items[i].volumeInfo.imageLinks.thumbnail;
        booksArray[i] = book;
        this.booksArray.push(booksArray[i]);

        if (i == 30) {
          break;
        }
      }
    },

CSS

基本はTailwindcssだが、ボタンやフォームなどのパーツはcssで書く。
Tailwindcssと差別化するために自作のcssは名前に"ex-"をつける。

<style>
.t-book-w {
  max-width: 150%;
}

.ex-test {
  display: flex;
  flex-wrap: wrap;
}

/* 入力フォーム */
.ex-form {
  height: 36px;
  background-color: #efefed;
  border-radius: 5px;
  font-family: "Noto Sans JP", sans-serif;
  line-height: 1.5;
  letter-spacing: 0.05em;
  padding: 8px 16px;
  outline: none;
  font-size: 14pt;
  color: #444444 !important;
  margin-top: 50px;

  width: 50% !important;
}
.ex-form:focus {
  box-shadow: 0 0 0 2px #ebd800;
  background-color: #ffffff;
}

/* ボタン */
.ex-btn_default {
  border-radius: 5px;
  background-color: #98f0ff;
  min-height: 40px;
  min-width: 160px;
  width: 160px;
  font-family: "M PLUS Rounded 1c", sans-serif;
  font-weight: Bold;
  font-size: 18px;
  letter-spacing: 0.05em;
  filter: drop-shadow(1px 3px 5px rgba(68, 68, 68, 0.1));
  outline: none;
  border-radius: 30px;
  color: white;
  padding: 1px 30px 1px 30px;
}

.ex-btn_default:hover {
  background-color: #77e6fa;
  filter: drop-shadow(1px 3px 5px rgba(68, 68, 68, 0.2));
}

.ex-btn_default:focus {
  outline: none;
}
</style>

感想

JavaScript書くの楽しい。
アニメーションとか入れて遊んでみよう、という気持ちになってきました。

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

[Vue]パスの変更を監視する

概要

Vue.jsでパスの変更を監視したかったので、手法をメモします。

環境

Vue.js 2.5.17

実装

<script>
export default {
  watch: {
    $route(to, from) {

    },
  },
}
</script>

$routeをwatchで監視すればパスの変更によって処理することができます。

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

vue-cliでのVueインスタンスの記述について

vue-cliでのVueインスタンスの記述について

背景

Vue-cliを使うと、Vueと違ってVueインスタンス部分のdataやmethodsなど、

  • data:data()

のように書き方が少し違う(自分はググって出てきた記事等を参照して書いていた)が、こちらは公式のガイド等はないのだろうか

結論

同じ種類のコンポーネントの異なるインスタンス間で、独立した data を扱うために、data はオブジェクトではなく、オブジェクトを返す関数でなくてはならない

コンポーネントの全てのインスタンスが同じデータオブジェクトを参照しているので、1つのリストのタイトルを変えることは、他の全てのリストのタイトルを変えることになる。

よって、CLIの場合は関数にしていないとやはりだめなよう

参考

CDNを使用した際は、data: {} でも data: function() { return {} }のどちらでもエラーは出ない

この記事は上記の公式をもう少し噛み砕いてマス

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

【Vue.js】vue-cliでのVueインスタンスの記述について

vue-cliでのVueインスタンスの記述について

背景

Vue-cliを使うと、Vueと違ってVueインスタンス部分のdataやmethodsなど、

  • data:data()

のように書き方が少し違う(自分はググって出てきた記事等を参照して書いていた)が、こちらは公式のガイド等はないのだろうか

結論

同じ種類のコンポーネントの異なるインスタンス間で、独立した data を扱うために、data はオブジェクトではなく、オブジェクトを返す関数でなくてはならない

コンポーネントの全てのインスタンスが同じデータオブジェクトを参照しているので、1つのリストのタイトルを変えることは、他の全てのリストのタイトルを変えることになる。

よって、CLIの場合は関数にしていないとやはりだめなよう
また、CDNの場合もオブジェクトを返す関数にした方がよいよう

参考

CDNを使用した際は、data: {} でも data: function() { return {} }のどちらでもエラーは出ない

この記事は上記の公式をもう少し噛み砕いてマス

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

.vueファイル

.vueファイル

HTMLのテンプレートとスクリプト・スタイルを一緒にかいておける

いわゆるコンポーネント化ができているということ

App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

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

【Vue.js】.vueファイル

.vueファイル

HTMLのテンプレートとスクリプト・スタイルを一緒にかいておける

いわゆるコンポーネント化ができているということ

App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

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

【Vuex】mapState使い方

mapState使い方

目的

Vuexで値を表示する際、$store.stateを省略して記述したい

実装

vuexからmapStateを抽出する

  • import { mapState } from 'vuex'

computedにmapStateを設定

computed: {
    ...mapState({ //他の算出プロパティと共有する(...)(表示を省略して記述するための設定)
      gender: 'gender',
      year: 'year',
      month: 'month',
      day: 'day',
      q1: 'q1',
      q2: 'q2',
      q3: 'q3',
      consultation: 'consultation',
    })
}

全体

<template>
<div>
  <div class="form">
    <div class="header">
      <p id="step">STEP4</p>
      <p id="inst">以下の内容をご確認ください</p>
    </div>
    <div class="body">
      <div>
        <p class="genre">-性別-</p>
        <p class="answer">{{ gender }}</p>
      </div>
      <div>
        <p class="genre">-生年月日-</p>
        <p class="answer">{{ year }}{{ month }}{{ day }}</p>
      </div>
      <div>
        <p class="genre">-現在、生命保険に加入されていますか?-</p>
        <p class="answer">{{ q1 }}</p>
      </div>
      <div>
        <p class="genre">-現在入院中ですか。または、最近3ヶ月以内に医師の診断・検査の結果、入院・手術をすすめられたことはありますか?-</p>
        <p class="answer">{{ q2 }}</p>
      </div>
      <div>
        <p class="genre">-過去5年以内に、病気やけがで、手術を受けたことまたは継続して7日以上の入院をしたことはありますか?-</p>
        <p class="answer">{{ q3 }}</p>
      </div>
      <div>
        <p class="genre">-ご相談内容-</p>
        <p class="answer">{{ consultation }}</p>
      </div>
    </div>
  </div>
  <div class="button-group">
    <router-link to="/consultation" class="button">前へ戻る ></router-link>
    <router-link to="/confirmation" class="button">送信 ></router-link>
  </div>
</div>
</template>

<script>
import { mapState } from 'vuex' //vuexからmapStateを抽出する

export default {
  computed: {
    ...mapState({ //他の算出プロパティと共有する(...)(表示を省略して記述するための設定)
      gender: 'gender',
      year: 'year',
      month: 'month',
      day: 'day',
      q1: 'q1',
      q2: 'q2',
      q3: 'q3',
      consultation: 'consultation',
    })
  }
}
</script>

<style scoped lang="scss">
* {
  // outline: auto;
    margin:0; padding:0;        /*全要素のマージン・パディングをリセット*/
  max-width: 100%;
}

#step{
  background: #1e90ff;
  color: #fff;
  font-size: 2px;
  margin: 0;
  max-width: 40px;
  padding: 2px;
  text-align: center;
}

#inst {
  font-size: 12px;
  color: #696969;
  text-align: center;
  padding-bottom: 10px;
}

.header {
  background: #afeeee;
  margin: 0%;
  padding: 0%;
  line-height: 10px;
  border-bottom: solid 1px #48d1cc;
}

.body {
  font-size: 9px;
  margin: 10px;
  padding-bottom: 10px;
  line-height: 30px;
  text-align: left;
}

.genre {
  color: #1e90ff;
  text-align: left;
  line-height:200%
}

.answer {
  margin: 0 0 0 10px;
}

textarea {
  border: 1px solid #dcdcdc;
}

.form{
  max-width: 80%;
  margin-top: 80px;
  margin-left: auto;
  margin-right: auto;
  border: 1px solid #48d1cc;
  border-bottom: 0.5px solid #000;
}

.button-group{
  text-align: center;
}

.button {
  background-color: #40e0d0;
  color: #fff;
  border: solid 1px #48d1cc;
  margin-top: 1rem;
  padding: 5px 10px;
  border-radius: 0.5rem 0.5rem;
  text-decoration: none;
  display:inline-block;
  margin: 10px;
}
</style>

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

Vuexを使って確認画面で値を表示させる

Vuexを使って確認画面で値を表示させる

背景

Vue-cliでアンケートフォーム作成中
Vuexを使って答えた解答を最後、確認画面で表示させたい

実装

値を取得する

  • @changeでメソッド起動
  • this.$store.commit('updateGender', e.target.value)で値を取得して、mutationsを実行する
BasicForm.vue
<template>
<div>
  <div class="form">
    <div class="header">
      <p id="step">STEP1</p>
      <p id="inst">お客様の情報を入力してください</p>
    </div>
    <div class="body">
      <p class="genre">-性別-</p>
        <label><input type="radio" name="gender" value="男性" @change="updateGender">男性</label>
        <label><input type="radio" name="gender" value="女性" @change="updateGender">女性</label>
      <p class="genre">-生年月日-</p>
      <select name="year" id="id_year" @change="updateYear">
        <option v-for="(year, key) in years" :key="key">{{ year }}</option>
      </select><select name="month" id="id_month" @change="updateMonth">
        <option v-for="(month, key) in months" :key="key">{{ month }}</option>
      </select><select name="day" id="id_day" @change="updateDay">
        <option v-for="(day, key) in days" :key="key">{{ day }}</option>
      </select></div>
  </div>
  <div class="button-group">
    <router-link to="/questionnaire" class="button">次へ進む ></router-link>
  </div>
</div>
</template>

<script>

//省略

  methods: {
    updateGender (e) {
      this.$store.commit('updateGender', e.target.value)
    },
    updateYear (e) {
      this.$store.commit('updateYear', e.target.value)
    },
    updateMonth (e) {
      this.$store.commit('updateMonth', e.target.value)
    },
    updateDay (e) {
      this.$store.commit('updateDay', e.target.value)
    },
  }
}
</script>

取得した値をVUexストアのstateに格納する

  • mutationsが起動して、設定しいたstateのプロパティに値を格納する
index.js
"use strict"

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

//どこからでも参照できる唯一の情報源store
const store = new Vuex.Store({
  state: {
    gender: '',
    year: '',
    month: '',
    day: '',
    q1: '',
    q2: '',
    q3: '',
    consultation: '',
  },
  mutations: {
    // ページ1
    updateGender (state, gender) {
      state.gender = gender
    },
    updateYear (state, year) {
      state.year = year += ''
    },
    updateMonth (state, month) {
      state.month = month += ''
    },
    updateDay (state, day) {
      state.day = day += ''
    },
    // ページ2
    updateQ1 (state, q1) {
      state.q1 = q1
    },
    updateQ2 (state, q2) {
      state.q2 = q2
    },
    updateQ3 (state, q3) {
      state.q3 = q3
    },
    // ページ3
    updateConsultation (state, consultation) {
      state.consultation = consultation
    },
  },
});

export default store

stateに格納した値をVueコンポーネントで表示させる

  • {{}}の中身にstateで設定したプロパティ名を記述する
  • mapStateを使用して表示する際の記述を省略している
確認画面.vue
<template>
<div>
  <div class="form">
    <div class="header">
      <p id="step">STEP4</p>
      <p id="inst">以下の内容をご確認ください</p>
    </div>
    <div class="body">
      <div>
        <p class="genre">-性別-</p>
        <p class="answer">{{ gender }}</p>
      </div>
      <div>
        <p class="genre">-生年月日-</p>
        <p class="answer">{{ year }}{{ month }}{{ day }}</p>
      </div>
      <div>
        <p class="genre">-現在、生命保険に加入されていますか?-</p>
        <p class="answer">{{ q1 }}</p>
      </div>
      <div>
        <p class="genre">-現在入院中ですか。または、最近3ヶ月以内に医師の診断・検査の結果、入院・手術をすすめられたことはありますか?-</p>
        <p class="answer">{{ q2 }}</p>
      </div>
      <div>
        <p class="genre">-過去5年以内に、病気やけがで、手術を受けたことまたは継続して7日以上の入院をしたことはありますか?-</p>
        <p class="answer">{{ q3 }}</p>
      </div>
      <div>
        <p class="genre">-ご相談内容-</p>
        <p class="answer">{{ consultation }}</p>
      </div>
    </div>
  </div>
  <div class="button-group">
    <router-link to="/consultation" class="button">前へ戻る ></router-link>
    <router-link to="/confirmation" class="button">送信 ></router-link>
  </div>
</div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState({
      gender: 'gender',
      year: 'year',
      month: 'month',
      day: 'day',
      q1: 'q1',
      q2: 'q2',
      q3: 'q3',
      consultation: 'consultation',
    })
  }
}
</script>

<style scoped lang="scss">
* {
  // outline: auto;
    margin:0; padding:0;        /*全要素のマージン・パディングをリセット*/
  max-width: 100%;
}

#step{
  background: #1e90ff;
  color: #fff;
  font-size: 2px;
  margin: 0;
  max-width: 40px;
  padding: 2px;
  text-align: center;
}

#inst {
  font-size: 12px;
  color: #696969;
  text-align: center;
  padding-bottom: 10px;
}

.header {
  background: #afeeee;
  margin: 0%;
  padding: 0%;
  line-height: 10px;
  border-bottom: solid 1px #48d1cc;
}

.body {
  font-size: 9px;
  margin: 10px;
  padding-bottom: 10px;
  line-height: 30px;
  text-align: left;
}

.genre {
  color: #1e90ff;
  text-align: left;
  line-height:200%
}

.answer {
  margin: 0 0 0 10px;
}

textarea {
  border: 1px solid #dcdcdc;
}

.form{
  max-width: 80%;
  margin-top: 80px;
  margin-left: auto;
  margin-right: auto;
  border: 1px solid #48d1cc;
  border-bottom: 0.5px solid #000;
}

.button-group{
  text-align: center;
}

.button {
  background-color: #40e0d0;
  color: #fff;
  border: solid 1px #48d1cc;
  margin-top: 1rem;
  padding: 5px 10px;
  border-radius: 0.5rem 0.5rem;
  text-decoration: none;
  display:inline-block;
  margin: 10px;
}
</style>

おまけ

以下確認画面です。

スクリーンショット 2021-03-14 18.47.51.png

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

vue-cliで外部JSファイルを読み込む方法

vue-cliで外部JSファイルを読み込む方法

背景

Vueコンポーネントの年月日のプルダウンを外部ファイルのjsで実装したい

理由

可読性やメンテナンス性が向上するため
大規模なプロジェクトになるほど、こういった構成にしたときのメリットを感じやすい

準備

vueCLIのディレクトリのsrcフォルダ直下にutilesという名前のフォルダを作成
その中にdefinition.jsとういう外部JSファイルを作成

実装

外部js

  1. 外部ファイル内に年月日の値を生成する(関数を作成、ループ関数を配列に格納する←どちらでも可能だが、definitionsの役割は値の生成なので、jsファイル側で格納する)
  2. 年月日それぞれの値をexport
definition.js
var this_year, today;
today = new Date();
this_year = today.getFullYear();

// 配列を変数に格納
const yearList = []
const monthList = []
const dayList = []

// 第一引数〜第二引数までの年月日を生成し、配列(list)に追加する関数を作成
const optionLoop = (start, end, list) => {
    for( let i = start; i <= end; i++)
        list.push(i)
}

optionLoop(1950, this_year, yearList);
optionLoop(1, 12, monthList);
optionLoop(1, 31, dayList);

// 年月日の値のforループを配列に格納し、vueコンポーネントにexportする
export {yearList, monthList, dayList}

読み込む側のVueコンポーネント

  1. import { yearList, monthList, dayList } from '@/utiles/definition';で外部jsファイルを読み込む
  2. dataプロパティ部分でv-forで回す際の配列(years, months, days)に値をセットする

※yearList, monthList, dayListはそれぞれ年月日の値が入っている配列である

BasicForm.vue

<template>
<div>
  <div class="form">
    <div class="header">
      <p id="step">STEP1</p>
      <p id="inst">お客様の情報を入力してください</p>
    </div>
    <div class="body">
      <p class="genre">-性別-</p>
        <label><input type="radio" name="gender" value="男性" @change="updateGender">男性</label>
        <label><input type="radio" name="gender" value="女性" @change="updateGender">女性</label>
      <p class="genre">-生年月日-</p>
      <select name="year" id="id_year" @change="updateYear">
        <option v-for="(year, key) in years" :key="key">{{ year }}</option>
      </select><select name="month" id="id_month" @change="updateMonth">
        <option v-for="(month, key) in months" :key="key">{{ month }}</option>
      </select><select name="day" id="id_day" @change="updateDay">
        <option v-for="(day, key) in days" :key="key">{{ day }}</option>
      </select></div>
  </div>
  <div class="button-group">
    <router-link to="/questionnaire" class="button">次へ進む ></router-link>
  </div>
</div>
</template>

<script>
import { yearList, monthList, dayList } from '@/utiles/definition';

export default {
  data() {
    return {
      years: yearList,
      months: monthList,
      days: dayList,
    }
  },
  methods: {
    updateGender (e) {
      this.$store.commit('updateGender', e.target.value)
    },
    updateYear (e) {
      this.$store.commit('updateYear', e.target.value)
    },
    updateMonth (e) {
      this.$store.commit('updateMonth', e.target.value)
    },
    updateDay (e) {
      this.$store.commit('updateDay', e.target.value)
    },
  }
}
</script>
  <!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
* {
  // outline: auto;
    margin:0; padding:0;        /*全要素のマージン・パディングをリセット*/
  max-width: 100%;
}

#step{
  background: #1e90ff;
  color: #fff;
  font-size: 2px;
  margin: 0;
  max-width: 40px;
  padding: 2px;
  text-align: center;
}

#inst {
  font-size: 12px;
  color: #696969;
  text-align: center;
  padding-bottom: 10px;
}

.header {
  background: #afeeee;
  margin: 0%;
  padding: 0%;
  line-height: 10px;
  border-bottom: solid 1px #48d1cc;
}

.body {
  font-size: 9px;
  margin: 10px;
  padding-bottom: 10px;
  line-height: 30px;
  text-align: left;
}

.body input {
  vertical-align:middle;
}

.genre {
  color: #1e90ff;
}

select {
  padding: 5px 10px 5px 0px;
  font-size: 9px;
  border: 1px solid #dcdcdc;
}

.form{
  max-width: 80%;
  margin-top: 80px;
  margin-left: auto;
  margin-right: auto;
  border: 1px solid #48d1cc;
  border-bottom: 0.5px solid #000;
}

.button-group{
  text-align: center;
}

.button {
  background-color: #40e0d0;
  color: #fff;
  border: solid 1px #48d1cc;
  margin-top: 1rem;
  padding: 5px 10px;
  border-radius: 0.5rem 0.5rem;
  text-decoration: none;
  display:inline-block;
  margin: 10px;
}
</style>

おまけ

プルダウンは、コンポーネント内だけで完結させる()こともできるが、可読性やメンテナンス性向上のため、値を生成するjsファイルを用意するという方が適切だと判断し、今回実装を行った。

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

【Vue.js】vue-cliで外部JSファイルを読み込む方法

vue-cliで外部JSファイルを読み込む方法

背景

Vueコンポーネントの年月日のプルダウンを外部ファイルのjsで実装したい

理由

可読性やメンテナンス性が向上するため
大規模なプロジェクトになるほど、こういった構成にしたときのメリットを感じやすい

準備

vueCLIのディレクトリのsrcフォルダ直下にutilesという名前のフォルダを作成
その中にdefinition.jsとういう外部JSファイルを作成

実装

外部js

  1. 外部ファイル内に年月日の値を生成する(関数を作成、ループ関数を配列に格納する←どちらでも可能だが、definitionsの役割は値の生成なので、jsファイル側で格納する)
  2. 年月日それぞれの値をexport
definition.js
var this_year, today;
today = new Date();
this_year = today.getFullYear();

// 配列を変数に格納
const yearList = []
const monthList = []
const dayList = []

// 第一引数〜第二引数までの年月日を生成し、配列(list)に追加する関数を作成
const optionLoop = (start, end, list) => {
    for( let i = start; i <= end; i++)
        list.push(i)
}

optionLoop(1950, this_year, yearList);
optionLoop(1, 12, monthList);
optionLoop(1, 31, dayList);

// 年月日の値のforループを配列に格納し、vueコンポーネントにexportする
export {yearList, monthList, dayList}

読み込む側のVueコンポーネント

  1. import { yearList, monthList, dayList } from '@/utiles/definition';で外部jsファイルを読み込む
  2. dataプロパティ部分でv-forで回す際の配列(years, months, days)に値をセットする

※yearList, monthList, dayListはそれぞれ年月日の値が入っている配列である

BasicForm.vue

<template>
<div>
  <div class="form">
    <div class="header">
      <p id="step">STEP1</p>
      <p id="inst">お客様の情報を入力してください</p>
    </div>
    <div class="body">
      <p class="genre">-性別-</p>
        <label><input type="radio" name="gender" value="男性" @change="updateGender">男性</label>
        <label><input type="radio" name="gender" value="女性" @change="updateGender">女性</label>
      <p class="genre">-生年月日-</p>
      <select name="year" id="id_year" @change="updateYear">
        <option v-for="(year, key) in years" :key="key">{{ year }}</option>
      </select><select name="month" id="id_month" @change="updateMonth">
        <option v-for="(month, key) in months" :key="key">{{ month }}</option>
      </select><select name="day" id="id_day" @change="updateDay">
        <option v-for="(day, key) in days" :key="key">{{ day }}</option>
      </select></div>
  </div>
  <div class="button-group">
    <router-link to="/questionnaire" class="button">次へ進む ></router-link>
  </div>
</div>
</template>

<script>
import { yearList, monthList, dayList } from '@/utiles/definition';

export default {
  data() {
    return {
      years: yearList,
      months: monthList,
      days: dayList,
    }
  },
  methods: {
    updateGender (e) {
      this.$store.commit('updateGender', e.target.value)
    },
    updateYear (e) {
      this.$store.commit('updateYear', e.target.value)
    },
    updateMonth (e) {
      this.$store.commit('updateMonth', e.target.value)
    },
    updateDay (e) {
      this.$store.commit('updateDay', e.target.value)
    },
  }
}
</script>
  <!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
* {
  // outline: auto;
    margin:0; padding:0;        /*全要素のマージン・パディングをリセット*/
  max-width: 100%;
}

#step{
  background: #1e90ff;
  color: #fff;
  font-size: 2px;
  margin: 0;
  max-width: 40px;
  padding: 2px;
  text-align: center;
}

#inst {
  font-size: 12px;
  color: #696969;
  text-align: center;
  padding-bottom: 10px;
}

.header {
  background: #afeeee;
  margin: 0%;
  padding: 0%;
  line-height: 10px;
  border-bottom: solid 1px #48d1cc;
}

.body {
  font-size: 9px;
  margin: 10px;
  padding-bottom: 10px;
  line-height: 30px;
  text-align: left;
}

.body input {
  vertical-align:middle;
}

.genre {
  color: #1e90ff;
}

select {
  padding: 5px 10px 5px 0px;
  font-size: 9px;
  border: 1px solid #dcdcdc;
}

.form{
  max-width: 80%;
  margin-top: 80px;
  margin-left: auto;
  margin-right: auto;
  border: 1px solid #48d1cc;
  border-bottom: 0.5px solid #000;
}

.button-group{
  text-align: center;
}

.button {
  background-color: #40e0d0;
  color: #fff;
  border: solid 1px #48d1cc;
  margin-top: 1rem;
  padding: 5px 10px;
  border-radius: 0.5rem 0.5rem;
  text-decoration: none;
  display:inline-block;
  margin: 10px;
}
</style>

おまけ

プルダウンは、コンポーネント内だけで完結させる()こともできるが、可読性やメンテナンス性向上のため、値を生成するjsファイルを用意するという方が適切だと判断し、今回実装を行った。

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

Vue.jsのSPAでVuetifyのパンくずリストを実装する

SPAで画面毎urlが変わる場合のパンくずリストをVuetifyのBreadcrumbsコンポーネントを使用して表示させます。

Vue.jsではパンくずを表示するプラグインでvue-breadcrumbsvue-2-breadcrumbsなどがあるようです。今回はvue-2-breadcrumbsを選択します。
vue-2-breadcrumbsを選択した理由は、vue-routerで画面の親子関係を設定しやすかったからです。

環境

  • Vue.js 2.5.17
  • Vuetify 2.4.3
  • vue-router 3.5.1
  • vue-2-breadcrumbs 0.7.12

実装方法

SPAでurlを変更させるためvue-routerを使用しますが、meta属性のbreadcrumbプロパティを設定します。
オブジェクトを設定した場合labelが表示名で、parentが親画面名です。parentには親のname属性を設定します。String型をセットした場合、labelに適用されます。
この設定で「ラビットハウス / 香風智乃」「ラビットハウス / 香風タカヒロ」という関係になります。

router.js
import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const router = {
    mode: 'history',
    routes: [
        {
            path: '/',
            name: 'Rabbithouse',
            component: RabbithouseComponent,
            meta: {
                breadcrumb: 'ラビットハウス',
            },
        },
        {
            path: '/charactor/chino',
            name: 'chino',
            component: KafuChinoComponent,
            meta: {
                breadcrumb:
                {
                    label: '香風智乃',
                    parent: 'Rabbithouse'
                }
            },
        },
        {
            path: '/charactor/takahiro',
            name: 'takahiro',
            component: KafuTakahiroComponent,
            meta: {
                breadcrumb:
                {
                    label: '香風タカヒロ',
                    parent: 'Rabbithouse'
                }
            },
        },
    ]
};

export default new VueRouter(router);

私の場合、App.jsとファイルを分けています。分けない場合は、App.jsに記述してください。vue-2-breadcrumbsをuseする時に、templateとcomputedをオプションで追加します。
Vuetifyではv-breadcrumbタグのitemsに画面情報のオブジェクトの配列をセットします。パンくずのデータはthis.$breadcrumbsに入っているのでv-breadcrumbタグの仕様に合わせた配列に作り直してセットします。

breadcrumb.js
import Vue from 'vue';
import VueBreadcrumbs from 'vue-2-breadcrumbs'

Vue.use(VueBreadcrumbs, {
    template:
        `<div>
          <v-breadcrumbs class='breadcrumb-item active' :items="items" />
        </div>`,
    computed: {
        items() {
            return this.$breadcrumbs.map((crumb, i) => {
                return {
                    text: this.getBreadcrumb(crumb.meta.breadcrumb),
                    disabled: this.$breadcrumbs.length - 1 === i,
                    to: this.getPath(crumb)
                };
            });
        }
    }
});

export default VueBreadcrumbs;
App.js
import breadcrumb from "./breadcrumb";

const app = new Vue({
    el: '#app',
    router: router,
    vuetify: vuetify,
    breadcrumb: breadcrumb,
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt Composition API ドキュメント抄訳

ドキュメント

useContext

Composition API中のNuxtコンテキストにアクセスできます。

useContext はNuxtのコンテキストを返します。これを使うことでNuxtコンテキストにより容易にアクセスできます。

import { defineComponent, useContext } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const { store } = useContext()
    store.dispatch('myAction')
  },
})

routequeryfrom および params はリアクティブなref(.value でアクセスできる)ですが、それ以外のコンテキストは異なることに注意してください。

Nuxt 3へのアップグレードをスムーズにするため、 it is recommended not to access route, query, from および params には useContext からアクセスせずに useRoute ヘルパー関数を使うことが推奨されます。

useAsync

一度だけ走ってクライアントサイドでデータを維持する非同期関数を定義できます。

useAsync を用いて非同期通信に依存するリアクティブな値を作成できます。

サーバー上では、このヘルパーは非同期通信の結果をHTMLに埋め込み、またクライアントモードに自動的に注入します。asyncData とまったく同様に、クライアントサイドで非同期通信を再び走らせません。

しかしながら、SSRで通信が実行されなければ(たとえば初期ロードの後でページを遷移した場合)、非同期通信が解決されたときにその結果を埋めるような null のrefを返します。

import { defineComponent, useAsync, useContext } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const { $http } = useContext()
    const posts = useAsync(() => $http.$get('/api/posts'))

    return { posts }
  },
})

そのとき、 useAsync は1回限りの用途にのみ適しており、その限りでユニークなキーを提供します。 詳しい情報.

useFetch

Composition APIの中でNuxtのfetch()フックにアクセスできます。

v2.12よりも新しいNuxtのバージョンは、サーバーサイドとクライアントサイドの非同期データフェッチングが可能な fetch と呼ばれるカスタムフック をサポートしています。

このパッケージでは以下のようにアクセスできます:

import { defineComponent, ref, useFetch } from '@nuxtjs/composition-api'
import axios from 'axios'

export default defineComponent({
  setup() {
    const name = ref('')

    const { fetch, fetchState } = useFetch(async () => {
      name.value = await axios.get('https://myapi.com/name')
    })

    // Manually trigger a refetch
    fetch()

    // Access fetch error, pending and timestamp
    fetchState

    return { name }
  },
})

useFetchsetup() の中で同期的に呼ばれなければなりません。コンポーネントのdataに対して、つまり、setup() から 返却される プロパティに対してなされるいかなる変更も、クライアントに送られ直接読み込まれます。useFetch フックのその他の副作用は後に残りません。

$fetch$fetchState はインスタンスにすでに定義されています。つまり、setupからfetchfetchState を返却する必要はありません。

useFetchonGlobalSetup の中で使えないことに注意してください。

useStatic

サイト生成時に静的なJSONを作り出す非同期通信を定義できます。

useStatic を使って処理コストの高い関数をあらかじめ走らせることができます。

import {
  defineComponent,
  useContext,
  useStatic,
  computed,
} from '@nuxtjs/composition-api'
import axios from 'axios'

export default defineComponent({
  setup() {
    const { params } = useContext()
    const id = computed(() => params.value.id)
    const post = useStatic(
      id => axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`),
      id,
      'post'
    )

    return { post }
  },
})

SSG

もしアプリ全体を生成する(あるいは単にいくつかのルートを nuxt build && nuxt generate --no-build でプリレンダリングする)なら、以下の挙動が解禁されます:

  • 生成時、 useStatic で呼んだ結果はJSONファイルに保存され、 /dist ディレクトリの中にコピーされます。
  • 生成されたページのハードリロード時、JSONはページにインラインで埋め込まれ、キャッシュされ、キャッシュされます。
  • 生成されたページへのクライアント遷移時、このJSONはフェッチされます。一度フェッチされると、後続の遷移のためにキャッシュされます。ページが事前に生成 されていなかった 時など、いかなる理由でこのJSONが存在しないような場合も、オリジナルの生成関数はクライアントサイドで走ります。

もしアプリ内でいくつかのページを事前に生成するなら、generate.interval を増やす必要があることに注意してください。(setupの説明 を参照。)

SSR

もしルートが事前に生成されていないなら(devモードの場合を含む):

  • ハードリロード時、サーバーは生成関数を走らせ、nuxtState の結果をインラインで埋め込みます。クライアントがAPIリクエストを再び走らせることのないようにするためです。結果は次のリクエストまでの間キャッシュされます。
  • クライアント遷移時、クライアントは生成関数を走らせます。

どちらの場合も、 useStatic が返す結果は null のrefで、生成関数やJSONフェッチが解決されたとき、その結果によって埋められます。

onGlobalSetup

グローバルなNuxtのsetup()関数で関数(またはdata)を走らせます。

グローバルなsetup関数でコールバック関数を走らせます。

import { onGlobalSetup, provide } from '@nuxtjs/composition-api'

export default () => {
  onGlobalSetup(() => {
    provide('globalKey', true)
  })
}

componentコンテキストの中ではなくpluginの中で呼ばないといけません。

リポジトリ

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

JavaScriptでクリップボードにコピーする機能を作成

概要

JSでクリップボードにコピーする機能を作成する方法を調べた際、navigatorのwriteTextメソッドを使用すると可能とあったのですが、HTTP環境だと使用できないので別の方法を調べました。
またVue.jsで処理している値を取得したかったので、どちらも可能な方法を調べました。
そもそもクリップボードにコピーする機能はライブラリを導入する方法もあったのですが、今回はライブラリを導入しない方法を探しました。

環境

PHP:v7.3.11
Vue:v2

実装

  • クリップボードにコピーするメソッド(ViewModelクラスに記載)
public copyClipBoard(str: string) {
    const targetSentence = str;
    let input = document.createElement('input');
    input.readOnly = true;
    document.body.appendChild(input);
    input.value = targetSentence;
    input.select();
    document.execCommand('copy');
    document.body.removeChild(input);
}
  • 今回はボタンを押した時にコピーできるようにしたかったので、トリガー用のメソッドを記載
doCopy() {
    _viewModel.copyBoard("コピーしたいテキスト");
}
  • HTMLの実装
<button class="copy-clipboard" @click="doCopy">クリップボードにコピー</button>

まとめ

  • 今回はJSでクリップボードにコピーする方法についてまとめました。
  • 2行くらいでシンプルにかけるかと思っていたのですが、意外と量が多かったため記事にしました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【フロントエンド】Angularを1年間触ってみてたどり着いたコンポーネント設計

個人的に思う最適なコンポーネント設計とその提案

前置き

 Angularを始めたはいいものの、しばらくして躓くことがありました。それは文法や英語のドキュメントなどよりも、なにより適切なコンポーネントの分け方がわからないということでした。

 そこでいろいろ検索をしてみたのですが、しっくりくる答えにたどり着かなかったため、自分で考えることにしました。今回はその現状報告のようなものです。(コンポーネント設計の話なので、ReactやVue.jsでも通ずる話題かと勝手に思っています)

 結果、Page-Layout-Presenter構造というコンポーネント設計を考えるに至ったのですが、この記事ではそこに至るまでの経緯を紹介し、この設計については別記事で投稿することにしました。ご興味がございましたら併せてご覧ください。
【Angular】最適なコンポーネント設計について考えてみた: Page-Layout-Presenter構造

注意:飽くまで個人的現状、最もうまくいくと考えている設計なので、プロジェクトや人によってもっと良い方法があるかもしれません!よりよいアイデアがあればコメントをいただければありがたいです。

これまでAngularを触っていて感じてきた課題

思ったよりコンポーネントを分割できない

 コンポーネントの分け方に明確なルールを持ち合わせていないため、感覚で分けるか、そもそも分けないかのどちらかになりがちでした。感覚で分けた場合、複数人で開発していると認識を合わせるのが大変です。そして、開発とともにコンポーネントを分けるコストが高くなり、それによりさらにコンポーネントがfatになるという悪循環で、どんどん動けなくなります。
 このことから、コンポーネントの分け方には明確なルールが必要だと感じました。

サービスの扱いが雑になりがち

 コンポーネントにビジネスロジックを書いてしまい、サービスとコンポーネントの境界があいまいになってしまうことがありました。また、どのコンポーネントがどのサービスを呼び出しているかを管理できず、親コンポーネントと子コンポーネントで同じサービスを繰り返し呼び出してしまうなどの無駄が発生することもありました。
 このことから、コンポーネントにビジネスロジックを書かないように意識しやすい設計にする必要があると感じました。また、サービスを呼び出せるコンポーネント群を決める必要があると感じました。

気軽にスタイルシートを変更できない

 <div class="some-block__some-element">のように、スタイルとテンプレートをclass属性で紐づけることはごく一般的だと思いますが、デザインの修正を行うたびに、classを参照して、スタイルシートに移動して、対象のスタイルを探して変更する、というのが私としては意外と手間だと感じました。そして、パーツの内容を考慮した上で、認識のずれが起こりづらいような、一意なクラス名を英語で考えるというのも結構疲れる作業だと思いました。そして、スタイルシートとテンプレートの分離がエンジニアさんとデザイナーさんとの壁になっているのではと思いました。
 また、プロジェクトによってはスタイルがすべてstyles.scssなどグローバルな場所に集められている場合もあり、変更の影響範囲がすぐに把握できないこともありました。私の現職のプロジェクトは実際そうなっていて、専任のエンジニアさんしかスタイルシートを触れないという状態でした。(そしてその方はすでに退職済みという...)
 このことから、設計を考えるうえでデザインないしデザイナーさんとのかかわり方を考えることは切っても切り離せないと感じました。

コストが高すぎてテストコードを書けない

 Angularの場合、コンポーネントやサービスなどを新規作成すると必ずついてくる*.spec.tsというテストファイルがあると思います。テスト駆動開発などではこのファイルにテストコードを書いていくことでテストを極力自動化させるのだと思っています。しかし、実際はやろうと思っても後回しにされることも多いのではないかと推測します。それは、JasmineやKarmaについて学ぶ必要があるという点もあるとは思いますが、なにより、先述のことが原因でコンポーネントがfatになっていて、テストコードの実装の難易度が上がりすぎているのではないかと考えられます。一つのボタンを押すだけでそのコンポーネントでテストすべき項目は一体いくつあるのでしょうか。
 このことから、コンポーネントを適切に分割し、最小限の役割を持たせることで、テストの範囲も分担させ、最小限にすることができるのではないかと思いました。

参考にしてきた設計思想

 これら課題を解決すべく、よりよいコンポーネント設計について考えていくことにしました。まずは、そのために参考にした設計思想などを紹介します。

【Page-Container-Presentational構造】

知ったきっかけ: Angular Webアプリケーションの最新設計手法

 紹介されているコンポーネントの分け方から、ここでは仮にPage-Container-Presentational構造と呼ばせていただくことにします。(下画像は上記リンクより引用)

image.png

参考にした点

 関心の分離の観点から、コンポーネントをグルーピングして役割を与えるという考え方が非常に参考になりました。例えばContainer Componentは状態を扱い、Presentational Componentは見た目を扱うなど。

課題

 課題というか、おそらく私の理解不足が問題なのですが、状態管理以外の点で考えた場合は、ContainerPresentationalの境界をどこで決めればよいかがあいまいに感じました。また、最小単位であるPresentationalがfatになることがあるので、共通処理などを抽出し、さらに分割したいと感じました。

【Atomic Designとドメイン駆動開発(DDD)】

知ったきっかけ: WEB+DB PRESS Vol.112
WEB+DB PRESS Vol.112の表紙

 この雑誌は発行が2019年と少し前ではありますが、最新設計手法としてAtomic DesignとDDDを両立させたコンポーネント設計について解説されていました。ちなみにAngularではなくReactとVue.jsのコードを例にして書かれていました。

Atomic Design

参考にした点

 コンポーネントを、ドメインを担うコンポーネントと、UIを担うコンポーネントに分ける考え方が参考になりました。これにより、特定のコンポーネントからしか参照しないドメイン依存なコンポーネントと、それに対し、不特定多数のコンポーネントから呼び出せるUIパーツなどの共通のコンポーネントを作れるため、再利用性が上がると考えられました。
 また、デザインの修正をページ単位よりも細かいコンポーネント単位で行えるため、ページ単位で修正を行うのに比べ、エンジニアがデザイナーを待つ時間とデザイナーがエンジニアを待つ時間が短縮されると雑誌で紹介されていました。これにより開発のサイクルがより小さく早くなります。デザイナーさんとの関わりを考慮した設計を考えるきっかけになり大変参考になりました。

課題

 Atomic Designの考えを取り入れると計5層の構造になり、さらに雑誌の手法にあるContainers層とLayout層を加える場合は7層以上になります。1つのページを作るだけでもコンポーネントの数が6つ以上必要で作るのが大変です。ほぼ記述がないコンポーネントをいくつも生成することになるので、フロントエンドのコンポーネント設計においては少し冗長に感じました。

Atomic Design + DDD Atomic Design
1 Pages Pages(ページ)
2 Containers
3 Domain Objects Templates(テンプレート)
4 Domain Elements Organisms(有機体)
5 Gui Groups Molecules(分子)
6 Gui Parts Atoms(原子)
- 必要な場所にLayout

【BEM記法】

 HTMLやCSSについて勉強しているとよく登場します。SASS記法と合わせてよく紹介されている印象があります。ざっくり言うと、自由度が高いHTMLのタグのclass属性の命名に、block__element--modifireの命名ルールを与えることで運用しやすくするという方法です。

BEM記法

参考にした点

 class属性値の命名方法にルールがあることで、他のエンジニアさんとの認識のずれを起こしづらくできることが非常にメリットと感じました。
 また、先述のAtomic Design + DDDと併用することで、スタイルのスコープをグローバルではなく上の表でいうDomain Elements単位で行えるようになりました。これにより、コンポーネントレベルでスタイルを表現できるようになり、さらに、スタイル修正のコストを減らすことにつながると考えられました。
 そして、スタイルの構造はテンプレートの構造とは関係がない、という考え方は自分にとって重要な学びでした(HTMLにどれだけ階層構造があったとしても、BEMで書くクラスはBlockとElement(Modified Element)との2層構造であるというルール)。これにより、スタイルをclass値単位で再利用可能にしたというわけです。

課題

 ただ、コンポーネント設計を考えるうえで、再利用性はコンポーネント側がすでに持っているので、もはやスタイル側に持たせる必要がありません。むしろ、異なるコンポーネントで見た目を少しだけ変えたいというような場合、再利用性があることが逆に修正を難しくする原因になりかねません。なので、スタイルの再利用はさせないほうがいいのではないかと考えています。
 そもそもクラス名を介してスタイルを紐づけるというやり方がよくないと思いました。なぜなら、その場合テンプレートからスタイルを参照する際に必ずスタイルシートに移動してさらにclass名を辿るという手順が発生するためです。これを毎度行うことになるので、やはり少々面倒です。また、class名はプロジェクトによって異なるものであり、共通認識のものではないため、ほかのエンジニアさんやデザイナーさんにとっては可読性を下げることになりかねません。また、大規模な場合は認識合わせを行ったり、ドキュメントを作成したりする必要が出てくると思うので、コストが意外と大きいのではないかと思っています。代わりに共通認識の何かで代用できれば、認識のすり合わせもドキュメント作成も不要になるのではないかと考えました。→ tailwindcss
 次に、実はコンポーネントを分割するという観点からいうと、BEMは相性がそこまでよくないという気がしています。Angularでは、親コンポーネントからは子コンポーネントのテンプレートを知ることはできません。なので、子コンポーネントのタグのclass属性も記述することができないため、コンポーネントの分割がそもそも不可能になってしまうからです。この点はangular-bemというモジュールで解決できます。これについても一苦労あったのですがその話はまたの機会とします。
 BEM記法はコンポーネントを分割しない場合には使いやすいですが、コンポーネント指向な開発にはあっていないと個人的に感じました。

【Windowsフォームアプリケーション】

知るきっかけ: 現職での仕事

Windows フォームとToolBox

参考にした点

 .NET FrameworkにWindowsフォームというものがあります。簡単に言えば一世代前の、Windows用アプリケーションを作るためのフレームワークです。Windowsフォームでアプリケーションを作成するときは、Visual StudioのToolBoxペインから、必要なコントローラ(UIパーツのようなもの)をドラッグ&ドロップすることで配置することができます。つまり、必要なUIのパーツはすでにフレームワークによって用意されていて、開発者はマウス操作一つで直感的にそれらを配置することができるわけです。
 Webアプリケーション開発でも、このように、より直感的にUIパーツを配置できれば楽なのにな、と考えるようになりました。

課題

 Angularやnpmによって、予めすべてのUIパーツが準備されていれば楽ですが、現状はそうはなっていません。また、Windowsフォームアプリケーションはスタイルの多様性が低く、スタイルをカスタマイズする場合についても考える必要があります。

【tailwindcss】

知ったきっかけ: The State of CSS 2020から読み解くWebフロントのトレンド

tailwindcss

参考にした考え方

 BEM記法はAngularで実用するには無理があると感じていたところ、tailwindを知りました。テンプレートに直接スタイルを記述できるため、頻繁に変更されると考えられるレイアウトに関するスタイルなどを効率よく操作でき、さらに、class名の運用に関する他のエンジニアさんとの認識のずれをなくせると考えられました。あと、ダークモードやレスポンシブ対応など向けの実装も簡単そうなのでいいと思いました。

課題

 現状、tailwindcssがclassとして提供できていないスタイルに関してはstyle属性か結局スタイルシートに書く必要があります。また、これはBEM記法でも同じ課題ですが、少しだけ見た目の違う場合に新しいコンポーネントとして分けるべきか悩ましく、まだ私の中で最善策が決まっていません。

【ディレクトリ構造】

中規模AngularアプリにおけるNgModule構成とディレクトリ構造

参考にした点

 アプリケーション全体で参照可能な共通のディレクトリと、同一feature内でのみ参照可能なfeaturesディレクトリを作るという点が参考になりました。また、ディレクトリ名を何にするか悩んだ時にいくつか参考にしました。

【その他参考にした記事など】

個人的に最適と思うコンポーネント設計

以上を踏まえ、できる限り課題を解決するような設計について私なりに考えてみました。
これについては別記事で投稿しましたので、よければ併せてご覧ください。
【Angular】最適なコンポーネント設計について考えてみた: Page-Layout-Presenter構造

おわりに

今後もフロントエンド開発に関して学びつつ、よりよい設計について考えていきたいと思います。
記事として未熟な点もあったかもしれませんが、一部でも参考になれば幸いです。

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