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

【Vue】Element-UIでフォーム全体のバリデーションを行う【TypeScript】

これまたすんなり行かなかったので共有しとく

その1 - Elementフォームのvalidateメソッドを参照したい

フォーム全体のバリデーションを行う方法については、Element-UI公式でCustom validation rulesのサンプルコードを丹念に読めば大体のところが把握できる。
が、TypeScriptでそのまま書こうとするとVSCodeに怒られてしまう。

サンプルコード(抜粋)
this.$refs[formName].validate(callback);
VSCodeでのエラーメッセージ
Property 'validate' does not exist on type 'Vue | Element | Vue[] | Element[]'.
  Property 'validate' does not exist on type 'Vue'.Vetur(2339)

$refsの定義を確認すると

node_modules/vue/types/vue.d.ts(抜粋)
export interface Vue {
  readonly $refs: { [key: string]: Vue | Element | Vue[] | Element[] };
}

となっているが、実際に$refs下に登録されているのはElementUIComponent経由でVueを継承したElFormであり、そこに定義されているvalidateメソッドはVueからは参照できない。

その1の解決法 - $refsを宣言し直す

以下、Vue.js公式のTypeScript SupportにあるClass-Style Vue Componentsを前提に進める。

まずElFormの定義を取り込む。

任意のVueファイル(抜粋)
<script lang="ts">

import { ElForm } from 'element-ui/types/form';

次に、インスタンスプロパティ$refsについて下記のように宣言。

任意のVueファイル(抜粋)
export default class MyComponent extends Vue {
  /* data */
  public $refs!: { [key: string]: ElForm };

定義を被せる都合上、$refsプロパティはVueに合わせてpublicで宣言する必要がある。TypeScriptでは、public修飾子(デフォルト)は省略可だが、筆者の趣味で明示的に記述しておいた。

プロパティ名の後ろに付く!Definite Assignment Assertionsというモノ。
本来であれば宣言時もしくはコンストラクタ内で初期化する必要のあるインスタンスプロパティが、コレを付けると「既に代入済みであり初期化不要」という扱いになる。このように記述した場合、当該のプロパティが$dataオブジェクトに取り込まれることもない。

$refsへ登録されるフォーム名はel-formタグのref属性で指定するが、名称が固定であれば以下のように宣言しても構わない。

el-formタグ
<el-form ref="myForm" :rules="myRules" :model="myModel">
フォーム名が固定の場合
export default class MyComponent extends Vue {
  /* data */
  public $refs!: { myForm: ElForm };

以上で、サンプルコードと同じような記述が可能となる。

記述例
this.$refs[formName].validate(callback);
/* あるいは */
this.$refs.myForm.validate(callback);

その2 - バリデーション結果を画面に反映させたい

フォーム全体のバリデーション結果を、リアルタイムで画面上(ボタンの使用可否など)に反映させたい、というような欲求はあるかと思う。
フォームのvalidateメソッドが、テンプレート内から直に呼び出すだけで結果(boolean)を取得できるような作りなら話は簡単だが、残念ながらそうなってはおらず上手く行かない。

node_modules/element-ui/types/form.d.ts(抜粋)
  /**
   * Validate the whole form
   *
   * @param callback A callback to tell the validation result
   */
  validate (callback: ValidateCallback): void
  validate (): Promise<boolean>

コールバックを渡して呼び出せば同期的にバリデーション結果を用いた処理を実行できるが、戻り値はvoidに限定される。
引数無しで呼び出せばバリデーション結果(boolean)を載せたPromiseを返すが、いかんせん非同期。
したがってどちらも使えない。

その2の解決法 - ライフサイクルフックを使う

テンプレートからメソッドを直接呼び出しても結果が取得できないとなると、残る手段はインスタンスプロパティを介して参照する以外に無い。そのためにはライフサイクルフックを利用することになる。

$dataオブジェクト下のデータがいずれかの入力フィールドに紐づいている場合、そのデータに変更が加わると紐付く入力フィールドで個別のバリデーションが実行される。しかる後に、そのバリデーション結果も踏まえたバーチャルDOMの再描画・書き替えが行われるが、その前後で動作するライフサイクルフックがbeforeUpdateupdatedである。今回はbeforeUpdateを使用するのが適切だろう。

validateメソッドについては同期・非同期どちらを用いても良いが、前者の方がテストを書くのも楽だし、もし必要なら入力フィールド毎のエラー詳細も利用できるので、今回はそちらを採用する。

まずvalidateメソッドに渡すコールバックValidateCallbackの型定義を取り込む。

任意のVueファイル(抜粋)
<script lang="ts">

import { ElForm, ValidateCallback } from 'element-ui/types/form';
node_modules\element-ui\types\form.d.ts(抜粋)
export interface ValidateCallback {
  /**
   * The callback to tell the validation result
   *
   * @param isValid Whether the form is valid
   * @param invalidFields fields that fail validation
   */
  (isValid: boolean, invalidFields: object): void
}

次に、バリデーションの結果を受け取るインスタンスプロパティを用意する。
初期化については入力フィールドのデフォルト値に合わせて決め打ちでも良いが、遷移で諸々入力済みの状態から開始するような場合はバリデーション未実施の状態を区別するために nullable な型(boolean | null)で宣言した上nullで初期化する手もある。

初期化決め打ち
export default class MyComponent extends Vue {
  /* data */
  public $refs!: { [key: string]: ElForm };
  private validationResult: boolean = false;
nullで初期化
export default class MyComponent extends Vue {
  /* data */
  public $refs!: { [key: string]: ElForm };
  private validationResult: boolean | null = null;

最後に、クラス直下へbeforeUpdateメソッドを追加し、その中でフォームのvalidateメソッドを実行する。
引数としては、バリデーションの結果をインスタンスプロパティへとセットするコールバックを用意する。

任意のVueファイル(抜粋)
  private beforeUpdate() {
    this.updateValidationResult();
  }

  private updateValidationResult() {
    const callback: ValidateCallback = (isValid, invalidFields) => {
      this.validationResult = isValid;
    };

    this.$refs.myForm.validate(callback);
  }

以上、分かってしまえば超簡単。

ライフサイクルフックを検証するテストの書き方については別記事で書く予定。

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

【Vue.js】Web API通信のデザインパターン (個人的ベストプラクティス)

はじめに

Vue.jsを使用したアプリケーションでのWeb API呼び出しのデザインパターンについて調べてみました。

しかし検索して出てくるチュートリアルやサンプルは、コンポーネント内でaxiosをインスタンス化していたり、Vuexの中でaxiosを使用するというサンプルがほとんどでした。

しかし実際のプロダクトでこれをしてしまうと

  • コンポーネント内でAPIアクセスの直書きによって単体テストが困難に
  • Vuex(actions)の肥大化(使い回さない処理はStoreに記述しないほうがいいとする文献もある)
  • API通信部分をPureJSでモジュール化しても依存度がイマイチ下がらない(コンポーネントでモジュールをインポートするため)。

などなど問題になることが多そうでした。
ある日、Jorge氏が投稿した「Vue API calls in a smart way」という記事にたどり着きました。
結果から言うとこのデザインパターンは自分にはベストな方法であったため、著者の許可を得て紹介します。以下拙訳(一部意訳)です。

ここから翻訳

長い間、APIを呼び出すための様々な方法を公開したいと思いました。この章では私にとって最適なパターンについて話したいと思います。RepositoryFactoryを紹介させてください。

私はこのアプローチが非常によくスケールでき、ほぼいつも上手くいくため大好きです。

なぜかを説明しましょう:

1つはリポジトリパターンを使用して、データを返す以外のロジックなしで分離された方法でリソースににアクセスできます。
もう1つはファクトリパターンを使用して、各ケースで必要なリポジトリ、または環境のロジックをインスタンス化します。必要に応じて、ファクトリがモックリポジトリと本番リポジトリのどちらをインスタンス化するかを決定できるという利点があります。

各コンポーネント内にaxiosのインスタンスを使用したサンプルをどれだけ見たでしょうか。
私はそれを見るたびに毎回疑問に思います?

  • 呼び出し処理を再利用するときはどうなりますか?
  • エンドポイントが変わったときはどうなりますか?
  • ほかのプロジェクトでAPI呼び出し処理を再利用したい場合はどうなりますか?
  • コードをリファクタしたり、Vuexのactionsに移動する場合はどうなりますか?
  • 複数のリソースがありますが、4つの異なるエンドポイントを定義する必要がありますか?
  • テストのためにモックなどのエンドポイントをどのように扱うことができますか?

0_5m-dDbSZ3gb8oaCJ.gif
30個のファイルを変えるよりも1個のファイルを変えるほうが簡単でしょう??

正しい方法

このケースではコードをシンプルにして、さまざまなリソースを定義するために、POJO(Plain Old JavaScript Objects)を使用します。(もちろん、必要に応じてクラスを使えます)axiosの設定を定義しましょう。ファイル名はリソースの接続を確立する責任があるため、Repository.jsとします。

serviceまたは単にAPIという用語を使う人もいますが、Repositoryがその機能を最もよく定義しているように思えます。
image.png

それから、それぞれのエンティティのリソース定義をする必要があります。
例えばブログがあるとしましょう。postsエンティティがあり、すべてのCRUD操作を独自のリポジトリファイルに定義できます。
postsRepositoryはこんな感じです。
image.png

そしてファクトリを作成します。
image.png

見ての通りリクエストしたリソースのシンプルなオブジェクトが返されます。
この段階でget()メソッドにさらにロジックを追加して複数の環境を扱ったり、モックリポジトリをインポートしたりできることに気付いたでしょう。

たとえば、現在私がいる会社(Snap.hr)では、いくつかの異なる環境が必要です。環境に応じて、モックまたは対応するエンドポイントに変更します。これはあなたが望むものと同じような複雑さでしょう。
でもこの例ではシンプルにしましょう?

.vueファイルのSFCでどのように使用するかお見せしましょう。
image.png
ロジックは完全に分離されているので、異なるパラダイムを持つ別のエンドポイントをGraphQLとして使用することもできます。

?結論

常に銀の弾丸は存在しないものです。このパターンは私のほとんど全部のプロジェクトでうまくいっていますが、あなたのプロジェクトにとって最適という意味ではありません。

しかし、以下のアドバンテージがあると思います。

  • 簡単な方法でテスト可能なコードを実行できます。
  • コンポーネントのコードがきれいになります。
  • 簡単に拡張できますよね?
  • DRYを保つことができます。

実用的なサンプルCodesandbox で作成し、このアプローチを実際に実装してみました。
この種の手法を既に使用しているのであれば、ぜひお聞かせください。
?

提案や質問がありますか?お気軽にコメントやTwitterでコンタクトください!

この記事は2018年に最も人気のあったVue.js記事の1つに選ばれました。


ここまで翻訳

謝辞(Appreciation)

Dear Jorge
Thank you for your amazing article!

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

Nuxt.js(vue.js)で画像を縮小してFirebaseStorageにアップロードする

やりたいこと

  • ユーザーが画像をアップロードできるページを作りたい
  • 確認用サムネイルも表示したい
    • サムネイルは「どんな画像か」がわかるだけの小さなもの
    • アップロードするファイルとは別物
  • フロントで圧縮してからアップすることで通信量を節約したい
    • Firebase Storageの使用量も節約したい
  • サーバーサイドは書きたくない

手順

  1. input[type="file"]でファイルを指定する
  2. new FileReader()でファイル情報を取得する
  3. new Image()でimg要素を作る
  4. img.srcに「2.」のファイル情報を放り込んで画像を作成する
  5. document.createElement('canvas')canvas要素を作る
  6. 「5.」のcanvasに「4.」の画像をリサイズしつつ貼り付ける
  7. あらかじめ用意しておいたサムネイル用のcanvas要素に「4.」の画像をリサイズしつつ貼り付ける
  8. toDataURL('image/jpeg')data_url形式の情報を取得する
  9. FirebaseStorageにアップする

コード

photoResize.vue
<template>
  <div>
    <input type="file" v-on:change="resize" accept=".jpg, .png" ref="input">
    <div>
      <canvas ref="thumbnail" :width="0" :height="0">
      <button v-on:click="reset">×</button>
    </div>
    <div>
      <button v-on:click="upload">upload</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      newImage: '',
    }
  },
  methods: {
    resize(e) {
      const file = e.target.files[0]
      const image = new Image()
      const reader = new FileReader()
      const vm = this

      reader.readAsDataURL(file)
      reader.onload = (e) => {
        image.src = e.target.result
        image.onload = () => {
          vm.newImage = this.width < 1280 ? this.src : vm.makeImage(image)
          vm.makeTumbnail(image)
        }
      }
    },
    makeImage(image) {
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')
      const ratio = image.height / image.width
      const width = 1280
      const height = width * ratio
      canvas.width = width
      canvas.height = height
      ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)
      return canvas.toDataURL('image/jpeg')
    },
    makeTumbnail(image) {
      const canvas = this.$refs.thumbnail
      const ctx = canvas.getContext('2d')
      const ratio = image.width / image.height
      const height = 120
      const width = height * ratio
      canvas.height = height
      canvas.width = width
      ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)
    },
    reset() {
      const canvas = this.$refs.thumbnail
      this.newImage = ''
      canvas.height = 0
      canvas.width = 0
      this.$refs.input.value = ''
    },
    upload() {
      const photo = this.newImage
      const storage = firebase.storage()
      const ref = storage.ref().child('main.jpg')
      const vm = this
      ref.putString(photo, 'data_url').then(snapshot => {
        console.log('photo uploaded')
        vm.reset()
      })
    },
  }
}
</script>


詳細

HTML部分

<template>
  <div>
    <input type="file" v-on:change="resize" accept=".jpg, .png" ref="input">
    <div>
      <!-- サムネイル用canvas -->
      <canvas ref="thumbnail" :width="0" :height="0">

      <!-- 選択した画像をリセットするためのボタン -->
      <button v-on:click="reset">×</button>
    </div>
    <div>
      <!-- アップロードボタン -->
      <button v-on:click="upload">upload</button>
  </div>
</template>

inputではv-modelを使いたいところですがtype="file"では使えません。
v-on="change"でメソッドを呼び出します。
また、サムネイルを表示するためのcanvas要素を予め書いています。
リセットボタンはあると便利かなという程度です。

画像を作る

  data() {
    return {
      newImage: '',
    }
  },
  methods: {
    resize(e) {
      const file = e.target.files[0]
      const image = new Image()
      const reader = new FileReader()
      const vm = this
      const maxWidth = 1280

      reader.readAsDataURL(file)

      reader.onload = (e) => {
        image.src = e.target.result

        image.onload = () => {
          vm.newImage = this.width < maxWidth ? this.src : vm.makeImage(image)
          vm.makeTumbnail(image)
        }
      }
    },
...
  1. new Reader()でファイル情報を取得
  2. new Image()img要素を作成
  3. 「2.」のimg要素に「1.」のファイル情報を与える
  4. imageの準備が整い次第makeImage()makeThumbnail()を呼び出す

この例では幅1280px以上の画像を1280pxに縮小するものです。
maxWidthの値は用途に応じて調整してください。
画像のwidthmaxWidth以上の場合は、makeImage()で縮小画像を生成、maxWidth以下の場合は入力した画像をそのままnewImageに格納します。

縮小画像を作る

    makeImage(image) {
      // canvas要素を作成
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')

      // 縦横比を算出
      const ratio = image.height / image.width

      // 生成する画像の横幅
      const width = 1280

      const height = width * ratio
      canvas.width = width
      canvas.height = height

      // canvas描画作成
      ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)

      // data_url形式に変換したものを返す
      return canvas.toDataURL('image/jpeg')
    },

前述の通り、この例では1280pxを基準としています。
用途に応じてwidthの値を調整してください。

ここまででアップロードする画像の作成は完了です。

サムネイルを作成する

    makeTumbnail(image) {
      // 予めHTMLに記述したcanvasを指定
      const canvas = this.$refs.thumbnail
      const ctx = canvas.getContext('2d')

      // 縦横比を算出
      const ratio = image.width / image.height

      // サムネイルのサイズを指定
      const height = 120
      const width = height * ratio

      // canvasの大きさを指定
      canvas.height = height
      canvas.width = width

      // サムネイルに画像を描画
      ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)
    },

この例ではheight="120px"の小さいサムネイルを作成しています。
用途に応じて調整してください。

入力をリセット

    reset() {
      const canvas = this.$refs.thumbnail
      this.newImage = ''

      // サムネイル用canvasのサイズを0に
      canvas.height = 0
      canvas.width = 0

      // inputの入力をリセット
      this.$refs.input.value = ''
    },

画像リセット用メソッドです。
用途に応じて。

画像をアップロードする

    upload() {
      const photo = this.newImage
      const storage = firebase.storage()

      // アップロード先のフォルダ、ファイル名を指定
      const ref = storage.ref().child('main.jpg')

      const vm = this

      // ファイルをアップロード
      ref.putString(photo, 'data_url').then(snapshot => {
        console.log('photo uploaded')

        // 入力をリセット
        vm.reset()
      })
    },

firebaseに画像をアップします。
storage.ref().child('main.jpg')がアップロード先です。
storage.ref().child('hozon/shitai/basho/main.jpg')とすると保存フォルダを指定できます。

まとめ

コンポーネントとして色んなシチュエーションで使いまわしたいということもあると思います。
現場ではprop$emitを使って色んな場面で使えるリサイズ用コンポーネントとして使っています。
その方法もそのうち…

参考

JavaScript で画像をリサイズする方法
ブラウザでローカル画像をリサイズしてアップロード

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

vue-chart.js を 使おうぜ

vue-chart.js を使おうぜ

参考
https://misc.0o0o.org/chartjs-doc-ja/axes/radial/linear.html

まずはインスコ。

npm install vue-chartjs chart.js --save

今回はレーダーチャートを作ってみる。
Radar なので綴に注意。

/resources/RadarChart.js
import { Radar, mixins } from 'vue-chartjs'
const { reactiveProp } = mixins

export default {
    extends: Radar,
    mixins: [reactiveProp],
    props: ['options'],
    mounted () {
        // this.chartData is created in the mixin.
        // If you want to pass options please create a local options object
        this.renderChart(this.chartData, this.options)
    }
}

/resources/components/Graph.vue

<template>
    <div class="small">
        <radar-chart :chart-data="datacollection" :options="options"></radar-chart>
    </div>
</template>

<script>
    import RadarChart from '../RadarChart.js'

    export default {
        components: {
            RadarChart,
        },
        data () {
            return {

                fontColor: {
                    red: 'rgb(255, 99, 132,0.6)',
                    orange: 'rgb(255, 159, 64,0.6)',
                    yellow: 'rgb(255, 205, 86,0.6)',
                    green: 'rgb(75, 192, 192,0.6)',
                    blue: 'rgb(54, 162, 235,0.6)',
                    purple: 'rgb(153, 102, 255,0.6)',
                    grey: 'rgb(201, 203, 207,0.6)',
                },


                datacollection: null,

                options: {


                    scale: {
                        pointLabels: {
                            fontSize: 35,//レーダーチャートのラベルを変更
                        },
                    },

                    title: {
                        display: true,
                        fontSize: 35,
                        text: '好きな動物'
                    },
                }
            }
        },
        created () {
            this.RadarChart()
        },




        methods: {



            RadarChart () {
                this.datacollection = {
                    labels: ["夢", "希望", "未来","賢さ", "可愛さ", "素敵さ","人気"],
                    datasets: [
                        {
                            backgroundColor: 'red',
                            backgroundColor: this.fontColor.blue,
                            borderWidth: 1,

                            label: "どんべぇ売れ筋カラー",

                            data: [1, 40, 5,2, 7, 5,9]
                        }
                    ],
                }
            },


        }
    }
</script>

<style>
    .small {
        max-width: 600px;
        margin:  150px auto;
    }
</style>




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

Atomic Designでコーディングしてたら規約ができたので、それを使って更にコーディングしてみた。

規約

  • Store, Routerにアクセスするのは、pagesのみにすること。
  • 「templates, organism」はslotによって要素を受け取り、属性によりデータを受け取らないこと。
  • 「molecules, atoms」は属性によってデータを受け取り、slotにより要素を受け取らないこと。
  • moleculesはAtoms
  • pagesは、「templates, organism, molecules」を用いて、アプリを組み立て、Atomsに依存しないこと。
  • moleculesはatomsのみに依存すること。
  • 同じ階層は相互に依存し合わないこと。
  • 「templates, organism, atoms」は他の階層に依存しないこと
  • templatesはネストしてはならない。
  • mixinは利用しない。
  • 汎用性を意識し過ぎて、手を止めないこと
  • コンポーネントを分けることを最優先すること
  • atomsはグローバルコンポーネントへの登録を避けること

依存関係

これらを守ると依存関係は図のようになる。

Atomic Design - hierarchy and dependency.png

データとイベントの向き

データとイベントの向きはこうなる

Atomic Design - data and event handling.png

コンポーネント名

namespace(アプリ・システム境界) + ファイル名で登録する。
Vueの場合は、パスカルケースがケバブになり、ちょっと便利。
Atomsはnamespaceを超えて使用したいので、moleculesの中でimportし、ローカルコンポーネントに登録する。

// namespace for Todo App
const namespace = "Td"

import Manage from './pages/Manage';
Vue.component(namespace + 'Manage', Manage);

Snippet

{
  "VueComponentRegist": {
    "scope": "javascript,typescript",
    "prefix": "VCR",
    "body": ["import $2 from './$1/$2'", "Vue.component(namespace + '$2', $2)"],
    "description": "Register Vue Component."
  }
}

デザイン

とりあえず、簡単に作れそうなTODOアプリを作ってみる。

プロトタイピング

初期のデザインは、簡単に変更するために、いきなりコーディングには入らないことが多い。
Excelでもいいが、Officeを用意する余裕はプロトタイピングツールに充てたほうがいい気がする。
ちなみに、おすすめはFigma

TODO Example.png

実装

Framework

  • Vue.js
  • Tailwind CSS
  • Webpack

Templates

Templateは、レイアウト制約だと考えている。
次のList Templateの例だと、Filterの位置を右上に制約した。
一方、Actionというのは抽象的な表現で、制約がゆるい。

List

List Template.png

画面幅いっぱいのdivの中に、containerを中央配置している。
filter, items, actionの3つのスロットを用意し、
それぞれのマージンなどもここで設定し、レイアウトを固定している。

List.vue
<template>
  <div class="w-screen h-screen flex justify-center bg-grey-lighter">
    <div class="container mt-20 max-w-sm">
      <div class="flex justify-end">
        <slot name="filter"></slot>
      </div>
      <div class="mt-4">
        <slot name="items"></slot>
      </div>
      <div class="mt-6">
        <slot name="action"></slot>
      </div>
    </div>
  </div>
</template>

Organisms

ドメインを表現したりする。
リストレンダリングしたり、階層化に使ったりする。

Todo Status

Todo Status Organism.png

TODOのステータスを表示するエリア。
イメージ通り、ただ枠を作るだけになる。
不要かもしれないが、今何もないだけであって、変更に強いアプリケーションにするためには必要になる。

TodoStatus.vue
<template>
  <div>
    <slot></slot>
  </div>
</template>

Todo Item

Todo Item Organism.png

TODOのアイテムを表示するエリア。
3つのエリアに分かれていて、それぞれのパディングなどを設定している。

TodoItem.vue
<template>
  <div class="bg-white rounded mb-4 shadow">
    <div class="pl-4 pr-3 pt-6 pb-4">
      <slot name="header"></slot>
    </div>
    <div class="pl-4 pr-3 py-1">
      <slot name="content"></slot>
    </div>
    <div class="pl-4 pr-3 pt-4 pb-3 flex flex-row-reverse">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

Todo Add

Todo Add Organism.png

TODOアイテムを追加する為の何かを入れるエリアになる。

TodoAdd.vue
<template>
  <div>
    <slot></slot>
  </div>
</template>

Molecules

正直、TemplateとOrganismを作っただけだと、中身が無くて、大抵何も表示されないことが多い。
なので、Moleculesあたりから少し楽しくなってくる。

Tags

Tags Molecules.png

タグの配列を受け取り、レンダリングし、selectイベントで選択を通知する。
Tag単体でMoleculeにするのも選択肢としてありうる。
ただ、AtomにするかMoleculeするかは、Atomの粒度によるので、どちらが正解ということもないと考えていい。
例えば、タグ=Text + Boxととらえて、TextとBoxだけでAtomにしたいならそれでもいい。
逆に、タグは1つのAtomと捉えてもいい。
そこに議論と思考の時間を割くのが一番もったいなくて、あとからリファクタリングできることの方が大事。

Tags.vue
<template>
  <div class="flex">
    <x-tag
      v-for="(label, index) in labels"
      :key="label"
      :class="{
        // first
        'rounded-tl-lg': labels[index - 1] === undefined,
        'rounded-bl-lg': labels[index - 1] === undefined,
        // last
        'rounded-tr-lg': labels[index + 1] === undefined,
        'rounded-br-lg': labels[index + 1] === undefined,
      }"
      style="margin-right: 1px"
      :active="label === selected"
      :label="label"
      @click="$emit('select', label)"
    ></x-tag>
  </div>
</template>

<script>
import Tag from "../atoms/Tag.vue";

export default {
  components: {
    XTag: Tag
  },
  model: {
    prop: "selected",
    event: "select"
  },
  props: {
    labels: {
      type: Array,
      required: true
    },
    selected: {
      type: String,
      default: ""
    }
  }
};
</script>

Title

Title Molecule.png

ぱっと見、ただのテキストなので、Atomのような気もしそうだが、Moleculeにした。
というのも、MoleculeはAtomの1個以上の集まりと捉えていて、Atom1個でもMoleculeでいい。
例えば、Text系はデフォルトで使う文字の色(黒じゃなく濃いグレー)みたいな統一はAtomでやっているから、Atomに依存することには意味がある。

また、デザイン上ではわからなかったが、このTitleは編集可能にしたため、実はInput用のAtomも混じっている。
この場合は、changeイベントのみを拾っているが、他のイベント(keypress.enterとか)をchangeとしてemitしてあげたりすると、使う側としては、changeのみを購読すればいいので、楽だしわかりやすくもなる。

Title.vue
<template>
  <span>
    <x-input
      :placeholder="placeholder"
      v-if="editable"
      :value="title"
      @change="$emit('change', $event.target.value)"
    ></x-input>
    <x-text v-else class="font-bold text-lg" :text="title"></x-text>
    <x-text class="text-grey-darker text-sm" :text="anchorText"></x-text>
  </span>
</template>

<script>
import Input from "../atoms/Input.vue";
import Text from "../atoms/Text.vue";

export default {
  components: {
    XInput: Input,
    XText: Text
  },
  model: {
    prop: "title",
    event: "change"
  },
  props: {
    title: {
      type: String,
      required: true
    },
    anchorText: {
      type: String,
      default: ""
    },
    editable: {
      type: Boolean,
      default: false
    },
    placeholder: {
      type: String,
      default: "入力してください。"
    }
  }
};
</script>

Date Labeled

Date Labeled Molecule.png

Titleと同じで、編集可能。
また、どうでもいいこだわりだが、あえて日付は文字列に限定している。
日付のフォーマットを決める責務をPagesかStoreに果たしてもらおうと考えている為だ。
特に日付だけというわけではないのだが、
どう表示したいかはロケールやドメインに依存すると考えているためだ。

DateLabeled.vue
<template>
  <div class="flex items-end">
    <span>
      <x-input
        v-if="editable"
        type="date"
        :value="date"
        :placeholder="placeholder"
        @change="$emit('change', $event.target.value)"
      ></x-input>
      <x-text v-else class="mr-4 text-sm" :text="date"></x-text>
      <x-text class="text-grey-darker text-sm" :text="label"></x-text>
    </span>
  </div>
</template>

<script>
import Input from "../atoms/Input.vue";
import Text from "../atoms/Text";

export default {
  components: {
    XInput: Input,
    XText: Text
  },
  model: {
    prop: "date",
    event: "change"
  },
  props: {
    date: {
      type: String,
      required: true
    },
    label: {
      type: String,
      default: ""
    },
    editable: {
      type: Boolean,
      default: false
    },
    placeholder: {
      type: String,
      default: "日付を入力してください。"
    }
  }
};
</script>

Button

Button Molecule.png

ボタンをAtomに分けていないのは、単なる怠惰だけれども、原理主義的になりすぎるのもいけない。という言い訳。。。

Button.vue
<template>
  <button
    class="py-2 bg-grey rounded bgcolor-transition font-sans text-white text-sm font-bold tracking-wide"
    :class="{
      'bg-teal': primary,
      'hover:bg-teal-dark': primary,
      'bg-red-lighter': danger,
      'hover:bg-red-light': danger,
      }"
    style="min-width: 80px"
    @click="$emit('click', $event)"
  >{{label}}</button>
</template>

<script>
export default {
  props: {
    primary: {
      type: Boolean,
      default: false
    },
    danger: {
      type: Boolean,
      default: false
    },
    label: {
      type: String,
      required: true
    }
  }
};
</script>

<style scoped>
.bgcolor-transition {
  transition: background-color 0.3s ease 0s;
}
</style>

Area Clickable

Area Clickable Molecule.png

AreaClickable.vue
<template>
  <div
    class="border border-dashed border-grey-dark py-4 text-center bg-grey-lighter hover:bg-grey-lightest rounded cursor-pointer bgcolor-transition"
    @click="$emit('click', $event)"
  >
    <x-text class="font-sans text-grey-darker tracking-wide" :text="label"></x-text>
  </div>
</template>

<script>
import Text from "../atoms/Text";

export default {
  components: {
    XText: Text
  },
  props: {
    label: {
      type: String,
      required: true
    }
  }
};
</script>

<style>
.bgcolor-transition {
  transition: background-color 0.15s ease 0s;
}
</style>

Atoms

主にHTMLのタグやらをそのまま使う階層になる。
Moleculesを作っていると、自然に出てくることが多い。
Atomの粒度は、人それぞれな気もするが、便利と思える粒度で問題ないと思う。

Tag

Tag Atom.png

Tag.vue
<template>
  <div
    class="tag bg-white shadow font-sans flex justify-center items-center cursor-pointer"
    :class="{
    'border': active,
    'border-indigo-dark': active,
  }"
    @click="$emit('click', $event)"
  >{{label}}</div>
</template>

<script>
export default {
  props: {
    active: {
      type: Boolean,
      default: false
    },
    label: {
      type: String,
      required: true
    }
  }
};
</script>

<style scoped>
.tag {
  width: 49px;
  height: 22px;
  font-size: 12px;
  color: #785e5e;
}
</style>

Text

Text Atom.png

Text.vue
<template>
  <span class="text-grey-darkest">{{text}}</span>
</template>

<script>
export default {
  props: {
    text: {
      type: String,
      required: true
    }
  }
};
</script>

Input

Input Atom.png

Input.vue
<template>
  <!-- normal text input -->
  <input
    v-if="type === 'text'"
    class="text-sm text-grey-darkest border-b appearance-none outline-none focus:border-grey-dark transition"
    type="text"
    :placeholder="placeholder"
    :value="value"
    @input="$emit('input', $event)"
    @change="$emit('change', $event)"
  >

  <input
    v-else-if="type === 'date'"
    class="text-sm text-grey-darkest"
    type="datetime-local"
    :placeholder="placeholder"
    :value="value"
    @input="$emit('input', $event)"
    @change="$emit('change', $event)"
  >
</template>

<script>
export default {
  model: {
    prop: "value",
    event: "change"
  },
  props: {
    value: {
      type: null
    },
    type: {
      type: String,
      default: "text"
    },
    placeholder: {
      type: String,
      default: ""
    }
  }
};
</script>

<style scoped>
.transition {
  transition: border-color 0.2s ease 0s;
}
</style>

Pages

上で、作ったものを組み立てたり、データのハンドリングやイベントハンドリングの処理を書いたり。
もしくは、上で作っているものは中は空っぽでもいいから、先にページを作ることもできる。
そうすると、属性とかイベントハンドリング・スロット名が全体感を見ながら決められるため、その方がよかったりする。

Manage

管理ページなので、Manage。
イベント処理、データ渡し、コンポーネントの配置などすべて行う為、非常に長くなる。
だが、ドメインとしての要素が全体感を通して見れるので、まあまあ可読性はあると思っている。
StoreとかUtilityは分離しているので、いいと思っている。
これはたぶんスキルの問題だが、ComputedやVuexのGettersを使いすぎると、変更検知されすぎて、自分でも動きがわからなくなってバグの原因になったりする。
それなら、多少冗長でも処理の因果関係が追いやすいことを意識して書いている。

Manage.vue
<template>
  <td-list>
    <template v-slot:filter>
      <td-todo-status>
        <td-tags :labels="status" v-model="selectedStatus" @select="refresh"></td-tags>
      </td-todo-status>
    </template>

    <template v-slot:items>
      <td-todo-item v-for="todoItem in todoItems" :key="todoItem.id">
        <template v-slot:header>
          <td-title :title="todoItem.name"></td-title>
        </template>
        <template v-slot:content>
          <td-date-labeled
            :date="todoItem.deadline | formatDate"
            :label="todoItem.status === 'WIP' ? 'までに終わらせる' : 'までに終わらせる予定が'"
          ></td-date-labeled>
          <td-date-labeled
            class="mt-4"
            v-if="todoItem.status === 'DONE'"
            :date="todoItem.completionDate | formatDate"
            label="に終わった"
          ></td-date-labeled>
        </template>
        <template v-slot:footer>
          <td-button
            v-if="todoItem.status === 'WIP'"
            primary
            label="DONE"
            @click="changeStatus(todoItem.id, 'DONE')"
          ></td-button>
          <td-button
            v-if="todoItem.status === 'DONE'"
            danger
            label="DELETE"
            @click="deleteTodo(todoItem.id)"
          ></td-button>
        </template>
      </td-todo-item>
    </template>

    <template v-slot:action>
      <td-todo-add v-if="!adding">
        <td-area-clickable label="TODOアイテムを追加する" @click="adding = true"></td-area-clickable>
      </td-todo-add>
      <td-todo-item v-if="adding">
        <template v-slot:header>
          <td-title v-model="newTodoName" editable anchor-text="を"></td-title>
        </template>
        <template v-slot:content>
          <td-date-labeled v-model="newTodoDeadline" label="までに終わらせる" editable></td-date-labeled>
        </template>
        <template v-slot:footer>
          <td-button primary label="OK" @click="addTodo(newTodoName, newTodoDeadline)"></td-button>
        </template>
      </td-todo-item>
    </template>
  </td-list>
</template>

<script>
import todoStore from "../store/todo";
import { formatDate } from "../util";
import moment from "moment";

export default {
  data() {
    return {
      status: ["WIP", "DONE", "ALL"],
      selectedStatus: "ALL",
      todoItems: [],
      adding: false,
      newTodoName: "",
      newTodoDeadline: ""
    };
  },
  computed: {
    targetStatus() {
      return this.selectedStatus === "ALL"
        ? ["WIP", "DONE"]
        : [this.selectedStatus];
    }
  },
  filters: {
    formatDate
  },
  methods: {
    refresh() {
      this.todoItems = todoStore.findAll({
        status: this.targetStatus
      });

      this.newTodoName = "";
      this.newTodoDeadline = "";
    },
    changeStatus(id, status) {
      todoStore.update(id, {
        status,
        // register now as completion date.
        completionDate: new Date()
      });

      this.refresh();
    },
    deleteTodo(id) {
      todoStore.delete(id);

      this.refresh();
    },
    addTodo(name, deadline) {
      // TODO validate input value.

      todoStore.add({
        name,
        deadline
      });

      this.adding = false;

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

感想

作っていて思ったのは、用語集が欲しい!とうこと。
こういうパーツのことなんて呼べばいいんだろう。。。みたいな命名時の悩みに無駄な時間を割かないよう、ハンドブックみたいなものがあると嬉しいと思った。

あと、やっぱりTailwind CSSは使いやすい。
CSSの知識は必要だが、テーマ機能も簡単だし、さくっと動かせてカスタマイズ性が高いのは非常にありがたかった。
今読んでいる途中だけれど、Refactoring UIと合わせて使うと効果が増す。
実装を考えたデザインができる。

これを作るのに、デザイン含め大体2日くらいかかったが、エンジニア的にストレスなく作れて、楽しかった。
若干作業が単純化するところや、モジュールを分けることで、エディターでファイルを飛び交うのが辛いところは欠点かもしれない。
(Alt + Tab何回押したことやら)

ソースコードはGithubに挙げたので、よかったら見てください。
atomic-design-vue-todo-example

ライセンスは、「勝手にしやがれ!好きにしな!」です。

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

Vue.js-Laravelを使用して、ファイル管理についておおざっぱに#8

なぜファイルを分けるのか

チュートリアルなんかでよく見かけることがありますが、htmlとcssとJavaScriptとphpなどのコードを一つのファイルに書いてある例があります。それは、チュートリアルとしては、一つのファイルでコードを書いたほうがページにまとめやすい・記事を見た人がみやすいなどの説明のためにそうしていることがほとんどです。

実践の場ではそのまま使ってはいけないほうが多いと思います。このページは記述量が少ないから、一つのファイルに書いてしまおうなどとしてしまうと、htmlとcssとJavaScriptとphpを同じファイルにあり、そのページのすべての部品の機能も同じファイルに記述された状態だと、この機能を足してほしいといったときに、どんどんファイルの記述量が肥大化していきます。問題としては、コードを管理しにくかったり、初めて見る人が理解するのに時間がかかってしまうなどがあります。
それをなるべく防ぐ方ためにファイルを分けます。ファイルを分けることを抽象化といいます。

ファイルの分け方もいろいろあります。例えば、Vue.jsでは単一ファイルコンポーネントという考え方があり、それはページの部品ごとにファイルを分けて、ページを作るときに、部品を一つずつ引っ張ってくることで、再利用性が高く、管理もしやすく、はじめてコードを見た人も理解がしやすくなります。他には、cssとJavaScriptとHtmlは別ファイルで管理するやり方などもあります。
今回はVue.jsを使って、下の例で、単一ファイルコンポーネントを体験してみます。

例をこなして体験してみる

Laravelプロジェクト配下で、コマンドnpm installをたたいて、Vue.jsを使うための準備を整えます。
- laravel Vue.js docker-compose環境構築
- laravelにVue.jsを入れる前の、5分間の事前学習

環境が整えられたら、LaravelでVue.jsを使って、表示してみます。
(ファイル分けないで)(もともと用意されているwelcome.blade.phpを編集します)

welcome.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Vue.js</title>
    <link href="{{ asset('css/app.css') }}" rel="stylesheet" type="text/css">
    <script src=" {{ asset('js/app.js') }} "></script>
</head>
<body>
    <div id="sampleDiv">
        <sample-component></sample-component>
    </div>
    <script>
        Vue.component('sample-component', {
            template:'<h1>今日も最高!!</h1>'
        });
        const vue = new Vue({
            el: '#sampleDiv',
        });
    </script>
</body>
</html>

Vue.jsのコンポーネントを使用して、このページではsample-componentという部品を利用しています。Vue.jsコンポーネント

まず、bladeだけを分けてみます。

構造

    resources -- js
              |
              -- lang
              |
              -- sass
              |
              -- views -- layouts -- app.blade.php
                       |
                       -- welocome.blade.php

layoutsフォルダを作成し、その下にapp.blade.phpを作成します
(さらにheader.blade.phpとして、headerを別に分ける方法もあり)

app.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Vue.js</title>
    <link href="{{ asset('css/app.css') }}" rel="stylesheet" type="text/css">
    <script src=" {{ asset('js/app.js') }} "></script>
</head>
<body>
    @yield('content')
</body>
</html>

もともとのwelocome.blade.phpからbodyの中身を@yield('content')に変えました。何がいいかというと、ページを複数作成するときに、app.blade.phpの中に記述していることは@extendsで再利用ができるという点で何回も記述しなくてすみます。
それでは、welcome.blade.phpを編集します。

welcome.blade.php
@extends('layouts.app')
@section('content')
    <div id="sampleDiv">
        <sample-component></sample-component>
    </div>
    <script>
        Vue.component('sample-component', {
            template:'<h1>今日も最高!!</h1>'
        });
        const vue = new Vue({
            el: '#sampleDiv',
        });
    </script>
@endsection

@extendsで引き継ぐbladeを指定しています。先ほど@yieldで指定したところへ、@sectionから@endsectionまでの中身を挿入しています。画面を表示すると、先ほどと同じように「今日も最高!!」と表示されています。

それでは、単一ファイルコンポーネントを利用してみます。

先ほどの構造のjsディレクトリの詳細

   js -- components -- ExampleComponent.vue
      |             |
      -- app.js     -- SampleComponent.vue
      |
      -- bootstrap.js

LaravelでVue.jsを使用する際にresources/js/app.jsをpublic/jsにコンパイル(出力)するため、npm run devをたたきますが、npm run devはwebpack.mix.jsの内容を見て、出力してくれます。webpackは複数のjsファイルが存在したとき、それの依存関係を解決し、一つのファイルにまとめてくれる便利なものです。今回でいえばnpm run devをたたけば、一つのファイルにまとめてくれます。

SampleComponent.vue作成

SampleComponent.vue
<template>
    <p>
        {{message}}
    </p>
</template>
<script>
    export default {
        data: () => {
            return {
                message: '今日も最高!!'
            }
        }
    }
</script>

Vue.jsのコンポーネントの決まりとして、タグの中に書きます。子コンポーネントなので、dataは関数です。
bladeでVue.jsのデータ変数を使う際は、@{{}}を使うのですが、.vueファイルのため{{}}内に書けます。

welocome.blade.php
@extends('layouts.app')
@section('content')
<div id="app">
    <sample-component></sample-component>
</div>
@endsection

id:appはもともと、app.jsに登録されているので、使用できます。idを変える場合はapp.jsに書くかwelocome.blade.phpでVueインスタンスを新たに作る必要があるため、今回記述量の少ないこちらを採用しました。
app.jsを編集します。

app.js
// Vue.component('example-component', require('./components/ExampleComponent.vue').default);
Vue.component('sample-component', require('./components/SampleComponent.vue').default);

exampleは使わないので、コメントアウトして、sample-componentを使うため、登録します。app.jsの下にid:appの記述があると思います。require default

npm run dev

これでwelcome.blade.phpを表示すれば、表示されると思います。

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

Vue.js-Laravelを使用して、ファイル管理について簡単に書いてみた#8

なぜファイルを分けるのか

チュートリアルなんかでよく見かけることがありますが、htmlとcssとJavaScriptとphpなどのコードを一つのファイルに書いてある例があります。それは、チュートリアルとしては、一つのファイルでコードを書いたほうがページにまとめやすい・記事を見た人がみやすいなどの説明のためにそうしていることがほとんどです。

実践の場ではそのまま使ってはいけないほうが多いと思います。このページは記述量が少ないから、一つのファイルに書いてしまおうなどとしてしまうと、htmlとcssとJavaScriptとphpを同じファイルにあり、そのページのすべての部品の機能も同じファイルに記述された状態だと、この機能を足してほしいといったときに、どんどんファイルの記述量が肥大化していきます。問題としては、コードを管理しにくかったり、初めて見る人が理解するのに時間がかかってしまうなどがあります。
それをなるべく防ぐ方ためにファイルを分けます。ファイルを分けることを抽象化といいます。

ファイルの分け方もいろいろあります。例えば、Vue.jsでは単一ファイルコンポーネントという考え方があり、それはページの部品ごとにファイルを分けて、ページを作るときに、部品を一つずつ引っ張ってくることで、再利用性が高く、管理もしやすく、はじめてコードを見た人も理解がしやすくなります。他には、cssとJavaScriptとHtmlは別ファイルで管理するやり方などもあります。
今回はVue.jsを使って、下の例で、単一ファイルコンポーネントを体験してみます。

例をこなして体験してみる

Laravelプロジェクト配下で、コマンドnpm installをたたいて、Vue.jsを使うための準備を整えます。
- laravel Vue.js docker-compose環境構築
- laravelにVue.jsを入れる前の、5分間の事前学習

環境が整えられたら、LaravelでVue.jsを使って、表示してみます。
(ファイル分けないで)(もともと用意されているwelcome.blade.phpを編集します)

welcome.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Vue.js</title>
    <link href="{{ asset('css/app.css') }}" rel="stylesheet" type="text/css">
    <script src=" {{ asset('js/app.js') }} "></script>
</head>
<body>
    <div id="sampleDiv">
        <sample-component></sample-component>
    </div>
    <script>
        Vue.component('sample-component', {
            template:'<h1>今日も最高!!</h1>'
        });
        const vue = new Vue({
            el: '#sampleDiv',
        });
    </script>
</body>
</html>

Vue.jsのコンポーネントを使用して、このページではsample-componentという部品を利用しています。Vue.jsコンポーネント

まず、bladeだけを分けてみます。

構造

    resources -- js
              |
              -- lang
              |
              -- sass
              |
              -- views -- layouts -- app.blade.php
                       |
                       -- welocome.blade.php

layoutsフォルダを作成し、その下にapp.blade.phpを作成します
(さらにheader.blade.phpとして、headerを別に分ける方法もあり)

app.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Vue.js</title>
    <link href="{{ asset('css/app.css') }}" rel="stylesheet" type="text/css">
    <script src=" {{ asset('js/app.js') }} "></script>
</head>
<body>
    @yield('content')
</body>
</html>

もともとのwelocome.blade.phpからbodyの中身を@yield('content')に変えました。何がいいかというと、ページを複数作成するときに、app.blade.phpの中に記述していることは@extendsで再利用ができるという点で何回も記述しなくてすみます。
それでは、welcome.blade.phpを編集します。

welcome.blade.php
@extends('layouts.app')
@section('content')
    <div id="sampleDiv">
        <sample-component></sample-component>
    </div>
    <script>
        Vue.component('sample-component', {
            template:'<h1>今日も最高!!</h1>'
        });
        const vue = new Vue({
            el: '#sampleDiv',
        });
    </script>
@endsection

@extendsで引き継ぐbladeを指定しています。先ほど@yieldで指定したところへ、@sectionから@endsectionまでの中身を挿入しています。画面を表示すると、先ほどと同じように「今日も最高!!」と表示されています。

それでは、単一ファイルコンポーネントを利用してみます。

先ほどの構造のjsディレクトリの詳細

   js -- components -- ExampleComponent.vue
      |             |
      -- app.js     -- SampleComponent.vue
      |
      -- bootstrap.js

LaravelでVue.jsを使用する際にresources/js/app.jsをpublic/jsにコンパイル(出力)するため、npm run devをたたきますが、npm run devはwebpack.mix.jsの内容を見て、出力してくれます。webpackは複数のjsファイルが存在したとき、それの依存関係を解決し、一つのファイルにまとめてくれる便利なものです。今回でいえばnpm run devをたたけば、一つのファイルにまとめてくれます。

SampleComponent.vue作成

SampleComponent.vue
<template>
    <p>
        {{message}}
    </p>
</template>
<script>
    export default {
        data: () => {
            return {
                message: '今日も最高!!'
            }
        }
    }
</script>

Vue.jsのコンポーネントの決まりとして、タグの中に書きます。子コンポーネントなので、dataは関数です。
bladeでVue.jsのデータ変数を使う際は、@{{}}を使うのですが、.vueファイルのため{{}}内に書けます。

welocome.blade.php
@extends('layouts.app')
@section('content')
<div id="app">
    <sample-component></sample-component>
</div>
@endsection

id:appはもともと、app.jsに登録されているので、使用できます。idを変える場合はapp.jsに書くかwelocome.blade.phpでVueインスタンスを新たに作る必要があるため、今回記述量の少ないこちらを採用しました。
app.jsを編集します。

app.js
// Vue.component('example-component', require('./components/ExampleComponent.vue').default);
Vue.component('sample-component', require('./components/SampleComponent.vue').default);

exampleは使わないので、コメントアウトして、sample-componentを使うため、登録します。app.jsの下にid:appの記述があると思います。require default

npm run dev

これでwelcome.blade.phpを表示すれば、表示されると思います。

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

Vue.js-Laravelを使用して、ファイル管理について簡単に書いてみた

なぜファイルを分けるのか

チュートリアルなんかでよく見かけることがありますが、htmlとcssとJavaScriptとphpなどのコードを一つのファイルに書いてある例があります。それは、チュートリアルとしては、一つのファイルでコードを書いたほうがページにまとめやすい・記事を見た人がみやすいなどの説明のためにそうしていることがほとんどです。

実践の場ではそのまま使ってはいけないほうが多いと思います。このページは記述量が少ないから、一つのファイルに書いてしまおうなどとしてしまうと、htmlとcssとJavaScriptとphpを同じファイルにあり、そのページのすべての部品の機能も同じファイルに記述された状態だと、この機能を足してほしいといったときに、どんどんファイルの記述量が肥大化していきます。問題としては、コードを管理しにくかったり、初めて見る人が理解するのに時間がかかってしまうなどがあります。
それをなるべく防ぐ方ためにファイルを分けます。ファイルを分けることを抽象化といいます。

ファイルの分け方もいろいろあります。例えば、Vue.jsでは単一ファイルコンポーネントという考え方があり、それはページの部品ごとにファイルを分けて、ページを作るときに、部品を一つずつ引っ張ってくることで、再利用性が高く、管理もしやすく、はじめてコードを見た人も理解がしやすくなります。他には、cssとJavaScriptとHtmlは別ファイルで管理するやり方などもあります。
今回はVue.jsを使って、下の例で、単一ファイルコンポーネントを体験してみます。

例をこなして体験してみる

Laravelプロジェクト配下で、コマンドnpm installをたたいて、Vue.jsを使うための準備を整えます。
- laravel Vue.js docker-compose環境構築
- laravelにVue.jsを入れる前の、5分間の事前学習

環境が整えられたら、LaravelでVue.jsを使って、表示してみます。
(ファイル分けないで)(もともと用意されているwelcome.blade.phpを編集します)

welcome.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Vue.js</title>
    <link href="{{ asset('css/app.css') }}" rel="stylesheet" type="text/css">
    <script src=" {{ asset('js/app.js') }} "></script>
</head>
<body>
    <div id="sampleDiv">
        <sample-component></sample-component>
    </div>
    <script>
        Vue.component('sample-component', {
            template:'<h1>今日も最高!!</h1>'
        });
        const vue = new Vue({
            el: '#sampleDiv',
        });
    </script>
</body>
</html>

Vue.jsのコンポーネントを使用して、このページではsample-componentという部品を利用しています。Vue.jsコンポーネント

まず、bladeだけを分けてみます。

構造

    resources -- js
              |
              -- lang
              |
              -- sass
              |
              -- views -- layouts -- app.blade.php
                       |
                       -- welocome.blade.php

layoutsフォルダを作成し、その下にapp.blade.phpを作成します
(さらにheader.blade.phpとして、headerを別に分ける方法もあり)

app.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Vue.js</title>
    <link href="{{ asset('css/app.css') }}" rel="stylesheet" type="text/css">
    <script src=" {{ asset('js/app.js') }} "></script>
</head>
<body>
    @yield('content')
</body>
</html>

もともとのwelocome.blade.phpからbodyの中身を@yield('content')に変えました。何がいいかというと、ページを複数作成するときに、app.blade.phpの中に記述していることは@extendsで再利用ができるという点で何回も記述しなくてすみます。
それでは、welcome.blade.phpを編集します。

welcome.blade.php
@extends('layouts.app')
@section('content')
    <div id="sampleDiv">
        <sample-component></sample-component>
    </div>
    <script>
        Vue.component('sample-component', {
            template:'<h1>今日も最高!!</h1>'
        });
        const vue = new Vue({
            el: '#sampleDiv',
        });
    </script>
@endsection

@extendsで引き継ぐbladeを指定しています。先ほど@yieldで指定したところへ、@sectionから@endsectionまでの中身を挿入しています。画面を表示すると、先ほどと同じように「今日も最高!!」と表示されています。

それでは、単一ファイルコンポーネントを利用してみます。

先ほどの構造のjsディレクトリの詳細

   js -- components -- ExampleComponent.vue
      |             |
      -- app.js     -- SampleComponent.vue
      |
      -- bootstrap.js

LaravelでVue.jsを使用する際にresources/js/app.jsをpublic/jsにコンパイル(出力)するため、npm run devをたたきますが、npm run devはwebpack.mix.jsの内容を見て、出力してくれます。webpackは複数のjsファイルが存在したとき、それの依存関係を解決し、一つのファイルにまとめてくれる便利なものです。今回でいえばnpm run devをたたけば、一つのファイルにまとめてくれます。

SampleComponent.vue作成

SampleComponent.vue
<template>
    <p>
        {{message}}
    </p>
</template>
<script>
    export default {
        data: () => {
            return {
                message: '今日も最高!!'
            }
        }
    }
</script>

Vue.jsのコンポーネントの決まりとして、タグの中に書きます。子コンポーネントなので、dataは関数です。
bladeでVue.jsのデータ変数を使う際は、@{{}}を使うのですが、.vueファイルのため{{}}内に書けます。

welocome.blade.php
@extends('layouts.app')
@section('content')
<div id="app">
    <sample-component></sample-component>
</div>
@endsection

id:appはもともと、app.jsに登録されているので、使用できます。idを変える場合はapp.jsに書くかwelocome.blade.phpでVueインスタンスを新たに作る必要があるため、今回記述量の少ないこちらを採用しました。
app.jsを編集します。

app.js
// Vue.component('example-component', require('./components/ExampleComponent.vue').default);
Vue.component('sample-component', require('./components/SampleComponent.vue').default);

exampleは使わないので、コメントアウトして、sample-componentを使うため、登録します。app.jsの下にid:appの記述があると思います。require default

npm run dev

これでwelcome.blade.phpを表示すれば、表示されると思います。

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

Vue.jsとaxiosでAPIからデータを取得して表示する

この記事でやること

静的なHTMLから検索条件を指定してAPIに接続してデータを取得、表示します。記事では手っ取り早くローカルPCに保存したHTMLからAPIに接続します。

接続するAPI

ユーザIDを指定して問い合わせると、ユーザ名とタイムスタンプをJSONで返してくるAPIをAWS上に用意しました。
以前書いたこっちの記事
Raspberry PiとPaSoRiでFelicaのNFCタグを読んでみる
の続きで、タッチ情報をIFTTTの代わりにAWS DynamoDBに蓄積するように変更。ユーザ情報を引いて名前と直近のタッチ時間をRambdaとAPI Gateway経由で取得できるようにしています。
こちらについては改めて記事にする予定です。

HTMLのソース

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>NFC demo</title>
  </head>
  <body>
    <div id="app">
        <input v-model="userid" placeholder="User ID">
        <button v-on:click="query">Query</button><br>
        <dl v-if="info">
          <dt>ID</dt><dd>{{info.id}}</dd>
          <dt>Name</dt><dd>{{info.name}}</dd>
          <dt>Date</dt><dd>{{info.timestamp|dateformat}}</dd>
        </dl>
        <div v-else="info">No touch data.</div>
    </div>
    <script src="https://unpkg.com/vue/dist/vue.min.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="app.js"></script>
  </body>
</html>

JavaScriptのソース

app.js
const apiUrl = "https://********.********.amazonaws.com/default/nfcDemoGetTouchTime"
const apiKey = "{APIキー}"
const config = {headers: {
    'Content-Type': 'application/json',
    'x-api-key': apiKey
}}

Vue.filter('dateformat', function(value){
    if (!value) return ''
    var zp = function(num){
        return (num < 10) ? '0'+ num: num
    }
    var dt = new Date(value * 1000)
    var dtstr = dt.getFullYear() + '-' + zp(dt.getMonth() + 1) + '-' + zp(dt.getDate()) + ' '
                + zp(dt.getHours()) + ':' + zp(dt.getMinutes())
    return dtstr
})

var app = new Vue({
    el: "#app",
    data:{
        info: null,
        userid: null
    },
    methods:{
        query: function(event){
            var querydata = {'id': this.userid}
            axios
            .post(apiUrl, querydata, config)
            .then(response => {
                this.info = response.data
                console.log(this.info)
            })
            .catch(error => console.log(error))
        }
    }
})

動かしてみる

index.htmlとapp.jsを適当なフォルダに保存して、ブラウザからindex.htmlを開きます。
初期画面.png

ユーザIDをフォームに入力してQueryボタンを押すと...
APIからJSONで問い合わせ結果が返ってきます。

{"id": "test1", "name": "\u30c6\u30b9\u30c8\u30e6\u30fc\u30b6\u30fc\uff11", "timestamp": 1557545262.413307}

実行画面.png
問い合わせ結果が画面に反映されました。

参考にしたドキュメント

Vue.jsは日本語版の公式ドキュメントが充実しているのでいろいろ助かります。
axios を利用した API の使用
フィルター — Vue.js

残課題

APIキーがソースから丸見えなので、外部にホスティングするときは、Cognitoで認証するとかアクセス制御の仕組みが必要になります。

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

Vue.js + Firebaseで自己紹介用の静的サイトを作ってみた 第2弾 実装編(ルーティング)

はじめに

Vue.js + Firebaseで自己紹介用の静的サイトを作ってみた 第1弾 環境構築編の続きとなります。

過去の記事はこちら・・・
Vue.js + Firebaseで自己紹介用の静的サイトを作ってみた 第1弾 環境構築編
続きの記事はこちら・・・(第3弾以降は作成中)
Vue.js + Firebaseで自己紹介用の静的サイトを作ってみた 第3弾 実装編(ヘッダー、フッター、SNSリンク設置)

要件

今回は以下画面構成と機能で簡単な静的サイトを実装していきます。
○画面構成
・トップページ(Home)
・プロフィールページ(About)
・スキルページ(Skill)
・お問い合わせページ(Contact)
○機能
・各ページへのルーティング(ヘッダー、フッターにナビゲーション配置)
・SNS(Twitter, Instagram, GitHub, Qiita, note)ページへのリンク(フッターにSNSアイコン配置)
・お問い合わせフォームからお問い合わせ。(名前、Eメール、内容入力フォームと必須入力)

実装

ルーティング

まずは各画面構成のページを作っていって各ページへのルーティング機能を実装していきたいと思います。
※vue add routerは第1弾 環境構築編で追加し忘れたプラグインなのでここで追加していますが、vue create時にdefaultでなくmanualで設定して追加する方法もあります。

$ vue add router
? Use history mode for router? (Requires proper server setup for index fallback 
in production) Yes

?  Invoking generator for core:router...
?  Installing additional dependencies...

removed 1 package and audited 23924 packages in 8.956s
found 0 vulnerabilities

✔  Successfully invoked generator for plugin: core:router
   The following files have been updated / added:

     src/router.js
     src/views/About.vue
     src/views/Home.vue
     package-lock.json
     package.json
     src/App.vue
     src/main.js

   You should review these changes with git diff and commit them.

そうするといくつかいくつかファイルが追加されてたり変更されてたりすることがterminalの内容から確認できます。
この時のファイル差分を見たい場合はgit diffで見れるので気になる方は見てみてください。

では少しApp.vueの差分を見てみます。

src/App.vue
 <template>
   <div id="app">
-    <img alt="Vue logo" src="./assets/logo.png">
-    <HelloWorld msg="Welcome to Your Vue.js App"/>
+    <div id="nav">
+      <router-link to="/">Home</router-link> |
+      <router-link to="/about">About</router-link>
+    </div>
+    <router-view/>
   </div>
 </template>

ナビゲーションが追加されているのが分かると思います。
画面の方も見てみます。
スクリーンショット 2019-05-12 午後0.03.29.png
前回の環境構築編の時の画面からロゴの上にHOME|ABOUTといったリンクが追加されていることが確認できます。
実際にABOUTをクリックするとABOUTページへ遷移、HOMEをクリックすると元の画面に戻るはずです。
これを踏まえて先ほど要件で決めた画面構成で各ページに遷移できるよう設定をしていきます。

共通表示部分

src/App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <router-link to="/skill">Skill</router-link> |
      <router-link to="/contact">Contact</router-link>
    </div>
    <router-view/>
  </div>
</template>

ルーティング部分

src/router.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import(/* webpackChunkName: "home" */ './components/Home.vue')
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import(/* webpackChunkName: "about" */ './components/About.vue')
    },
    {
      path: '/skill',
      name: 'skill',
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import(/* webpackChunkName: "about" */ './components/Skill.vue')
    },
    {
      path: '/contact',
      name: 'contact',
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import(/* webpackChunkName: "about" */ './components/Contact.vue')
    }
  ]
})

トップページ(Home)

src/components/Home.vue
<template>
  <div class="home">
    <p>HOME</p>
  </div>
</template>

<script>
// @ is an alias to /src
import Home from '@/components/Home.vue'

export default {
  name: 'home',
  components: {
    Home
  }
}
</script>

プロフィールページ(About)

src/components/About.vue
<template>
  <div class="about">
    <p>ABOUT</p>
  </div>
</template>

<script>
// @ is an alias to /src
import About from '@/components/About.vue'

export default {
  name: 'about',
  components: {
    About
  }
}
</script>

スキルページ(Skill)

src/components/Skill.vue
<template>
  <div class="skill">
    <p>SKILL</p>
  </div>
</template>

<script>
// @ is an alias to /src
import Skill from '@/components/Skill.vue'

export default {
  name: 'skill',
  components: {
    Skill
  }
}
</script>

お問い合わせページ(Contact)

src/components/Contact.vue
<template>
  <div class="contact">
    <p>CONTACT</p>
  </div>
</template>

<script>
// @ is an alias to /src
import Contact from '@/components/Contact.vue'

export default {
  name: 'contact',
  components: {
    Contact
  }
}
</script>

これでルーティングの実装は終わりです。
画面の方も見てみます。
スクリーンショット 2019-05-12 午後0.59.38.png
こんなページになると思います。
実際に動かしてみるとAboutをクリックしたらAboutのページにSkillをクリックしたらSkillのページにContactをクリックしたらContactのページにそれぞれ各画面から遷移できることが確認できたと思います。
今回は、各ページ共通で扱いたい部分(今回で言うとナビゲーションの部分)はApp.vueに書いてます。
このように共通化できるものは共通化して書くことをオススメです。

まとめ

今回の対応で以下画面ページの作成と各ページへのルーティングの設定を行いました。(ナビゲーション配置)
・トップページ(Home)
・プロフィールページ(About)
・スキルページ(Skill)
・お問い合わせページ(Contact)

次回は今回作ったナビゲーションをヘッダーとフッターに設置する実装とフッターにSNSページへのリンクを設置する実装を行いたいと思います。
※実際に実装しながら備忘録として手順やポイントを記事に残している手前、もしかしたら今後他の実装をやっていく上で今までの実装を変える恐れもあるのでご容赦ください。
以下に続く・・・
Vue.js + Firebaseで自己紹介用の静的サイトを作ってみた 第3弾 実装編(ヘッダー、フッター、SNSリンク設置)

○リポジトリ
https://github.com/stkzk3110/vue_firebase-project
今回のコミット部分
Add Routing and Create Pages

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

Vue.js と Bulma でボタンにアクションつけるやつ

今日は、これをやりました

Bulma と Vue.js でボタンのべたなアクションどうやんのかなと

  • マウスの Hoover でボタンが反応するやつ
  • 入力値をAPIに渡して返ってくるまで待つみたいなやつ

button-action.gif

Bulma が用意してくれている class を Vue でいじるだけで簡単にできた

HTML

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">

<div id="button" class="section">
  <div class="field has-addons has-addons-right">
    <div class="control">
      <input class="input is-large" type="text" v-model="name" placeholder="wassap?">
    </div>
    <div class="control">
      <a
        class="button is-large is-danger"
        v-bind:class="buttonStatus"
        v-on:mouseover="mouseover"
        v-on:mouseleave="mouseleave"
        @click="loading(); api()">
        Hey!
      </a>
    </div>
  </div>
</div>

JS

var vm = new Vue({
  el: '#button',
  data: {
    name: '',
    buttonStatus: {
      'is-focused': false,
      'is-loading': false,
    }
  },
  methods: {
    mouseover: function () {
      this.buttonStatus["is-focused"] = true
    },
    mouseleave: function () {
      this.buttonStatus["is-focused"] = false
    },
    loading: function () {
      this.buttonStatus["is-loading"] = true
    },
    api: function() {
      // pretending api call which takes 5sec.
      setTimeout(function() {
        console.log(vm.name)
        vm.buttonStatus["is-loading"] = false
      }, "5000");
    }
  }
});

これだけで、なんとなくアプリがいい感じになってくるので不思議。UI 結構大事やな

CODEPEN

CODEPENで動くやつをここに貼っときます

Cheers,

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