- 投稿日:2019-05-12T21:36:58+09:00
【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の再描画・書き替えが行われるが、その前後で動作するライフサイクルフックがbeforeUpdateとupdatedである。今回は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); }以上、分かってしまえば超簡単。
ライフサイクルフックを検証するテストの書き方については別記事で書く予定。
- 投稿日:2019-05-12T21:08:52+09:00
【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つの異なるエンドポイントを定義する必要がありますか?
- テストのためにモックなどのエンドポイントをどのように扱うことができますか?
30個のファイルを変えるよりも1個のファイルを変えるほうが簡単でしょう??正しい方法
このケースではコードをシンプルにして、さまざまなリソースを定義するために、POJO(Plain Old JavaScript Objects)を使用します。(もちろん、必要に応じてクラスを使えます)axiosの設定を定義しましょう。ファイル名はリソースの接続を確立する責任があるため、Repository.jsとします。
serviceまたは単にAPIという用語を使う人もいますが、Repositoryがその機能を最もよく定義しているように思えます。
それから、それぞれのエンティティのリソース定義をする必要があります。
例えばブログがあるとしましょう。postsエンティティがあり、すべてのCRUD操作を独自のリポジトリファイルに定義できます。
postsRepositoryはこんな感じです。
見ての通りリクエストしたリソースのシンプルなオブジェクトが返されます。
この段階でget()メソッドにさらにロジックを追加して複数の環境を扱ったり、モックリポジトリをインポートしたりできることに気付いたでしょう。たとえば、現在私がいる会社(Snap.hr)では、いくつかの異なる環境が必要です。環境に応じて、モックまたは対応するエンドポイントに変更します。これはあなたが望むものと同じような複雑さでしょう。
でもこの例ではシンプルにしましょう?.vueファイルのSFCでどのように使用するかお見せしましょう。
ロジックは完全に分離されているので、異なるパラダイムを持つ別のエンドポイントをGraphQLとして使用することもできます。?結論
常に銀の弾丸は存在しないものです。このパターンは私のほとんど全部のプロジェクトでうまくいっていますが、あなたのプロジェクトにとって最適という意味ではありません。
しかし、以下のアドバンテージがあると思います。
- 簡単な方法でテスト可能なコードを実行できます。
- コンポーネントのコードがきれいになります。
- 簡単に拡張できますよね?
- DRYを保つことができます。
実用的なサンプルをCodesandbox で作成し、このアプローチを実際に実装してみました。
この種の手法を既に使用しているのであれば、ぜひお聞かせください。
?提案や質問がありますか?お気軽にコメントやTwitterでコンタクトください!
この記事は2018年に最も人気のあったVue.js記事の1つに選ばれました。
ここまで翻訳
謝辞(Appreciation)
Dear Jorge
Thank you for your amazing article!
- 投稿日:2019-05-12T20:45:37+09:00
Nuxt.js(vue.js)で画像を縮小してFirebaseStorageにアップロードする
やりたいこと
- ユーザーが画像をアップロードできるページを作りたい
- 確認用サムネイルも表示したい
- サムネイルは「どんな画像か」がわかるだけの小さなもの
- アップロードするファイルとは別物
- フロントで圧縮してからアップすることで通信量を節約したい
- Firebase Storageの使用量も節約したい
サーバーサイドは書きたくない手順
input[type="file"]でファイルを指定するnew FileReader()でファイル情報を取得するnew Image()でimg要素を作るimg.srcに「2.」のファイル情報を放り込んで画像を作成するdocument.createElement('canvas')でcanvas要素を作る- 「5.」の
canvasに「4.」の画像をリサイズしつつ貼り付ける- あらかじめ用意しておいたサムネイル用の
canvas要素に「4.」の画像をリサイズしつつ貼り付けるtoDataURL('image/jpeg')でdata_url形式の情報を取得する- 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) } } }, ...
new Reader()でファイル情報を取得new Image()でimg要素を作成- 「2.」の
img要素に「1.」のファイル情報を与えるimageの準備が整い次第makeImage()とmakeThumbnail()を呼び出すこの例では幅
1280px以上の画像を1280pxに縮小するものです。
maxWidthの値は用途に応じて調整してください。
画像のwidthがmaxWidth以上の場合は、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を使って色んな場面で使えるリサイズ用コンポーネントとして使っています。
その方法もそのうち…参考
- 投稿日:2019-05-12T20:22:57+09:00
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.jsimport { 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>
- 投稿日:2019-05-12T16:35:26+09:00
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はグローバルコンポーネントへの登録を避けること
依存関係
これらを守ると依存関係は図のようになる。
データとイベントの向き
データとイベントの向きはこうなる
コンポーネント名
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。実装
Framework
- Vue.js
- Tailwind CSS
- Webpack
Templates
Templateは、レイアウト制約だと考えている。
次のList Templateの例だと、Filterの位置を右上に制約した。
一方、Actionというのは抽象的な表現で、制約がゆるい。List
画面幅いっぱいの
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のステータスを表示するエリア。
イメージ通り、ただ枠を作るだけになる。
不要かもしれないが、今何もないだけであって、変更に強いアプリケーションにするためには必要になる。TodoStatus.vue<template> <div> <slot></slot> </div> </template>Todo Item
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アイテムを追加する為の何かを入れるエリアになる。
TodoAdd.vue<template> <div> <slot></slot> </div> </template>Molecules
正直、TemplateとOrganismを作っただけだと、中身が無くて、大抵何も表示されないことが多い。
なので、Moleculesあたりから少し楽しくなってくる。Tags
タグの配列を受け取り、レンダリングし、
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
ぱっと見、ただのテキストなので、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
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
ボタンを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
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.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.vue<template> <span class="text-grey-darkest">{{text}}</span> </template> <script> export default { props: { text: { type: String, required: true } } }; </script>Input
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ライセンスは、「勝手にしやがれ!好きにしな!」です。
- 投稿日:2019-05-12T15:07:44+09:00
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.phplayoutsフォルダを作成し、その下に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.jsLaravelで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> @endsectionid: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を表示すれば、表示されると思います。
- 投稿日:2019-05-12T15:07:44+09:00
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.phplayoutsフォルダを作成し、その下に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.jsLaravelで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> @endsectionid: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を表示すれば、表示されると思います。
- 投稿日:2019-05-12T15:07:44+09:00
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.phplayoutsフォルダを作成し、その下に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.jsLaravelで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> @endsectionid: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を表示すれば、表示されると思います。
- 投稿日:2019-05-12T14:50:07+09:00
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.jsconst 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を開きます。
ユーザIDをフォームに入力してQueryボタンを押すと...
APIからJSONで問い合わせ結果が返ってきます。{"id": "test1", "name": "\u30c6\u30b9\u30c8\u30e6\u30fc\u30b6\u30fc\uff11", "timestamp": 1557545262.413307}参考にしたドキュメント
Vue.jsは日本語版の公式ドキュメントが充実しているのでいろいろ助かります。
axios を利用した API の使用
フィルター — Vue.js残課題
APIキーがソースから丸見えなので、外部にホスティングするときは、Cognitoで認証するとかアクセス制御の仕組みが必要になります。
- 投稿日:2019-05-12T13:18:01+09:00
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>ナビゲーションが追加されているのが分かると思います。
画面の方も見てみます。
前回の環境構築編の時の画面からロゴの上に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.jsimport 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>これでルーティングの実装は終わりです。
画面の方も見てみます。
こんなページになると思います。
実際に動かしてみると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
- 投稿日:2019-05-12T11:10:16+09:00
Vue.js と Bulma でボタンにアクションつけるやつ
今日は、これをやりました
Bulma と Vue.js でボタンのべたなアクションどうやんのかなと
- マウスの Hoover でボタンが反応するやつ
- 入力値をAPIに渡して返ってくるまで待つみたいなやつ
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
Cheers,
























