- 投稿日:2021-07-20T23:24:20+09:00
pythonを使ってリアクティブなWebアプリを作りたい【開発編3-2】
こちらの続きです。 pythonを使ってリアクティブなWebアプリを作りたい【開発編3】 今回は表示系をつくります 最終的なゴールはこちら ToDoの登録画面及び、登録されたToDoの表示部分を作ります。 変更を伴うファイルは以下の通り frontend/src/views/todo/ToDo.vue frontend/src/store/modules/todo.js backend/main.py dist distについてはbulid後のファイルそのままなので、特に自分で作るというものではありません。 ToDo.vueについて frontend/src/views/todo/ToDo.vue frontend/src/views/todo/ToDo.vue mounted() { this.$store .dispatch('fetchTodo', { user_id: this.$store.state.auth.user_id, }) .then(() => { //console.log('getToDos-----') //console.log(getToDos) }) .catch(() => { this.loading = false }) }, 見慣れないmountedという項目が出てきました。 これは、このページがロードされた際に実行するという意味です。 dispatchでfetchTodoを呼び出していますが、画面がロードされた際に、当該ユーザが持っているToDoのレコードを取得しますよという意味です。 user_id: this.$store.state.auth.user_id これも見慣れない表現ですが、auth.jsのstateに入っているuser_idを呼び出しますという意味です。 データの格納先は frontend/src/store/modules/auth.js で定義されており、ログイン時にUserテーブルに格納されているユーザーのIDを登録するようになっています。 画面ロード時はそれくらいですね。 登録部分について handleSubmitFormを呼び出してキーワードを登録しています。 frontend/src/views/todo/ToDo.vue methods: { handleSubmitForm() { console.log('handleSubmitForm') console.log(this.formModel) console.log('todo: ' + this.formModel.todo) this.loading = true if (this.formModel.todo != null) { this.$store .dispatch('addtodo', { todo: this.formModel.todo, user_id: this.$store.state.auth.user_id }) .then(() => { this.loading = false }) .catch(() => { this.loading = false }) } }, addtodoにJSON煮詰めたデータを渡しているというおなじみな形です。 削除部分について これは所見部分があります。 <v-data-table v-model="selected" show-select :headers="todo_headers" :items="$store.state.todo.todos" hide-default-footer class="elevation-0 table-striped" > v-modelにselectedというものが配置されているのですが、これは同ファイルのdataの中で定義されたただの配列の入れ物です。 v-data-tableでチェックを入れたデータがseledtedの中に入ります。あとはそれを使ってdispatchでデータを送信するだけですね。 このような形で、データの入力及び削除ができるようになります。お試しください。 関連リンク pythonを使ってリアクティブなWebアプリをお手軽に1分で作りたい pythonを使ってリアクティブなWebアプリを作りたい【基礎編】 pythonを使ってリアクティブなWebアプリを作りたい【開発編】 pythonを使ってリアクティブなWebアプリを作りたい【開発編2】 pythonを使ってリアクティブなWebアプリを作りたい【開発編3】 pythonを使ってリアクティブなWebアプリを作りたい【開発編3-2】←イマココ pythonを使ってリアクティブなWebアプリを作りたい【開発編4】パスワードの暗号化
- 投稿日:2021-07-20T19:50:22+09:00
Vue.jsでJestを導入する際に詰まったところまとめ
概要 Vue.jsのUT実施のためにJestを採用 設定関連でたくさん詰まったのでエラーと解決方法をまとめる。 前提 package.jsonに以下の設定を行い「npm test」でテストを実行できる状態にする "test": "NODE_ENV=test jest", jest.config.jsを以下のように記載(パスは環境により書き換える) module.exports = { moduleNameMapper: { '^@/(.*)$': '<rootDir>/$1', '^~/(.*)$': '<rootDir>/$1', '^vue$': 'vue/dist/vue.common.js' }, moduleFileExtensions: ['js', 'vue', 'json'], transform: { '^.+\\.js$': 'babel-jest', '.*\\.(vue)$': 'vue-jest' }, collectCoverage: false, collectCoverageFrom: [ '<rootDir>/components/**/*.vue', '<rootDir>/pages/**/*.vue' ] } 以下のコマンドで必要なパッケージをインストールしておく npm i --save-dev vue-jest npm i jest --save npm i @vue/test-utils [エラー①]Requires Babel "^7.0.0-0", but was loaded with "6.xx.x" Requires Babel "^7.0.0-0", but was loaded with "6.xx.x". If you are sure you have a compatible version of @babel/core, it is likely that something in your build process is loading the wrong version. Inspect the stack trace of this error to look for the first entry that doesn't mention "@babel/core" or "babel-core" to see what is calling Babel. npm test実行時に上記のエラーが発生。 上記より以下のコマンドを実行することにより回避 npm install --save-dev "babel-core@^7.0.0-bridge.0" ただし、babel-coreのバージョンを変更することになるので、既存バージョンを考慮した上で変更すること。 [エラー②]Your test suite must contain at least one test. npm test実行時に上記のエラーが発生。 以下のコマンドを実行して、jsdomをインストールすることにより回避 npm install --save-dev jsdom jsdom-global 対象のテストファイルに記載(setupファイルがあればそちらに記載する) require('jsdom-global')() [エラー③]Cannot read property 'child' of undefined jest.config.js直下に以下の設定を追加 testEnvironment: "jsdom", https://github.com/vuejs/vue-test-utils/issues/1192
- 投稿日:2021-07-20T18:48:59+09:00
plunkerでvue その50
概要 plunkerでvueやってみた。 落ち物ゲーム、作ってみた。 参考にしたページ 写真 最初に、ランダムに色を決める。 function getBalls() { var balls = []; let sn = 0; for (let i = 0; i < 5; i++) { for (let j = 0; j < 5; j++) { let ballInfo = { iro: ballClass[Math.floor(Math.random() * ballClass.length)], x: j, y: i, sn: sn, df: 0, } balls.push(ballInfo); sn++; } } return balls }; クリックされたら、隣り合う石を上下左右1ずつ取得し同色か否かを判定 同色の場合、その同色を起点とし再度隣り合う石を1ずつ取得し同色か否かを判定し、消す。 breakCheckRecursive(startingBall, selectedClassName) { const leftBall = this.balls.find(b => b.x === startingBall.x - 1 && b.y === startingBall.y); this.targetDelBall(leftBall, selectedClassName); const rightBall = this.balls.find(b => b.x === startingBall.x + 1 && b.y === startingBall.y); this.targetDelBall(rightBall, selectedClassName); const topBall = this.balls.find(b => b.x === startingBall.x && b.y === startingBall.y - 1); this.targetDelBall(topBall, selectedClassName); const bottomBall = this.balls.find(b => b.x === startingBall.x && b.y === startingBall.y + 1); this.targetDelBall(bottomBall, selectedClassName); }, targetDelBall(delBall, selectedClassName) { if (delBall && delBall.iro === selectedClassName && delBall.df === 0) { let ball = this.balls.find(b => b.x === delBall.x && b.y === delBall.y) ball.df = 1; ball.iro = "item-0"; this.breakCheckRecursive(delBall, selectedClassName); } }, 石が消えた後、空中に浮いている石を下方向へ詰める処理を行わないといけません。 downArea() { var bballs; var j; var k; var b0; var b1; bballs = this.balls.filter(b => b.df === 1); while (bballs.length > 0) { if (bballs[0].y == 0) { k = 0; b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); //b1.sn = -1; b1.iro = "item-0"; b1.df = 0; } if (bballs[0].y == 1) { j = 1; k = 0; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; k = 0; b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); //b1.sn = -1; b1.iro = "item-0"; b1.df = 0; } if (bballs[0].y == 2) { j = 2; k = 1; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; j = 1; k = 0; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; k = 0; b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); //b1.sn = -1; b1.iro = "item-0"; b1.df = 0; } if (bballs[0].y == 3) { j = 3; k = 2; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; j = 2; k = 1; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; j = 1; k = 0; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; k = 0; b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); //b1.sn = -1; b1.iro = "item-0"; b1.df = 0; } if (bballs[0].y == 4) { j = 4; k = 3; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; j = 3; k = 2; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; j = 2; k = 1; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; j = 1; k = 0; b0 = this.balls.find(b => b.x === bballs[0].x && b.y === j); b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); b0.df = b1.df; b0.iro = b1.iro; k = 0; b1 = this.balls.find(b => b.x === bballs[0].x && b.y === k); //b1.sn = -1; b1.iro = "item-0"; b1.df = 0; } bballs = this.balls.filter(b => b.df === 1); } }, 縦列が全て消えた時に列ごと、右に詰めないといけない rightArea() { let xAxis = 4; while (xAxis >= 0) { let xBalls = this.balls.filter(b => b.x === xAxis && b.iro === "item-0"); if (xBalls.length > 4) { //alert("ok") if (xAxis == 1) { this.move(1) this.cls() } if (xAxis == 2) { this.move(2) this.move(1) this.cls() } if (xAxis == 3) { this.move(3) this.move(2) this.move(1) this.cls() } if (xAxis == 4) { this.move(4) this.move(3) this.move(2) this.move(1) this.cls() } } xAxis--; } }, move(l) { var i; for (i = 0; i < 5; i++) { var j = l; var k = l - 1; var b0 = this.balls.find(b => b.x === j && b.y === i); var b1 = this.balls.find(b => b.x === k && b.y === i); b0.df = b1.df; b0.iro = b1.iro; } }, cls() { var i; for (i = 0; i < 5; i++) { var k = 0; var b1 = this.balls.find(b => b.x === k && b.y === i); b1.iro = "item-0"; b1.df = 0; } }, 成果物 以上。
- 投稿日:2021-07-20T17:32:14+09:00
img pdfが開発画面では表示されるのに、デプロイすると表示されないのが解決した
imgやpdfがlocalhostでは表示されるのに、いざデプロイすると、画像が表示されたりされなかったり、 pdfの場合、破損して見れなかったりする問題が発生していました。 ただ、同じコードを書いてるのに、片方は表示され、片方は表示されないみたいな。 ほんとに解決方法が謎でした。 デプロイする時にbuildをしてますが、問題ないし、なんで?!?!?!という状態。 ついに原因を突き止めました!!! 今までは作った画像ファイルを一回フォルダの中に入れて、vsコードを開いて開発していました こんな感じ なので、開発画面で開発している状態のところに追加する様にしました なんとこれで全て解決しました。 ぜんぜんカンケーないでしょって思ってたのですが、これが原因でした、 vue jsのquasar cliを使っているので、開発中に画像を入れた時と、フォルダに入れた状態で開発をするときでちょっとバグが出て他のかもしれません。 開発中に画像を入れた方が、適正な処理をしてくれるっぽいです。 かなり困っていたので解決してよかったです。ほんとに。
- 投稿日:2021-07-20T13:20:20+09:00
input type="date"で昨日以前を選択できないようにする方法
概要 <input type="date>"を用いることで、簡単にカレンダーによる選択を実装できる。 昨日以前を選択できないようにしたかったが、検索しても中々ヒットしなかった。 個人的な備忘録の意味でも、方法と流れをまとめてみた ▼使用スキル HTML、Vue.js 手順① HTML <input type="date" min="2021-07-18"> HTMLにinput type="date"を設置。min='XXXX-XX-XX'を指定することで、'XXXX-XX-XX'がより前の日付に関しては、選択できなくなる。例えば、2021-07-18とすれば、7/17以前の日付は、選択できないようになる。 デモコード 詳しくは公式のリファレンスをご確認ください。 手順➁ ユーザーがサービスを利用する日にちによって、'XXXX-XX-XX'の中身を変更するようにしたい。つまり、動的に管理したい。本記事では、Vue.jsにて実装していく。 HTML <div id="app"> <input type="date" v-bind:min="today"> </div> Vue.js var vue = new Vue ({ el: '#app', data: { today:'' }, created: function(){ this.today= new Date(); } }) ここまでのデモはこちら 解説 this.today= new Date();により、今日の日付を取得している。今日が7/18ならば、minには7/18が入る。しかし、このままでは正常に動作しない。上記のデモは確認してみて欲しい。 検証ツールを確認すると分かりやすいのだが、 この時点では、min=Tue Jul 20 2021 12:56:01 GMT+0900 (日本標準時)とminに入っている形式がおかしい。minが正常に動作するためには、min='XXXX-XX-XX'という形式になっている必要がある。 todayの形式を'XXXX-XX-XX'に変更する記述は以下になる。 手順➂ Vue.js created: function(){ let todaySet = new Date(); let YYYY = todaySet.getFullYear(); let MM = ('00' + (todaySet.getMonth()+1)).slice(-2); let DD = ('00' + todaySet.getDate()).slice(-2); this.today = YYYY + '-' + MM + '-' + DD }, 解説 恐縮ではあるが、getFullYear()等の詳しい解説は割愛させていただく。別途、公式のリファレンスや、他Qiita記事をご覧願いたい。 一点解説しておくと、下記コードの'00'及びslice(-2)がポイントである。下記部分がない場合、例えば今日が7/7だとすると、todayは2021-7-7という形式になる。下記部分があることで、2todayは2021-07-07となる。minが正常に動作するのは、2021-07-07の方だ。 let MM = ('00' + (todaySet.getMonth()+1)).slice(-2); let DD = ('00' + todaySet.getDate()).slice(-2); 完成形はこちらでご確認いただきたい。 https://jsfiddle.net/zxyaukLh/2/ 最後に 分かりにくい点があったら大変申し訳ないが、以上とさせていただく。
- 投稿日:2021-07-20T12:48:56+09:00
nuxtで作成したSPAをs3ホスティングし、cloudfront + lambda@edgeでbasic認証をかけて配信する
タイトルの通り。 nuxt % npx create-nuxt-app app SPAなので以下のあたりの設定になります。 他はお好みで。僕はtypescriptを使います。typescriptしか勝たん。 > ? Rendering mode: Single Page App > ? Deployment target: Static (Static/JAMStack hosting) % cd app % npm run build % npm run generate generateを実行すると、デフォルトだと dist/ とその中にファイル群が生成されます。 s3 適当にバケットを作成でOK アクセスも全てプライベートでOK 静的ウェブサイトホスティングも無効のままでOK dist/ の中身を直下に全てアップします。 cloudfront ディストリビューションの作成 Origin domain: 先ほど作ったs3バケット s3ののアクセス: OAIを新規に作成( Create new OAI )してそれを利用。バケットポリシーのアップデートをする。(以下の画像の状態) Viewer protocol policy: Redirect HTTP to HTTPS Allowed HTTP methods: GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE その他は全てそのままでOK lambda@edge nuxtのルーティング用とbasic認証用で2つ用意する 参考: Lambda@Edge イベント構造 ビューアーリクエスト basic認証用 参考: CloudFront Lambda@Edge with Python 3.8 で Basic 認証を実現する 参考: Amazon CloudFrontとAWS Lambda@EdgeでSPAのBasic認証をやってみる オリジンリクエスト nuxtアプリケーションでのルーティング用 リクエストを受けてパスを書き換えるような処理 参考: nuxt generate + S3 + CloudFront + Lambda Edge で静的サイト構築&ハマりポイントと解決法 / Lambda Edge でオリジンパスのハンドリングを設定する 参考URLをもとに、python3.8に書き換えたものが以下のコードとなっている import re def lambda_handler(event, context): suffix = '/index.html' append_to_dirs = 'index.html' # e.g.) "/some/page" but not "/", "/some/" or "/some.jpg" regex_suffixless = r'^/(?=.+)(?!.+\..+$)(?!.*/$).*$' # e.g. "/some/" or "/some/page/" but not root "/" regex_trailing_slash = r'^.+/$' request = event["Records"][0]["cf"]["request"] uri = request["uri"] if suffix and re.match(regex_suffixless, uri): request["uri"] = uri + suffix return request if append_to_dirs and re.match(regex_trailing_slash, uri): request["uri"] = uri + append_to_dirs return request return request これでSPAにbasic認証をかけて周りに共有することができた。 (今回はIP制限かけられない縛りでこの方法をとってみた)
- 投稿日:2021-07-20T10:23:41+09:00
[Vuetify]プロフ画像と削除アイコン
こうするとどうなるでしょう? 環境 Vue2 Vuetify ソースコード <v-badge offset-x="20" offset-y="20" overlap color="rgba(0,0,0,0)"> <v-btn slot="badge" color="grey white--text" class="delete-button" fab height="20" width="20" @click="onClickDelete" > <v-icon small>fas fa-times</v-icon> </v-btn> <v-avatar size="80" class="mb-3 avatar" @click="onClickImage"> <img src="@/assets/logo.png" alt="avatarImage" class="avatar-image" /> </v-avatar> </v-badge> 実装した結果 解説 今回実装したかったのは、クリックで画像アップロードできるようなプロフィール画像表示と、それを削除するボタンです。 avatar プロフィール画像には v-avatar を使用します。 これはユーザープロフィール画像用のVuetifyコンポーネントです。 <v-avatar size="80" class="mb-3 avatar" @click="onClickImage"> <img src="@/assets/logo.png" alt="avatarImage" class="avatar-image" /> </v-avatar> badge 削除ボタンには v-badge v-btn を使用します。 v-badge はアバターのようなコンテンツに付属する部品として 用意されているコンポーネントです。 ただし、削除ボタンはクリックイベントが必要です。 v-badgeに直接クリックイベントを設けると、 画像をクリックした時のイベントと被ってしまいます。 それを回避するために v-btn を v-badge にスロットさせています。 <v-badge offset-x="20" offset-y="20" overlap color="rgba(0,0,0,0)"> <!-- 削除クリックイベントを発生させるためのボタン --> <v-btn slot="badge" color="grey white--text" class="delete-button" fab height="20" width="20" @click="onClickDelete" > <!-- ×アイコン --> <v-icon small>fas fa-times</v-icon> </v-btn> <!-- 画像描画領域 --> <v-avatar>略</v-avatar> </v-badge> まとめると <v-badge> <v-btn> <v-icon></v-icon> </v-btn> <v-avatar></v-avatar> </v-badge> このように、一見するとヘンテコなDOM構成で プロフィール画像と削除アイコンを実装することができます。 リポジトリ
- 投稿日:2021-07-20T09:35:52+09:00
【Vue.js】フロントエンドでリアクティブな検索機能を実装する方法
「リアクティブな検索機能をvueで実装したい」 「複数の値をdataに格納したけど、できればフロントで検索をかけて処理したい」 とお考えの方にコピペで完結する記事です。 デモはこちら。 はじめに 多階層の配列にアクセスするには、1つのデータ郡それぞれを個別にみてkeyによって判別します。 デモではそれぞれの配列にあるnameというkeyと検索値が正規表現で部分一致な状態で検索することを想定しています。 検索機能を実装 検索にあたって下準備ですが、最初にaxiosなどでapiから取得したデータをプロパティ名が異なる2つに格納しましょう。 今回だとchat_usersとtemp_chat_usersですね。 <script> export default { data(){ return{ chat_users:[ { id:1, name:'たなか美穂', message:'ありがとう...', user_img:'woman2.png', chat_id:10 }, { id:2, name:'みたか宗治', message:'ありがとう!...', user_img:'man.png', chat_id:11 }, { id:3, name:'東なるみ', message:'うーん。。...', user_img:'woman.png', chat_id:12 }, ], temp_chat_users:[ { id:1, name:'たなか美穂', message:'ありがとう...', user_img:'woman2.png', chat_id:10 }, { id:2, name:'みたか宗治', message:'ありがとう!...', user_img:'man.png', chat_id:11 }, { id:3, name:'東なるみ', message:'うーん。。...', user_img:'woman.png', chat_id:12 }, ], chat_id:'', search_list:[], user_sidebar_height: 500, search_user:'' } }, . . . </script> 検索ボックスにはv-modelと@input="searchUser"をつけて検索値を監視します。 <div id="search_user_box" class="mb-1 p-2 bg-white flex"> <p>名前検索</p> <input v-model="search_user" @input="searchUser" class="p-1 w-10/12 border border-gray-300 focus:border-2 focus:outline-none focus:border-blue-500" type="text" > </div> 検索値を監視し、データにあるkeyの値と照合をかけます。 <script> export default { . . . methods:{ searchUser(){ if(this.search_user){ var serach_text = new RegExp(this.search_user + '(.*?)', 'g'); const callback = (user) => user.name.match(serach_text); var list = JSON.parse(JSON.stringify(this.temp_chat_users)).filter( user => callback(user)); this.chat_users = list; } else { this.chat_users = this.temp_chat_users; } }, }, } </script> デフォルトで表示しているのはchat_usersにあるデータですが、検索をかけたタイミングでtemp_chat_usersからデータを参照してmatchしたもののみvar listに入れてthis.chat_users = list;とすることで置き換えができます。こうすることでデフォルトで表示したデータを失うことなく、裏にあるデータで常に置き換えるような仕組みがつくれます。 また、var serach_text = new RegExp(this.search_user + '(.*?)', 'g');では正規表現でマッチするもの全てとしています。const callback = (user) => user.name.match(serach_text);でmatchする条件をserach_textとして処理を行い、filter()で配列にあるデータを個別に参照してcallbackで照合をかけます。 こうすることで、 filter()で個別に参照->callbackでmatch処理->正規表現で該当する全てを返す、 という流れをフロントで構築できます。 あとは値をthis.chat_usersに置き換えることでマッチしたもののみが表示されます。 今回は割愛していますが、 返り値がnull、lengthで0の場合「データがありませんでした。」を返す処理もif分岐でできますね。 検索値がない場合はデフォルトの状態が望ましいので、elseでthis.chat_users = this.temp_chat_users;としています。 まとめ 今回は検索処理を実装しました。 プロパティのもつ値をフロントでかぎりなく処理できるなら有用で夢が広がりますよね。 次回は他コンポーネントと連携して親->子、子→親関係を全体的に反映できる方法を紹介したいと思います。
- 投稿日:2021-07-20T09:35:52+09:00
【Vue.js】リアクティブな検索機能を実装する方法
「リアクティブな検索機能をvueで実装したい」 「複数の値をdataに格納したけど、できればフロントで検索をかけて処理したい」 とお考えの方にコピペで完結する記事です。 デモはこちら。 はじめに 多階層の配列にアクセスするには、1つのデータ郡それぞれを個別にみてkeyによって判別します。 デモではそれぞれの配列にあるnameというkeyと検索値が正規表現で部分一致な状態で検索することを想定しています。 検索機能を実装 検索にあたって下準備ですが、最初にaxiosなどでapiから取得したデータをプロパティ名が異なる2つに格納しましょう。 今回だとchat_usersとtemp_chat_usersですね。 <script> export default { data(){ return{ chat_users:[ { id:1, name:'たなか美穂', message:'ありがとう...', user_img:'woman2.png', chat_id:10 }, { id:2, name:'みたか宗治', message:'ありがとう!...', user_img:'man.png', chat_id:11 }, { id:3, name:'東なるみ', message:'うーん。。...', user_img:'woman.png', chat_id:12 }, ], temp_chat_users:[ { id:1, name:'たなか美穂', message:'ありがとう...', user_img:'woman2.png', chat_id:10 }, { id:2, name:'みたか宗治', message:'ありがとう!...', user_img:'man.png', chat_id:11 }, { id:3, name:'東なるみ', message:'うーん。。...', user_img:'woman.png', chat_id:12 }, ], chat_id:'', search_list:[], user_sidebar_height: 500, search_user:'' } }, . . . </script> 検索ボックスにはv-modelと@input="searchUser"をつけて検索値を監視します。 <div id="search_user_box" class="mb-1 p-2 bg-white flex"> <p>名前検索</p> <input v-model="search_user" @input="searchUser" class="p-1 w-10/12 border border-gray-300 focus:border-2 focus:outline-none focus:border-blue-500" type="text" > </div> 検索値を監視し、データにあるkeyの値と照合をかけます。 <script> export default { . . . methods:{ searchUser(){ if(this.search_user){ var serach_text = new RegExp(this.search_user + '(.*?)', 'g'); const callback = (user) => user.name.match(serach_text); var list = JSON.parse(JSON.stringify(this.temp_chat_users)).filter( user => callback(user)); this.chat_users = list; } else { this.chat_users = this.temp_chat_users; } }, }, } </script> デフォルトで表示しているのはchat_usersにあるデータですが、検索をかけたタイミングでtemp_chat_usersからデータを参照してmatchしたもののみvar listに入れてthis.chat_users = list;とすることで置き換えができます。こうすることでデフォルトで表示したデータを失うことなく、裏にあるデータで常に置き換えるような仕組みがつくれます。 また、var serach_text = new RegExp(this.search_user + '(.*?)', 'g');では正規表現でマッチするもの全てとしています。const callback = (user) => user.name.match(serach_text);でmatchする条件をserach_textとして処理を行い、filter()で配列にあるデータを個別に参照してcallbackで照合をかけます。 こうすることで、 filter()で個別に参照->callbackでmatch処理->正規表現で該当する全てを返す、 という流れをフロントで構築できます。 あとは値をthis.chat_usersに置き換えることでマッチしたもののみが表示されます。 今回は割愛していますが、 返り値がnull、lengthで0の場合「データがありませんでした。」を返す処理もif分岐でできますね。 検索値がない場合はデフォルトの状態が望ましいので、elseでthis.chat_users = this.temp_chat_users;としています。 まとめ 今回は検索処理を実装しました。 プロパティのもつ値をフロントでかぎりなく処理できるなら有用で夢が広がりますよね。 次回は他コンポーネントと連携して親->子、子→親関係を全体的に反映できる方法を紹介したいと思います。
- 投稿日:2021-07-20T00:23:37+09:00
Astroとは何者なのか。
「Astro」について触れてみる Astroについての記載が全くなかったので、自分なりに公式のページを見ての内容をまとめる。 今後も何か触れる機会があれば記述していきたい。 詳細については公式ページへ ※あくまでもベータ版とのこと Astroとは ●特徴 Javascriptのフレームワークを組み合わせることやHTML+Javascriptを組み合わせることができる ビルドしたページはhtmlに変換されるためJavascriptを使用しない オンデマンドコンポーネントのため、Javascriptが必要なページについてもアクセス時に読み込まれる TypescriptやCSS,CSS Modules,Sassなどのnpmパッケージをサポートしている SEO対応によりシンジケーションの手間が省ける ●個人的にいいなぁと思った点 ReactやVueにも対応しているため、フレームワークからフレームワークへの技術移行が容易になる ポートフォリオを作成する際にReactもVueもかけますというアピールが一つのプロジェクトで行える HTMLのみになるため初期読み込みが高速になるよう(どれぐらい早いのかは不明) 様々なJavascriptに対応しているとプロジェクトへの入り口が広くなる コンポーネントは技術上位者が作成し、初心者はHTMLのみでページを作成するなど役割分担がしやすい snowpackを使用したモジュールバンドル 実際に触ってみる ●動作環境 Macbook:Big Sur version11.4 Node.js:v14.17.0 npm:6.14.13 ●構築 まずは公式ページに記載されている通りに実行する 今回は「qiitaAstro」のプロジェクト名で進めていく。 # create your project >> mkdir SampleAstro >> cd SampleAstro >> npm init astro npx: installed 11 in 2.21s Welcome to Astro! (create-astro v0.5.0) If you encounter a problem, visit https://github.com/snowpackjs/astro/issues to search or file a new issue. > Prepare for liftoff. > Gathering mission details... ? Which app template would you like to use? › - Use arrow-keys. Return to submit. ❯ Starter Kit (Generic) Blog Documentation Portfolio [Starter Kit]を選択していく ? Which frameworks would you like to use? › Instructions: ↑/↓: Highlight option ←/→/[space]: Toggle selection a: Toggle all enter/return: Complete answer ◉ Preact ◉ React ◉ Svelte ◉ Vue 使うフレームワークは全てを選択するため、全て選択してEnterを押下 ✔ Which frameworks would you like to use? › Preact, React, Svelte, Vue > Copying project files... ✔ Done! Next steps: 1: npm install (or pnpm install, yarn, etc) 2: git init && git add -A && git commit -m "Initial commit" (optional step) 3: npm start (or pnpm, yarn, etc) To close the dev server, hit Ctrl-C Stuck? Visit us at https://astro.build/chat 必要なファイルが用意されたら[Next steps]に記載されている通りにコマンドを実行するのもよし。 今回はドキュメント通りに以下のコマンドを実行していく。 # install your dependencies >> npm install # start the dev server and open your browser >> npm start ・・・(省略)・・・ [HH:MM:SS] [snowpack] Ready! [dev server] Server started in 16197ms. [dev server] Local: http://127.0.0.1:3000/ [HH:MM:SS] [snowpack] watching for file changes... 上記の指示通りにhttp://localhost:3000を開くと実行画面が表示される。 ●フォルダ構成 上記で構築した場合のフォルダ構成になります。 ├── node_modules ├── public │ ├── assets │ │ └── logo.svg │ ├── favicon.svg │ ├── robots.txt │ └── style │ ├── global.css │ └── home.css ├── src │ ├── components │ │ ├── PreactCounter.jsx │ │ ├── ReactCounter.jsx │ │ ├── SvelteCounter.svelte │ │ ├── Tour.astro │ │ └── VueCounter.vue │ └── pages │ └── index.astro ├── .gitignore ├── .npmrc ├── astro.config.mjs ├── package-lock.json ├── package.json └── README.md ●各コンポーネント プロジェクトの準備時点で各フレームワークでのコンポーネントでのカウントアップが作成されているので見ていく components/PreactCounter.jsx import { h } from 'preact'; import { useState } from 'preact/hooks'; export default function PreactCounter({ children }) { const [count, setCount] = useState(0); const add = () => setCount((i) => i + 1); const subtract = () => setCount((i) => i - 1); return ( <div id="preact" class="counter"> <button onClick={subtract}>-</button> <pre>{count}</pre> <button onClick={add}>+</button> </div> ); } components/ReactCounter.jsx import React, { useState } from 'react'; export default function ReactCounter({ children }) { const [count, setCount] = useState(0); const add = () => setCount((i) => i + 1); const subtract = () => setCount((i) => i - 1); return ( <div id="react" className="counter"> <button onClick={subtract}>-</button> <pre>{count}</pre> <button onClick={add}>+</button> </div> ); } components/SvelteCounter.svelte <script> let count = 0; function add() { count += 1; } function subtract() { count -= 1; } </script> <div id="svelte" class="counter"> <button on:click={subtract}>-</button> <pre>{ count }</pre> <button on:click={add}>+</button> </div> vue/components/VueCounter.vue <template> <div id="vue" class="counter"> <button @click="subtract()">-</button> <pre>{{ count }}</pre> <button @click="add()">+</button> </div> </template> <script> import { ref } from 'vue'; export default { setup() { const count = ref(0) const add = () => count.value = count.value + 1; const subtract = () => count.value = count.value - 1; return { count, add, subtract } } } </script> components/Tour.astro --- import { Markdown } from 'astro/components'; --- <article> <div class="banner"> <p><strong>?? Seasoned astronaut?</strong> Delete this file. Have fun!</p> </div> <section> <Markdown> ## ? Project Structure Inside of your Astro project, you'll see the following folders and files: ``` / ├── public/ │ ├── robots.txt │ └── favicon.ico ├── src/ │ ├── components/ │ │ └── Tour.astro │ └── pages/ │ └── index.astro └── package.json ``` Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. Any static assets, like images, can be placed in the `public/` directory. </Markdown> </section> <section> <h2>? Want to learn more?</h2> <p>Feel free to check <a href="https://github.com/snowpackjs/astro">our documentation</a> or jump into our <a href="https://astro.build/chat">Discord server</a>.</p> </section> </article> <style> article { padding-top: 2em; line-height: 1.5; } section { margin-top: 2em; display: flex; flex-direction: column; gap: 1em; max-width: 70ch; } .banner { text-align: center; font-size: 1.2rem; background: var(--color-light); padding: 1em 1.5em; padding-left: 0.75em; border-radius: 4px; } pre, code { font-family: var(--font-mono); background: var(--color-light); border-radius: 4px; } pre { padding: 1em 1.5em; } .tree { line-height: 1.2; } code:not(.tree) { padding: 0.125em; margin: 0 -0.125em; } </style> ●コンポーネントを使用したページについて 以下のファイルにてそれぞれのコンポーネントをインポートしてページを作成している pages/index.astro --- // Component Imports import Tour from '../components/Tour.astro'; // You can import components from any supported Framework here! import PreactCounter from '../components/PreactCounter.jsx'; import ReactCounter from '../components/ReactCounter.jsx'; import SvelteCounter from '../components/SvelteCounter.svelte'; import VueCounter from '../components/VueCounter.vue'; // Component Script: // You can write any JavaScript/TypeScript that you'd like here. // It will run during the build, but never in the browser. // All variables are available to use in the HTML template below. let title = 'My Astro Site'; // Full Astro Component Syntax: // https://docs.astro.build/core-concepts/astro-components/ --- <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{title}</title> <link rel="icon" type="image/svg+xml" href="/favicon.svg"> <link rel="stylesheet" href="/style/global.css"> <link rel="stylesheet" href="/style/home.css"> <style> header { display: flex; flex-direction: column; gap: 1em; max-width: min(100%, 68ch); } </style> </head> <body> <main> <header> <div> <img width="60" height="80" src="/assets/logo.svg" alt="Astro logo"> <h1>Welcome to <a href="https://astro.build/">Astro</a></h1> </div> </header> <Tour /> <!-- - You can also use imported framework components directly in your markup! - - Note: by default, these components are NOT interactive on the client. - The `:visible` directive tells Astro to make it interactive. - - See https://docs.astro.build/core-concepts/component-hydration/ --> <PreactCounter client:visible /> <ReactCounter client:visible /> <SvelteCounter client:visible /> <VueCounter client:visible /> </main> </body> </html> ●レンダーする設定 以下のファイルにてAstroプロジェクトについての設定を保持している 下記の[renderes]に記載されているフレームワークについてレンダリングされるようです。 astro.config.mjs export default { // projectRoot: '.', // Where to resolve all URLs relative to. Useful if you have a monorepo project. // pages: './src/pages', // Path to Astro components, pages, and data // dist: './dist', // When running `astro build`, path to final static output // public: './public', // A folder of static files Astro will copy to the root. Useful for favicons, images, and other files that don’t need processing. buildOptions: { // site: 'http://example.com', // Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs. sitemap: true, // Generate sitemap (set to "false" to disable) }, devOptions: { // port: 3000, // The port to run the dev server on. // tailwindConfig: '', // Path to tailwind.config.js if used, e.g. './tailwind.config.js' }, renderers: [ "@astrojs/renderer-preact", "@astrojs/renderer-react", "@astrojs/renderer-svelte", "@astrojs/renderer-vue" ], }; 感想について 個人開発ではとても良いかと思いました。 ポートフォリオを作る上でもReactが人気ですが、Vueの方がとっつきやすさはあると思うので、最初のうちはVueでいくつか作った後にReactにも挑戦をしたポートフォリオを作成することもできますし、snowpackの恩恵で開発時の待ち時間も減らせることでしょう。 まだまだ、ベータ版であり正式リリースについてはまだ先のようですが、これからの個人開発にはastroをしばらく使用しながら腕を磨いていきたいと思います。 そろそろ何かサービスを作って運用してみたいとも思っていますので、astroで作ったプロジェクトでもチャレンジしてみたいと思います。