- 投稿日:2021-12-03T23:34:44+09:00
Blazor with Zeu.js
Zeu.jsは様々なデータをビジュアル化し、アニメーションで表示するJavaScriptライブラリです。 今回はBlazor WebAssemblyアプリで、Zeu.jsを動かしてみたいと思います Zeu.js 今回はバリバリのJavaScriptライブラリを使って、Blazorアプリケーションを作っていきたいと思います Demo サンプルコード canvasの初期化 Zeu.jsはcanvasを使ってますので、それぞれのコントロールを初期化する処理をC#側から呼び出します。 Index.razor <div class="col-lg-8"> <div class="row"> <div class="col-lg-8"> <h1>MY COMMAND CENTER</h1> <div><a href="https://github.com/shzlw/zeu" style="font-size: 18px;">by Zeu.js</a></div> <div style="font-size: 22px; font-weight: bold;">US-EAST-01</div> </div> <div class="col-lg-4"> <h2>TIME</h2> <canvas id="digital-clock" width="200" height="50"></canvas> </div> </div> <canvas id="heartbeat" width="730" height="150"></canvas> ?ここに配置 </div> index.html <script> window.Init = (_) => { /* Heartbeat */ heartbeat = new zeu.Heartbeat('heartbeat', { viewWidth: 730, speed: 2, fontColor: COLOR.lightGrey, maxQueueCapacity: 50 }); heartbeat.scaleByHeight(150); // 中略 </script> canvasの初期化は OnAfterRenderの初回レンダリング時に行うようにします。 Index.razor @code{ protected override void OnAfterRender(bool firstRender) { if (firstRender) { JSRuntime.InvokeVoidAsync("Init", new object[] { }); } } } コントロールへ値のセット コントロールの初期化が行えればあとは簡単です。 injectされたJavaScriptの呼び出し機能を使用して、scriptのファンクションにセットしたい値を渡すだけです。 Index.raozr <script> window.valueChangeHertbeat = (p1, p2) => { heartbeat.beat({ color: p1, space: p2 }); } </script> Index.razor @code{ void OnClick() { var r = new Random(); var ret = @"#" + System.Drawing.Color.FromArgb(r.Next(256), r.Next(256), r.Next(256)).Name.Substring(2); // Heartbeat JSRuntime.InvokeVoidAsync("valueChangeHertbeat", new object[] {ret,r.Next(0,10) }); } } JavaScriptライブラリを使う BlazorはJavaScriptを使わずにC#だけでWebアプリケーションをコーディングできるのが大きな特徴でありますが、逆にJavaScriptとの親和性の高さも大きな特徴です。 UIに関してのユーザービリティやビジュアルなギミックは、純粋にBlazorだけで表現するのはまだまだ難しいですね いつか、すべてをC#でコーディングできる日がやってくるのでしょうか
- 投稿日:2021-12-03T23:28:18+09:00
class構文って何?どう使うの?
「class構文」って普段jQueryとWordPressだけ使ってたら絶対に見ない。 知らずに遭遇したら「class構文アレルギー」になっちゃうので、免疫を付けたい。 なので、いろんな記事を漁りました。JSとPHPで、理解しやすい方から学ぶスタイル。 一番わかり易い例文は、コレだと思いました。JSです。 <body> <script> class Person { constructor(名前) { this.なまえ = 名前; } say() { console.log("こんにちは。" + this.なまえ + "です"); } } var nishi = new Person("西 大輔"); nishi.say(); var pikkoro = new Person("ピッコロ大魔王") pikkoro.say(); </script> </body> 実行すると、コンソールに表示される文字は こんにちは。西 大輔です こんにちは。ピッコロ大魔王です javascriptで、昔ながらの書き方だと function Person(名前) { this.setName(名前); }; Person.prototype = { setName: function(名前) { this._なまえ = 名前; }, getName: function() { return this._なまえ; }, say: function() { console.log('こんにちは。' + this._なまえ + 'です'); } }; var nishi ="西 大輔"; Person.say(nishi); var pikkoro = ("ピッコロ大魔王") Person.say(pikkoro); 急に「体になじむ感じ」がしてきました。(newしてないから微妙にちがうかも?) つまりJavascriptの function() の下に prototype を書くのと一緒だということ。 インスタンス(分身)を生成するから、引数を保持したままで、 いろんな呼び出し方でくり返し使えるよっていう、シンプルにそれだけだと思う。 「コンストラクタ」とは「最初に実行されるやつ」です。 「一番上に書く」のと何が違うのか?多分一番上に書くより先に実行されるということでしょう。 以上です。
- 投稿日:2021-12-03T22:25:11+09:00
Slackに追加されたカスタム絵文字を通知するBOTを作った
今年もAdvent Calendarに参加します! この記事はニフティグループ Advent Calendar 2021の4日目です。 社会人3年目。このご時世のせいか、今年が一番早く時間過ぎた気がします。 昨日は @yasu_m_0123 さんのITエンジニアがMBAを取るとどうなるのかという話でした。 私は社会人大学院という存在をあまり意識していなかったので、どんなものなのか、どんな雰囲気なのか知ることができました。 Slackの絵文字は好きですか? 私は好きでよく使っています。 手軽にコミュニケーションが取れるのがいいですよね。 特にカスタム絵文字が好きです。汎用性の高いものから、面白いものまでいろいろありますよね。 ちなみに現時点(2021/12/03)での、弊社のカスタム絵文字の数は 約2500個以上 ありました。 多いのかな、少ないのかな 新しい絵文字が追加されるのを楽しみにしていますが、 さすがにこの絵文字数になると、せっかくいい絵文字が追加されても気づけないんですよね。 ということで、新しく追加されたカスタム絵文字を通知するBOTをGASで作りました。 追加されたカスタム絵文字を通知するBOT こんな感じです (やばい絵文字映ってないよな) 多い日だと、にぎやかになります。 当然1個のときも。 追加されてない日はランダムで1個通知してます。 構成 GASで実行します。 SlackAPIでカスタム絵文字を全て取得して、Google Driveに保存し、 前日とのファイルの差分をみてSlackに通知します。 GASのトリガーで毎朝実行してます。出社時の楽しみです。 今回使うSlackAPIはカスタム絵文字の一覧を取得するという、マニアックなAPIです。 (社内で他に使ってる人いるのかな) 申請やスコープの設定などが必要です。 https://api.slack.com/methods/emoji.list ソース 4箇所TODOがあるので、各自の値に書き換えてください。 フォルダーIDは絵文字一覧ファイルを保存するGoogle Drive上のフォルダーで、 https://drive.google.com/drive/folders/ここの値 を入れてください。 通知用のWebhookURLとSlackAPIのTokenは各自で用意してください。 function myFunction() { // ①古い絵文字old_stamp.jsonを削除 // 保存フォルダID設定 const folder_id = "";//TODO①絵文字一覧を保存するフォルダーID const file_name = 'emoji.json' const old_file_name = 'old_emoji.json' removeFile_(old_file_name, folder_id) // ②emoji.jsonをold_emoji.jsonにファイル名を変更 const folder = DriveApp.getFolderById(folder_id); const files = folder.getFiles(); while (files.hasNext()) { const file = files.next(); const name = file.getName().replace('emoji', 'old_emoji'); file.setName(name); } // ③新しい絵文字一覧emoji.jsonを取得 exportJson(file_name, folder_id) // ④差分確認 const emoji_json =getDataFromMyTextFile(file_name, folder_id) const emoji_list = Object.keys(emoji_json.emoji) const old_emoji_json = getOldDataFromMyTextFile(old_file_name, folder_id) const old_emoji_list = Object.keys(old_emoji_json.emoji) // 差分を出す let result = emoji_list.filter(itemA => // 配列Bに存在しない要素が返る old_emoji_list.indexOf(itemA) == -1 ); let message = '' let username = '' let icon = '' // 差分を通知、なければランダムに1個通知 if (result.length) { username = '新しい仲間が加わったよ' icon = ':hatching_chick:'; for (const emoji of result) { message += ':' + emoji + ':'; } }else { username = '今日の絵文字'; icon = ':smile:'; const emoji = emoji_list[Math.floor(Math.random() * emoji_list.length)] message = ':' + emoji +':'; } // ⑤送信 const postUrl = '';//TODO②Slack通知用URL const channel = '';//TODO③通知先チャンネル const jsonData = { "username" : username, "icon_emoji": icon, "channel": channel, "text" : message }; const payload = JSON.stringify(jsonData); const options = { "method" : "post", "contentType" : "application/json", "payload" : payload }; // 通知 UrlFetchApp.fetch(postUrl, options); } // 新しいjsonファイル function getDataFromMyTextFile(file_name, folder_id) { const contents = DriveApp.getFolderById(folder_id) .getFilesByName(file_name) .next() .getBlob() .getDataAsString("utf-8") .replace(/\r?\n/g, ''); //改行削除 return JSON.parse(contents); } // 古いjsonファイル function getOldDataFromMyTextFile(old_file_name,folder_id) { const contents = DriveApp.getFolderById(folder_id) .getFilesByName(old_file_name) .next() .getBlob() .getDataAsString("utf-8") .replace(/\r?\n/g, ''); //改行削除 return JSON.parse(contents); } // 既存ファイルを削除 function removeFile_(file_name, folder_id) { const folder = DriveApp.getFolderById(folder_id) //DriveAppクラスからファイル名でファイル(ファイル名一致した分)を取得する const fileData = folder.getFilesByName(file_name); //next()でファイルを取得し、ゴミ箱のフラグをtrueにする const getData = fileData.next().setTrashed(true); } // emoji.json取得 // JSONファイルをエクスポート function exportJson(file_name, folder_id) { // Slack APIで絵文字取得 const url = 'https://slack.com/api/emoji.list?token=hogehoge'//TODO④SlackAPIのURL const options = {"method" : "GET"} const response = UrlFetchApp.fetch(url, options); // ファイル作成 makeFile_(response, file_name, folder_id) } // ファイル作成 function makeFile_(jsonData, file_name, folder_id) { const contentType = "text/plain"; const charSet = "UTF-8"; //blobに変換して、データをsetする。 const blob = Utilities.newBlob("", contentType, file_name).setDataFromString(jsonData, charSet); DriveApp.getFolderById(folder_id).createFile(blob) } まとめ GASとGoogle Driveでお手軽にBOT作れました。 面白い絵文字があると、話のネタにもなるので気になる方はBOT作ってみてください。 絵文字最高 明日は @mh326 さんの記事です。楽しみですね。
- 投稿日:2021-12-03T21:41:38+09:00
スプレッドシートのそれ、Slackに通知させようぜ!
ある日のパイセン「あれ、トニーって今日休みかい?」 え、ゴシップ好きのトニーが今日休みかって? いや確かに今日は雑談のチャンネルに出没してないし 休みなのかもしれないな そう思ったけど、 そのときは総務のキャサリンが教えてくれて解決したんだ 「トニー?ああ、休みよ休み、ずっと休みだったらいいのにね」 おいおい、トニー、嫌われてるな。 さては最近、キャサリンについて何かゴシップをでっち上げたな? まったく困ったやつだよ キャサリン「これ、どうにかSlackに通知とかできないわけ?」 ウチはまだ小さい会社だから、有給申請はGoogleフォームでやっていて 集計されるスプレッドシートは総務しか見られないんだ (どうやら申請系が全部まとまったシートらしい) だから休む人は別にカレンダーに登録しておかないといけないし 今回みたいに、「あれ?今日休みだっけ?」みたいなことが起きるんだ! キャサリンが半ギレになるのも無理はないさ。 トニーにあることないこと、でっち上げられたあげく トニーの有給申請の尻拭いをさせられてるんだからね! ああ、愛しのキャサリン 任せておけ! 僕がBotを作って解決してやろう! GAS、JS書けたらピースオブケーク! 知っている人も使ったことある人も多いだろうGoogle Apps Script そしてSlackのIncoming webhookを使ってBotをつくり、 キャサリンに捧げるんだ! Google Apps Script 通称、GASは Javascriptを使えるFaaS(Function as a Service)だ 何言っとんじゃ、という人は、ぜひ調べてみてくれ! 簡単に言えば、サーバーを用意しなくてもプログラムを実行できて いろんな便利なメソッドも用意してくれているワークスペースってことさ! SlackのIncoming webhookは、 他のアプリからSlackにメッセージを送るためのものだ Botが作れると思っておけばいいだろう! ああ、それから… すまないが、これらの基本的な使い方は、この記事では書かないんだ! なぜかって? 書いている時間がないんだ キャサリンとのディナーがあるからさ! キャサリンを落とす、Slack Botができるまで キャサリンにもらった、スプレッドシートの、有給申請のシートはこんな形式になっていた 整理してみよう! 実現したいこと 今日、有給で休みの人の名前を、朝9時から10時くらいにSlackの挨拶チャンネルに通知する 必要なこと 1. 申請一覧のスプレッドシートから有給申請シートを取得する 2. 有給申請シートにある、すべてのデータの開始と終了を取得する 3. 2のそれぞれのデータに対して、今日が有給かどうかを判定する 4. 3で有給だったら、そのデータの氏名を取得して、保持する 5. 全部のデータについて2〜4が終わったら、Slack用のメッセージに整形して送信する こんな感じだろう! そして、忘れちゃいけない 6. そもそも休日や祝日だったらBotに通知させない これは大事だ、休日に通知がくるとキャサリンが怒ってしまうからね! 必要なこと、を順に実行していくメソッドはこんな感じになった! function notifyHolidayStaffToSlack() { const currentDate = new Date(); if(isHoliday(currentDate)) return; // 休日や祝日だったらBotに通知させない const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(), sheet = spreadsheet.getSheetByName('有給申請'), dataCnt = sheet.getLastRow(), nameCell = 2, // 氏名のセル fromCell = 3, // 開始のセル toCell = 4; // 終了のセル let absenceMembers = [] for(let i = 2; i <= dataCnt; i++){ const holidayFrom = new Date(sheet.getRange(i, fromCell).getValue()), holidayTo = new Date(sheet.getRange(i, toCell).getValue()), is_absence = isAbsenceToday(currentDate, holidayFrom, holidayTo) // 今日が有給かどうかの判定 if(is_absence) absenceMembers.push(sheet.getRange(i, nameCell).getValue()) // 有給だったら、そのデータの氏名を取得して保持 } const message = formatMessage(absenceMembers, currentDate) // Slackへのメッセージに整形 notifyToSlack(message) // Slackに通知 } メッセージの整形は、キャサリンが喜んでくれるように 少し明るくしておいたんだ! function formatMessage(items) { let message = 'おはようございます? 本日お休みのメンバーはいません!\n今日も1日よろしくお願いします?'; if(items.length == 0) return message; message = 'おはようございます? 本日お休みのメンバーは\n'; items.forEach(item => { message = `${message}${item}さん\n` }) message = `${message}です! 今日も1日よろしくお願いします?` return message } 一応、slackへの通知のメソッドも載せておこう シンプルで、普通にPOSTすればようさそうだったよ! function notifyToSlack(body) { const endpoint = 'Incoming webhookで取得したURL', data = { 'channel' : '#チャンネル名', 'username' : '本日のお休みメンバーBot', 'attachments': [{ 'text' : body, }] }; const payload = JSON.stringify(data), options = { 'method' : 'POST', 'contentType' : 'application/json', 'payload' : payload }; UrlFetchApp.fetch(endpoint, options); } その後 ここまで読んでくれてありがとう! Botは、こんな感じで送られてくるようになった! (これはテストの時の画像だから、日付を変えて2回送っているが、運用後はちゃんと毎日定時に教えてくれている!) え?なに? その後って、Botじゃなくてキャサリンとどうなったんだって? まったく、キミも知りたがりだな! Botが大成功したお陰で、キャサリンにディナーに誘われたんだ! なんでも、お礼がしたいんだってさ! これからどうなるか? 急遽、明日に有給申請いれるかもしれないから Botが流れてきたら、察してくれよな! Follow ME !!! I'm sure to follow you back! twitter: @marty_ojiya
- 投稿日:2021-12-03T20:33:50+09:00
【Vue.js】フォームバリデーションで使うgetter/setter
Tips1 公式リファレンス 入力文字列が20文字以上だった場合にリアルタイムでerrorの表示をさせたい。 算出プロパティのget/setを使用して入力を監視する。 vue <script> let app = new Vue({ el:'#app', data(){ return { contact:{ yourName:'', //input要素を定義する。 }, hasError:{ nameError: false //error判定用の初期値 } } }, computed:{ yourName:{ get(){ //yourNameで入力された取得。 return this.contact.yourName }, set(value){ //yourNameで入力された値を引数で受け取る。 if(value.length <= 20){ this.hasError.nameError = false //20文字以内だったらfalseをセット } else { this.hasError.nameError = true //20文字以上だったらtrueをセット } return this.contact.yourName = value //valueをyourNameに帰す } } } }) </script> html CSS:20文字以上だった場合のerrorクラスにcssをあてる。 文字数カウント:20文字以上だった場合に、errorクラスを付与させる。 v-show:20文字以上の時にメッセージを表示 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .error{ //20文字以上だった場合のerrorクラスにcssをあてる。 color:red; } </style> </head> <body> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> <div id="app"> <form> <label for="yourName">氏名</label> <input id="yourName" type="text" v-model="yourName"><br> <!-- computedで監視している値 --> <p :class="{error: hasError.nameError}">{{yourName.length}}/20</p> //20文字以上だった場合に、errorクラスを付与させる。 <p v-show="hasError.nameError" class="error">氏名は20文字以内</p> //v-show:20文字以上の時にメッセージを表示 {{contact.yourName}} </form> </div> 全体 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .error{ color:red; } </style> </head> <body> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> <div id="app"> <form> <label for="yourName">氏名</label> <input id="yourName" type="text" v-model="yourName"><br> <!-- computedで監視している値 --> <p :class="{error: hasError.nameError}">{{yourName.length}}/20</p> <p v-show="hasError.nameError" class="error">氏名は20文字以内</p> {{contact.yourName}} </form> </div> <script> let app = new Vue({ el:'#app', data(){ return { contact:{ yourName:'', }, hasError:{ nameError: false //error判定用の初期値 } } }, computed:{ yourName:{ get(){ return this.contact.yourName }, set(value){ //yourNameで入力された値を引数で受け取る。 if(value.length <= 20){ this.hasError.nameError = false } else { this.hasError.nameError = true } return this.contact.yourName = value //valueをyourNameに帰す } } } }) </script> </body> </html>
- 投稿日:2021-12-03T19:52:39+09:00
【p5.js】p5.Geometryであのドーナツをつくる
この記事は、Processing Advent Calendar 2021 3日目の記事です。 みんな大好きなあのドーナツを、p5.jsのクラスp5.Geometryを活用して作っていきます。 (この記事を書いた際に使ったp5.jsのバージョンは、1.4.0です) ソースコードは下のOpenProcessingのリンクから見ることができます。 0. はじめに 0.1 p5.Geometryって何? WebGLでは3Dモデルを描画する際に、あらかじめ作成した各頂点の位置ベクトルや法線ベクトルなどの頂点データが入った頂点バッファと、どの頂点3つを用いた3角形を描くかの情報(インデックス配列)が入ったインデックスバッファを呼び出すことで同じ形状のモデルを効率的に描画しています。 例えば、以下の頂点の位置情報が入った頂点バッファと、[ 0, 1, 2 ]の頂点3つと[ 1, 3, 2 ]の頂点3つの頂点番号の配列(インデックス配列)の入ったインデックスバッファを作成し、それを呼び出すことで、三角形2つが組み合わさって四角形を描画することができます。 頂点番号 x座標 y座標 z座標 0 0.0 0.0 0.0 1 1.0 0.0 0.0 2 0.0 1.0 0.0 3 1.0 1.0 0.0 p5.Geometryはp5.jsライブラリで定義されているクラスであり、この頂点バッファとインデックスバッファのもととなる頂点データやインデックス配列をクラス変数としてまとめて定義することができます。 reference : https://p5js.org/reference/#/p5.Geometry 0.2 p5.jsで立体を描画する関数 box()やsphere()などの立体を描画する関数の処理では、p5.Geometryの作成とそれをもとに作成されたバッファの呼び出しが行われています。p5.jsライブラリの立体描画のソースコードは、以下のリンクから見ることができます。 1. ドーナツ関数を書く 1.1 下準備 まずは、ドーナツ以外の背景や光の設定などを行いましょう。 作成するドーナツの代わりとして、トーラスを描画します。 sketch.js function setup() { // WebGLモードにする createCanvas(800, 600, WEBGL); } function draw() { // 背景・光の設定 background(250, 200, 200); directionalLight(255, 255, 255, 0.5, 0.5, -1); ambientLight(150); // 回転するドーナツの描画 rotateX(millis() / 1000); rotateY(millis() / 3000); noStroke(); fill(230, 180, 100); torus(120, 40, 60, 40); // 作成するドーナツの代わり } 無事下の結果が出力されたでしょうか。 1.2 トーラス関数を読み解く 今回は、p5.jsライブラリのtorus関数をもとにドーナツ関数を作成していきます。 torus関数のソースコードは下のリンクから見ることができます。 torus関数の処理内容について、簡単に解説を行います。完全に理解できなくても大丈夫です。 torus関数は、輪の半径のradius、チューブ半径のtubeRadius、輪一周の頂点数を決めるdetailX、チューブ周り一周の頂点数を決めるdetailYの4つの引数で呼び出されます。 torus関数の各処理を見ていきましょう。 初めの4つのif文では、引数が定義されなかった時の値の代入について書かれています。 if (typeof radius === 'undefined') { radius = 50; } else if (!radius) { return; // nothing to draw } if (typeof tubeRadius === 'undefined') { tubeRadius = 10; } else if (!tubeRadius) { return; // nothing to draw } if (typeof detailX === 'undefined') { detailX = 24; } if (typeof detailY === 'undefined') { detailY = 16; } tubeRatioは、radiusとtubeRadiusの比をトーラスの形を決める値として定義しています。 const tubeRatio = (tubeRadius / radius).toPrecision(4); 次に定義されるgIdはバッファの作成、呼び出し時に使われるジオメトリの名前です。 形や頂点数が異なる場合は異なるジオメトリとして定義する必要があるため、tubeRatio、detailX、detailYを含んでいます。 全体のスケールが違っても同じジオメトリをもとに描画することができるため、radiusとtubeRadiusは含まず、tubeRatioのみを含みます。 const gId = `torus|${tubeRatio}|${detailX}|${detailY}`; 次のif文の条件は、上のgIdについて既にバッファが作成されているかをチェックしています。 バッファが作成されていない場合は作成・描画を行い、すでに作成されている場合は描画のみを行います。 if (!this._renderer.geometryInHash(gId)) { // ・・・ if文内ではまず、各頂点の位置ベクトル、法線ベクトル、UV座標を計算するコールバック関数を定義しています。 vertices、vertexNormals、uvsはそれぞれ位置ベクトル、法線ベクトル、UV座標を格納するp5.Geometryのクラス変数です。位置ベクトルと法線ベクトルはp5.Vectorで、UV座標は0~1の数で定義します。 位置ベクトル、法線ベクトルの計算の数学的な解説はここでは割愛します。 (ヒント:thetaはトーラス中心から見た角度、phiはチューブの断面の円の中心から見た角度を指しています) const _torus = function() { for (let i = 0; i <= this.detailY; i++) { const v = i / this.detailY; const phi = 2 * Math.PI * v; const cosPhi = Math.cos(phi); const sinPhi = Math.sin(phi); const r = 1 + tubeRatio * cosPhi; for (let j = 0; j <= this.detailX; j++) { const u = j / this.detailX; const theta = 2 * Math.PI * u; const cosTheta = Math.cos(theta); const sinTheta = Math.sin(theta); const p = new p5.Vector( r * cosTheta, r * sinTheta, tubeRatio * sinPhi ); const n = new p5.Vector(cosPhi * cosTheta, cosPhi * sinTheta, sinPhi); this.vertices.push(p); this.vertexNormals.push(n); this.uvs.push(u, v); } } 上で定義したコールバック関数を用いて、p5.Geometryを作成します。 detailX、detailYを引数として与えていれば、p5.Geometryのクラス関数computeFaces()でインデックス配列を自動で計算することができます。 const torusGeom = new p5.Geometry(detailX, detailY, _torus); torusGeom.computeFaces(); computeFaces()によるインデックス配列の決め方を解説します。 このコードでは、各頂点が下の図のように並んでいる平面のY方向の端と端をつなげてチューブにし、さらにX方向の端と端をつなげることでトーラスをつくっています(XはdetailXの省略、YはdetailYの省略です)。 このとき、全ての面を描くためには全てのi(0≤i<Y)、j(0≤j<X)について、 [ i * ( X + 1 ) + j , i * ( X + 1 ) + j + 1 , ( i + 1 ) * ( X + 1 ) + j ] [ ( i + 1 ) * ( X + 1 ) + j , i * ( X + 1 ) + j + 1 , ( i + 1 ) * ( X + 1 ) + j + 1 ] の2つの三角形を描画する必要があります。 これを並べた配列がcomputeFaces()で計算するインデックス配列です。 _makeTriangleEdges()._edgesToVertices()ではストロークの始点ベクトル・終点ベクトルを計算しています。detailX、detailYが大きすぎる場合はストロークは描画できません。 if (detailX <= 24 && detailY <= 16) { torusGeom._makeTriangleEdges()._edgesToVertices(); } else if (this._renderer._doStroke) { console.log( 'Cannot draw strokes on torus object with more' + ' than 24 detailX or 16 detailY' ); } バッファ未作成の場合の最後の処理として、今まで計算してきたトーラスの頂点データ、インデックス配列をもとに頂点バッファ・インデックスバッファを作成します。 this._renderer.createBuffers(gId, torusGeom); バッファが作成されたら、それをもとにトーラスを描画します。 gIdのあとの3つの引数は、x方向、y方向、z方向のスケールを表します。 this._renderer.drawBuffersScaled(gId, radius, radius, radius); 以上がトーラス関数の処理内容です。 1.3 ドーナツ関数の雛形を作る トーラス関数をもとに、ドーナツ関数の雛形を作ります。 トーラス関数をコピペしたうえで、以下の3つの変更を加えます。 ・関数の定義方法をfunction命令に変える ・torusの文字列を全てdonutに変える ・最初の2行を削除する(ユーザー定義の関数では不要なため) sketch.js function donut(radius, tubeRadius, detailX, detailY) { // 関数の定義方法をfunction命令に変える // 最初の2行は削除する if (typeof radius === 'undefined') { radius = 50; } else if (!radius) { return; // nothing to draw } if (typeof tubeRadius === 'undefined') { tubeRadius = 10; } else if (!tubeRadius) { return; // nothing to draw } if (typeof detailX === 'undefined') { detailX = 24; } if (typeof detailY === 'undefined') { detailY = 16; } const tubeRatio = (tubeRadius / radius).toPrecision(4); const gId = `donut|${tubeRatio}|${detailX}|${detailY}`; // torus -> donut if (!this._renderer.geometryInHash(gId)) { const _donut = function() { // torus -> donut for (let i = 0; i <= this.detailY; i++) { const v = i / this.detailY; const phi = 2 * Math.PI * v; const cosPhi = Math.cos(phi); const sinPhi = Math.sin(phi); const r = 1 + tubeRatio * cosPhi; for (let j = 0; j <= this.detailX; j++) { const u = j / this.detailX; const theta = 2 * Math.PI * u; const cosTheta = Math.cos(theta); const sinTheta = Math.sin(theta); const p = new p5.Vector( r * cosTheta, r * sinTheta, tubeRatio * sinPhi ); const n = new p5.Vector(cosPhi * cosTheta, cosPhi * sinTheta, sinPhi); this.vertices.push(p); this.vertexNormals.push(n); this.uvs.push(u, v); } } }; const donutGeom = new p5.Geometry(detailX, detailY, _donut); // torus -> donut donutGeom.computeFaces(); // torus -> donut if (detailX <= 24 && detailY <= 16) { donutGeom._makeTriangleEdges()._edgesToVertices(); // torus -> donut } else if (this._renderer._doStroke) { console.log( 'Cannot draw strokes on torus object with more' + ' than 24 detailX or 16 detailY' ); } this._renderer.createBuffers(gId, donutGeom); // torus -> donut } this._renderer.drawBuffersScaled(gId, radius, radius, radius); return this; } この関数を定義したうえでdraw()内のtorus関数の呼び出しをdonut関数に変更すると、変更前と同様の結果が得られます。 1.4 ドーナツの形を変更する 頂点データを計算しているのはコールバック関数_donutなので、ここを書き換えます。 あのドーナツを作るためには、中心から見た角度に応じてチューブの半径を変化させる必要があります。 今回は、以下の式・グラフのように、連続した弧を描くのにthetaに関する三角関数の絶対値を使います。 r=1 + \frac{1}{2} | sin4\theta | チューブのもとの半径を表す変数はtubeRatioであるため、これを中心から見た角度thetaをもとに変化させた、新しいチューブ半径newTubeRatioを定義します。 newTubeRatioの計算はthetaの計算以降に行う必要があるため、元々tubeRatioを使って計算していたrをjに関するfor文内に移動させる必要があります。 const _donut = function() { // torus -> donut for (let i = 0; i <= this.detailY; i++) { const v = i / this.detailY; const phi = 2 * Math.PI * v; const cosPhi = Math.cos(phi); const sinPhi = Math.sin(phi); // const r = 1 + tubeRatio * cosPhi; for (let j = 0; j <= this.detailX; j++) { const u = j / this.detailX; const theta = 2 * Math.PI * u; const cosTheta = Math.cos(theta); const sinTheta = Math.sin(theta); const newTubeRatio = tubeRatio * (1 + 0.5 * abs(sin(theta * 4))); // 追加 const r = 1 + newTubeRatio * cosPhi; // 場所を移動させてtubeRatio -> newTubeRatio const p = new p5.Vector( r * cosTheta, r * sinTheta, newTubeRatio * sinPhi // tubeRatio -> newTubeRatio ); const n = new p5.Vector(cosPhi * cosTheta, cosPhi * sinTheta, sinPhi); this.vertices.push(p); this.vertexNormals.push(n); this.uvs.push(u, v); } } }; 上の変更を加えることで、あのドーナツを表示させることができました。 1.5 法線ベクトルを変更する まだ法線の変更がされていないため、影の付き方が不自然です。 しかし、変更された形状に合わせた法線を決めるにはとても複雑な計算が必要になってしまいます。 そこで、p5.Geometryのクラス関数computeNormals()を使います。この関数は、ジオメトリ内の各頂点について、その頂点が含まれる面の法線ベクトルの平均を計算し、それをその頂点の法線ベクトルとして設定する関数です。 この関数はインデックス配列の定義後に使う必要があるため、computeFaces()の後ろに書きます。 function donut(radius, tubeRadius, detailX, detailY) { // 省略 if (!this._renderer.geometryInHash(gId)) { const _donut = function() { for (let i = 0; i <= this.detailY; i++) { // 省略 for (let j = 0; j <= this.detailX; j++) { // 省略 // 削除 : const n = new p5.Vector(cosPhi * cosTheta, cosPhi * sinTheta, sinPhi); this.vertices.push(p); // 削除 : this.vertexNormals.push(n); this.uvs.push(u, v); } } }; const donutGeom = new p5.Geometry(detailX, detailY, _donut); donutGeom.computeFaces(); donutGeom.computeNormals(); // 追加 // 以下省略 } 法線を計算しなおすことができましたが、つなぎ目にがたつきができてしまいました。 これは下の図上の、0とX、0とY×(X+1)、などの端同士が同じ位置にあるにかかわらず違う頂点として定義されていることによります。 よって、がたつきをなくすためにはインデックス配列を変更する必要があります。 1.6 インデックス配列を変更する 同じ位置の頂点を2度定義しないよう、下のような平面を丸めてトーラス(ドーナツ)を作ることとします。 ただし、jがX-1のときにj+1がXではなく0になる必要があるため、(j+1)%Xを用います。iについても同様です。 p5.Geometryのインデックス配列は、facesというクラス変数内に格納されています。この配列にpush()を用いてインデックス配列を入れていきます。 function donut(radius, tubeRadius, detailX, detailY) { // 省略 if (!this._renderer.geometryInHash(gId)) { const _donut = function() { for (let i = 0; i < this.detailY; i++) { // <= から < に // 省略 for (let j = 0; j < this.detailX; j++) { // <= から < に // 省略 // ↓ 追加 this.faces.push([ i * this.detailX + j, i * this.detailX + (j + 1) % this.detailX, (i + 1) % this.detailY * this.detailX + j ]); this.faces.push([ (i + 1) % this.detailY * this.detailX + j, i * this.detailX + (j + 1) % this.detailX, (i + 1) % this.detailY * this.detailX + (j + 1) % this.detailX ]); } } }; const donutGeom = new p5.Geometry(detailX, detailY, _donut); // 削除 : donutGeom.computeFaces(); donutGeom.computeNormals(); // 以下省略 } これでドーナツ関数の完成です。 おまけ:トゥーンシェーダーと組み合わせる 以下の記事で解説しているトゥーンシェーダーと組み合わせることで、よりかわいくドーナツを描画することができます。 2. 最後に 2.1 p5.Geometryを使った作例 参考として、私がp5.Geometryを使用して作った作品例のリンクを載せます。 この例では、コールバック関数を使わない形でp5.Geometryを作成しています。 2.3 あとがき p5.Geometryを使うことでp5.js上で好きな3Dモデルを作ることができます。 この記事が、p5.Geometryへの挑戦の一助となれば幸いです。
- 投稿日:2021-12-03T17:09:57+09:00
ファミレスのメニューもどき管理画面を作ってみた
下記サンプルサイトは同じDBから書き出したメニュー注文ページです。 ▶︎サンプルサイト 下記の記事の続きです ▶︎ファミレスのメニューシステムもどき作ってみた やりたいこと DBの連携、入力、変更、削除をブラウザで操作 画像アップは別フォルダへ DB一覧書き出し バリデーションで同じ名前と画像はアップさせない 作成ファイル index.php(ログイン画面) logout.php(ログアウト画面) control_top.php(トップページと追加入力) change.php(変更UPDATE) change_check.php(UPDATEのバリデーション) delete.php(削除) db_join.php(インクルード用、DBの接続や共通で使う関数と配列を格納) select_cat.php(インクルード用、formのselectパーツ) select_mt.php(インクルード用、formのselectパーツ) 管理画面のUI jsで各操作をタブで切り替え 変更と削除はID検索で各ページにsubmitで移行。 ※管理画面はDBにアクセスしてデータの変更が出来てしまう為、セキュリティーの心配もあるので一般公開は控えています。 画像は最初DBに入れるつもりでしたが、XAMPでMySQLに入れようとしたらサイズの上限に引っかかりUP不可。 なのでUP専用のディレクトリを作りデータはそっちへ、画像の名前だけDBに保存しておくことに。 数と名前を合わせておかないと後々厄介なので、入れ替え、削除のあった時は同じく連動して合わせていく仕様に設計しました。 DBのカラムだけ配列として取得したい 1列の値だけ取り出す方法が分かりませんでした。 半日かけて調べてみたところarray_column()という関数を使えば出来るのでは? という方法に辿り着く。 control_top.php //商品名だけ配列化 $itemArray = array_column($data, 'item'); ///////省略///// //エラー対処 if(!check_word($item,25)){ $err['item'] = '空文字か入力値が超えています'; } elseif (in_array($item,$itemArray,true)) { //DBに同じ名前を入れさせない $err['item']= '同じ名の商品があります'; } else { $err['item']= ''; } 同じ商品名を被らせたくなかったので、DBのカラムに同じ商品名があるかチェック。 array_column(全配列の入った変数, ‘カラム名’)で商品名だけ配列化してif文で振り分け。 同じだったら省く仕組みです。 IDのカラムも配列に まずIDのカラム値を配列にします。 $id_Array = array_column($data, 'ID'); if(in_array($id,$id_Array))でDBから取り出したIDと入力されたIDを照合します。 change.php //inputから持ってきたname属性値 $id = html_escape($_POST['stockid']); //idを全角もOKにする $id = mb_convert_kana($id, 'n', 'UTF-8'); //DBより一覧表書き出し ID照合用 $sql_list = 'SELECT * FROM menulist'; $stmt = $dbh->prepare($sql_list); $stmt->execute(); $data =array(); $count = $stmt->rowCount();//レコード数取得 //FETCH_ASSOCで配列として書き出して代入 while($row = $stmt->fetch(PDO::FETCH_ASSOC)){ $data[] = $row; } //IDのカラムだけ配列にする $id_Array = array_column($data, 'ID'); //DBとの検索IDを照合する、DBのID以外は全て弾くので空文字やテキストのバリデーションにもなる if(in_array($id,$id_Array,true)){ //DBよりID書き出し $sql_id = "SELECT * FROM menulist WHERE id = :id"; $stmt_id = $dbh->prepare($sql_id); $stmt_id->bindParam( ':id', $id, PDO::PARAM_INT); $stmt_id->execute(); //配列にする if($stmt_id) { $data = $stmt_id->fetch(PDO::FETCH_ASSOC); } $item = $data['item']; $select_cat = $data['category']; $select_mt = $data['material']; $plice = number_format($data['plice']); $change_img = $data['image']; $submit_btn = '<input class="toBtn" type="submit" value="変更">'; } else { $item = ''; $select_cat = ''; $select_mt = ''; $plice = ''; $change_img = ''; $submit_btn = '<p class="errComent">※ID欄が空かDBにないIDです。一覧に戻って選び直してください。</p>'; } DBのID以外は全て弾くので空文字や数字以外のテキストのバリデーションにもなりました。 change.php change_check.php change_check.phpではエラーがあったらDBに入れず、入力し直しへ。 エラーがなっかたら更新して完了を表示させます。 画像の入れ替え操作は苦労しました。 変更がある場合と無い場合を想定してファイルアップの有無を0で判断し、0でなかったら新しい画像をアップし古い画像は削除。 古い画像のままだったら、古い画像を削除して同じものを入れ直します。 なのでID検索時に古い画像のファイル名も取得して運んでいきます。 change_check.php if(isset($_FILES['stockimg'])){ $image = $_FILES['stockimg']; $image['name'] = html_escape($image['name']); //英小文字に変換 $image['name'] = strtolower($image['name']); //ファイル名と拡張子を切り分けて.を除去、ピリオドを重複させない為 $image_parts = pathinfo($image['name']); $extension = 'jpg'; $image_name = $image_parts['filename']; $image_name = str_replace('.','',$image_name); if($image['size'] > 1000000){ $err['imgsize'] = '画像が1MBを超えています'; } elseif(file_exists('./img_up/'.$image_name.'.'.$extension) === TRUE) { //file_exists関数でディレクトリ内を調べて、同じファイル名があった場合はアップさせない $err['imgsize'] = '同名のファイルがあります。違うファイル名にしてください。'; } else { $err['imgsize'] = ''; } } ///////省略///// //エラーが無かったら更新へ if($err['item']== '' && $err['imgsize'] == '' && $err['plice']== ''){ //ファイルアップがあった時の対処、0じゃなっかたらディレクトリにアップ、ここは苦労した if((int)$image_name !== 0){ move_uploaded_file($image['tmp_name'],'./img_up/'.$image_name.'.'.$extension); //画像が変更されたら古い画像はフォルダより削除 unlink('./img_up/'.$old_img); //DBに持っていくファイル名 $image = $image_name.'.'.$extension; //見本表示 $prev_img = '<p class="center">変更画像<br><img class="thumb12" src="'.$img_path.$image_name.'.'.$extension.'" alt=""></p>'; } else { //DBに持っていくファイル名を置かないとDBからファイル名が消えてしまう $image = $old_img; $prev_img = '<p>変更画像無し</p>'; } ファイルアップは前にプチ・クラウドストレージを作ったので、ベースはそこから引用してきました。 ▶︎プチ・クラウドストレージ作ってみた delete.phpもchange_check.phpとほぼ同じ作りです。 管理画面は注文ページより、ほぼ思うように作れました。 期間は2週間+α、かかりました。 作り終えて振り返り 調べていくとDB接続の書き方も人によりマチマチで「この方法知らない…」ということも多々遭遇。 関数、条件の立て方も同様で、長いと頭がクラクラしてきます。 システム構築の実務経験がないので、使う人の行動を想像しながら作りましたが 実務があったら、もっと配慮ができる設計が出来たのかなとも思っています。 以下サンプルコードです より実用的なコードに近づけるようご意見いただけると嬉しいです。 サンプルコード ・index.php(ログイン画面) ・logout.php(ログアウト画面) ・db_join.php(インクルード用、DBの接続や共通で使う関数と配列を格納) ・select_cat.php(インクルード用、formのselectパーツ) ・select_mt.php(インクルード用、formのselectパーツ) 上記5個のファイルは省略します。 control_top.php <?php //ログインしていないとアクセスさせない session_start(); session_regenerate_id(true); if(isset($_SESSION['login']) === false){ header('Location: index.php'); exit(); } try{ include_once(dirname(__FILE__).'/db_join.php'); //DBより一覧表書き出し $sql_list = 'SELECT * FROM menulist order by ID ASC'; $stmt = $dbh->prepare($sql_list); $stmt->execute(); $data =array(); $count = $stmt->rowCount();//レコード数取得 while($row = $stmt->fetch(PDO::FETCH_ASSOC)){ $data[] = $row;//FETCH_ASSOCで配列として書き出して代入 } //DBのカラムに同じ商品名があるかチェック //商品名だけ配列化 $itemArray = array_column($data, 'item'); $item = ''; $category=''; $material = ''; $plice = ''; $err = ['item'=>'','cat'=>'','mat'=>'','plice'=>'','imgsize'=>'']; $dberr = ''; if($_SERVER['REQUEST_METHOD'] === 'POST'){ $item = html_escape($_POST['stock']); $category = html_escape($_POST['category']); $material = html_escape($_POST['material']); $plice = html_escape($_POST['stockplice']); $plice = mb_convert_kana($plice, 'n', 'UTF-8'); $image = $_FILES['stockimg']; $image['name'] = html_escape($image['name']); //英小文字に変換 $image['name'] = strtolower($image['name']); $plice = strtolower($plice); //ファイル名と拡張子を切り分けて.を除去、ピリオドを重複させない為 $image_parts = pathinfo($image['name']); //$extension = $image_parts['extension']; $extension = 'jpg'; $image_name = $image_parts['filename']; $image_name = str_replace('.','',$image_name); //base64でencode追加 $image_name = base64_encode($image_name); //エラー対処 if(!check_word($item,25)){ $err['item'] = '空文字か入力値が超えています'; } elseif (in_array($item,$itemArray,true)) { //DBに同じ名前を入れさせない $err['item']= '同じ名の商品があります'; } else { $err['item']= ''; } if($category === ''){ $err['cat'] = '選択してください'; } else { $err['cat']= ''; } if($material === ''){ $err['mat'] = '選択してください'; } else { $err['mat']= ''; } if(!check_word($plice,8)){ $err['plice'] = '空文字か入力値が超えています'; } else { $err['plice']= ''; } if($image['size'] > 1000000 && $image['size'] == 0){ $err['imgsize'] = '画像が選択されていないかサイズが1MBを超えています'; } elseif(file_exists('./img_up/'.$image_name.'.'.$extension) === TRUE) { //file_exists関数でディレクトリ内を調べて、同じファイル名があった場合はアップさせない $err['imgsize'] = '同名のファイルがあります。違うファイル名にしてください。'; } else { $err['imgsize'] = ''; } //empty($err)ではダメで下の書き方でtrueになった if($err['item']== '' && $err['cat']== '' && $err['mat']== '' && $err['imgsize'] == '' && $err['plice']== ''){ //デコードして画像アップ $image_name = base64_decode($image_name); move_uploaded_file($image['tmp_name'],'./img_up/'.$image_name.'.'.$extension); $dberr = '<img class="thumb" src="'.$img_path.$image_name.'.'.$extension.'" alt="">商品「'.$item.'」は正常にUPされました'; //DBに入力データを入れる $sql = "INSERT INTO menulist(item,image,category,material,plice) VALUE(:item,:image,:category,:material,:plice)"; $stmt_in = $dbh->prepare($sql); $stmt_in->bindValue(':item',$item,PDO::PARAM_STR); //DBには画像ファイル名のみUP $stmt_in->bindValue(':image',$image_name.'.'.$extension,PDO::PARAM_STR); $stmt_in->bindValue(':category',$category,PDO::PARAM_STR); $stmt_in->bindValue(':material',$material,PDO::PARAM_STR); $stmt_in->bindValue(':plice',$plice,PDO::PARAM_INT); $stmt_in->execute(); } //再読み込みを防ぐ為、同ページだけど飛ばす header('Location:/menulist/control/control_top.php'); } } catch (PDOException $e){ print($e->getMessage()); die(); } ?> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>DBテスト管理画面</title> <link rel="stylesheet" href="/menulist/common/sanitize.css"> <link rel="stylesheet" href="/menulist/common/style.css"> <link rel="stylesheet" href="/menulist/common/control.css"> <!--ファビコン32x32--> <link rel="shortcut icon" href="/menulist/favicon.ico" type="image/vnd.microsoft.icon"> </head> <body> <div id="wrapper"> <header id="header"> <p id="logout"><a href="logout.php">ログアウト</a></p> <h1 class="topTitle">DBテスト管理画面</h1> <p class="center notice1">入力の場合IDの設定は不要、金額は数値のみで<br>ブラウザの再読み込みはしないでください。エラーになります。</p> <p class="center notice2">IDで商品を検索してください</p> </header> <main id="main"> <ul class="formChange"> <li data-id="insart" class="act">入力</li> <li data-id="update">変更</li> <li data-id="deleteForm">削除</li> </ul> <!-- 追加入力 --> <form class="changeBox" id="insart" method="post" action="" enctype="multipart/form-data"> <div class="formLow"> <div class="stockId"> <p>ID</p> <p class="formInput">-</p> </div> <div class="stock"> <label>商品名</label> <input type="text" name="stock"> <p><?php echo $err['item']; ?></p> </div> <div class="stockCategory"> <label>カテゴリー</label> <select name="category"> <option value="" selected="selected">選択する</option> <!-- selectはインクルード --> <?php include_once(dirname(__FILE__).'/select_cat.php'); ?> </select> <p><?php echo $err['cat']; ?></p> </div> <div class="materialForm"> <label>素材</label> <select name="material"> <option value="" selected="selected">選択する</option> <!-- selectはインクルード --> <?php include_once(dirname(__FILE__).'/select_mt.php'); ?> </select> <p><?php echo $err['mat']; ?></p> </div> <div class="stockplice"> <label>金額</label> <input type="text" name="stockplice"> <p><?php echo $err['plice']; ?></p> </div> </div> <!-- //.formLow --> <div class="center"> <label>商品画像:サイズ横640px縦420px</label><br> <input type="file" name="stockimg"> <p><?php echo $err['imgsize']; ?></p> <?php echo $dberr; ?> </div> <input class="toBtn" type="submit" value="追加"> </form> <!-- //#insartBox --> <!-- 変更入力 --> <!-- 検索 --> <div id="update" class="changeBox"> <form method="post" action="/menulist/control/change.php"> <div class="formLow"> <label class="sarchId">ID</label> <input class="inputId" type="text" name="stockid"> </div> <input class="toBtn" type="submit" value="検索"> </form> </div> <!-- //#updateId --> <!-- 削除 --> <div id="deleteForm" class="changeBox"> <form method="post" action="/menulist/control/delete.php"> <div class="formLow"> <label class="sarchId">ID</label> <input class="inputId" type="text" name="stockid"> </div> <input class="toBtn" type="submit" value="検索"> </form> </div> <!-- //#deleteForm --> <!-- メニュー一覧データ --> <div class="stockBox"> <!-- カテゴリー絞り込みタグ --> <ul class="topNav"> <?php for($i = 0; $i < count($cat_list); $i++): ?> <li id="<?php echo 'cat_select_'.$i; ?>"><?php echo $cat_list[$i]; ?></li> <?php endfor; ?> <li id="all">ALL</li> </ul> <p class="totalStock"><span>全登録数</span><?php echo $count; ?>項目</p> <p id="catShow"></p> <table id="stockList"> <tr> <th class="stocklistId">ID</th><th class="stocklist">商品名</th><th class="stockImg">商品画像</th><th class="stocklisutoCat">カテゴリー</th><th class="stocklistMat">素材</th><th class="stocklistPlice">金額</th> </tr> <!-- foreachの外にforで囲み、カテゴリー数をループ --> <!-- foreachの中に$cat_list[$i]を入れるとエラーになる --> <?php for($i = 0; $i < count($cat_list); $i++): ?> <?php $cat_item = $cat_list[$i]; $cat_select = 'cat_select_'.$i; ?> <?php foreach($data as $row): ?> <?php if($row['category'] == $cat_item): ?> <tr class="<?php echo $cat_select; ?>"> <td class="stocklistId"><?php echo $row['ID']; ?></td> <td class="stocklist"><?php echo $row['item']; ?></td> <td class="stockImg"><img src="<?php echo $img_path.$row['image']; ?>" alt=""></td> <td class="stocklisutoCat"><?php echo $row['category']; ?></td> <td class="stocklistMat"><?php echo $row['material']; ?></td> <td class="stocklistPlice"><?php echo number_format($row['plice']); ?>円</td> </tr> <?php endif; ?> <?php endforeach; ?> <?php endfor; ?> </table> </div> <!-- //.stockBox --> </main> <footer id="footer"> <small>DBテストメニューsystem</small> </footer> </div> <!-- //# wrapper--> <script src="/menulist/common/jquery-3.6.0.min.js"></script> <script src="/menulist/common/control.js"></script> </body> </html> change.php <?php //ログインしていないとアクセスさせない session_start(); session_regenerate_id(true); if(isset($_SESSION['login']) === false){ header('Location: index.php'); exit(); } try { include_once(dirname(__FILE__).'/db_join.php'); $id = html_escape($_POST['stockid']); //idを全角もOKにする $id = mb_convert_kana($id, 'n', 'UTF-8'); //DBより一覧表書き出し ID照合用 $sql_list = 'SELECT * FROM menulist'; $stmt = $dbh->prepare($sql_list); $stmt->execute(); $data =array(); $count = $stmt->rowCount();//レコード数取得 //FETCH_ASSOCで配列として書き出して代入 while($row = $stmt->fetch(PDO::FETCH_ASSOC)){ $data[] = $row; } //IDのカラムだけ配列にする $id_Array = array_column($data, 'ID'); //DBとの検索IDを照合する、DBのID以外は全て弾くので空文字やテキストのバリデーションにもなる if(in_array($id,$id_Array)){ //DBよりID書き出し $sql_id = "SELECT * FROM menulist WHERE id = :id"; $stmt_id = $dbh->prepare($sql_id); $stmt_id->bindParam( ':id', $id, PDO::PARAM_INT); $stmt_id->execute(); //配列にする if($stmt_id) { $data = $stmt_id->fetch(PDO::FETCH_ASSOC); } $item = $data['item']; $select_cat = $data['category']; $select_mt = $data['material']; $plice = number_format($data['plice']); $change_img = $data['image']; $submit_btn = '<input class="toBtn" type="submit" value="変更">'; } else { $item = ''; $select_cat = ''; $select_mt = ''; $plice = ''; $change_img = ''; $submit_btn = '<p class="errComent">※ID欄が空かDBにないIDです。一覧に戻って選び直してください。</p>'; } } catch (PDOException $e){ print($e->getMessage()); die(); } ?> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>DBテスト管理画面・変更</title> <link rel="stylesheet" href="/menulist/common/sanitize.css"> <link rel="stylesheet" href="/menulist/common/style.css"> <link rel="stylesheet" href="/menulist/common/control.css"> <!--ファビコン32x32--> <link rel="shortcut icon" href="/menulist/favicon.ico" type="image/vnd.microsoft.icon"> </head> <body> <div id="wrapper"> <header id="header"> <h1 class="topTitle">DBテスト管理画面・変更</h1> <p class="center notice1">変更箇所のみ書き換えてください</p> </header> <main id="main"> <!-- 変更 --> <form method="post" action="change_check.php" enctype="multipart/form-data"> <div class="formLow"> <div class="stockId"> <p>ID</p> <p class="formInput"><?php echo $id; ?></p> <input type="hidden" name="stockid" value="<?php echo $id; ?>"> </div> <div class="stock"> <label>商品名</label> <input type="text" name="stock" value="<?php echo $item; ?>"> </div> <div class="stockCategory"> <label>カテゴリー</label> <select name="category" value="<?php echo $select_cat; ?>"> <!-- selectはインクルード --> <?php include_once(dirname(__FILE__).'/select_cat.php'); ?> </select> </div> <div class="materialForm"> <label>素材</label> <select name="material" value="<?php echo $select_mt; ?>"> <!-- selectはインクルード --> <?php include_once(dirname(__FILE__).'/select_mt.php'); ?> </select> </div> <div class="stockplice"> <label>金額</label> <input type="text" name="stockplice" value="<?php echo $plice; ?>"> </div> </div> <!-- //.formLow --> <div class="center"> <label>商品画像:サイズ横640px縦420px<br><span class="text12">※変更のある場合のみUP</span></label><br> <input type="file" name="stockimg"> <p class="center"> <img class="thumb12" src="<?php echo $img_path.$change_img; ?>" alt=""> <!-- 削除のある場合に古い画像も持っていく --> <input type="hidden" name="old_img" value="<?php echo $change_img; ?>"> </p> <p class="text12 center mg0">変更前の画像です</p> </div> <?php echo $submit_btn; ?> </form> <div id="toList"><a href="/menulist/control/control_top.php">管理画面トップへ</a></div> </main> <footer id="footer"> <small>DBテストメニューsystem</small> </footer> </div> <!-- //# wrapper--> <script src="/menulist/common/jquery-3.6.0.min.js"></script> <script src="/menulist/common/control.js"></script> </body> </html> change_check.php <?php //ログインしていないとアクセスさせない session_start(); session_regenerate_id(true); if(isset($_SESSION['login']) === false){ header('Location: index.php'); exit(); } try { include_once(dirname(__FILE__).'/db_join.php'); $err = ['item'=>'','plice'=>'','imgsize'=>'']; $id = html_escape($_POST['stockid']); $item = html_escape($_POST['stock']); $category = html_escape($_POST['category']); $material = html_escape($_POST['material']); $plice = html_escape($_POST['stockplice']); $plice = mb_convert_kana($plice, 'n', 'UTF-8'); $old_img = html_escape($_POST['old_img']); if(isset($_FILES['stockimg'])){ $image = $_FILES['stockimg']; $image['name'] = html_escape($image['name']); //英小文字に変換 $image['name'] = strtolower($image['name']); //ファイル名と拡張子を切り分けて.を除去、ピリオドを重複させない為 $image_parts = pathinfo($image['name']); $extension = 'jpg'; $image_name = $image_parts['filename']; $image_name = str_replace('.','',$image_name); if($image['size'] > 1000000){ $err['imgsize'] = '画像が1MBを超えています'; } elseif(file_exists('./img_up/'.$image_name.'.'.$extension) === TRUE) { //file_exists関数でディレクトリ内を調べて、同じファイル名があった場合はアップさせない $err['imgsize'] = '同名のファイルがあります。違うファイル名にしてください。'; } else { $err['imgsize'] = ''; } } //var_dump(is_number($plice)); //エラー対処 if(!check_word($item,25)){ $err['item'] = '空文字か入力値が超えています'; } else { $err['item']= ''; } if(!check_word($plice,8)){ $err['plice'] = '空文字か入力値が超えています'; } elseif(is_numeric($plice) == FALSE) { //金額が数字じゃなかったらNG $err['plice'] = '数字を入力してください'; //金額変更でNGが出ると下記の変数に値が無くなるので対処 $prev_img = ''; } else { $err['plice']= ''; $plice = number_format($plice); } //エラーが無かったら更新へ if($err['item']== '' && $err['imgsize'] == '' && $err['plice']== ''){ //ファイルアップがあった時の対処、0じゃなっかたらディレクトリにアップ、ここは苦労した if((int)$image_name !== 0){ move_uploaded_file($image['tmp_name'],'./img_up/'.$image_name.'.'.$extension); //画像が変更されたら古い画像はフォルダより削除 unlink('./img_up/'.$old_img); //DBに持っていくファイル名 $image = $image_name.'.'.$extension; //見本表示 $prev_img = '<p class="center">変更画像<br><img class="thumb12" src="'.$img_path.$image_name.'.'.$extension.'" alt=""></p>'; } else { //DBに持っていくファイル名を置かないとDBからファイル名が消えてしまう $image = $old_img; $prev_img = '<p>変更画像無し</p>'; } //指定のidのDBを更新 $sql_up = 'UPDATE menulist SET item=:item,image=:image,category=:category,material=:material,plice=:plice WHERE id = :id'; $stmt_up = $dbh->prepare($sql_up); $stmt_up->bindParam( ':id', $id, PDO::PARAM_INT); $stmt_up->bindValue(':item',$item,PDO::PARAM_STR); $stmt_up->bindValue(':image',$image,PDO::PARAM_STR); $stmt_up->bindValue(':category',$category,PDO::PARAM_STR); $stmt_up->bindValue(':material',$material,PDO::PARAM_STR); $stmt_up->bindValue(':plice',$plice,PDO::PARAM_INT); $stmt_up->execute(); $notice = '以下の内容で更新しました'; $back_btn = '<div id="toList"><a href="/menulist/control/control_top.php">管理画面トップへ</a></div>'; } else { $notice = 'エラーがあります。戻って修正してください。'; //お手軽にhistory.back使いましたが「フォーム再送信の確認」が出る可能性有り //かといってa hrefだと入力値が消える。手間だけどもう一つ入力値を持っていった再入力用ページを作る? $back_btn = '<div id="toList"><a onclick="history.back()">修正する</a></div>'; } } catch (PDOException $e){ print($e->getMessage()); die(); } ?> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>DBテスト管理画面・変更確認</title> <link rel="stylesheet" href="/menulist/common/sanitize.css"> <link rel="stylesheet" href="/menulist/common/style.css"> <link rel="stylesheet" href="/menulist/common/control.css"> <!--ファビコン32x32--> <link rel="shortcut icon" href="/menulist/favicon.ico" type="image/vnd.microsoft.icon"> </head> <body> <div id="wrapper"> <header id="header"> <h1 class="topTitle">DBテスト管理画面・変更確認</h1> <p class="center notice1"><?php echo $notice; ?></p> </header> <main id="main"> <div class="formLow"> <div class="stockId"> <p>ID</p> <p class="formInput"><?php echo $id; ?></p> </div> <div class="stock"> <label>商品名</label> <p class="formInput"><?php echo $item; ?></p> <p><?php echo $err['item']; ?></p> </div> <div class="stockCategory"> <label>カテゴリー</label> <p class="formInput"><?php echo $category; ?></p> </div> <div class="materialForm"> <label>素材</label> <p class="formInput"><?php echo $material; ?></p> </div> <div class="stockplice"> <label>金額</label> <p class="formInput"><?php echo $plice; ?></p> <p><?php echo $err['plice']; ?></p> </div> </div> <!-- //.formLow --> <div class="center"> <?php echo $prev_img; ?> <p class="center"><?php echo $err['imgsize']; ?></p> </div> <?php echo $back_btn; ?> </main> </div> <!-- //#wrapper --> <footer id="footer"> <small>DBテストメニューsystem</small> </footer> </div> <!-- //# wrapper--> <script src="/menulist/common/jquery-3.6.0.min.js"></script> <script src="/menulist/common/control.js"></script> </body> </html> delete.php <?php //ログインしていないとアクセスさせない session_start(); session_regenerate_id(true); if(isset($_SESSION['login']) === false){ header('Location: index.php'); exit(); } try { include_once(dirname(__FILE__).'/db_join.php'); $id = html_escape($_POST['stockid']); $id = mb_convert_kana($id, 'n', 'UTF-8'); //DBより一覧表書き出し ID照合用 $sql_list = 'SELECT * FROM menulist'; $stmt = $dbh->prepare($sql_list); $stmt->execute(); $data =array(); $count = $stmt->rowCount();//レコード数取得 while($row = $stmt->fetch(PDO::FETCH_ASSOC)){ $data[] = $row;//FETCH_ASSOCで配列として書き出して代入 } //IDのカラムだけ配列にする $id_Array = array_column($data, 'ID'); //DBとの検索IDを照合する if(in_array($id,$id_Array)){ $sql_id = "SELECT * FROM menulist WHERE id = :id"; $stmt_id = $dbh->prepare($sql_id); $stmt_id->bindParam(':id', $id, PDO::PARAM_INT); $stmt_id->execute(); //配列にする if($stmt_id) { $data = $stmt_id->fetch(PDO::FETCH_ASSOC); } $item = $data['item']; $plice = number_format($data['plice']); $select_cat = $data['category']; $select_mt = $data['material']; $change_img = $data['image']; $submit_btn = '<input class="toBtn" type="submit" value="削除">'; } else { $item = ''; $select_cat = ''; $select_mt = ''; $plice = ''; $change_img = ''; $submit_btn = '<p class="errComent">※DBにないIDです。一覧に戻って選び直してください。</p>'; } if($_SERVER['REQUEST_METHOD'] === 'POST'){ //isset置かないとNoticeが出る if(isset($_POST['deleteid'])){ $id = html_escape($_POST['deleteid']); $image = html_escape($_POST['deleteimg']); //DBより削除 $sql_delete = "DELETE FROM menulist WHERE id = :id"; $stmt_delete = $dbh->prepare($sql_delete); $stmt_delete->bindParam( ':id', $id, PDO::PARAM_INT); $stmt_delete->execute(); //画像フォルダからも削除 unlink('./img_up/'.$image); //実行されたらトップに飛ばす header('Location:/menulist/control/control_top.php'); } } } catch (PDOException $e){ print($e->getMessage()); die(); } ?> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>DBテスト管理画面・変更</title> <link rel="stylesheet" href="/menulist/common/sanitize.css"> <link rel="stylesheet" href="/menulist/common/style.css"> <link rel="stylesheet" href="/menulist/common/control.css"> <!--ファビコン32x32--> <link rel="shortcut icon" href="/menulist/favicon.ico" type="image/vnd.microsoft.icon"> </head> <body> <div id="wrapper"> <header id="header"> <h1 class="topTitle">DBテスト管理画面・削除</h1> <p class="center notice1">以下のメニューを削除します</p> </header> <main id="main"> <!-- 変更 --> <form method="post" action=""> <div class="stockBox"> <table> <tr> <th>ID</th><th>商品名</th><th>商品画像</th><th>カテゴリー</th><th>素材</th><th>金額</th> </tr> <tr> <td class="stocklistId"><?php echo $id; ?><input type="hidden" name="deleteid" value="<?php echo $id; ?>"></td> <td class="stocklist"><?php echo $item; ?></td> <td class="stockImg"><img src="<?php echo $img_path.$change_img; ?>" alt=""><input type="hidden" name="deleteimg" value="<?php echo $change_img; ?>"></td> <td class="stocklisutoCat"><?php echo $select_cat; ?></td> <td class="stocklistMat"><?php echo $select_mt; ?></td> <td class="stocklistPlice"><?php echo $plice ; ?>円</td> </tr> </table> </div> <?php echo $submit_btn; ?> </form> <div id="toList"><a href="/menulist/control/control_top.php">管理画面トップへ</a></div> </main> <footer id="footer"> <small>DBテストメニューsystem</small> </footer> </div> <!-- //# wrapper--> <script src="/menulist/common/jquery-3.6.0.min.js"></script> <script src="/menulist/common/control.js"></script> </body> </html>
- 投稿日:2021-12-03T16:46:48+09:00
ajaxでControllerに飛ばした処理がDELETEだけ動かない
はじめに この前、ajaxを使用し、画面内でデータ更新を行う処理を作成しました。 その際、新規(CREATE)と更新(UPDATE)の処理は動くのに削除(DELETE)の処理だけなかなか動かずに時間を取られてしまったので備忘録です。 結論 結論から先に言います。 新規や更新処理の時にエラーチェックで使用していたRequestの判定を削除処理でも使いまわした結果、削除処理では使用していなかった引数のチェック処理が勝手に走っていたため、インスタンスエラーが起きていました。 原因をいろいろ探しまわった時に、DELETE処理はajaxのPOSTでは飛ばないというような記述があり、データの飛ばし方に拘っていた結果、気づくのが遅れた初歩的なミスでした。 ちなみに返ってきていたエラーは422エラーでした。 動き自体は正しいのですが、Request処理が内部的に処理できなかったためこのエラーが返ってきていたのかなあと思います。 コード 新規・更新処理時に使っていたコードがこれです。 hogehoge.js // 新規・更新 $.ajax({ url: "hogehoge/update", type: "POST", dataType: "json", data:{ Id: document.getElementById('Id').value, Nm: document.getElementById('Nm').value, hoge: document.getElementById('hoge').value } }).done(function(result) { // 成功時 // 完了メッセージ表示 var message = '更新が完了しました。'; console.log(message); }).fail(function() { // 失敗時 // エラーメッセージ表示 var message = 'エラーが発生しました。'; console.log(message); }); web.php Route::post('hogehoge/update', [HogehogeController::class, 'updateHogehoge']); HogehogeController.php public function updateHogehoge(HogehogeRequest $request) { DB::connection('db')->beginTransaction(); try { $DATABASE= new DATABASE(); $DATABASE->fill([ 'ID' => $request->ID, 'NAME' => $request->NAME, 'HOGEHOGE' => $request->HOGEHOGE, ]); $DATABASE->save(); // コミット DB::connection('db')->commit(); return true; } catch (Exception $ex) { // ロールバック DB::connection('db')->rollback(); return false; } } これらのコードは動きました。 削除処理時に使っていたコードがこれです。 hogehoge.js // 削除 $.ajax({ url: "hogehoge/delete", type: "POST", dataType: "json", data:{ Id: document.getElementById('Id').value } }).done(function(result) { // 成功時 // 完了メッセージ表示 var message = '削除が完了しました。'; console.log(message); }).fail(function() { // 失敗時 // エラーメッセージ表示 var message = 'エラーが発生しました。'; console.log(message); }); web.php Route::post('hogehoge/delete', [HogehogeController::class, 'deleteHogehoge']); HogehogeController.php public function deleteHogehoge(HogehogeRequest $request) { // レコード削除 DATABASE::where('ID', '=', $request->Id)->delete(); // コミット DB::connection('db')->commit(); } これらのコードが422エラーを返していた訳ですね。 このコントローラーのプロシージャで受け取った際に、requestのエラーの判定をHogehogeRequestで行っていたのですが、削除の際、不要になって削っていたNmやHogehogeといった引数(?)の判定もHogehogeRequest内で行っていました。 だって主キーだけで削除できるんだから他の渡しても意味ないじゃん……。 とどのつまり、このHogehogeRequestを指定せずに普通のRequestを指定すれば良かったわけですね。 直すとしたら…… public function deleteHogehoge(HogehogeRequest $request) { ここを…… public function deleteHogehoge(Request $request) { こんな感じ……? さいごに 結局のところ、凡ミスには気を付けようねってことですね。
- 投稿日:2021-12-03T15:45:00+09:00
【Vue.js】v-modelを使用したフォームの作成
Tips1 基本的に公式リファレンスに丁寧に記述されているので細かいところはリファレンスを見てもらうのがいいと思う。 公式リファレンス そのなかで気になった点だけ、記述する。 selectbox optionにdisabledを使用しないとiosに上手く表示されないらしい <option disabled value="">年齢を選択してください。</option> v-model.オプションを選択できる。 .lazy 全ての入力が完了されないとバインディングされない。 .number 文字列ではなく整数で値がセットされる。 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> <div id="app"> <form action=""> 氏名 <input type="text" v-model="contact.yourName" /> <br /> 電話番号 <input type="tel" v-model="contact.tel" /> <br /> メールアドレス <input type="email" v-model.lazy="contact.email" /> <br /> 性別 <input type="radio" value="male" v-model="contact.gender" />男性 <input type="radio" value="female" v-model="contact.gender" />女性 <input type="radio" value="other" v-model="contact.gender" />その他 <br /> 年齢 <select type="text" v-model="contact.age"> <option disabled value="">年齢を選択してください。</option> <option>10代</option> <option>20代</option> <option>30代</option> <option>40代〜</option> </select> <br /> メッセージ <textarea type="text" v-model="contact.message"></textarea> <br /> このサイトを知った理由 <input type="checkbox" value="webサイト" v-model="contact.attracts" />webサイト <input type="checkbox" value="チラシ" v-model="contact.attracts" />チラシ <input type="checkbox" value="その他" v-model="contact.attracts" />その他 <br /> 注意事項に同意する <input type="checkbox" v-model="contact.caution" /> </form> </div> <script> let app = new Vue({ el: "#app", data() { return { contact: { yourName: "", tel: "", email: "", gender: "", age: "", message: "", attracts: [], caution: false, }, }; }, }); </script> </body> </html> Tips2 formにバリデーションをつけたい場合は、@submit.preventを使う。 送信ボタンを押してもrequestが飛ばない。 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <style> .error{ color:red; } </style> </head> <body> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> <div id="app"> <p v-if="errors.length"> <ul> <li class="error" v-for="error in errors">{{error}}</li> </ul> </p> <form @submit.prevent='validate'> //この部分 氏名 <input type="text" v-model="contact.yourName" /> <br /> 電話番号 <input type="tel" v-model="contact.tel" /> <br /> メールアドレス <input type="email" v-model.lazy="contact.email" /> <br /> 性別 <input type="radio" value="male" v-model="contact.gender" />男性 <input type="radio" value="female" v-model="contact.gender" />女性 <input type="radio" value="other" v-model="contact.gender" />その他 <br /> 年齢 <select type="text" v-model="contact.age"> <option disabled value="">年齢を選択してください。</option> <option>10代</option> <option>20代</option> <option>30代</option> <option>40代〜</option> </select> <br /> メッセージ <textarea type="text" v-model="contact.message"></textarea> <br /> このサイトを知った理由 <input type="checkbox" value="webサイト" v-model="contact.attracts" />webサイト <input type="checkbox" value="チラシ" v-model="contact.attracts" />チラシ <input type="checkbox" value="その他" v-model="contact.attracts" />その他 <br /> 注意事項に同意する <input type="checkbox" v-model="contact.caution" /> <button type="submit" value="送信">送信</button> </form> </div> <script> let app = new Vue({ el: "#app", data() { return { contact: { yourName: "", tel: "", email: "", gender: "", age: "", message: "", attracts: [], caution: false, }, errors:[] }; }, methods:{ validate(){ this.errors = [] //初期化 if(!this.contact.caution){ this.errors.push('注意事項にチェックを入れてください。') } } } }); </script> </body> </html>
- 投稿日:2021-12-03T15:30:58+09:00
Lisk SDKでブロックチェーンアプリケーションを作る
はじめに Liskって名前を知っていてもLisk SDKに触れたことがある人は少ないのでは、ということで今回記事にしました。 丁度いいところに、アドベントカレンダーもあったしね。 構築手順を書いているので少し長いですがお付き合いください。 Lisk SDK ってなんぞ ブロックチェーンとその上で動作するアプリケーション(ブロックチェーンアプリケーション)を構築するための開発キット。 これを使用して作成されたアプリケーションは独自のブロックチェーン上で動作します。 また、将来的にサイドチェーンとしてLiskにつながることが可能になります。 なお、JavaScript、TypeScriptで構築できるので非常に楽。(いや、ホントに) サイドチェーンとしてLiskと繋ぐ部分(インターオペラビリティ周り)はまだ開発中 サイドチェーンとなる必要がないなら、今のままでも新プロジェクトとして立ち上げられるくらいの実力有り 推奨構築環境 OS ubuntu 18.04 (LTS) 20.04 (LTS) mac 10.13 (High Sierra) 10.14 (Mojave) 10.15 (Catalina) 11.04 (Big Sur) Windowsは残念ながら... RAM 2GB以上 Node.js v12.22.7 (NPM version 6.14.15) v12系以外を使用するとうまく動作しません 構築方法 1. 下準備 とりあえずこの辺いれとく sudo apt-get install curl wget tar unzip zip ntp jq git build-essential 2. Lisk Commander の導入 npm i -g lisk-commander とても便利なCLI 入れなくても構築できるが、入れておいて損はなし 今回の記事では入れている前提 3. ブロックチェーンアプリケーションのプロジェクトを作成 lisk init omikuji lisk init [アプリケーション名] 4. 作成したプロジェクトを開く 以降 Visual Studio Codeなどを使って作業するのをおすすめ 5. 便利なプラグインの追加 以下のファイルを編集 /src/app/plugin.ts import { Application, HTTPAPIPlugin } from 'lisk-sdk'; // add HTTPAPIPlugin import { DashboardPlugin } from '@liskhq/lisk-framework-dashboard-plugin'; // add DashboardPlugin // @ts-expect-error Unused variable error happens here until at least one module is registered export const registerPlugins = (app: Application): void => { app.registerPlugin(HTTPAPIPlugin); // register HTTPAPIPlugin app.registerPlugin(DashboardPlugin); // register DashboardPlugin }; HTTPAPIPlugin:ブラウザ等でAPIをたたきたいときに必須 DashboardPlugin:構築したアプリケーションの動作確認に使用できるので開発中は入れておくと便利 6. モジュールとアセットの作成 Lisk SDKではモジュールとアセットで1つのトランザクションとして扱われます。 Lisk標準のモジュール・アセット(一部抜粋) moduleID assetID name 2 0 token:transfer 5 0 dpos:register 5 1 dpos:vote 5 2 dpos:unlock 今回はおみくじを作りたいので、以下のような感じにします。 moduleID assetID name 3535 0 omikuji:pull ということで lisk generate:module omikuji 3535 lisk generate:asset omikuji pull 0 lisk generate:module [モジュール名] [モジュールID] lisk generate:asset [モジュール名] [アセット名] [アセットID] ここまでやると以下のファイルが作成されます。 /src/app/modules/omikuji/omikuji_module.ts /* eslint-disable class-methods-use-this */ import { AfterBlockApplyContext, AfterGenesisBlockApplyContext, BaseModule, BeforeBlockApplyContext, TransactionApplyContext } from 'lisk-sdk'; import { PullAsset } from "./assets/pull_asset"; export class OmikujiModule extends BaseModule { ...... public name = 'omikuji'; public transactionAssets = [new PullAsset()]; public events = [ // Example below // 'omikuji:newBlock', ]; public id = 3535; ...... public async afterGenesisBlockApply(_input: AfterGenesisBlockApplyContext) { // Get any data from genesis block, for example get all genesis accounts // const genesisAccounts = genesisBlock.header.asset.accounts; } } /src/app/modules/omikuji/asset/pull_asset.ts import { BaseAsset, ApplyAssetContext, ValidateAssetContext } from 'lisk-sdk'; export class PullAsset extends BaseAsset { public name = 'pull'; public id = 0; // Define schema for asset public schema = { $id: 'omikuji/pull-asset', title: 'PullAsset transaction asset for omikuji module', type: 'object', required: [], properties: {}, }; public validate({ asset }: ValidateAssetContext<{}>): void { // Validate your asset } // eslint-disable-next-line @typescript-eslint/require-await public async apply({ asset, transaction, stateStore }: ApplyAssetContext<{}>): Promise<void> { throw new Error('Asset "pull" apply hook is not implemented.'); } } また、以下のファイルに作成されたモジュールが自動で追加されます。 /src/app/modules.ts import { Application } from 'lisk-sdk'; import { OmikujiModule } from "./modules/omikuji/omikuji_module"; // add // @ts-expect-error Unused variable error happens here until at least one module is registered export const registerModules = (app: Application): void => { app.registerModule(OmikujiModule); // add }; 7. モジュールとアセットの編集 あとは自由にどんなアプリケーションにしたいか考えながら作成されたファイルを編集していきます。 今回のおみくじは以下のような仕様 1LSK以上つかってお願いすると「大吉、吉、中吉、小吉、末吉、凶、大凶」を教えてくれる 1LSK未満だと神様に怒られる おみくじの結果はアカウント情報のomikuji.kekkaに設定される おみくじを引く際のパラメータは {"onegai": 数値} /src/app/modules/omikuji/omikuji_module.ts /* eslint-disable class-methods-use-this */ import { AfterBlockApplyContext, AfterGenesisBlockApplyContext, BaseModule, BeforeBlockApplyContext, TransactionApplyContext } from 'lisk-sdk'; import { PullAsset } from "./assets/pull_asset"; export class OmikujiModule extends BaseModule { public actions = {}; public reducers = {}; public name = 'omikuji'; public transactionAssets = [new PullAsset()]; public events = []; public id = 3535; // アカウント情報に追加するおみくじの結果を設定するためのスキーマの設定 public accountSchema = { type: 'object', properties: { kekka: { fieldNumber: 1, dataType: 'string', maxLength: 64, }, }, default: { kekka: '', }, }; // Lifecycle hooks public async beforeBlockApply(_input: BeforeBlockApplyContext) {} public async afterBlockApply(_input: AfterBlockApplyContext) {} public async beforeTransactionApply(_input: TransactionApplyContext) {} public async afterTransactionApply(_input: TransactionApplyContext) {} public async afterGenesisBlockApply(_input: AfterGenesisBlockApplyContext) {} } /src/app/modules/omikuji/asset/pull_asset.ts import { BaseAsset, ApplyAssetContext, ValidateAssetContext, cryptography } from 'lisk-sdk'; export class PullAsset extends BaseAsset { public name = 'pull'; public id = 0; // トランザクション実行時のパラメータ用のスキーマを設定 public schema = { $id: 'omikuji/pull-asset', title: 'PullAsset transaction asset for omikuji module', type: 'object', required: ["onegai"], properties: { onegai: { dataType: 'uint64', fieldNumber: 1, }, }, }; public validate({ asset }: ValidateAssetContext<{onegai:BigInt}>): void { // 1LSK未満なら怒る if (asset.onegai < BigInt(100000000)) { throw new Error( '1LSKくらいはらうのじゃー' ); } } // eslint-disable-next-line @typescript-eslint/require-await public async apply({ asset, transaction, reducerHandler, stateStore }: ApplyAssetContext<{onegai:BigInt}>): Promise<void> { // おみくじ const kuji = ["大吉","吉","中吉","小吉", "末吉", "凶", "大凶"]; const kekka = kuji[parseInt(cryptography.bufferToHex(transaction.id).slice(0, 8), 16) % kuji.length]; // 送信者の情報に結果を設定 const senderAddress = transaction.senderAddress; const senderAccount:{address: Buffer, omikuji:{kekka:string}} = await stateStore.account.get(senderAddress); senderAccount.omikuji.kekka = kekka; stateStore.account.set(senderAccount.address, senderAccount); // 送信者の残高からお願いに使ったLSKを減らす await reducerHandler.invoke("token:debit", { address: senderAddress, amount: asset.onegai, }); } } 8. Genesis Blockの作成準備 ./bin/run genesis-block:create --output config/default A genesis_block file already exists at the given location. Do you want to overwrite it?と聞かれたらy すると、config/defaultに以下のファイルが生成されます accounts.json forging_info.json genesis_block.json password.json config/default内の各JSONファイル(config.jsonを含む)はGitHubなどで公開する際は十分気をつけましょう。 ここで生成されるファイルを公開し、そのままアプリをリリースしようものなら大変なことになります。 絶対にアカウントのパスフレーズや、パスワードは公開してはいけません。 forging_info.jsonおよびpassword.jsonの内容をconfig.jsonに反映 tmp=$(mktemp) jq '.forging.delegates = input' config/default/config.json config/default/forging_info.json > "$tmp" && mv "$tmp" config/default/config.json jq '.forging += input' config/default/config.json config/default/password.json > "$tmp" && mv "$tmp" config/default/config.json 9. ブロックチェーンアプリケーションの起動 ./bin/run start --api-ws 10. 動作確認 ブラウザから http://localhost:4005 にアクセス Send transactionから作成したモジュール・アセットを選択して「Submit」 動作確認の際は accounts.json に記載されているものを使用すると楽 なお、1LSK = 100,000,000 成功するとこんな感じ onegai を 1LSK未満にすると怒られる HttpAPIで成功した際のトランザクションIDを確認してみる HttpAPIでおみくじの結果(送信者のアカウント情報)を確認してみる 「大凶」だったみたいです... ということで SDKの機能の1割も使ってないんじゃないかというくらい簡単なアプリでしたがなんとなくイメージは掴めたでしょうか? 必要スペックも低い、ブロックチェーン構築も楽、アプリケーションの実装も楽 言うことなしだと思いません!? ぜひ触ってみてくださいね! なお、開発で困ったことがあれば Lisk 公式Discord や、Lisk Dev forum で尋ねるとコミュニティの誰かがきっと助けてくれます! また、ブロックチェーンアプリケーションを本格的なプロジェクトとして構築していくのであればGrant Programに応募してみてはどうでしょうか? 読んでいただきありがとうございました! お疲れ様でした! 補足:ブロックチェーンアプリケーションの削除 rm -rf ~/.lisk/[アプリケーション名] データのみ消す場合は rm -rf ~/.lisk/[アプリケーション名]/data
- 投稿日:2021-12-03T15:05:57+09:00
【Vue.js】v-bindによる双方向バインディングの仕組み
case v-bindで双方向のバインディングを行う場合, 入力された値が格納されている$event.target.valueへvue側が参照している状態になる。 これをシンプルに記述できるようにしているのがv-modelディレクティブ <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> <div id="app"> <input :value="test" @input="test = $event.target.value"> //この部分 </div> <script> let app = new Vue({ el:'#app', data(){ return { test:'aaa' } } }) </script> </body> </html>
- 投稿日:2021-12-03T14:01:28+09:00
[JavaScript] 配列要素の追加方法
JavaScriptにおける配列要素の追加についてまとめておく。 今回サンプルとして使用する配列は以下としておく。 const color = ["red", "white", "blue", "green"] ちなみにconstは再代入ができないが、今回紹介するpush()などのメソッド操作は行うことができる。また、今回紹介するpush()とunshift()は配列(今回では配列color)そのものへ要素を追加し、内容が変更されていまう破壊的メソッドであることに注意が必要である。 新しい要素の追加 配列の末尾に追加 配列の末尾に1つ以上の要素を追加する場合は、push()を使用する。返り値は、追加後の配列の要素数。 color.push("yellow","black"); console.log(color); // [ 'red', 'white', 'blue', 'green', 'yellow', 'black' ] 配列の先頭に追加 配列の先頭に1つ以上の要素を追加する場合は、unshift()を使用する。返り値は、追加後の配列の要素数。 color.unshift("yellow","black"); console.log(color); // [ 'yellow', 'black', 'red', 'white', 'blue', 'green' ] 配列の指定位置に追加 配列の指定位置に追加したい場合は、splice()を使用する。splice()の引数は以下となる。 第一引数:配列の追加・削除を行う位置(負の数で配列末尾から引いた位置も指定可) 第二引数:削除する要素数(今回は追加のため0を指定) 第三引数:追加する要素 返り値は、削除された要素の配列が返される。削除されない場合は空の配列が返される。 const result = color.splice(2,0,"yellow","black"); console.log(color); // [ 'red', 'white', 'yellow', 'black', 'blue', 'green' ] console.log(result) // [] 今回は要素の追加だけを取り上げたが、splice()は要素の置き換えや削除も行うことができる。
- 投稿日:2021-12-03T09:46:50+09:00
【JavaScript】Reflectの使い方
はじめに Udemyの【JS】ガチで学びたい人のためのJavaScriptメカニズムの講座の振り返りです。 前回の記事 目的 Reflectについての理解を深める 本題 1.Reflect ReflectとはJSエンジンの内部の汎用的な関数を呼び出すメソッドが格納されているオブジェクト 内部メソッド Reflect [[Get]] get [[Set]] set [[Delete]] deleteProperty [[Construct]] construct JSエンジンの内部のメソッドをReflectで呼び出せる Reflectの使用目的 内部メソッドを呼び出す関数の格納場所 Proxyと合わせて使用するため 例1 前提 クラスを定義している main.js class C { constructor(a, b) { this.a = a; this.b = b; } } 今までのやり方通り、new演算子を用いてインスタンス化を行う main.js class C { constructor(a, b) { this.a = a; this.b = b; } } // インスタンス化したものを変数に格納 const obj1 = new C(1,2) // C {a: 1, b: 2}と出力される console.log(obj1); これをReflectを使用してインスタンス化 main.js class C { constructor(a, b) { this.a = a; this.b = b; } } // new演算子は内部的にReflectのConstructと同様に内部メソッドとして呼んでいる // 下記のようにメソッドを使用してインスタンス化できる // 第一引数にコンストラクタ関数を、第二引数に配列として値を設定する const obj2 = Reflect.construct(C, [1,2]); // obj1と同様にC {a: 1, b: 2}と出力される console.log(obj2); 演算子を用いていたものを関数表記にできるというのがReflectの特徴 例2 in演算子もReflectを用いて関数表記にできる main.js class C { constructor(a, b) { this.a = a; this.b = b; } } const obj2 = Reflect.construct(C, [1,2]); console.log(obj2); // aという値がobj2に含まれているかどうか確認できる(true or falseで出力) console.log("a" in obj2); // 上記は Reflectのhasというメソッドと同様の内部メソッドを呼んでいる // 第一引数にはオブジェクト、第二引数には判定する値(ここではfalse)を入れる console.log(Reflect.has(obj2, "c")) 例3 静的メソッド(インスタンス化しなくても使えるメソッド)もReflectで使える main.js // 従来の書き方 Object.defineProperty // 下記のように書くことも可 Reflect.defineProperty Objectではエラーを返すが、Reflectではfalseを返す main.js // try...catch構文でエラー出力 try{ Object.defineProperty } catch(e) { } // 上記のように書くのは面倒 if(Reflect.defineProperty){ // trueであればここのブロック結果が出力 } else { // falseであればここのブロック結果が出力 } 例4 前提 二つのオブジェクトを用意 main.js const bob = { name: 'Bob', _hello: function () { console.log(`hello ${this.name}`); } } const tom = { name: 'Tom', _hello: function () { console.log(`hello ${this.name}`); }, get hello() { return this._hello(); }, } getをReflectを用いて使う main.js const bob = { name: 'Bob', _hello: function () { console.log(`hello ${this.name}`); } } const tom = { name: 'Tom', _hello: function () { console.log(`hello ${this.name}`); }, get hello() { return this._hello(); }, } // 一般的なオブジェクトの呼び方 // hello Tom と出力される tom.hello; // これは内部的にはReflectのgetを呼んでいるのと同じ意味になる // 第一引数にはオブジェクト、第二引数には値を入れる Reflect.get(tom, "hello") // 第三引数にはreceiverを入れることも可能 // receiverに登録されたオブジェクトがgetメソッドの中で使用されるthisに束縛される = bind // 下記のようにbobを渡すとhello Bobと出力される Reflect.get(tom, "hello", bob) // 第三引数を書かなければ第一引数と同じになる 今日はここまで! 参考にさせて頂いた記事 【JS】ガチで学びたい人のためのJavaScriptメカニズム Let'sプログラミング JavaScript入門
- 投稿日:2021-12-03T09:36:33+09:00
Vue.jsについて振り返る
はじめに 今回は、普段開発する際に書いているコードは、ちゃんと中身を理解してかけているかという点を振り返るために記事を投稿します。使用言語は"Vue.js"です。 ここで振り返りを行う目的は、 ・なんとなく書いていたコードをきちんと理解すること ・今後エラーが発生した際に、自分の力で対応できるようになること になります。 ※実際のコードで説明します と記載がある部分は後ほど記事を修正する予定です。 目次 本記事の目次になります 1.Vueのライフサイクル 2.コンポーネントの構図・仕組 3.propsと$emit 4.グリッドkeyの役割 5.DOM直接参照($refs) 6.v-slot 7.Vuex 1. Vueのライフサイクル 各 Vue インスタンスは、生成時に一連の初期化を行います。例えば、データの監視のセットアップやテンプレートのコンパイル、DOM へのインスタンスのマウント、データが変化したときの DOM の更新などがあります。その初期化の過程で、特定の段階でユーザー自身のコードを追加する、いくつかの ライフサイクルフック(lifecycle hooks) と呼ばれる関数を実行します。 それぞれの関数の流れを示した図と一覧は以下の通り。 -Vue.js公式ドキュメントから引用 ライフサイクル タイミング 備考 beforeCreate インスタンスは生成されたがデータが初期化される前 - created インスタンスが生成され、且つデータが初期化された後 - beforeMount インスタンスが DOM 要素にマウントされる前 - mounted インスタンスが DOM 要素にマウントされた後 コンポーネントがHTML要素の一員として画面に描画されている状態。DOM にアクセス可能ということ beforeUpdate データは更新されたが DOM に適用される前 - updated データが更新され、且つ DOM に適用された後 値の変更タイミングで DOM を自動的に更新し、再描画する beforeDestroy Vue インスタンスが破壊される前 v-if,v-forなどのインスタンスが表示されなくなるタイミングで呼ばれる destroyed Vue インスタンスが破壊された後 - 以下参考 2. コンポーネントの構図・仕組 コンポーネントは、Vue.js の強力な機能の一つである。機能を持つUI部品ごとにテンプレートとJavaScriptを一つのセットにして、他の部品とは切り離した開発及び管理ができるようにする仕組み・機能。 自身が使ってみて感じたメリットを記載します。 自身が感じたメリット ... ①. 単一ファイルコンポーネント※で開発できるため、管理し易い このファイルは何をするものかがわかりやすい ※単一ファイルコンポーネント ... HTML, CSS, JavaScriptを1ファイルにまとめて書くこと ②. 記述量が多くなった場合でもに複雑に感じにくい どこでイベントを呼ぶ、メソッドを書く、といったことを記述する場所がはっきりわかれていから 以下はアプリケーションがネストされたコンポーネントのツリーイメージ図 -Vue.js 公式ドキュメントから引用 3. propsと$emit 以下の定義で整理する。また整理した図も引用する。 属性 定義 props 親から子へのデータの受け渡し $emit 子から親へのイベントの通知 ※実際のコードで説明します -こちらの記事から引用(https://www.hypertextcandy.com/vuejs-components-introduction-communication-between-components) [補足]プロパティを使用した子コンポーネントへのデータの受け渡し -Vue.js 公式ドキュメントから引用 """ 表示する特定のコンテンツなどのデータをコンポーネントに渡すことができない限り、そのコンポーネントは役に立たないということです。プロパティはここで役立ちます。 プロパティはコンポーネントに登録できるカスタム属性です。値がプロパティ属性に渡されると、そのコンポーネントインスタンスのプロパティになります。ブログ投稿コンポーネントにタイトルを渡すには、props オプションを使用して、このコンポーネントが受け入れるプロパティのリストにそれを含めることができます: """ Vue.component('blog-post', { props: ['title'], template: '<h3>{{ title }}</h3>' }) 4. グリッドのkeyの役割 グリッドの Key の役割とは... 要素の識別と効率的な描画処理を可能とするもの Keyを指定する場合としない場合で、グリッドに対して操作した場合以下のような違いが起きる。 Key ある場合 Key がない場合 消滅した key の DOM が削除されるだけ 要素の文字が変更されたと解釈し、順番の変わった要素を全て更新してしまう ※実際のコードで説明します 5. DOM直接参照($refs) そもそも ref 属性とは... 分散されたネーム・レゾリューションを容易に行ったり、複数のサーバーに渡って検索を行ったりするために使用される属性を指す。 参照するサーバー内で指定する項目に出現します。ref 属性の値は、参照されるサーバー内で保持されている項目を指します。 言い換えると ... 取得したいコンポーネントのタグ内に、ref属性を付与することでマーキングし、実際にref 属性でマーキングしたコンポーネントを取得することができる。 以下参考 6. v-slot v-slotとは ... 親となるコンポーネント側から、子のコンポーネントのテンプレートの一部を差し込む機能 ※実際のコードで説明します 以下参考 7. Vuex ※Vue.js 公式ドキュメントより概要をおさらいし、実際のコードで説明 Vuex ... Vue.js アプリケーションのための 状態管理パターン + ライブラリ 以下はVuexの全体像を表した図 -Vue.js 公式ドキュメントから引用 ※実際のコードで説明します まとめ なんとなくイメージしているコードを、ちゃんと理解することは、エラーが出た際にも何が原因かを自力で追えるようになると思います。今回記事にできなかった分も追加していく予定です。
- 投稿日:2021-12-03T09:03:32+09:00
知ってると得をするReactコンポーネントのイケてる書き方
はじめに 基本的にReact + TypeScriptでフロントの開発をしているんですが、実際にコードを書いている時に気をつけていること、便利な書き方として知っておくと得をするReactコンポーネントの書き方を紹介します。 Propsが多くなりすぎたら やたらpropsが多くなってしまうことありませんか?しかも同じような名称ばっかりを何回も書くことになるという。そうゆうときはできる限りショートハンドで書きましょう。 return ( <SampleComponent type={user.type} name={user.name} email={user.email} image={user.image} /> ) Componentに全てのPropsを渡す場合は下記のようにするとコード量がだいぶ減りますね。 return ( <SampleComponent {...user} /> ) Component側に渡すのが全てではなくいくつか決められたものの場合はこちらです。 const {type, name, email, image} = user return ( <SampleComponent {...{type, name, email, image}} /> ) とはいえ、ショートハンドを覚えるのも大事ですが、Propsが多すぎる場合にはそもそComponentの分割を検討した方が良いでしょう。 React.FCの拡張 ComponentをCSS modulesやCSS in JSで作る際に className をPropsで渡せるようにしたいですよね?もちろん下記のようにすればclassNameを渡せるんですけど、いちいち数十個、数百個もあるComponentに対して型定義するのは正直めんどくさい。 type Props = { className?: string } export const Component: FC<Props> = ({ children, className }) => { return <div className={classnames(style.heading, className)}>{children}</div> } こちらの React.FC 型を拡張する - Qiita を参考にさせてもらって、上記問題も無事解決! ざっくりやり方を説明すると別の型を用意して、declare module に新しい型を追加するだけ! プロジェクトの任意のディレクトリに type.ts を作ってReact.FCに className を追加した FCX を定義します。 declare module 'react' { type FCX<P = {}> = FunctionComponent<P & { className?: string }> } もちろん VFCも同じように作ることができます。 ComponentのタグをPropsで渡すようにする ページの見出しで使う用にComponentをh1で作ったはいいけど、ここはh2の方が適切じゃね?ってことありませんか。「あー、コンポーネントのタグ変えられたら楽なのに」ってなりますよね。それ、実はできます。 export const Heading: FCX<Props> = ({ children }) => { return <h1 className={classnames(style.heading, className)}>{children}</h1> } propsに渡すことでできます。注意点としては Component のように最初を大文字にしてください。というのもReactでコンポーネントを書くときは <Component></Component> のように最初の文字は大文字ですよね。それと同じです。 型はもちろん string でもいけるんですが、存在しないタグはダメなので、下記のように型ガードさせるようにしましょう。 type Props = { Component?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' } export const Heading: FCX<Props> = ({ children, Component = 'h1' }) => { return <Component className={classnames(style.heading, className)}>{children}</Component> } Function as children childrenにpropsを渡すことができるって知ってました?例えば下記のようにボタンをクリックしたらinput要素が現れるComponentを作ることもできます。 比較的同じような動作をしてかつ、同一ページに複数存在する可能性があるようなComponentに向いている気がしますね。例えば、モーダルとかドロップダウンメニューとか。 type Props = { children: (collapsed: boolean, toggleCollapse: () => void) => ReactNode; }; export const CollapsibleComponent: FC<Props> = ({ children }) => { const [collapsed, setCollapsed] = useState(false); const toggleCollapse = () => { setCollapsed(!collapsed); }; return <div>{children(collapsed, toggleCollapse)}</div>; }; export const HogeComponent = () => { return ( <div> // ・・・ <CollapsibleComponent> {(collapsed, toggleCollapse) => { if (collapsed) { return ( <div> <input type="text" value="React 太郎" /> <button type="button" onClick={toggleCollapse}> 閉じる </button> </div> ); } return ( <div> React 太郎 <button type="button" onClick={toggleCollapse}> 開く </button> </div> ); }} </CollapsibleComponent> // ・・・ </div> ) } Componentのジェネリクスの書き方 Componentに対してジェネリクス書けたらいいのにって時ありません?実は書けます。 今回は render props を使ったComponentでジェネリクスを使ってみましょう。 render propsは受け取ったpropsを描画に使うことができます。ほとんどFunction as Childrenと同じなんですけどね。以前はApollo Clientでもこのrender propsを押していたような気もします。今はhooks推しですね。 早速ですがReact Queryで取得したデータを render に渡して描画させてみます。まず全体像はこちら。 type Props<T> = { queryKey: string render: (queryResult: UseQueryResult<T>) => JSX.Element } export const Fetch = <T,>({ render, queryKey }: Props<T>): JSX.Element => { const query = useQuery(queryKey, async (): Promise<T> => api.get(queryKey), { enabled: !!queryKey }) if (query.isLoading) { return <h2>Loading...</h2> } return render(query) } export const Home: React.FC = () => { return ( <Fetch<Item[]> queryKey="items" render={({ data }) => { return ( <div> {data?.map((e) => { return <div key={e.id}>{e.title}</div> })} </div> ) }} /> ) } FetchコンポーネントではrenderとqueryKeyを受け取ってrenderにReact Queryで取得したqueryResultを渡しています。あと、 取得するデータによって型が違うので Fetch<T> のようにジェネリクスになっていますね。 で、ここで注意点なんですが、Componentでジェネリクスをアロー関数で使いたい場合は <T,> のように , を入れるようにしてください。それでできます。自分もこれは知らなかった。, をつけていないとReactプロジェクトだと「終了タグどうした!!!」って怒られますww また、loading中は Loading... と表示させて終わったら return render(queryResult) としています。 type Props<T> = { queryKey: string render: (queryResult: UseQueryResult<T>) => JSX.Element } export const Fetch = <T,>({ render, queryKey }: Props<T>): JSX.Element => { const query = useQuery(queryKey, async (): Promise<T> => api.get(queryKey), { enabled: !!queryKey }) if (query.isLoading) { return <h2>Loading...</h2> } return render(query) } // こっちの書き方でもOK! export function Fetch<T>({ render, queryKey }: Props<T>): JSX.Element { const queryResult = useQuery(queryKey, async (): Promise<T> => api.get(queryKey), { enabled: !!queryKey }) if (queryResult.isLoading) { return <h2>Loading...</h2> } return render(queryResult) } 次にFetchコンポーネントの使用部分をみてみます。まず目に着くのは <Fetch<Item[]> /> ですよね。なんとも気持ち悪い感じ笑 実はこれがComponentのジェネリクスの書き方なんです。render内で受け取ったqueryResultからdataを取り出せていますね。 export const Home: React.FC = () => { return ( <Fetch<Item[]> queryKey="items" render={({ data }) => { return ( <div> {data?.map((e) => { return <div key={e.id}>{e.title}</div> })} </div> ) }} /> ) } hooksの登場であまり活躍の場面がないrender propsですが、hooksと組み合わせることで使える場面は出てきそうですね。というかこのパターンだと毎回 Loading しなくていいしめっちゃ良さげです。 また、Componentのジェネリクスも覚えておくと便利ですね。例えば取得したデータを表示させるTableがあったとして、ジェネリクスがあれば汎用的に使えそうです。 オブジェクトリテラルをComponentに活用する 何でもかんでもif文とかswitch文で出しわけしてませんか?オブジェクトリテラルを活用するとスッキリ宣言的に書くことができますよ。 まずは条件分岐パターンです。よく見るやつですが、typeが追加されると変更箇所が ACCOUNT_TYPE と switch に追加する必要が出てきますね const SampleComponent = (account) => { const { type } = account; const ACCOUNT_TYPE = { ADMIN: "ADMIN", OPERATOR: "OPERATOR", VISITOR: "VISITOR", }; switch (type) { case ACCOUNT_TYPE.ADMIN: return <Admin />; case ACCOUNT_TYPE.OPERATOR: return <Operator />; case ACCOUNT_TYPE.VISITOR: return <Visitor />; default: return null; } }; オブジェクトリテラルを使うとこんな感じです。めっちゃスッキリ!!変更箇所もcomponentsに追加するだけ! const SampleComponent = (account) => { const {type} = account const components = { ADMIN: Admin, OPERATOR: Operator, VISITOR: Visitor }; const Component = components[type]; return <Component />; } おわりに はじめてのアドカレだったんで色々調べながら記事にしました。ReactのComponentは関数なので JavaScript、TypeScriptでできることはほぼほぼできるんですよね。 また、結構知らないこともあってしかも早速プロジェクトに実践できそうです。
- 投稿日:2021-12-03T08:12:20+09:00
ファミレスのメニューシステムもどき作ってみた
しばらくぶりにシステム作ってみました。 飲食店のチェーン店でタブレットのメニューシステムを見てた時、「作れそうかも」と思ったこと。 ですがPHPはまだまだ初心者。想像しながらの穴だらけ試作システムです。 DBも初めて連携しました。 やりたいこと - DBからメニューを書出し - メニュー注文機能の作成(追加、変更、削除) - 検索システム ブラウザからDBにメニューを入れる管理システムも作ってみました。 ▶︎ファミレスのメニューもどき管理画面を作ってみた 下記が相関関係図です。 トップページ index.php XDでざっくりデザイン作成後、HTMLとCSSで組み立て。 こんな感じのUIです。 メニューボックスをクリックするとメニュー注文へ。 ▶︎サンプルサイトはこちら 共通のDB接続はパーツ化してインクルード。 DBからSELECTで全データを取得後配列化。 index.php //DB接続のインクルードは省略 //DBより一覧表書き出し $sql_list = 'SELECT * FROM menulist order by ID ASC'; $stmt = $dbh->prepare($sql_list); $stmt->execute(); $data =array(); $count = $stmt->rowCount();//レコード数取得 while($row = $stmt->fetch(PDO::FETCH_ASSOC)){ $data[] = $row; メニュー一覧はカテゴリーごとにまとめたかったので、それぞれforeachでループしました。 カテゴリーの順番も任意で。 メインの後にドリンクがくると何かしっくりいかないし、やはりサラダ、スープ、前菜など順番は大切かなと。 foreachの中にifでカテゴリーごとにループさせるのは分かったのですが、 if($row['category'] == 'メイン') だといちいちカテゴリーごとにブロックを書かないといけないので、面倒い。時間を置いて考えて、for文で試してみました。 $cat_listという配列を手作り。任意の順番で並べたかったので下記のように記述。 $cat_list = ['メイン','サラダ','スープ','サイドメニュー','麺・パスタ','デザート','ドリンク']; それぞれのsectionに同一ページスクロール用のidも付けたかったのでid用の配列も作成。 $cat_listの内容と順番を合わせておきます。 $category_id = ['mainmenu','salad','soup','sideMenu','pasta','dessert','drink']; forの$iはforeachの中で使うとうまく出力しなかったので外側で変数に代入。 $cat_item = $cat_list[$i]; index.php <?php for($i = 0; $i < count($cat_list); $i++): ?> <!-- $iはforeachの中に入れるとちゃんと表示されないので外で変数にした --> <?php $cat_item = $cat_list[$i]; ?> <!-- idを付与 --> <section id="<?php echo $category_id[$i]; ?>"> <h2 class="catTitle"><span>Category</span><?php echo $cat_list[$i]; ?></h2> <ul class="menuBox"> <?php foreach($data as $row): ?> <!-- カテゴリーごとにまとめて書き出し --> <?php if($row['category'] == $cat_item): ?> <li class="selectOrder"> <div class="img"> <img src="<?php echo $img_path.$row['image']; ?>" alt=""> <p class="cart">注文する</p> </div> <div class="details"> <p class="material"><?php echo $row['material']; ?></p> <p class="menuName">商品名<span><?php echo $row['item']; ?></span></p> <dl> <dt>金額</dt> <dd><span class="orderPlic"><?php echo number_format($row['plice']); ?></span>円<span class="smallText">(税込)</span></dd> </dl> </div> </li> <!-- //.selectOrder --> <?php endif; ?> <?php endforeach; ?> </ul> </section> <?php endfor; ?> 思うようにリスト化出来ました。 嬉しい。 注文確認フォーム 注文確認フォームは移行せず、同一ページ内で最初は非表示にしてメニューBOXがクリックされたらformが表示。 選んだアイテムだけが書き出されるようjsで操作。 ここはcount数で悩まされました。 jsで++countを置いているので、初期値を-1に <input id="count" type="hidden" name="count_menu" value="-1"> bace.js //submitで持っていく値の個数と番号の変数 let count; //商品をクリックしたら注文リストへ jQuery('.selectOrder').on('click',function(){ //選んだ商品データを変数に入れておく const orderName = jQuery('.menuName span',this).text(); const orderImg = jQuery('.img img',this).attr('src'); const orderPlice = jQuery('.orderPlic',this).text(); //カウントを取得しておかないとおかしくなる count = jQuery('#count').attr('value'); //count位置重要 ++count; //TOP注文リスト表示、メニュー一覧非表示 jQuery('#orderBox').css('display','block'); jQuery('#header,#main').css('display','none'); //追加・変更ページ jQuery('#orderChangeBox').css('display','block'); jQuery('#headerChange,#mainChange').css('display','none'); //注文の商品名、値段、画像、個数を書き出し、カウント数と合わせておく //tableの中に要素書き出し jQuery('#orderMenu,#changeMenu').append('<tr><th class="orderName">'+orderName+'</th><td class="orderImg"><img src="'+orderImg+'" alt=""></td><td class="orderOneplice">'+orderPlice+'円<span class="smallText">(税込)</span><img src="image/icon_x.svg" alt=""></td><td class="num"><input type="number" name="num'+count+'" min="1" max="50" value="1">個</td><td><p class="deleteMenu">削除<img src="image/icon_x.svg" alt=""></p></td></tr>'); //name属性+カウント数でgetで持っていく値と数を指定 jQuery('#count').before('<input type="hidden" name="title'+count+'" value="'+orderName+'"><input type="hidden" name="plice'+count+'" value="'+orderPlice+'"><input type="hidden" name="img'+count+'" value="'+orderImg+'">'); jQuery('#count').attr('value',count); }); //MENU削除 jQuery(document).on('click', '.deleteMenu', function(){ //デリートボタンを押したtrのみを削除 jQuery(this).parents('tr').remove(); //現在のカウントを取得 count = jQuery('#count').attr('value'); //削除によって揃わなくなった個数と順番の連動を合わせる for(let i=0; i < count; ++i){ jQuery('input[name^="title"]').eq(i).attr('name','title'+i); jQuery('input[name^="plice"]').eq(i).attr('name','plice'+i); jQuery('input[name^="num"]').eq(i).attr('name','num'+i); jQuery('input[name^="img"]').eq(i).attr('name','img'+i); } //1個ずつ持っていく数を減らす --count; jQuery('#count').attr('value',count); //確認リストから商品がなくなったらメニュー一覧を表示 if(count < 0){ jQuery('#orderBox,#orderChangeBox').css('display','none'); jQuery('#header,#main,#headerChange,#mainChange').css('display','block'); } }); jQueryでappendメソッドで後から追加した要素にclickイベントを効かせるようにするには要素に対してではなく、documentに対してイベントを設定しないとダメみたいです。 受け取り先の$_GETでは for($i = 0; $i < $count_menu+1; $i++) こうすると数が合うようになりました。 order.php $count_menu = isset($_GET['count_menu']); //XSS関数作成html_escape() $count_menu = html_escape($_GET['count_menu']); $count_menu = (int)$count_menu; $order_name = []; $one_plice = []; $num = []; $sub_plise = []; $order_img = []; //設定したカウント数でname値を書き出し配列へ入れる for($i = 0; $i < $count_menu+1; $i++){ $order_name[$i] = html_escape($_GET['title'.$i]); $one_plice[$i] = html_escape($_GET['plice'.$i]); $order_img[$i] = html_escape($_GET['img'.$i]); //文字列内にある”,”を""に変換 $one_plice[$i] = str_replace(',', '',$one_plice[$i]); $one_plice[$i] = (int)$one_plice[$i]; $num[$i] = html_escape($_GET['num'.$i]); $num[$i] = (int)$num[$i]; //値段と個数を掛けておく $sub_plise[$i] = $one_plice[$i] * $num[$i]; } 決定的な問題は検索機能だった TOPページの注文表示はjsのclickイベント表示、検索機能はformのsubmitで表示。 いずれも同一ページ内での操作。 clickイベントでアイテムがあった場合、検索すると再読み込みになってデフォルトに戻る。 要はclickで溜まったアイテムは消えて、検索結果から選んだアイテムに上書きされてしまいました。 最初はTOPページの注文表でリストを作り、追加変更が決まったらオーダー画面へ移行だった。 それが、TOPページの注文表で一つメニューを選んだら、オーダー画面に移行。その後追加変更ページという中途半端なフローになってしまいました。 もともとオーダー画面から追加変更ページに戻る設計は考えていました。 合計を見てから、「やっぱり変えたい」というお客さんはいると思うので。 その場合、データはJSONに格納します。ページ以降するたびformでname値を持っていくのはややこしいので。 追加変更ページ ここは検索機能を削除したトップページと同じ作り。 ただ、メニュー一覧は非表示で注文リストが表示された状態。 オーダーから移行してきたら注文リストへ行きたかったので。 検索機能は一番最後に作ったので、最後の最後で苦肉の策でした。 出来れば金額とキーワード検索も試したかったのですが、とてもそれどころじゃなかったです… 以下サンプルコードです。 共通インクルードファイル db_join.php <?php //DBに接続 $dsn = 'mysql:dbname=sample;host=localhost;charset=utf8'; $user ='root'; $password = ''; $dbh = new PDO($dsn,$user,$password); $dbh->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION); //DBへ格納した画像パス $img_path = '/menulist/control/img_up/'; //XSS function html_escape($word){ return htmlspecialchars($word,ENT_QUOTES,'UTF-8'); } //エラーチェック function check_word($word,$length){ if(mb_strlen($word) === 0 || mb_strlen($word) > $length){ return FALSE; } else{ return TRUE; } } //書き出し一覧のカテゴリーループに使う配列 $cat_list = ['メイン','サラダ','スープ','サイドメニュー','麺・パスタ','デザート','ドリンク']; //一覧の#セレクタ、同一スクロール用配列 $category_id = ['mainmenu','salad','soup','sideMenu','pasta','dessert','drink']; select_cat.php <?php //カテゴリーoption $cat_item = ['メイン','サラダ','スープ','サイドメニュー','麺・パスタ','デザート','ドリンク']; $selected = ''; ?> <?php for($i=0; $i < count($cat_item); $i++): ?> <?php if(isset($select_cat)){ if($select_cat === $cat_item[$i]){ $selected = 'selected="selected"'; } else { $selected = ''; } } ?> <option value="<?php echo $cat_item[$i]; ?>" <?php echo $selected; ?>><?php echo $cat_item[$i]; ?></option> <?php endfor; ?> ※select_mt.phpは素材のoption書出しインクルードファイルで、作りはselect_cat.phpと同じです。 index.php <?php try{ include_once(dirname(__FILE__).'/control/db_join.php'); //DBより一覧表書き出し $sql_list = 'SELECT * FROM menulist order by ID ASC'; $stmt = $dbh->prepare($sql_list); $stmt->execute(); $data =array(); $count = $stmt->rowCount();//レコード数取得 while($row = $stmt->fetch(PDO::FETCH_ASSOC)){ $data[] = $row;//FETCH_ASSOCで配列として書き出して代入 } //検索フォーム $cat_name = ''; $mt_name = ''; $sarch_plice = ''; $sarch_cat = ' none'; $sarch_mt = ' none'; $sarch_cat_mt = ' none'; $sarch_class = 'none'; if($_SERVER['REQUEST_METHOD'] === 'GET'){ if(isset($_GET['cat'])){ $cat_name = html_escape($_GET['cat']); $sarch_class = ''; } if(isset($_GET['material'])){ $mt_name = html_escape($_GET['material']); $sarch_class = ''; } if($cat_name && $mt_name== ''){ $sarch_cat = ''; $sarch_mt = ' none'; $sarch_cat_mt = ' none'; } elseif($mt_name && $cat_name == '') { $sarch_mt = ''; $sarch_cat = ' none'; $sarch_cat_mt = ' none'; } elseif($cat_name !== '' && $mt_name !== ''){ $sarch_cat_mt = ''; $sarch_mt = ' none'; $sarch_cat = ' none'; } else { $sarch_cat_mt = ' none'; $sarch_mt = ' none'; $sarch_cat = ' none'; } } } catch (PDOException $e){ print($e->getMessage()); die(); } ?> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>DBテストメインメニュー</title> <link rel="stylesheet" href="/menulist/common/sanitize.css"> <link rel="stylesheet" href="/menulist/common/style.css"> <!--ファビコン32x32--> <link rel="shortcut icon" href="favicon.ico" type="image/vnd.microsoft.icon"> </head> <body> <div id="wrapper"> <header id="header"> <h1 class="topTitle">DBテストメニュー</h1> <p class="center">同一ページスクロール</p> <nav> <!-- 同一ページスクロール --> <ul class="topNav"> <!-- $cat_list配列はdb_join.phpに記述 --> <!-- href=#だと検索の時、挙動が怪しいのでdata属性にした リンクに飛ぶせい?--> <?php for($i = 0; $i < count($cat_list); $i++): ?> <li> <a data-id="<?php echo $category_id[$i]; ?>"><?php echo $cat_list[$i]; ?></a> </li> <?php endfor; ?> </ul> </nav> <!-- 検索ボタン --> <div id="sarch" class="btn"><a href="dummy.html">検索BOX</a></div> <!-- 検索BOX --> <div id="sarchBox"> <h3>AND検索BOX</h3> <form id="andSarch" method="get" action=""> <div id="ones"> <div id="sarchCat" class="ones__div"> <p class="onesText">カテゴリー</p> <select name="cat"> <option value="" selected="selected">選択する</option> <!-- selectはインクルード --> <?php include_once(dirname(__FILE__).'/control/select_cat.php'); ?> </select> </div> <!-- //.ones__div --> <img src="image/icon_plus.svg" alt=""> <div id="sarchMaterial" class="ones__div"> <p class="onesText">素材</p> <select name="material"> <option value="" selected="selected">選択する</option> <!-- selectはインクルード --> <?php include_once(dirname(__FILE__).'/control/select_mt.php'); ?> </select> </div> <!-- //.ones__div --> </div> <!-- //#ones --> <div id="btn"><input type="submit" value="この条件で検索する"></div> </form> <!-- 検索結果 --> <section id="sarchMenu" class="<?php echo $sarch_class; ?>"> <?php include_once(dirname(__FILE__).'/sarch_box.php'); ?> </section> <!-- // #sarchMenu--> </div> <!-- // #sarchBox--> </header> <main id="main"> <!-- $cat_list配列はdb_join.phpに記述 --> <?php for($i = 0; $i < count($cat_list); $i++): ?> <!-- $iはforeachの中に入れるとちゃんと表示されないので外で変数にした --> <?php $cat_item = $cat_list[$i]; ?> <section id="<?php echo $category_id[$i]; ?>"> <h2 class="catTitle"><span>Category</span><?php echo $cat_list[$i]; ?></h2> <ul class="menuBox"> <?php foreach($data as $row): ?> <?php if($row['category'] == $cat_item): ?> <li class="selectOrder"> <div class="img"> <img src="<?php echo $img_path.$row['image']; ?>" alt=""> <p class="cart">注文する</p> </div> <div class="details"> <p class="material"><?php echo $row['material']; ?></p> <p class="menuName">商品名<span><?php echo $row['item']; ?></span></p> <dl> <dt>金額</dt> <dd><span class="orderPlic"><?php echo number_format($row['plice']); ?></span>円<span class="smallText">(税込)</span></dd> </dl> </div> </li> <!-- //.selectOrder --> <?php endif; ?> <?php endforeach; ?> </ul> </section> <?php endfor; ?> </main> <!-- 注文確認フォーム --> <div id="orderBox"> <h2 class="topTitle">注文へ</h2> <p class="center">個数の変更ができます。まずは「注文確認」ボタンを押してください。<br>注文リストで追加変更ができます。<br>キャンセルする場合は削除ボタンを押してください。</p> <form method="get" action="/menulist/order.php"> <table class="orderMenu" id="orderMenu"> <!-- jsより書き出し --> </table> <input id="count" type="hidden" name="count_menu" value="-1"> <!-- countは-1にしないとオーダー画面で数が合わない click時にjsで++countにしている--> <input class="toBtn" type="submit" value="注文確認"> </form> </div> <!-- //#orderBox --> <footer id="footer"> <small>DBテストメニューsystem</small> </footer> </div> <!-- //#wrapper --> <script src="/menulist/common/jquery-3.6.0.min.js"></script> <script src="/menulist/common/base.js"></script> </body> </html> 検索BOXインクルードファイル sarch_box.php <h2 class="catTitle"><span>SARCH</span>検索結果</h2> <p id="sarchCount"></p> <!-- カテゴリーの検索結果 --> <ul class="menuBox<?php echo $sarch_cat; ?>"> <?php foreach($data as $row): ?> <?php if($row['category'] == $cat_name): ?> <li class="selectOrder"> <div class="img"> <img src="<?php echo $img_path.$row['image']; ?>" alt=""> <p class="cart">注文する</p> </div> <div class="details"> <p class="material"><?php echo $row['material']; ?></p> <p class="menuName">商品名<span><?php echo $row['item']; ?></span></p> <dl> <dt>金額</dt> <dd><span class="orderPlic"><?php echo number_format($row['plice']); ?></span>円<span class="smallText">(税込)</span></dd> </dl> </div> </li> <?php endif; ?> <?php endforeach; ?> </ul> <!-- 素材の検索結果 --> <ul class="menuBox<?php echo $sarch_mt; ?>"> <?php foreach($data as $row): ?> <?php if($row['material'] == $mt_name): ?> <li class="selectOrder"> <div class="img"> <img src="<?php echo $img_path.$row['image']; ?>" alt=""> <p class="cart">注文する</p> </div> <div class="details"> <p class="material"><?php echo $row['material']; ?></p> <p class="menuName">商品名<span><?php echo $row['item']; ?></span></p> <dl> <dt>金額</dt> <dd><span class="orderPlic"><?php echo number_format($row['plice']); ?></span>円<span class="smallText">(税込)</span></dd> </dl> </div> </li> <!-- //.selectOrder --> <?php endif; ?> <?php endforeach; ?> </ul> <!-- カテゴリーand素材の検索結果 --> <ul class="menuBox<?php echo $sarch_cat_mt; ?>"> <?php foreach($data as $row): ?> <?php if($row['category'] == $cat_name && $row['material'] == $mt_name): ?> <li class="selectOrder"> <div class="img"> <img src="<?php echo $img_path.$row['image']; ?>" alt=""> <p class="cart">注文する</p> </div> <div class="details"> <p class="material"><?php echo $row['material']; ?></p> <p class="menuName">商品名<span><?php echo $row['item']; ?></span></p> <dl> <dt>金額</dt> <dd><span class="orderPlic"><?php echo number_format($row['plice']); ?></span>円<span class="smallText">(税込)</span></dd> </dl> </div> </li> <!-- //.selectOrder --> <?php endif; ?> <?php endforeach; ?> </ul> base.js jQuery(function(){ jQuery('#orderBox').css('display','none'); //同一ページスクロール jQuery('.topNav a[data-id]').on ('click',function() { let dataID= jQuery(this).attr('data-id'); let position = jQuery('#'+dataID).offset().top-20; // スムーススクロール jQuery('body,html').animate({scrollTop:position}, 500, 'swing'); }); //検索ボタン jQuery('#sarchBox').css('display','block'); jQuery('#sarch a').on('click',function(){ if(jQuery('#sarchBox').css('display') == 'block'){ jQuery('#sarchBox').slideUp('first'); jQuery(this).css('background','#FA9600 url(/menulist/image/icon_plus_wh.svg) 338px center no-repeat'); return false; } else { jQuery('#sarchBox').slideDown('first'); jQuery(this).css('background','#FA9600 url(/menulist/image/icon_minus_wh.svg) 338px center no-repeat'); return false; } }); //検索件数表示 let sarchLi = jQuery('#sarchMenu .menuBox').find('li'); let liNone = jQuery('#sarchMenu ul.none').find('li'); let sarchCount = sarchLi.length - liNone.length; if(sarchCount != 0){ jQuery('#sarchCount').text(sarchCount+'件見つかりました'); } else { jQuery('#sarchCount').text('該当メニューは見つかりませんでした'); } //submitで持っていく値の個数と番号の変数 let count; //商品をクリックしたら注文リストへ jQuery('.selectOrder').on('click',function(){ //選んだ商品データを変数に入れておく const orderName = jQuery('.menuName span',this).text(); const orderImg = jQuery('.img img',this).attr('src'); const orderPlice = jQuery('.orderPlic',this).text(); //カウントを取得しておかないとおかしくなる count = jQuery('#count').attr('value'); //count位置重要 ++count; //注文リスト表示 jQuery('#orderBox').css('display','block'); jQuery('#header,#main').css('display','none'); //変更ページ jQuery('#orderChangeBox').css('display','block'); jQuery('#headerChange,#mainChange').css('display','none'); //注文の商品名、値段、画像、個数を書き出し、カウント数と合わせておく jQuery('#orderMenu,#changeMenu').append('<tr><th class="orderName">'+orderName+'</th><td class="orderImg"><img src="'+orderImg+'" alt=""></td><td class="orderOneplice">'+orderPlice+'円<span class="smallText">(税込)</span><img src="image/icon_x.svg" alt=""></td><td class="num"><input type="number" name="num'+count+'" min="1" max="50" value="1">個</td><td><p class="deleteMenu">削除<img src="image/icon_x.svg" alt=""></p></td></tr>'); //name属性+カウント数でgetで持っていく値と数を指定 jQuery('#count').before('<input type="hidden" name="title'+count+'" value="'+orderName+'"><input type="hidden" name="plice'+count+'" value="'+orderPlice+'"><input type="hidden" name="img'+count+'" value="'+orderImg+'">'); jQuery('#count').attr('value',count); }); //MENU削除 jQuery(document).on('click', '.deleteMenu', function(){ //デリートボタンを押したtrのみを削除 jQuery(this).parents('tr').remove(); //現在のカウントを取得 count = jQuery('#count').attr('value'); //削除によって揃わなくなった個数と順番の連動を合わせる for(let i=0; i < count; ++i){ jQuery('input[name^="title"]').eq(i).attr('name','title'+i); jQuery('input[name^="plice"]').eq(i).attr('name','plice'+i); jQuery('input[name^="num"]').eq(i).attr('name','num'+i); jQuery('input[name^="img"]').eq(i).attr('name','img'+i); } //1個ずつ持っていく数を減らす --count; jQuery('#count').attr('value',count); //確認リストから商品がなくなったら注文リストへ移行 if(count < 0){ jQuery('#orderBox,#orderChangeBox').css('display','none'); jQuery('#header,#main,#headerChange,#mainChange').css('display','block'); } }); //変更メニューページ jQuery('#headerChange,#mainChange').css('display','none'); //追加ボタン jQuery('#toList').on('click',function(){ jQuery('#orderBox').css('display','none'); jQuery('#header,#main').css('display','block'); //変更ページ jQuery('#orderChangeBox').css('display','none'); jQuery('#headerChange,#mainChange').css('display','block'); }); }); 注文確認ページorder.php JSONは空ファイルを用意しておきます。 order.php <?php //XSS function html_escape($word){ return htmlspecialchars($word,ENT_QUOTES,'UTF-8'); } $count_menu = isset($_GET['count_menu']); $count_menu = html_escape($_GET['count_menu']); $count_menu = (int)$count_menu; $order_name = []; $one_plice = []; $num = []; $sub_plise = []; $order_img = []; //設定したカウント数でname値を書き出し配列へ入れる for($i = 0; $i < $count_menu+1; $i++){ $order_name[$i] = html_escape($_GET['title'.$i]); $one_plice[$i] = html_escape($_GET['plice'.$i]); $order_img[$i] = html_escape($_GET['img'.$i]); //文字列内にある”,”を""に変換 $one_plice[$i] = str_replace(',', '',$one_plice[$i]); $one_plice[$i] = (int)$one_plice[$i]; $num[$i] = html_escape($_GET['num'.$i]); $num[$i] = (int)$num[$i]; $sub_plise[$i] = $one_plice[$i] * $num[$i]; } //配列の値の合計を計算 $total_plice = array_sum($sub_plise); //JSONに入れる為、配列へ $order = [$order_name,$num,$one_plice,$order_img]; //JSON形式に変換 $json = json_encode($order,JSON_UNESCAPED_UNICODE); //JSONへ格納 file_put_contents('./order.json',$json); ?> <!DOCTYPE html> <html lang="ja"> <head> <!-- 省略 --> </head> <body> <div id="wrapper"> <header id="header"> <h1 class="topTitle">注文確認</h1> <p class="center">確定ボタンで注文完了です。<br>変更される場合は「追加・変更」で戻ってください。</p> </header> <main id="main"> <form method="get" action="thanks.php"> <table class="orderMenu"> <?php for($i = 0; $i < $count_menu+1; $i++): ?> <tr> <th><?php echo $order_name[$i]; ?><input type="hidden" name="order_name<?php echo $i; ?>" value="<?php echo $order_name[$i]; ?>"></th> <!-- number_format3桁区切り --> <td class="plice"><?php echo number_format($one_plice[$i]); ?>円<span class="smallText">(税込)</span><img src="image/icon_x.svg" alt=""><input type="hidden" name="one_plice<?php echo $i; ?>" value="<?php echo $one_plice[$i]; ?>"></td> <td class="num"><?php echo $num[$i]; ?>個<input type="hidden" name="num<?php echo $i; ?>" value="<?php echo $num[$i]; ?>"></td> <td class="plice"><?php echo number_format($sub_plise[$i]); ?>円<span class="smallText">(税込)</span><input type="hidden" name="sub_plise<?php echo $i; ?>" value="<?php echo $sub_plise[$i]; ?>"></td> </tr> <?php endfor; ?> </table> <div id="total"><span>合計</span><?php echo number_format($total_plice); ?>円<span class="smallText">(税込)</span><input type="hidden" name="total_plice" value="<?php echo $total_plice; ?>"></div> <!-- メニューのクリックは無いのでcount-1の調整はいらない --> <input id="count" type="hidden" name="count_menu" value="<?php echo $count_menu; ?>"> <input class="toBtn" type="submit" value="注文確定する"> </form> <div id="toList"><a href="order_change.php">追加・変更する</a></div> </main> <footer id="footer"> <small>DBテストメニューsystem</small> </footer> </div> <!-- //#wrapper --> </body> </html> order_change.php <?php try{ include_once(dirname(__FILE__).'/control/db_join.php'); //DBより一覧表書き出し $sql_list = 'SELECT * FROM menulist order by ID ASC'; $stmt = $dbh->prepare($sql_list); $stmt->execute(); $data =array(); $count = $stmt->rowCount();//レコード数取得 while($row = $stmt->fetch(PDO::FETCH_ASSOC)){ $data[] = $row;//FETCH_ASSOCで配列として書き出して代入 } //jsonファイルを取得 $json =file_get_contents('./order.json'); $json = mb_convert_encoding($json, 'UTF8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN'); $json = json_decode($json,true); //jsonの中に入ったアイテム数、タイトルから取得 $count_menu = count($json[0]); //var_dump($json); //PHPではJSON形式のデータをそのまま扱うことができないので配列の形にする必要があり //https://hacknote.jp/archives/42243/ $order_name =array(); $num = array(); $one_plice = array(); $order_img = array(); //foreachだと値を書き出せなかった for($i = 0; $i < $count_menu; $i++){ array_push($order_name,$json[0][$i]); array_push($num,$json[1][$i]); array_push($one_plice,$json[2][$i]); array_push($order_img,$json[3][$i]); } } catch (PDOException $e){ print($e->getMessage()); die(); } ?> <!DOCTYPE html> <html lang="ja"> <head> <!-- 省略 --> </head> <body> <div id="wrapper"> <header id="headerChange"> <h1 class="topTitle">DBテスト変更メニュー</h1> <p class="center">ブラウザの再読み込みボタンは使わないでください。エラーになります。</p> <nav> <ul class="topNav"> <!-- $cat_list配列はdb_join.phpに記述 --> <?php for($i = 0; $i < count($cat_list); $i++): ?> <li> <a data-id="<?php echo $category_id[$i]; ?>"><?php echo $cat_list[$i]; ?></a> </li> <?php endfor; ?> </ul> </nav> </header> <main id="mainChange"> <!-- ここはindex.phpと同じなので省略します --> </main> <div id="orderChangeBox"> <h2 class="topTitle">注文リスト</h2> <p class="center">メニューの追加と個数の変更ができます。追加はメニュー画面へ戻ってください。<br>メニューが決まりましたら注文確定ボタンへ、キャンセルしたいメニューは削除を押してください。</p> <form method="get" action="order.php"> <table class="orderMenu" id="changeMenu"> <?php for($i = 0; $i < $count_menu; $i++): ?> <tr> <th class="orderName"> <?php echo $order_name[$i]; ?> <input type="hidden" name="title<?php echo $i; ?>" value="<?php echo $order_name[$i]; ?>"> </th> <td class="orderImg"> <img src="<?php echo $order_img[$i]; ?>" alt=""> <input type="hidden" name="img<?php echo $i; ?>" value="<?php echo $order_img[$i]; ?>"> </td> <td class="orderOneplice"> <?php echo $one_plice[$i]; ?>円<span class="smallText">(税込)</span><img src="/menulist/image/icon_x.svg" alt=""> <input type="hidden" name="plice<?php echo $i; ?>" value="<?php echo $one_plice[$i]; ?>"> </td> <td class="num"> <input type="number" name="num<?php echo $i; ?>" min="1" max="50" value="<?php echo $num[$i]; ?>">個 </td> <td> <p class="deleteMenu">削除<img src="/menulist/image/icon_x.svg" alt=""></p> </td> </tr> <?php endfor; ?> </table> <input id="count" type="hidden" name="count_menu" value="<?php echo $count_menu-1; ?>"> <div id="toList">メニューを見る</div> <input class="toBtn" type="submit" value="合計を見る"> <div id="toList"><a href="/menulist/">メニューをリセットする</a></div> </form> </div> <!-- //#orderBox --> <footer id="footer"> <small>DBテストメニューsystem</small> </footer> </div> <!-- //#wrapper --> <script src="/menulist/common/jquery-3.6.0.min.js"></script> <script src="/menulist/common/base.js"></script> </body> </html> thanks.phpは特に気になることはなく、データの表示だけなので省略します。
- 投稿日:2021-12-03T06:06:25+09:00
JavaScriptでウマ娘診断テストを作ってみた
この記事はN・S高等学校 (1) Advent Calendar 2021 の3日目の記事です。 はじめに こんにちは。N高等学校3年次のなっかのうです!もうすぐ卒業です... 12月3日...なんか三連単みたいなのでこの日付を選びました! この記事では、JavaScriptを用いてそこそこ本格的な診断テストを作ったので、そちらの簡単な仕組みや感想などを話していこうと思います! 作ったもの あなたに近いウマ娘を診断する、「ウマ娘シンダン」というものを作りました! 実際にやってみる ソースコード 主な機能 ・自分の性格がどちらに傾いているかわかるパラメータのグラフ的なものが見れる! ・自分に一番近いウマ娘、遠いウマ娘がわかる機能 ・Twitterや、URLコピペで自分の結果を共有できる機能 ・ウマ娘がアップデートで増えた際にURLさえあれば対応できる機能 ※ちなみに作者の結果です 特にこだわりのポイント 大きく二つあります。 URLで全てを管理するシステム 例えば、私の結果のURLは、 https://nakano1120.github.io/uma-sindan/question/question12.html?q1=1&q2=2&q3=2&q4=2&q5=1&q6=1&q7=2&q8=2&q9=4&q10=4&q11=1 になります。 この?q1=1のように質問ページごとに回答をURLクエリに保存することで、最後の結果のページだけで計算をし、あとはページ遷移+結果をURLクエリに書き込みだけすれば済むので、例えば質問が増えてもすぐに対応できます。 もう一つ利点があり、結果のURLさえあれば再現できるので、簡単にシェアできます。 採点システム この個性をどう評価するか考えたところ、色々調べた挙句 「ビビリさ」 「活発さ」 「好奇心の高さ」 「協調性の高さ」 「真面目さ」 の5つのパラメータを質問ごとに上下させて、最終的に出た数値を結果として出すという仕組みになっています。 例えば質問1「夏にデートをするとしたら?」で「海に行く」を答えた場合、初期値はそれぞれ50なので 「ビビリさ」 56(+6) 「活発さ」 56(+6) 「好奇心の高さ」 56(+6) 「協調性の高さ」 50 「真面目さ」 50 となります。 そしてこの結果から、それぞれのウマ娘が持っているパラメータとの差異を計算します。 私の結果は 「ビビリさ」 56 「活発さ」 100 「好奇心の高さ」 96 「協調性の高さ」 74 「真面目さ」 58 でした。この結果と一番近かったエルコンドルパサーは、 「ビビリさ」 46(差異10) 「活発さ」 99(差異1) 「好奇心の高さ」 82(差異14) 「協調性の高さ」 56(差異18) 「真面目さ」 46(差異12) 差異の合計55(これが最終結果の「距離」) となり、私の(推しであり)一番近いウマ娘になります。 さいごに 自分の推しを出すRTAするもよし、身内で楽しむもよし、共有して楽しむもよしな診断テスト、いかがでしたでしょうか? この診断テスト、結構自信作なのでぜひ広めていただけると幸いです! 来年はN高校卒業しますが、ちょくちょくqiita使っていこうと思います! 追記(おまけ) 実はこのサイトなんですが、「W3C」「WHATWG」などのWEB標準に準拠することにできる限り気を使って制作しています。 例えば、border-radiusなどの角丸は完全な推奨ではないため使っていません。 これに準拠することにより、比較的古いようなwebブラウザでも動くようになっており、 VoiceOverなどの読み上げ機能にも優しい構造になってます! 今後WEBデザインなどしていく方々はぜひWEB標準のドキュメント、目を通しておいた方がいいと思います! 一個人の意見ですが、今後のWeb業界のためにもよろしくお願いします!
- 投稿日:2021-12-03T02:54:27+09:00
複雑にネストされたMapオブジェクトをJSONに変換
Map、便利だよね キーと値のセットを持てるMapオブジェクト。 配列や既存のオブジェクトでは再現できないときに超便利。 https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map JSONに変換してPOSTしたい しかし、これをバックエンドにPOSTするためにJSONに変換するときにひと悶着起きる。 var map = new Map(); map.set('hoge','value1'); map.set('fuga','value2'); JSON.stringify(map); // '{}' からっぽ! これは以下のようにすることで解決できる var map = new Map(); map.set('hoge','value1'); map.set('fuga','value2'); JSON.stringify(Object.fromEntries(map)); // '{"hoge":"value1","fuga":"value2"}' なお、JSON.stringify([...map]); でもJSON出力は可能だが、{'hoge', 'value1'}という形式になってしまうので変換後に扱いにくい。 階層化したMapオブジェクトで躓く しかしここで、Mapの値にMapがある場合やオブジェクト配列内に存在するMapという状況が発生するとややこしくなる。 var map1 = new Map(); var map2 = new Map(); map1.set('hoge','value1'); map1.set('fuga','value2'); map2.set('hogehoge','value3'); map2.set('fugafuga','value4'); map2.set('piyo', map1); JSON.stringify(Object.fromEntries(map2)); // '{"hoge":"value1","fuga":"value2","piyo":{}}' piyoがからっぽ! var obj = { name: 'hoge', maps: map2 }; // JSON.stringify(Object.fromEntries(obj)); // MAPじゃないのでエラー JSON.stringify(obj); // '{"name":"hoge","maps":{}}' 当然Mapはからっぽ! JSON.stringifyのreplacerを使う replacerを使って型チェックを行い、MapオブジェクトならObject.fromEntriesをかけてやる。 function replacer(key, value) { if (value instanceof Map) { return Object.fromEntries(value); } else { return value; } } JSON.stringify(map2, replacer); /* { "hogehoge": "value3", "fugafuga": "value4", "piyo": { "hoge": "value1", "fuga": "value2" } } */ JSON.stringify(obj, replacer); /* { "name": "hoge", "maps": { "hogehoge": "value3", "fugafuga": "value4", "piyo": { "hoge": "value1", "fuga": "value2" } } } */ 階層が3つ、4つとなっても問題なく変換できるので、これをバックエンドにPOSTしてやれば良い。
- 投稿日:2021-12-03T01:13:36+09:00
Next2D NoCode Toolでテキストを挿入する
まず、初めに訂正を Next2D PlayerはWebGLサポートしたライブラリと説明をしましたが、テキスト(デバイスフォント)はCanvas2Dで描画しTextureに書き出しています。 後続のBlendModeやFilterといった機能の描画はWebGLで描画を行っています。 テキストツール Tool Areaにある文字ボタンがテキストツールになります。 描画ツールと同じく、ツールを選択してScreen Areaで範囲指定を行うことでテキストエリアが追加されます。 作成されたテキストエリアをダブルクリックすると入力モードに切り替わり、任意のテキストを挿入する事が可能になります。 テキストのスタイル変更 Screen Areaのテキストを選択すると、Controller Areaに選択したテキストのスタイル情報が表示されます。 Font 指定できるフォントはmacとwindowsでデフォルトで入っているであろうデバイスフォントを選べるようにしています。(フォント追加は今後の課題です。。。) Auto Resize No Resize 指定されたフォントサイズでテキストを描画します。 枠より大きいサイズを指定した場合、何も描画されません。 Box Resize フォントサイズに合わせて枠のサイズが自動で拡大縮小します。 Text Resize 枠以上のフォントサイズに設定しても、枠に収まるようフォントサイズが自動で縮小します。 Style フォントスタイルの設定を行えます。 - Normal - Bold(太文字) - Italic(斜体) - Bold/Italic(太文字/斜体) Align Left(左揃え) Center(中央揃え) Right(右揃え) Behavior Single Line 単一行:改行コードが無効になり、全て一行で描画されます。 Multiline 複数行:改行コードが有効になり、複数行で文字が描画されます。 Auto Wrap(デフォルト) 自動改行:文字数が枠を超えた場合、自動で改行されます。 Input ReadOnly(デフォルト) 読み込み専用となり、Playerでのテキスト入力は行えません。 Write 書き込みが許可され、Playerでのテキスト入力が行えるようになります。 Scroll y軸のテキストが枠外になった際にスクロールバーを表示します(実装中) - Off(デフォルト) - On Border 枠のborderの線の有効/無効設定 - Off(デフォルト) - On Size フォントサイズの指定が行えます。 サイズの幅は10から255までとなります。 Color テキストの塗りの色を指定できます。 Line Space 行の高さの設定を行えます。 Character Width テキストの文字間隔の設定を行えます。 Left Margin 左マージンの設定を行えます。 Right Margin 右マージンの設定を行えます。 Stroke Size フォント枠の幅の指定が行えます。 サイズの幅は0から255までとなります。 Stroke Color テキスト枠の塗りの色を指定できます。 動作サンプル動画 もし、この記事を見て、興味が出た方はこちらか動作確認ができます。 NoCode Tool 明日はBlendMode機能を紹介できればと思います。