20210906のJavaScriptに関する記事は22件です。

jQueryプラグイン【Multiple Select】を使って複数選択可能なselectタグを生成してみよう

最近、業務でjQueryプラグインでselectタグをカスタマイズする機会がありました。 Qiita内にあまり記事を見かけなかった為、 せっかくの機会なのでアウトプットを兼ねて記事を作成してみました! ※随時内容を追記予定です。 作成例 以下の様なプルダウンを簡単に作成可能です。 コード 公式のサンプルを少し弄ったものになります。 最低限利用するために必要なのは ・各種JS,CSSのインポート ・selectタグ内にmultiple="multiple"の指定 ・初期化処理の$('select').multipleSelect() と、非常にシンプルです。 index.html <!doctype html> <html lang="jp"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script> <script src="https://unpkg.com/multiple-select@1.5.2/dist/multiple-select.min.js"></script> <link rel="stylesheet" href="https://unpkg.com/multiple-select@1.5.2/dist/multiple-select.min.css"> <title>複数選択できるselectタグ</title> </head> <body> <select multiple="multiple"> <option value="1">1月</option> <option value="2">2月</option> <option value="3">3月</option> <option value="4">4月</option> <option value="5">5月</option> <option value="6">6月</option> <option value="7">7月</option> <option value="8">8月</option> <option value="9">9月</option> <option value="10">10月</option> <option value="11">11月</option> <option value="12">12月</option> </select> <script> $(function () { $('select').multipleSelect({ width: 200, formatSelectAll: function() { return 'すべて'; }, formatAllSelected: function() { return '全て選択されています'; } }); }); </script> </body> </html> 解説 $('select').multipleSelect()内に、オプションを指定する形となっています。 今回は以下の3つのオプションを指定してあります。 width: 200, formatSelectAll: function() { return 'すべて'; }, formatAllSelected: function() { return '全て選択されています'; } width: 200 Multipe Selectのオプションの一つです。 生成時の$('select').multipleSelect()内に記載する事で、 生成された選択エリアにstyle="width: 200px"の指定をしてくれます。 formatSelectAll/formatAllSelected 公式ドキュメントにはローカライズ用APIとして紹介されています。 formatSelectAllは【すべて選択】チェックボックスで、 formatAllSelectedは【すべて選択】時の表示メッセージです。 formatSelectAllの初期値は[Select all]、 formatAllSelectedの初期値は[All selected]ですが、 return の値を任意の値に変更する事で、表示させたい値を変更できます。 最後に リッチなUIを簡単に実現できるので、非常に便利だなと思います。 参考文献 Multiple Select公式サイト https://multiple-select.wenzhixin.net.cn/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js でタイピング練習ウェブアプリを作ってみた

Vue.jsで、多言語タイピング練習ウェブアプリを作成しました。 URL: https://www.typing-up.pro/ この記事では、主に技術的なところでポイントとなる部分を、備忘録的に解説していきます。githubでコードを公開しているので、細かいところは省き、ポイントのみの解説です。 対象は、Vue.jsの基礎をある程度知っている方です。 プログラミングの練習課題として、タイピング練習ソフトを作ることは定番かと思います。あくまで解法の一つですが、どうぞ参考にしてください。 Githubリポジトリはこちら ※ソースコードは、ローカル環境での個人利用に限り改変自由です。 仕様の概要 このアプリの特徴は、主に次の2つです。 オリジナル問題の利用: 自分で作った問題集をアップロードして、タイピング練習をすることができます。ユーザー登録は必要ありません。 多言語対応: アルファベット入力をする言語であれば、おおよそどの言語でも対応しています。自然言語だけでなく、プログラミング言語なども対応しています。日本語のように変換が必要だったり、複雑な入力体系を持つ言語は対応していません。 使い方の詳細は、ブログ記事をご覧ください。 【オリジナル問題が使える!】多言語タイピング練習アプリを公開しました タイピング正誤判定のロジック まずタイピングソフトの肝となる、キーの正誤判定のロジックです。 大まかに、次の3ステップで正誤判定をしています。 data で問題文、入力されたテキストを定義する 問題文の文字を1文字ずつ分解し配列に変換 入力された文字と問題文の配列の中身を比較し、一致していれば正、不一致であれば誤とする 1.data 定義 data () { return { question: 'This is test', //問題文。サンプルテキストを入れています typed: '', //入力された文字 mistyped: '', //ミスタイプならこちらへ表示 charnum: 0, //正答入力された文字数をカウント wordtoArray: "", //問題文を1文字ずつ分解した配列を格納 } }, 2.問題文の分解 wordtoArrayに、問題文を1文字ずつバラバラにして格納します。  this.wordtoArray = this.question.split(''); 3.問題文と入力文字の比較 キー入力は、mountedのイベントリスナで監視しています。v-modelでtexareaへの入力を監視する方法もあります。 mounted: function() { document.addEventListener('keypress', this.onKeyDown); } methodsのonKeydownで正誤判定をしています。charnumでいま何文字目まで入力しているか把握しています。wordtoArrayのn文字目と比較するというロジックです。 onKeyDown: function(e) { //正しい入力の時 if(this.wordtoArray[this.charnum] == e.key){ this.typed += e.key; //入力された文字列 this.charnum += 1; //現在n文字目 //間違えた時 } else { this.mistyped = e.key; } } あとはHTMLを設定すれば、基本の仕組みは完成です。 <p>{{question}}<p> <textarea placeholder="type here" autofocus readonly>{{typed}}</textarea> <p>{{mistyped}}<p> 問題文の読み込み 基本的な正誤判定ができたら、次は問題文を読み込む仕組みを作ります。 問題文はコードに直接記載するのではなく、外部ファイルから読み込むようにしました。 ステップとしてはおおよそ次の通りです。 assetまたはstaticフォルダにcsvファイルを設置 csvファイルをfetchで読み込み 最大問題数までランダムで取得しdataへ 2のステップからコードを載せます。 2.csvファイルをfetchで読み込み 筆者はwebpackを使いましたが、外部ファイル設置はassetでは上手くいかず、staticに設置しました。 fetch('/static/english.csv') .then(res => res.text()) .then(data => (this.questions_all = this.convertCsvStringToArray(data))) } 参考:Vue.js で CSV ファイルの読み込み methodsのconvertCsvStringToArray問題文を抽出します。 convertCsvStringToArray(str) { str = str.replace( /[^\u0000-\u1FFF]+/g, ''); //unicodeの範囲を指定。範囲外の文字を削除 str = str.split("\n").filter(Boolean); //1行ずつ問題文を抽出。空の行は削除。 var arr = []; //ランダムで15問のみ抽出して返す for (let i = 0 ; i < 15 ; i++){ let num = Math.floor(Math.random() * str.length); } return arr; }, これで question_allに15問の問題が格納されます。 それをquestion へ1つずつ取り出して、問題を表示しタイピング練習をします。 正解なら次の問題へ行き、最後の問題になったら成績ウィンドウを表示しますが、その辺りは省略します。 Unicodeで文字の範囲を指定したのは、このタイピング正誤判定ロジックでは対応できない漢字などを排除したかったからです。 かといって英語だけに限定したくなかったので、ローマ字やキリル文字、ギリシャ文字などを含むアルファベットの文字を示すUnicodeのざっくりとした範囲を設定しました。 参考:Unicode一覧 0000-0FFF - wikipedia オリジナル問題の読み込み このタイピング練習アプリでは、独自の問題を使えるという点が特徴です。 csvファイルをアップロードする仕組みを作り、あとのロジックは前述の「問題文の読み込み」と共通のfunctionを使います。 csvファイルのアップロード inputでtype="file" のボタンを設置します。loadCsvのメソッドを設定します。 <input type="file" @change="loadCsv"> loadCsvは次のように記述し、前述の を使います。 loadCsv(e) { let file = e.target.files[0]; let reader = new FileReader(); if (!file.type.match("text/csv")) { this.errorMessage = "Chose only csv file."; return; } reader.readAsText(file); reader.onload = () => { let data = reader.result; localStorage.setItem('questions_all', data); this.questions_all = this.convertCsvStringToArray(data); }; } 参考:Vue.js CSVファイルを読み込み表示[FileReader] 本番公開 本番公開はNetlify、Github Pageなどで行えます。 Vue.jsしか使っていないので、DB環境の構築などは不要です。 筆者はLaravelを使うことが多いため、Forge Laravel で本番公開しました。Forge Laravelでvueアプリを公開する方法については別記事にまとめてあります。 Laravel Forgeにvue.jsだけのSPAをデプロイする方法 最後に 実用性と「動くこと」を第一にコードを書きました。 「もっとこういうロジックがある」というようなコメントがあれば、ぜひお願いします。 なおデザインの部分はCSSフレームワークBulmaのこちらのテンプレートを使っています。 Bulma、余分なjsが無く、使いやすくて良いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Handsontable】セル内に複数選択セレクト入力(ComboTree)の表示

はじめに Handsontable でリスト選択をするには、セル種類に AutocompleteとDropdownおよびselect(正確にはセル種類ではない)を指定する方法があります。 種類 内容 デモ Autocomplete セル右端の▼ボタンのクリックでリストが表示され、リストから1つを選択する。セル内の文字入力が出来る。オプション設定でリストにないものは検証エラーにしないようにも出来るし、検証エラー(セル背景色が赤表示)にすることでも出来る。 https://handsontable.com/docs/autocomplete-cell-type/ Dropdown Autocompleteの派生、セル内の文字入力が出来る。リストにないものは検証エラーとなる。 https://handsontable.com/docs/dropdown-cell-type/ Select セルをダブルクリックすることでリストが表示され、その中から1つを選択する。セル内の文字入力が出来ない。 https://handsontable.com/docs/select-cell-type/ どれを選択したとしても、リストから1つしか選択できません。 以前、.NETで同僚から複数選択のコンボボックスってないですか? と聞かれ調査して記事を書いたことがあります。 リスト内にチェックボックスが表示されて、複数選択するとカンマ区切りでセットされるようになっています。 今回、Javascriptでリスト内にチェックボックスが表示されて複数選択できるものを調査して、「ComboTree jQuery Plugin」というのを見つけました。名前にTreeが付くように階層構造も可能になっています。 環境 HandsontableはMITライセンス版のバージョン 6.2.2を使用しています。 一応、有償版バージョン 8.3.2でも動作は確認しています。 CDN+α <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/5.9.55/css/materialdesignicons.min.css"> <link rel="stylesheet" href="https://rawcdn.githack.com/erhanfirat/combo-tree/a216a2bd4e53223ac25a3b68b319608770253861/style.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="https://rawcdn.githack.com/erhanfirat/combo-tree/a216a2bd4e53223ac25a3b68b319608770253861/comboTreePlugin.js"></script> ComboTreeの階層用アイコン表示に、MaterialDesign-WebFontを使用しています。 ComboTreeがマイナーのため、CDNに登録がありません。GitHub上のファイルをCDNとして参照するようにしました。 GitHub に上がっているJavascriptやCSSをCodePenで参照しようとしても、text/plain として返却されるため実行できないが、下記サイトで変換したURLにすることで適切な Content-Type を返してくれるため実行することができる。 仕様 表示設定 設定 値 初期値 説明 isMultiple true/false false 複数選択か単一選択かを決める cascadeSelect true/false false 複数選択時に、親の選択が子に連動するかどうかを決める source JSON Data Array   リストをJSON配列で指定する selected JSON Data Array   初期選択としてソースから対応するIDのリストをJSON配列(例 selected: [[0],[11]])で指定する collapse true/false false makes sub lists collapsed. Handsontableで使用する場合、selectedを無視します。 セルの値で一致する項目名を選択します。もし同じ項目名があった場合、最初に見つけたところが選択されます。 リスト設定 let SampleJSONData = [ { id: 0, title: 'Horse'}, { id: 1, title: 'Birds', isSelectable: false, subs: [ { id: 10, title: 'Pigeon', isSelectable: false }, { id: 11, title: 'Parrot' }, { id: 12, title: 'Owl' }, { id: 13, title: 'Falcon' } ] } 基本はリスト用識別IDのidと表示名のtitle、選択可能/不可設定のisSelectableで、falseに設定すると一覧から選択できないようなります。階層構造として、subsとして同じように指定します。また、subsの中にsubsを指定することも出来ます。 カラム設定 Handsontableのcolumns設定に、comboWidthオプションを新規に追加してあります。comboWidthオプションを指定しない場合、セルの横幅と同じ幅でリストが表示されますが、指定した場合は設定した値の横幅でリストを表示します。 let hot = new Handsontable(document.getElementById("grid"), { data: data, columns:[ { data: 'A', type: 'text', width: 200, renderer: 'autocomplete', editor: 'combotree', comboTreeConfig: { source: SampleJSONData, isMultiple: true, cascadeSelect: false } }, { data: 'B', type: 'text', width: 200, renderer: 'autocomplete', editor: 'combotree', comboTreeConfig: { source: SampleJSONData2, isMultiple: true, cascadeSelect: true } }, { data: 'C', type: 'text', width: 150, renderer: 'autocomplete', editor: 'combotree', comboTreeConfig: { source: SampleJSONData, isMultiple: false }, comboWidth: 200 } ], 注意 複数値はカンマ区切りですが、カンマの後に空白が1つ付いています。 let data = [ { A:'Parrot, Horse, Fox',B:'Two Wheels, Cycle, Motorbike, Scooter, Bus', C:'Parrot' } ]; 値を分解する際には、カンマ区切り後の値にtrimStart()をかけています。 実装 See the Pen Handsontable ComboTree by やじゅ (@yaju-the-encoder) on CodePen. ソースコード <!DOCTYPE html> <html lang="jp"> <body> <head> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/5.9.55/css/materialdesignicons.min.css"> <link rel="stylesheet" href="https://rawcdn.githack.com/erhanfirat/combo-tree/a216a2bd4e53223ac25a3b68b319608770253861/style.css"> </head> <div id="grid"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="https://rawcdn.githack.com/erhanfirat/combo-tree/a216a2bd4e53223ac25a3b68b319608770253861/comboTreePlugin.js"></script> <script type="text/javascript"> (function(Handsontable){ 'use strict'; const ComboTreeEditor = Handsontable.editors.SelectEditor.prototype.extend(); const findItembyTitle = (item, source) => { for (let i = 0; i < source.length; i++) { if (source[i].title == item) return source[i].id; if (source[i].hasOwnProperty("subs")) { let found = findItembyTitle(item, source[i].subs); if (found) return found; } } }; ComboTreeEditor.prototype.open = function() { event.stopPropagation(); this.refreshDimensions(); if(this.comboInput) this.comboInput.destroy(); this.combo = document.createElement('input'); this.combo.setAttribute('type', 'text'); this.comboStyle = this.combo.style; this.comboParent = document.createElement('DIV'); this.comboParentStyle = this.comboParent.style; let wid = this.TD.clientWidth; if(this.cellProperties.comboWidth != undefined) wid = this.cellProperties.comboWidth; this.comboParentStyle.width = wid + 'px'; this.comboParent.appendChild(this.combo); this.instance.rootElement.appendChild(this.comboParent); const that = this; this.comboInput = $(this.combo).comboTree(this.cellProperties.comboTreeConfig); this.comboInput.onChange(() => { that.instance.setDataAtCell(that.row, that.col, that.combo.value); }); this.comboInput._elemInput.val(this.originalValue); this.comboStyle.display = 'block'; $('.comboTreeArrowBtn').hide(); $(document).off('mouseup.' + this.comboInput.comboTreeId); this.combo.focus(); this.comboStyle.display = 'none'; let $td = $(this.TD); let offset = $td.offset(); let $pop = $('.comboTreeDropDownContainer'); let locate = { top: offset.top + $td.height() + 2, left: offset.left }; $pop.offset(locate); const _this = this.comboInput; if(_this.options.isMultiple) { let selectData = []; let value = _this._elemInput.val().trim(); if(value != '') { let selects = value.split(','); for(let item of selects) { let ary = []; ary.push(findItembyTitle(item.trimStart(), _this.options.source)); selectData.push(ary); } this.comboInput.setSelection(selectData); } } else { $('span.comboTreeItemTitle.selectable').each(function(i, elem) { if($(elem).text() == _this._elemInput.val()) { _this.dropDownMenuHover(elem); } }); } const mouseUp = () => { $(document).on('mouseup.' + this.comboInput.comboTreeId, function (e){ if (!_this._elemWrapper.is(e.target) && _this._elemWrapper.has(e.target).length === 0 && _this._elemDropDownContainer.is(':visible')) _this.closeDropDownMenu(); }); } setTimeout(mouseUp, 300); }; ComboTreeEditor.prototype.close = function() { }; ComboTreeEditor.prototype.getValue = function(){ return this.comboInput._elemInput.val(); }; ComboTreeEditor.prototype.setValue = function(newValue){ if(this.comboInput != undefined) this.comboInput._elemInput.val(newValue); }; ComboTreeEditor.prototype.focus = function() {}; Handsontable.editors.ComboTreeEditor = ComboTreeEditor; Handsontable.editors.registerEditor('combotree', ComboTreeEditor); }(Handsontable)); let SampleJSONData = [ { id: 0, title: 'Horse'}, { id: 1, title: 'Birds', isSelectable: false, subs: [ { id: 10, title: 'Pigeon', isSelectable: false }, { id: 11, title: 'Parrot' }, { id: 12, title: 'Owl' }, { id: 13, title: 'Falcon' } ] }, { id: 2, title: 'Rabbit' }, { id: 3, title: 'Fox'}, { id: 5, title: 'Cats', subs: [ { id: 50, title: 'Kitty' }, { id: 51, title: 'Bigs', subs: [ { id: 510, title: 'Cheetah' }, { id: 511, title: 'Jaguar' }, { id: 512, title: 'Leopard' } ] } ] }, { id: 6, title: 'Fish' } ]; let SampleJSONData2 = [ { id: 1, title: 'Four Wheels', subs: [ { id: 10, title: 'Car' }, { id: 11, title: 'Truck' }, { id: 12, title: 'Transporter'}, { id: 13, title: 'Dozer' } ] }, { id: 2, title: 'Two Wheels', subs: [ { id: 20, title: 'Cycle' }, { id: 21, title: 'Motorbike' }, { id: 22, title: 'Scooter' } ] }, { id: 2, title: 'Van' }, { id: 3, title: 'Bus' } ]; let data = [ { A:'Parrot, Horse, Fox',B:'Two Wheels, Cycle, Motorbike, Scooter, Bus', C:'Parrot' } ]; let hot = new Handsontable(document.getElementById("grid"), { data: data, columns:[ { data: 'A', type: 'text', width: 200, renderer: 'autocomplete', editor: 'combotree', comboTreeConfig: { source: SampleJSONData, isMultiple: true, cascadeSelect: false } }, { data: 'B', type: 'text', width: 200, renderer: 'autocomplete', editor: 'combotree', comboTreeConfig: { source: SampleJSONData2, isMultiple: true, cascadeSelect: true } }, { data: 'C', type: 'text', width: 150, renderer: 'autocomplete', editor: 'combotree', comboTreeConfig: { source: SampleJSONData, isMultiple: false }, comboWidth: 200 } ], colHeaders: ["Multi Selection", "Multi Selection With Cascade", "Single Selection"], manualColumnResize: true, contextMenu: { items:{ 'row_below': { name: '1行挿入' }, 'remove_row': { name: '1行削除', disabled: function(){ return hot.countRows() < 2; } }, "hsep": "---------", 'undo': { name: '戻る' }, }, }, }); ポイント 今回はリストが表示されるまでが苦労しました。リスト表示がされれば、そこからはComboTreeの世界なので、あとは値をセルにセットしたり初期値のチェックを付けたりするくらいです。 リスト表示 前回のClockPickerやDatetimePickerの際には、inputタグの親であるDIVタグは不要でしたが今回は必要でした。 リスト表示された後、余分なinputタグを非表示にしています。 this.combo = document.createElement('input'); this.combo.setAttribute('type', 'text'); this.comboStyle = this.combo.style; this.comboParent = document.createElement('DIV'); this.comboParentStyle = this.comboParent.style; this.comboParent.appendChild(this.combo); this.instance.rootElement.appendChild(this.comboParent); const that = this; this.comboInput = $(this.combo).comboTree(this.cellProperties.comboTreeConfig); this.comboStyle.display = 'none'; リスト表示が即閉 リストが表示されたらすぐに閉じてしまう。 解決方法は、フォーカスをセットしたらリストが表示されます、その前にmouseupイベントを解除します。 表示後にmouseupイベントを300ms後に再セットすることで、フォーカスが外れたらリストを閉じるようにしています。 $(document).off('mouseup.' + this.comboInput.comboTreeId); this.combo.focus(); const mouseUp = () => { $(document).on('mouseup.' + this.comboInput.comboTreeId, function (e){ if (!_this._elemWrapper.is(e.target) && _this._elemWrapper.has(e.target).length === 0 && _this._elemDropDownContainer.is(':visible')) _this.closeDropDownMenu(); }); } setTimeout(mouseUp, 300); 外部モジュールをHandsontableに組み込む上では、ソースコードがあるからこそ何とか対応できるわけです。 リスト表示の横幅 ここで親のDIVタグが役に立ちました。横幅を指定しないと画面いっぱいに横幅が広がってしまいます。 セルの横幅(clientWidth)を取得してセットしていますが、comboWidthオプション指定があれば値をセットします。 this.comboParent = document.createElement('DIV'); this.comboParentStyle = this.comboParent.style; let wid = this.TD.clientWidth; if(this.cellProperties.comboWidth != undefined) wid = this.cellProperties.comboWidth; this.comboParentStyle.width = wid + 'px'; 最後に jQuery Pluginで作られたコンポーネントって素晴らしいのが結構あります。 別に脱Jqueryを無理にする必要なんてない気がします。 まだ、Handsontableに追加したいコンポーネントがあるので、もう少し頑張ります。 日本だとコンポーネント関連は、GrapeCityやInfragisticsが強いです、だけども価格が高いです。 Handsontable有償版もそこそこの値段ですけどね。 日本のデジタルトランスフォーメーション(DX)を考えたら、MITライセンスで手軽にできるようにしていきたいな。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Handsontable】セル内に複数選択セレクト入力(Multi Select ComboTree )の表示

はじめに Handsontable でリスト選択をするには、セル種類に AutocompleteとDropdownおよびselect(正確にはセル種類ではない)を指定する方法があります。 種類 内容 デモ Autocomplete セル右端の▼ボタンのクリックでリストが表示され、リストから1つを選択する。セル内の文字入力が出来る。オプション設定でリストにないものは検証エラーにしないようにも出来るし、検証エラー(セル背景色が赤表示)にすることでも出来る。 https://handsontable.com/docs/autocomplete-cell-type/ Dropdown Autocompleteの派生、セル内の文字入力が出来る。リストにないものは検証エラーとなる。 https://handsontable.com/docs/dropdown-cell-type/ Select セルをダブルクリックすることでリストが表示され、その中から1つを選択する。セル内の文字入力が出来ない。 https://handsontable.com/docs/select-cell-type/ どれを選択したとしても、リストから1つしか選択できません。 以前、.NETで同僚から複数選択のコンボボックスってないですか? と聞かれ調査して記事を書いたことがあります。 リスト内にチェックボックスが表示されて、複数選択するとカンマ区切りでセットされるようになっています。 今回、Javascriptでリスト内にチェックボックスが表示されて複数選択できるものを調査して、「ComboTree jQuery Plugin」というのを見つけました。名前にTreeが付くように階層構造も可能になっています。 環境 HandsontableはMITライセンス版のバージョン 6.2.2を使用しています。 一応、有償版バージョン 8.3.2でも動作は確認しています。 CDN+α <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/5.9.55/css/materialdesignicons.min.css"> <link rel="stylesheet" href="https://rawcdn.githack.com/erhanfirat/combo-tree/a216a2bd4e53223ac25a3b68b319608770253861/style.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="https://rawcdn.githack.com/erhanfirat/combo-tree/a216a2bd4e53223ac25a3b68b319608770253861/comboTreePlugin.js"></script> ComboTreeの階層用アイコン表示に、MaterialDesign-WebFontを使用しています。 ComboTreeがマイナーのため、CDNに登録がありません。GitHub上のファイルをCDNとして参照するようにしました。 GitHub に上がっているJavascriptやCSSをCodePenで参照しようとしても、text/plain として返却されるため実行できないが、下記サイトで変換したURLにすることで適切な Content-Type を返してくれるため実行することができる。 仕様 表示設定 設定 値 初期値 説明 isMultiple true/false false 複数選択か単一選択かを決める cascadeSelect true/false false 複数選択時に、親の選択が子に連動するかどうかを決める source JSON Data Array   リストをJSON配列で指定する selected JSON Data Array   初期選択としてソースから対応するIDのリストをJSON配列(例 selected: [[0],[11]])で指定する collapse true/false false サブリストを折りたたむかを決める Handsontableで使用する場合、selectedを無視します。 セルの値で一致する項目名を選択します。もし同じ項目名があった場合、最初に見つけたところが選択されます。 リスト設定 let SampleJSONData = [ { id: 0, title: 'Horse'}, { id: 1, title: 'Birds', isSelectable: false, subs: [ { id: 10, title: 'Pigeon', isSelectable: false }, { id: 11, title: 'Parrot' }, { id: 12, title: 'Owl' }, { id: 13, title: 'Falcon' } ] } 基本はリスト用識別IDのidと表示名のtitle、選択可能/不可設定のisSelectableで、falseに設定すると一覧から選択できないようなります。階層構造として、subsとして同じように指定します。また、subsの中にsubsを指定することも出来ます。 カラム設定 Handsontableのcolumns設定に、comboWidthオプションを新規に追加してあります。comboWidthオプションを指定しない場合、セルの横幅と同じ幅でリストが表示されますが、指定した場合は設定した値の横幅でリストを表示します。 let hot = new Handsontable(document.getElementById("grid"), { data: data, columns:[ { data: 'A', type: 'text', width: 200, renderer: 'autocomplete', editor: 'combotree', comboTreeConfig: { source: SampleJSONData, isMultiple: true, cascadeSelect: false } }, { data: 'B', type: 'text', width: 200, renderer: 'autocomplete', editor: 'combotree', comboTreeConfig: { source: SampleJSONData2, isMultiple: true, cascadeSelect: true } }, { data: 'C', type: 'text', width: 150, renderer: 'autocomplete', editor: 'combotree', comboTreeConfig: { source: SampleJSONData, isMultiple: false }, comboWidth: 200 } ], 注意 複数値はカンマ区切りですが、カンマの後に空白が1つ付いています。 let data = [ { A:'Parrot, Horse, Fox',B:'Two Wheels, Cycle, Motorbike, Scooter, Bus', C:'Parrot' } ]; 値を分解する際には、カンマ区切り後の値にtrimStart()をかけています。 実装 See the Pen Handsontable ComboTree by やじゅ (@yaju-the-encoder) on CodePen. ソースコード <!DOCTYPE html> <html lang="jp"> <body> <head> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/5.9.55/css/materialdesignicons.min.css"> <link rel="stylesheet" href="https://rawcdn.githack.com/erhanfirat/combo-tree/a216a2bd4e53223ac25a3b68b319608770253861/style.css"> </head> <div id="grid"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="https://rawcdn.githack.com/erhanfirat/combo-tree/a216a2bd4e53223ac25a3b68b319608770253861/comboTreePlugin.js"></script> <script type="text/javascript"> (function(Handsontable){ 'use strict'; const ComboTreeEditor = Handsontable.editors.SelectEditor.prototype.extend(); const findItembyTitle = (item, source) => { for (let i = 0; i < source.length; i++) { if (source[i].title == item) return source[i].id; if (source[i].hasOwnProperty("subs")) { let found = findItembyTitle(item, source[i].subs); if (found) return found; } } }; ComboTreeEditor.prototype.open = function() { event.stopPropagation(); this.refreshDimensions(); if(this.comboInput) this.comboInput.destroy(); this.combo = document.createElement('input'); this.combo.setAttribute('type', 'text'); this.comboStyle = this.combo.style; this.comboParent = document.createElement('DIV'); this.comboParentStyle = this.comboParent.style; let wid = this.TD.clientWidth; if(this.cellProperties.comboWidth != undefined) wid = this.cellProperties.comboWidth; this.comboParentStyle.width = wid + 'px'; this.comboParent.appendChild(this.combo); this.instance.rootElement.appendChild(this.comboParent); const that = this; this.comboInput = $(this.combo).comboTree(this.cellProperties.comboTreeConfig); this.comboInput.onChange(() => { that.instance.setDataAtCell(that.row, that.col, that.combo.value); }); this.comboInput._elemInput.val(this.originalValue); this.comboStyle.display = 'block'; $('.comboTreeArrowBtn').hide(); $(document).off('mouseup.' + this.comboInput.comboTreeId); this.combo.focus(); this.comboStyle.display = 'none'; let $td = $(this.TD); let offset = $td.offset(); let $pop = $('.comboTreeDropDownContainer'); let locate = { top: offset.top + $td.height() + 2, left: offset.left }; $pop.offset(locate); const _this = this.comboInput; if(_this.options.isMultiple) { let selectData = []; let value = _this._elemInput.val().trim(); if(value != '') { let selects = value.split(','); for(let item of selects) { let ary = []; ary.push(findItembyTitle(item.trimStart(), _this.options.source)); selectData.push(ary); } this.comboInput.setSelection(selectData); } } else { $('span.comboTreeItemTitle.selectable').each(function(i, elem) { if($(elem).text() == _this._elemInput.val()) { _this.dropDownMenuHover(elem); } }); } const mouseUp = () => { $(document).on('mouseup.' + this.comboInput.comboTreeId, function (e){ if (!_this._elemWrapper.is(e.target) && _this._elemWrapper.has(e.target).length === 0 && _this._elemDropDownContainer.is(':visible')) _this.closeDropDownMenu(); }); } setTimeout(mouseUp, 300); }; ComboTreeEditor.prototype.close = function() { }; ComboTreeEditor.prototype.getValue = function(){ return this.comboInput._elemInput.val(); }; ComboTreeEditor.prototype.setValue = function(newValue){ if(this.comboInput != undefined) this.comboInput._elemInput.val(newValue); }; ComboTreeEditor.prototype.focus = function() {}; Handsontable.editors.ComboTreeEditor = ComboTreeEditor; Handsontable.editors.registerEditor('combotree', ComboTreeEditor); }(Handsontable)); let SampleJSONData = [ { id: 0, title: 'Horse'}, { id: 1, title: 'Birds', isSelectable: false, subs: [ { id: 10, title: 'Pigeon', isSelectable: false }, { id: 11, title: 'Parrot' }, { id: 12, title: 'Owl' }, { id: 13, title: 'Falcon' } ] }, { id: 2, title: 'Rabbit' }, { id: 3, title: 'Fox'}, { id: 5, title: 'Cats', subs: [ { id: 50, title: 'Kitty' }, { id: 51, title: 'Bigs', subs: [ { id: 510, title: 'Cheetah' }, { id: 511, title: 'Jaguar' }, { id: 512, title: 'Leopard' } ] } ] }, { id: 6, title: 'Fish' } ]; let SampleJSONData2 = [ { id: 1, title: 'Four Wheels', subs: [ { id: 10, title: 'Car' }, { id: 11, title: 'Truck' }, { id: 12, title: 'Transporter'}, { id: 13, title: 'Dozer' } ] }, { id: 2, title: 'Two Wheels', subs: [ { id: 20, title: 'Cycle' }, { id: 21, title: 'Motorbike' }, { id: 22, title: 'Scooter' } ] }, { id: 2, title: 'Van' }, { id: 3, title: 'Bus' } ]; let data = [ { A:'Parrot, Horse, Fox',B:'Two Wheels, Cycle, Motorbike, Scooter, Bus', C:'Parrot' } ]; let hot = new Handsontable(document.getElementById("grid"), { data: data, columns:[ { data: 'A', type: 'text', width: 200, renderer: 'autocomplete', editor: 'combotree', comboTreeConfig: { source: SampleJSONData, isMultiple: true, cascadeSelect: false } }, { data: 'B', type: 'text', width: 200, renderer: 'autocomplete', editor: 'combotree', comboTreeConfig: { source: SampleJSONData2, isMultiple: true, cascadeSelect: true } }, { data: 'C', type: 'text', width: 150, renderer: 'autocomplete', editor: 'combotree', comboTreeConfig: { source: SampleJSONData, isMultiple: false }, comboWidth: 200 } ], colHeaders: ["Multi Selection", "Multi Selection With Cascade", "Single Selection"], manualColumnResize: true, contextMenu: { items:{ 'row_below': { name: '1行挿入' }, 'remove_row': { name: '1行削除', disabled: function(){ return hot.countRows() < 2; } }, "hsep": "---------", 'undo': { name: '戻る' }, }, }, }); ポイント 今回はリストが表示されるまでが苦労しました。リスト表示がされれば、そこからはComboTreeの世界なので、あとは値をセルにセットしたり初期値のチェックを付けたりするくらいです。 リスト表示 前回のClockPickerやDatetimePickerの際には、inputタグの親であるDIVタグは不要でしたが今回は必要でした。 リスト表示された後、余分なinputタグを非表示にしています。 this.combo = document.createElement('input'); this.combo.setAttribute('type', 'text'); this.comboStyle = this.combo.style; this.comboParent = document.createElement('DIV'); this.comboParentStyle = this.comboParent.style; this.comboParent.appendChild(this.combo); this.instance.rootElement.appendChild(this.comboParent); const that = this; this.comboInput = $(this.combo).comboTree(this.cellProperties.comboTreeConfig); this.comboStyle.display = 'none'; リスト表示が即閉 リストが表示されたらすぐに閉じてしまう。 解決方法は、フォーカスをセットしたらリストが表示されます、その前にmouseupイベントを解除します。 表示後にmouseupイベントを300ms後に再セットすることで、フォーカスが外れたらリストを閉じるようにしています。 $(document).off('mouseup.' + this.comboInput.comboTreeId); this.combo.focus(); const mouseUp = () => { $(document).on('mouseup.' + this.comboInput.comboTreeId, function (e){ if (!_this._elemWrapper.is(e.target) && _this._elemWrapper.has(e.target).length === 0 && _this._elemDropDownContainer.is(':visible')) _this.closeDropDownMenu(); }); } setTimeout(mouseUp, 300); 外部モジュールをHandsontableに組み込む上では、ソースコードがあるからこそ何とか対応できるわけです。 リスト表示の横幅 ここで親のDIVタグが役に立ちました。横幅を指定しないと画面いっぱいに横幅が広がってしまいます。 セルの横幅(clientWidth)を取得してセットしていますが、comboWidthオプション指定があれば値をセットします。 this.comboParent = document.createElement('DIV'); this.comboParentStyle = this.comboParent.style; let wid = this.TD.clientWidth; if(this.cellProperties.comboWidth != undefined) wid = this.cellProperties.comboWidth; this.comboParentStyle.width = wid + 'px'; 最後に jQuery Pluginで作られたコンポーネントって素晴らしいのが結構あります。 別に脱Jqueryを無理にする必要なんてない気がします。 まだ、Handsontableに追加したいコンポーネントがあるので、もう少し頑張ります。 日本だとコンポーネント関連は、GrapeCityやInfragisticsが強いです、だけども価格が高いです。 Handsontable有償版もそこそこの値段ですけどね。 日本のデジタルトランスフォーメーション(DX)を考えたら、MITライセンスで手軽にできるようにしていきたいな。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】Uncaught TypeError: Cannot read property ‘プロパティ名’ of nullのエラーについて

すごく初歩的なエラーで1時間くらい時間を使ってしまったので 備忘録として残したいと思います。 前提 学習のためにメルカリのようなアプリを作成中です。 JavaScriptの非同期通信を用いて、販売価格を入力すれば 手数料と、入力した価格から手数料を差し引いた金額が自動で表示される機能を実装しようと下記のようにコードを記述しました。 application.js require("@rails/ujs").start() require("@rails/activestorage").start() require("channels") require("../item_price"); items/new.html.erb <%# 販売価格 %> <div class="sell-price"> <div class="weight-bold-text question-text"> <span>販売価格<br>(¥300〜9,999,999)</span> <a class="question" href="#">?</a> </div> <div> <div class="price-content"> <div class="price-text"> <span>価格</span> <span class="indispensable">必須</span> </div> <span class="sell-yen">¥</span> <%= f.text_field :price, class:"price-input", id:"item-price", placeholder:"例)300" %> </div> <div class="price-content"> <span>販売手数料 (10%)</span> <span> <span id='add-tax-price'></span>円 </span> </div> <div class="price-content"> <span>販売利益</span> <span> <span id='profit'></span>円 </div> </span> </div> </div> <%# /販売価格 %> layouts/application.html.erb <!DOCTYPE html> <html> <head> <title>Furima</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <script type="text/javascript" src="https://js.pay.jp/v1/"></script> <%= stylesheet_link_tag 'application', media: 'all'%> <%= javascript_pack_tag 'application' %> </head> <body> <%= yield %> </body> </html> item_price.js const priceInput = document.getElementById("item-price"); priceInput.addEventListener("input", () => { const inputValue = priceInput.value; const addTaxDom = document.getElementById('add-tax-price'); addTaxDom.innerHTML = Math.floor( parseInt(inputValue) * 0.1 ); const profitDom = document.getElementById('profit'); profitDom.innerHTML = Math.floor( parseInt(inputValue) - parseInt(addTaxDom.innerHTML) ); }) 本題 挙動を確認しようと、ブラウザの検証ツールのコンソールで見てみると Uncaught TypeError: Cannot read properties of null (reading 'addEventListener') at Object../app/javascript/item_price.js (item_price.js:3) at __webpack_require__ (bootstrap:19) at Object../app/javascript/packs/application.js (application.js:9) at __webpack_require__ (bootstrap:19) at bootstrap:83 at bootstrap:83 このようなエラーが出ていました。 最初"TypeError"と出ているのでスペルミスかな?と思いましたが、 そうではなく、エラー文しっかり調べてみると 「nullのプロパティからは値を引っ張ってこれないよ」と言われているようです。 ということはビューの記述がおかしいのかと思って調べていると、ある記事に似たような内容で悩んでいる人の記事に答えが載っていました。 HTMLは上から順に解釈されていく仕様になっており、 <head>に記述されている"JavaScriptを読み込む"と言う記述が <body>に記述されている内容にたどり着く前に読み込まれてしまうため、 JavaScriptの処理は<body>に入力してある値を取得する前に実行されてしまいます。 そのためnullとなり、値がないよ〜と言われてしまうわけです。 解決方法 ではどうすれば良いのか。 方法はいくつかあるようですが、今回は下記の記述をJSファイルに追記しました。 item_price.js window.addEventListener('load', function(){ const priceInput = document.getElementById("item-price"); priceInput.addEventListener("input", () => { const inputValue = priceInput.value; const addTaxDom = document.getElementById('add-tax-price'); addTaxDom.innerHTML = Math.floor( parseInt(inputValue) * 0.1 ); const profitDom = document.getElementById('profit'); profitDom.innerHTML = Math.floor( parseInt(inputValue) - parseInt(addTaxDom.innerHTML) ); }) }) window.addEventListener('load', function(){}) を記述することで、最初に情報を全て取得したから以降の処理を行うことができるので、 nullになることなく、意図した挙動を行うことができました。 しっかり勉強したつもりでしたが、基本的なことをすっかり忘れてしまっていたがために起きたエラーでした。 もう一度復習しなければ。 終わり
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firebase JS SDK v9 でログイン認証(Authentication)をする時の変更点

またFirebaseのv9.0.0以降の変更点です。 プログラミング初心者なので誤りがあるかもしれません。 その際はご指摘お願いいたします。 Firebase Authenticationをこれから使用する人向け Firebase v9はつい先日(2021/08/25)アップデートが行われ、正式にv9.0.0になりました。 npm install --save firebase ってバージョンを明示せずにインストールするとv9になってるってことですね。 大きな変更点としては、Option API方式からComposition API方式になった事です。 といっても私もよくわかんないのでコードで説明すると、 v8_10_0.js import firebase from "firebase/app" import "firebase/auth" const config = { //... } firebase.initializeApp(config); const auth = firebase.auth(); v9_0_0.js import { initializeApp } from "firebase/app" import { getAuth } from "firebase/auth" const config = { //... } initializeApp(config); const auth = new getAuth(); のように、特定の関数をimportで明示的に呼び出す形のことをComposition APIと呼ぶようで、Vue3でも同様にComposition APIを使用するように変更が行われています。 これはページのロード高速化のためで、そりゃ毎回ライブラリ全部をimportするより明示的に一部分をimportした方が速いよねってことだと理解してます。 これからFirebaseやVue.jsで何かを作る場合は、Composition APIでコードを書くようにした方がいいみたいですね。 だいたいの引数にgetAuth()を入れる 色々なFirebase v8以前のサンプルコードを見ながら、どうv9に対応させればよいのか悩みましたが結局getAuth()を使えばほとんど解決する事に気づきました。 当然と言われれば当然なんですが…。 例えばセッションの永続性を設定するsetPersistence()だと、 v8_10_0.js import firebase from "firebase/app" import "firebase/auth" firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION).then(() => { //... } v9_0_0.js import { getAuth, setPersistence, browserSessionPersistence } from "firebase/auth" setPersistence(getAuth(), browserSessionPersistence).then(() => { //... } と書きます。 ここでの違いは、Composition APIだと関数の引数にAuthインスタンスを含むようになっていて、ここにgetAuth()を入れると解決します。 getAuth()は既存のAuthインスタンスを呼ぶか、既存のAuthインスタンスが無ければ新規でAuthインスタンスを生成します。 なので、firebase.js等のファイルで最初に初期化をし、setPersistence()でセッションの永続性を設定しておけばあとはFirebase Authenticationを使いたいところでgetAuth()を引数に入れておけばよいみたいです。 FirebaseのCodelabにあるサンプルコード(Friendly-chat)でも、最初に初期化した後は全部getAuth()してるので多分合ってます。 私がしてた勘違い 私が色んなサイトを見て混乱してたのは、例えばfirebase.jsでAuthインスタンスの初期化をした時に import { getAuth } from "firebase/auth" const auth = new getAuth(); export { auth } として、それを他のAuthenticationを使いたい場所で import auth from "@/firebase.js" import { signOut } from "firebase/auth" signOut(auth).then(() => { //... } と書く必要があるのかと思って混乱していました。 実際にはこんな事をせずに、 import { signOut, getAuth } from "firebase/auth" signOut(getAuth()).then(() => { //... } 同じ勘違いをされる方がいらっしゃるかはわかりませんが、そう解説してあるサイトもあったので一応…。(そのコードではそう書く必要があったのかもしれません。) Firebase Authentication の初期化 onAuthStateChanged() の使い方で3~4日悩んだんですが、こうすればいいってわかったのでまた別投稿で書きます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kintoneのフィールド値変更時イベントで計算フィールドとJavaScriptカスタマイズの計算を一緒にしちゃだめ!という話

今回は、「フィールド値変更イベント」で計算フィールドとJavaScriptカスタマイズの計算を一緒にしないようにしましょう!というお話です。 ためしに、アプリを作ってみましょう。 アプリの準備 数値フィールドのA,Bに値を入力したら、 計算フィールドCと数値フィールドDに自動的に計算値が入る。という仕組みにします。 フィールド種類 フィールドコード 計算式 数値 A 数値 B 計算 C A + 10 数値 D JavaScriptで B+C を計算する JavaScriptの準備 計算フィールドはフィールド値変更時イベントで指定できないので、 数値フィールドのA,Bの値変更時に計算をすることにします。 kintone.events.on( [ "app.record.create.change.A", "app.record.create.change.B", "app.record.edit.change.A", "app.record.edit.change.B", ], (event) => { // D = B + C event.record.D.value = Number(event.record.B.value) + Number(event.record.C.value); return event; } ); 動かしてみましょう? 動かしてみると、こんな感じになると思います。 数値フィールドAの値を更新しても、 最終的な計算結果の数値フィールドDの値が更新されません。 何故!? 先程のJavaScriptのコードの // D = B + Cの上辺りに、 console.log(event); を忍ばせてから、Aの値を10から15に変更してみましょう。 すると 計算フィールドの計算よりも先に「フィールド値変更時イベント」が動いている!? と思って、JavaScript計算の方を先に動かす作戦でやってみても のような変な計算結果になって・・・・だめでした。 計算フィールドの方が先とか後とかではなくて、「フィールド値変更時イベント」とは非同期に動いています。 どちらが先とかはわからないですね。 というわけで、JavaScriptで「フィールド値変更時イベント」を使って計算する場合は「計算フィールド」が絡む計算をしないようにしましょう。 ※ボタンクリックで発火するようなイベントのときは計算フィールド使って計算しても大丈夫です✨
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kintoneで計算フィールドとJavaScriptカスタマイズの計算を一緒にしちゃだめ!という話

今回は、計算フィールドとJavaScriptカスタマイズの計算を一緒にしないようにしましょう!というお話です。 ためしに、アプリを作ってみましょう。 アプリの準備 数値フィールドのA,Bに値を入力したら、 計算フィールドCと数値フィールドDに自動的に計算値が入る。という仕組みにします。 フィールド種類 フィールドコード 計算式 数値 A 数値 B 計算 C A + 10 数値 D JavaScriptで B+C を計算する JavaScriptの準備 計算フィールドはフィールド値変更時イベントで指定できないので、 数値フィールドのA,Bの値変更時に計算をすることにします。 kintone.events.on( [ "app.record.create.change.A", "app.record.create.change.B", "app.record.edit.change.A", "app.record.edit.change.B", ], (event) => { // D = B + C event.record.D.value = Number(event.record.B.value) + Number(event.record.C.value); return event; } ); 動かしてみましょう? 動かしてみると、こんな感じになると思います。 数値フィールドAの値を更新しても、 最終的な計算結果の数値フィールドDの値が更新されません。 何故!? 先程のJavaScriptのコードの // D = B + Cの上辺りに、 console.log(event); を忍ばせてから、Aの値を10から15に変更してみましょう。 すると 計算フィールドの計算よりも先に「フィールド値変更時イベント」が動いている!? と思って、JavaScript計算の方を先に動かす作戦でやってみても のような変な計算結果になって・・・・だめでした。 計算フィールドの方が先とか後とかではなくて、「フィールド値変更時イベント」とは非同期に動いています。 どちらが先とかはわからないですね。 というわけで、JavaScriptで「フィールド値変更時イベント」を使って計算する場合は「計算フィールド」が絡む計算をしないようにしましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】React ことはじめ (ただの学習ログ) Part2

きっかけ 前回の以下記事の続き 学習の進め方 前回の本で触れられていなかったAPI呼び出しやルーティングの内容が触れられているReactハンズオンラーニング第2版で学習したいところだけメモしています。 前回と同じく、記事の記載に間違いがある場合、マサカリをお願いします… 学習内容 ステート管理 アプリケーション全体のステート管理 複数コンポーネントがステートを持つと複雑になるため、1箇所でステートを管理したい→最上位のルートコンポーネントで管理、参照は親から子コンポーネントへプロパティとして渡す、変更は子から親コンポーネントへ伝えステートを更新する コンテキスト 巨大アプリケーションでのステート伝達の際に、中間コンポーネントが伝達のためだけにプロパティ設定が必要となり、複雑なアプリケーションとなってしまう。→コンテキストを使用することで解決。コンテキストプロバイダへデータを渡し、コンテキストコンシューマから読み出す データ API呼び出し fetch APIでHTTPリクエストを送信可能。GET, POST, PUTの場合はfetch APIのオプションとしてmethod内で指定。(axiosで実現する方法は別途確認) WebStorageへの保存 saveJSONでlocalStorageへの保存、loadJSONでlocalStorageからの読み出す。 ルーティング React Routerでルーティングを設定 遭遇したエラーとその対処 Module not found: You attempted to import XXX which falls outside of the project src/ directory. Relative imports outside of src/ are not supported. srcより上階層の資源を読み込もうとしたら発生。src配下に資源を移動、importのパスを修正 Syntax error: Unterminated JSX contents returnで書いていたJSXのタグの閉じ忘れが原因。 Manifest: Line: 1, column: 1, Syntax error Manifest.json create-react-app後にpublicの内容をindex.jsを除き、削除していたことが原因。manifest.jsonを復活。 ESLintで’jsx-a11y/href-no-hash’ was not found .eslintrc.json内でhref-no-hashをoffにする設定を追加。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

mapとreduceでforやif文を置き換える

はじめに 最近、mapとreduceを用いた面白い配列操作を学んだので備忘録としてこちらにまとめます。 for文をmapで書き換える 配列内の値に変更を加えて再度配列に格納し直したい場合を考えてみます。 for文だと配列のlengthの数だけループを回して処理を加えたのち、新しく定義した配列にpushするという方法が考えられます。 forを使ったケース const numArray = [1, 2, 3, 4, 5, 6]; const array = []; for(let i = 0; i < numArray.length; i++){ const doubleNum = numArray[i] * 2; array.push(doubleNum); } //出力 [2, 4, 6, 8, 10, 12] console.log(array); mapを使ったケース これをmapで書くと以下のようにすっきりと書くことができます。 const numArray = [1, 2, 3, 4, 5, 6]; const array = numArray.map(el => el * 2); //出力 [2, 4, 6, 8, 10, 12] console.log(array); reduceで配列内の数字の合計を出す 配列内の値を全て足し合わせたい場合。 forやmapで一つずつ取り出して足してもできますがreduceを使えば簡単です。 reduceで合計値を計算 const numArray = [1, 2, 2, 3, 3, 3, 3]; const getSum = array => { return numArray.reduce((a, b) => a + b, 0); } // 出力 17 console.log(getSum(numArray)); ループ処理とif文をreduceと三項演算子ですっきり書き換える こちらは配列内に2以上の数字がいくつあるかを数える関数です。 for文とif文を組み合わせて算出することも可能ですが、これもreduceを使えばすっきり書けます。 forとif文のケース const numArray = [1, 2, 3, 4, 5, 6]; const getCount = array => { let count = 0; array.forEach(num => { if(num > 2){ count += 1; } }); return count; } //出力 4 console.log(getCount(num)); reduceのケース 初期値を0に設定することで、三項演算子で記載した条件をクリアする値の数をカウントすることができています。 const count = numArray.reduce((a, b) => { return b > 2 ? a + 1 : a + 0; }, 0); //出力 4 console.log(count); さいごに mapもreduceもまだまだいろいろな使い方があります。 今後も新たな発見があればまとめていきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【TypeScript】find()でundefinedを返さない方法

はじめに find()はJavaScriptの関数ですが、undefinedを返す可能性があるので、TypeScriptだとそのままでは使えません。そんな時にどうやって使えば良いのかを解説します。 結論:Nullish coalescing operatorを使う。 find()は配列要素orundefinedを返す Array.prototype.find()は、配列の要素を最初から順番に渡し、 条件に一致する(return true)場合→その要素 条件に全て一致しない場合→undefined を返します。下のような、オブジェクトを要素に取る配列で使うことが多いです。 const language = [ { value: "js", label: "JavaScript" }, { value: "php", label: "PHP" }, { value: "rb", label: "Ruby" }, { value: "", label: "Others" } ]; // (value: "js")に一致するオブジェクトが欲しい const documentLanguage = language.find((obj) => { return obj.value === "js"; }); // { value: "js", label: "JavaScript" } 一見何の変哲もないコードですが、条件に合うものが一つもないと、undefinedが返ってきてしまいます。 // (value: "py")に一致するオブジェクトが欲しい const documentLanguage = language.find((obj) => { return obj.value === "py"; }); // undefined これがエラーの原因になっちゃったりする訳です。 しかし、Null絶対許さないマンのTypeScriptは、このエラーの火種を見事に見つけて叱ってくれます。(この機能をstrictNullChecksといいます) console.log(documentLanguage.label); // ERR: Object is possibly 'undefined'. // Objectは'undefined'かもしれない。 この時のdocumentLanguageの型は次のようになっています。 const documentLanguage: { value: string; label: string; } | undefined undefinedがついているのが分かると思います。 この時に、どのようにfind()を使えばいいかを解説します。 [解決策]undefinedの場合に違う値を用いる(Nullish coalescing operator) Nullish coalescing operatorを使います。 ??の左の値がnullishの場合に、代わりに右の値を用いる機能です。 console.log(null ?? "これはnullです"); // これはnullです 元の文を書き直すとこんな感じになります。 const documentLanguage = language.find((obj) => { return obj.value === "py"; }) ?? language.slice(-1)[0]; // { value: "", label: "Others" } value: "py"は存在しないので、undefinedの代わりに最後の要素が代入されます。 (slice(-1)[0]で末端の要素を取り出しています。sliceは配列のまま返すので、[0]で要素を指定します。) おまけ Array.prototype.findIndex()は要素ではなく配列インデックスを返しますが、find()と違ってundefinedではなく-1を返します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node-webrtcでWebRTCを入門

環境 Ubuntu 20.04 (AWS) node v10.19.0 (npm 6.14.4) npm packages wrtc 0.4.7 (node-webrtc) mkdirp 1.0.4 chokidar 3.5.2 dateformat 4.5.1 作ったもの WebRTCのデータチャンネルを使って文字列データのやり取りをするサンプル オファー側、アンサー側、共に node.js で実装、コマンドで実行 P2Pを始めるまでの情報のやり取りをファイルシステム経由で行っている WebRTCでP2P通信をする流れ オファー側とアンサー側の2つあったり、イベントハンドラが複数あったりして混乱しがちだが、 setLocalDescription()、setRemoteDescription()、createOffer()、createAnswer()の4つのメソッドと onicecandidate イベントハンドラ をおさえれば、通信確立の全体像をつかみやすい 以下はフローを図にまとめたもの コード 上記のフロー図について、コードのコメントで処理の内容をまとめる オファー側とアンサー側でP2P通信を確立する部分に焦点を絞った(全体の流れを俯瞰して理解できる)コードになるように努めた イベントハンドラの処理を隠蔽している イベントハンドラなどの処理については、、githubのコードで確認できる オファー側 const RTCPeerConnection = require('wrtc').RTCPeerConnection; const RTCPeerConnectionUtils = require('./libs/RTCPeerConnectionUtils') const DataChannelEventListenerForOffer = require('./libs/DataChannelEventListenerForOffer') const SDPFileForOffer = require('./libs/SDPFileForOffer') const rtcPeerConnection = new RTCPeerConnection({}) const dataChannelEventListenr = new DataChannelEventListenerForOffer("channel", rtcPeerConnection) const sdpFile = new SDPFileForOffer(); (async () => { console.log("This is OFFERER.") // 通信の確立に焦点を当てているため、データチャンネル関連のイベントを隠蔽 dataChannelEventListenr.listen(); // シグナリングをファイルでやりとりするための前処理 await sdpFile.setUp(); // 経路の候補(ICE Candidates)一覧を取得するプロミスオブジェクトを生成 const candidates = []; const waitAllCandidates = RTCPeerConnectionUtils.createPromiseWaitAllCandidates(rtcPeerConnection, candidates); // オファーを作成 const offer = await rtcPeerConnection.createOffer(); // オファーを設定(設定するとICE Candidatesのイベントが発火する) await rtcPeerConnection.setLocalDescription(offer); // 経路の候補(ICE Candidates)一覧がすべて取得できるまで待つ await waitAllCandidates; // 経路の候補(ICE Candidates)一覧をファイルに書きだす(※1) sdpFile.writeOffer(JSON.stringify(candidates)); // アンサーがファイルに書かれるのを待つ(answer.js側の処理)(※2の処理を待つ) const answer = await sdpFile.waitAnswer(); // アンサー情報を設定(オファーからみてリモート側) rtcPeerConnection.setRemoteDescription(answer[0]); })(); アンサー側 const RTCPeerConnection = require('wrtc').RTCPeerConnection; const RTCPeerConnectionUtils = require('./libs/RTCPeerConnectionUtils') const SDPFileForAnswer = require('./libs/SDPFileForAnswer') const DataChannelEventListenerForAnswer = require('./libs/DataChannelEventListenerForAnswer') const rtcPeerConnection = new RTCPeerConnection({}) const sdpFileForAnswer = new SDPFileForAnswer(); const dataChannelEventListener = new DataChannelEventListenerForAnswer(rtcPeerConnection); (async () => { console.log("This is ANSWERER.") // 通信の確立に焦点を当てているため、データチャンネル関連のイベントを隠蔽 dataChannelEventListener.listen() // シグナリングをファイルでやりとりするための前処理 await sdpFileForAnswer.setUp(); // オファーがファイルに書かれるのを待つ(offer.jsの処理)(※1の処理を待つ) const offer = await sdpFileForAnswer.waitOffer(); // オファー情報を設定(アンサーからみてリモート側) await rtcPeerConnection.setRemoteDescription(offer[0]); // 経路の候補(ICE Candidates)一覧を取得するプロミスオブジェクトを生成 const candidates = []; const waitAllCandidates = RTCPeerConnectionUtils.createPromiseWaitAllCandidates(rtcPeerConnection, candidates); // アンサーを作成 const answer = await rtcPeerConnection.createAnswer(); // アンサーを設定(設定するとICE Candidatesのイベントが発火する) await rtcPeerConnection.setLocalDescription(answer); // 経路の候補(ICE Candidates)一覧がすべて取得できるまで待つ await waitAllCandidates; // アンサーをファイルに書く(※2) sdpFileForAnswer.writeAnswer(JSON.stringify(candidates)); })(); ソースコード
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでコードに影響を与える記号などを表示したい場合の方法

概要 JavaScriptにてシングルクオーテーション(')など、パラメータ内で使用している記号をそのまま表示しようとすると  エラーが発生してしまうため、その表示方法についてのまとめとなります。 内容     以下の文章を表示したい場合を例に説明したいと思います。    I'm Japanese これをそのままプログラムで表示しようとした場合、    document.write('I'm Japanese'); となりますが、シングルクオーテーションとアポストロフィが同じ記号であるためエラーが発生してしまいます。 このように、特殊な文字を表示したい場合はエスケープシーケンスと呼ばれるものを使って表示するようにできます。 上記の場合だとシングルクオーテーションであることを出力したいので「\'」と記載することで表示が可能となります。    document.write('I\'m Japanese'); その他まとめ エスケープシーケンスには他にもいくつかあるので、表示でエラーが発生した場合に参考してみてください。 記号 意味 \b バックスペース \t 水平タブ \v 垂直タブ \n 改行 \r 復帰 \f 改ページ \' シングルクォーテーション \'' ダブルクォーテーション \ 円マーク(またはバックスラッシュ) \0 NULL文字 \xXX 2桁のXX(16進数)が表すLatin-1文字 \uXXXX 4桁のXXXX(16進数)が表すUnicode文字
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】forEarchメソッド

はじめに こんにちは。 今回は、JavaScriptのforEarchメソッドについてアウトプットしていきます! forEarchメソッドとは forEach() メソッドは与えられた関数を、配列の各要素に対して一度ずつ実行します。 参照:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach JavaScript const arry = [1,2,3,4,5]; arry.forEach(function(v){ console.log(v); //1,2,3,4,5 }) 配列arryには、1から5までの要素が格納されているので、要素の数だけ処理を実行している。 JavaScript const ary = [1,2,3,4,5]; for(let i = 0; i< ary.length; i++){ const v = ary[i]; console.log(v); //1,2,3,4,5 } 上記のfor文とforEachメソッドの実行結果は同じだが、forEarchメソッドの方が記述を少なくできる。 forEachメソッドの引数 JavaScript const arry = [1,2,3]; arry.forEach(function(v,i,ary){ console.log(v,i,ary); }) //実行結果 //1 0 [1, 2, 3] //2 1 [1, 2, 3] //3 2 [1, 2, 3] forEachメソッドの第1引数には配列の値、第2引数には添字、第3引数には配列が渡される。 最後に ここまでforEarchメソッドについてまとめました。 これからもJavaScriptの理解を深めていきます!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Stimulusでa11yを考慮したタブを実装する

Stimulusとは Stimulusとは、JavaScriptで書かれたクライアントサイドのライブラリです。 有名どころだとStack Overflowが採用していたりします。 StimulusはHTMLを中心に考えられていて、JavaScriptで書かれた振る舞いをHTMLから呼び出せるように設計されています。 それゆえ私のようなHTMLコーダー上がりの人間にとっても、とっつきやすいライブラリだと思います。 また、アクセシブルな実装は基本HTMLありきなので、その辺の親和性も高いと思っています。 タブとは 1画面に収まり切らない、または同時に見せるべきではない複数の情報のまとまりを、(画面遷移やスクロールを発生させずに)すばやく表示するためのUIです。 ソシオメディア | タブの並列切替にて詳細な説明が見れます。 a11yを考慮したタブ WAI-ARIA Authoring Practices 1.2に全ての答えが載っています。 我々はそれに従いただ実装するのみです。 前提として、今回はタブボタンがアクティブになると、関連する内容(タブパネル)が自動で切り替わる「Tabs With Automatic Activation」で考えていきます。 HTMLを書いてみる <div> <ul role="tablist"> <li role="presentation"> <button type="button" role="tab" aria-selected="true" aria-controls="tab1-panel" id="tab1-btn" tabindex="0" >タブ1</button> </li> <li role="presentation"> <button type="button" role="tab" aria-selected="false" aria-controls="tab2-panel" id="tab2-btn" tabindex="-1" >タブ2</button> </li> <li role="presentation"> <button type="button" role="tab" aria-selected="false" aria-controls="tab3-panel" id="tab3-btn" tabindex="-1" >タブ3</button> </li> </ul> <div> <div role="tabpanel" aria-labelledby="tab1-btn" id="tab1-panel" tabindex="0" > <p>あいうえお</p> </div> <div role="tabpanel" aria-labelledby="tab2-btn" id="tab2-panel" tabindex="0" > <p>かきくけこ</p> </div> <div role="tabpanel" aria-labelledby="tab3-btn" id="tab3-panel" tabindex="0" > <p>さしすせそ</p> </div> </div> </div> 要素の説明 要素 属性 説明 <div> ラッパー。 <ul> role="tablist" tabロールの付いた要素の親要素。 <li> role="presentation" <li>要素には暗黙のlistitemロールがデフォルトで付いており、listロールの付いた要素の子要素でなければならない。今回はタブ用にtablistロールを付与しているので、listロールの子要素になれなくなってしまったので、アクセシビリティツリー上意味を持たないpresentationロールを付与している。 <button> type="button" ボタン。 role="tab" tabpanelロールを制御する。tablistロールの付いた要素の子要素である必要がある。 aria-selected="boolean" 制御しているtabpanelロールが開いていればtrue、閉じていればfalse。 aria-controls="tab1-panel" どのtabpanelロールを制御しているか。tabpanelロールの付いた要素のidに紐づく。 id="tab1-btn" ID。tabpanelロールの付いた要素のaria-labelledbyに紐づく。 tabindex="0|-1" 開いているパネルのボタンはフォーカス可能(0)、閉じているパネルのボタンはフォーカス不可(-1)。 <div> ラッパー。 <div> role="tabpanel" メインで見せたい情報。 aria-labelledby="tab1-btn" どのtabロールにラベリングされているか。tabロールの付いた要素のidに紐づく。 id="tab1-panel" ID。tabロールの付いた要素のaria-cotrolsに紐づく。 tabindex="0" 開いているパネルはフォーカス可能。閉じている(display: none;された)パネルはそもそもフォーカスできないので、-1にする必要はない。 以上で意味的にはアクセシブルなタブの完成です。 次はこれにStimulusで振る舞いをアタッチしていきます。 Tabs Controller 与えたい振る舞いとしてはボタンのクリックやキーボード操作による挙動、それによって連動するパネルの表示非表示です。 タブ全体のラッパーにdata-controller="tabs"を付与して、Tabs Controllerを作ります。 <div data-controller="tabs"> <ul role="tablist"> . . . </ul> </div> 次に取得したい要素にdata-tabs-target属性を付与します。 今回の対象要素はボタンとパネルなので、<button>と<div role="tabpanel">に付与します。 ボタン <button type="button" role="tab" data-tabs-target="btn" >タブ1</button> <button type="button" role="tab" data-tabs-target="btn" >タブ2</button> <button type="button" role="tab" data-tabs-target="btn" >タブ3</button> パネル <div role="tabpanel" data-tabs-target="panel" >タブ1の内容</div> <div role="tabpanel" data-tabs-target="panel" >タブ2の内容</div> <div role="tabpanel" data-tabs-target="panel" >タブ3の内容</div> タブに求められる要件 data-action属性を付与して、要件を満たしていきます。 基本的にはボタンをクリックしたら、該当するパネルが表示される、という振る舞いでOKです。 ここではそれ以外のキーボードインタラクションを見てみます。 キーボード操作 説明 Tab タブリストにフォーカスが移動すると、アクティブなタブボタンにフォーカスされる。タブボタンの次はタブパネル全体にフォーカスされる。Shift + Tabはその逆。 タブボタン上でのキーボード操作 説明 ArrowRight 次の(右の)タブボタンにフォーカスを移動する。最後の(一番右の)タブボタンにフォーカスがある場合、最初の(一番左の)タブボタンにフォーカスを移動する。 ArrowLeft 前の(左の)タブボタンにフォーカスを移動する。最初の(一番左の)タブボタンにフォーカスがある場合、最後の(一番右の)タブボタンにフォーカスを移動する。 他はオプションなので省略します。 詳しくは3.23 TabsのKeyboard Interactionを参照ください。 data-action属性の値は{event}->{controller}#{method_name}のフォーマットで記述しますが、今回使うイベントはclickとkeydownの2つでOKです。 Stimulusでの実装例 See the Pen QIITA_STIMULUS_ACCESIBLE_TABS by yoruaki (@yoruaki) on CodePen. 基本的にやっていることは、{name}Targetsで対象要素を取得し、アクティブな要素に必要な属性値を付け替えているだけです。 強いて挙げるとすれば、パネルの非表示をis-hideというクラス名でCSSで制御しているのですが、それをJS側でelm.classList.add('is-hide')みたいに書くのではなく、Class APIを用いてあくまで状態はHTML側が持っているという体にしています。 これは非常に単純な作用に見えますが、非常に奥深く、機能の汎化に役立ちます。 JavaScript内で直に特定のclassの付与してしまっていては、このcontrollerは同名のclassをふる時にしか再利用できませんが、class apiを利用することでHTML側を変えるだけでJavaScriptはそのまま再利用できるという汎用性を手にできるのです。 最後に Stimulusでa11yを考慮したタブを実装してみました。 アクセシビリティを考慮した実装をするとなると、ランドマークロールやウィジェット属性の付与、キーボード操作などの挙動や場合によってはDOMの見直しなども発生するかもしれません。 そんな中でも学習コストが低く、HTMLに寄り添ったライブラリであるStimulusは、強い味方になってくれるはずです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【javascript】stringのyyyy年mm月dd日をyyyymmddにフォーマットする。

業務の中でどうしても年月日表記のデータを数字だけの表記に変換したい。 例) 渡ってくるデータ → 2021年09月06日(月) 変換後の値    → 20210906 moment.jsを使用して変更 index.html <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.js"></script> </head> <div id="date">2021年09月06日(月)</div> <script> var get_id = document.getElementById("date") var get_value = get_id.textContent console.log(moment(get_value, 'YYYYMMDD').format('YYYYMMDD')) //20210906 </script> replace()で変更 コメントにて @gsw213 様より index.html <div id="date">2021年09月06日(月)</div> <script> const elem = document.getElementById('date'); console.log(elem.textContent.replace(/\D/g, '')); // 20210906 </script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AzureにNode.jsから画像を保存したりDLしたりする

何かと画像を保存したいってシーンは出てきますよね。 AzureにNode.jsから画像を保存する 準備編の続きですが、Azure Blob Storageに画像をアップロード、逆にダウンロードしてローカルに保存を試してみます。 とりあえず、Blob Storage内での概念として、コンテナ=フォルダ的な概念、blob=ファイル的な概念でコンテナ内に作成されると現状思っています。 画像用意など まずは利用する画像を用意してみました。n0bisuke.jpg 前回試したコードでhelloという文字列を送っていたので、そのコードをそのまま使ってbase64変換して送ってみます。 画像をアップロード uploadimage.js 'use strict'; const { BlobServiceClient, StorageSharedKeyCredential, ContainerClient } = require("@azure/storage-blob"); //ファイルを利用 const fs = require(`fs`); const FILE_PATH = `n0bisuke.jpg`; //画像ファイルをbase64文字列へ const image_data = fs.readFileSync(FILE_PATH); const base64_data = "data:image/jpeg;base64," + image_data.toString('base64'); async function main() { // Enter your storage account name and shared key const account = process.env.ACCOUNT_NAME || "ストレージアカウント名"; const accountKey = process.env.ACCOUNT_KEY || "キー"; const sharedKeyCredential = new StorageSharedKeyCredential(account, accountKey); const blobServiceClient = new BlobServiceClient( `https://${account}.blob.core.windows.net`, sharedKeyCredential ); //コンテナを作成した名前に指定 const containerName = `n0bisuke-container1630888214169`; const containerClient = blobServiceClient.getContainerClient(containerName); // Create a blob const content = base64_data; //変換したbase64文字列を使う const blobName = "newblob" + new Date().getTime(); const blockBlobClient = containerClient.getBlockBlobClient(blobName); const uploadBlobResponse = await blockBlobClient.upload(content, Buffer.byteLength(content)); console.log(`Upload block blob ${blobName} successfully`, uploadBlobResponse.requestId); } main().catch((err) => console.error("Error running sample:", err.message)); $ node uploadimage.js Upload block blob newblob1630891763317 successfully 37db3c29-201e-004e-52c6-a28313000000 newblob1630894950109というblob名でアップロードされました。 ポータル上でも確認できました。6.93KiBということでしっかり保存されてそうです。 画像をダウンロードして保存 今後は逆にDLを試してみます。テキストだけでは仕方ないので画像ファイルとして保存する処理まで書いていきます。 参考: Node.jsで画像をダウンロードして保存する(axios利用) 参考: [Node.js] Base64エンコードされたファイルデータをデコードして、S3にputObjectする この辺りが参考になりました。 先程アップロードしたnewblob1630891763317のblob名を直指定で使います。 公式サンプルにあるstreamToBuffer()を利用して文字列にエンコードして、base64文字列からデータの中身を取り出してwriteFileSync()でファイル保存しています。 dl.js 'use strict'; const { BlobServiceClient, StorageSharedKeyCredential, ContainerClient } = require("@azure/storage-blob"); const fs = require(`fs`); const FILE_PATH = `n0bisuke.jpg`; async function main() { // Enter your storage account name and shared key const account = process.env.ACCOUNT_NAME || "ストレージアカウント名"; const accountKey = process.env.ACCOUNT_KEY || "キー"; const sharedKeyCredential = new StorageSharedKeyCredential(account, accountKey); const blobServiceClient = new BlobServiceClient( `https://${account}.blob.core.windows.net`, sharedKeyCredential ); //コンテナを作成した名前に指定 const containerName = `n0bisuke-container1630888214169`; const containerClient = blobServiceClient.getContainerClient(containerName); //blobにアクセス const targetBlobName = `newblob1630891763317`; const blockBlobClient = containerClient.getBlockBlobClient(targetBlobName); //blobからデータをダウンロード const downloadBlockBlobResponse = await blockBlobClient.download(0); //データを文字列に const encodedData = (await streamToBuffer(downloadBlockBlobResponse.readableStreamBody)).toString(); //今回は元が画像ファイルだったのでbase64形式のテキストがDLされるので中身を抽出 const fileData = encodedData.replace(/^data:\w+\/\w+;base64,/, ''); //ローカルファイルに書き込み fs.writeFileSync(`./dl/${FILE_PATH}`, fileData, 'base64'); } // A helper method used to read a Node.js readable stream into a Buffer async function streamToBuffer(readableStream) { return new Promise((resolve, reject) => { const chunks = []; readableStream.on("data", (data) => { chunks.push(data instanceof Buffer ? data : Buffer.from(data)); }); readableStream.on("end", () => { resolve(Buffer.concat(chunks)); }); readableStream.on("error", reject); }); } main().catch((err) => console.error("Error running sample:", err.message)); 実行すると... 無事に保存された! encodedData.replace(/^data:\w+\/\w+;base64,/, '');の処理を忘れると、うまくファイルが表示出来なくなってしまうので注意って感じでした。 まとめ "画像をAzure Blob Storageにアップロードとダウンロードする"をNode.jsから試してみました。 終わってみるとあっさりでしたが、最初が発狂しそうでした。笑
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AzureにNode.jsから画像を保存する 準備編

画像の保存などをやりたいなと思ってAzureでやってみよ〜と思った時の話です。 まずは情報に行き着くまでが大変でした Azureに限らずAWSもGCPもそんな感じだとは思いますが、サービス多いので概念理解をするのに時間がかかるといったやつです。 Azureに画像を保存したいけどBlobストレージとかFilesとか調べるとごちゃごちゃ... って感じです。 調べるとAzureのBlob Storageというサービスが画像とか保存できるっぽい話が出てきます。ただAzure Filesってのも出てきたり、なんかもっと色々出てきます。 少しイラつき。笑 Azule Filesとはとかを調べてみて雰囲気こんなかなぁという印象が Azure Blob Storage => API経由で利用想定 Azure Files => NASとか社内共有フォルダ的な利用想定(APIもある) といった印象。 とりあえずBlob Storageの方を使ってみます。 ポータルからblobって調べるとストレージアカウントというまた新ワードが出てくる 割と分からんけど、これかなぁ、、という感じで進めてみたけど合ってたみたい。 今だによく分かってはないけど使えたのでよしとします。 (きっとこの記事をみた詳しい人が教えてくれるはず!) と言うかうんじでストレージアカウントなるものを作成します。この時の気持ち的にはストレージを使いたいのにアカウント作るのか?という雰囲気。 こんな感じで作ってみました。 プレミアムって表記もありますが、とりあえずStandardで。 作成してリソースグループに移動するとこんな感じの画面になります。 Azure Blob StorageをNode.jsから利用する とりあえずリソースは作ったっぽいけど、どう使うんだということで調べてたらSDKを見つけました。 Azure/azure-sdk-for-jsこちらのサンプルを触ってみます。 どのキーを使うのか basic.jsを見ると、ACCOUNT_NAMEとACCOUNT_KEYを入れたら使えるっぽい雰囲気があるので調べてみます。 どうやらこれらっぽいですね。キーの表示を押すとキーが表示されます。 basic.jsを試してみました。 $ npm i @azure/storage-blob dotenvは使わないので11行目は削除して試します。 $ node basic.js Create container newcontainer1630887559036 successfully e5e72525-101e-0037-7eb4-a27f37000000 Upload block blob newblob1630887559208 successfully e5e72597-101e-0037-6bb4-a27f37000000 Blob 1: newblob1630887559208 Downloaded blob content hello deleted container ほうほう... サンプルコードで何行われてたかコード読まずに実行(危ないw)したけどどうやら、コンテナってのを作る -> データをアップロードする -> データのダウンロードをする -> コンテナを消す みたいな雰囲気で処理をとりあえず実行しましたよってサンプルっぽいですね。 とりあえずsuccessっぽい挙動なのでとりあえずキーなどは合ってた模様。 Azure Blob Storageにコンテナを作成する とりあえず先程のコードだとコンテナ作成して削除してまでをやってるので、とりあえずコンテナ作成部分だけ抜き出して見ました。 名前もn0bisuke-container~~的な名前にしてみます。 cc.js 'use strict'; const { BlobServiceClient, StorageSharedKeyCredential } = require("@azure/storage-blob"); async function main() { // Enter your storage account name and shared key const account = process.env.ACCOUNT_NAME || "ストレージアカウント名"; const accountKey = process.env.ACCOUNT_KEY || "キー"; const sharedKeyCredential = new StorageSharedKeyCredential(account, accountKey); const blobServiceClient = new BlobServiceClient( `https://${account}.blob.core.windows.net`, sharedKeyCredential ); //コンテナの作成 const containerName = `n0bisuke-container${new Date().getTime()}`; const containerClient = blobServiceClient.getContainerClient(containerName); const createContainerResponse = await containerClient.create(); console.log(`Create container ${containerName} successfully`, createContainerResponse.requestId); } main().catch((err) => console.error("Error running sample:", err.message)); 実行してみます。 $ node cc.js Create container n0bisuke-container1630888214169 successfully 947cbed6-101e-0027-7bb6-a2ba5f000000 お、成功したっぽい。 管理画面を見ると指定した名前のコンテナーが表示されてました。 まだコンテナーが何なのか分かってないですが多分大枠のフォルダみたいな概念だと思っておきます。 作成したコンテナにアクセス cc.jsを一部変更してshow.jsを作ってみました。 show.js 省略 //コンテナを作成した名前に指定 const containerName = `n0bisuke-container1630888214169`; const containerClient = blobServiceClient.getContainerClient(containerName); console.log(containerClient); 省略 実行するとコンテナの情報が表示されました。 $ node show.js ContainerClient { url: 'https://xxxxxxxxxxxx.blob.core.windows.net/n0bisuke-container1630888214169', accountName: 'xxxxxxxxxxxxxx', pipeline: Pipeline { factories: [ [Object], 省略 コンテナにデータをアップロードしてみる この辺までくると割とイメージ掴めてきました。 basic.jsにあったコードを持ってきてアップロード出来ました。 basic.jsで既にやってた処理ですけどね。 参考: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/storage/storage-blob/samples/javascript/basic.js#L62-L66 upload.js 省略 //コンテナを作成した名前に指定 const containerName = `n0bisuke-container1630888214169`; const containerClient = blobServiceClient.getContainerClient(containerName); // Create a blob const content = "hello"; const blobName = "newblob" + new Date().getTime(); const blockBlobClient = containerClient.getBlockBlobClient(blobName); const uploadBlobResponse = await blockBlobClient.upload(content, Buffer.byteLength(content)); console.log(`Upload block blob ${blobName} successfully`, uploadBlobResponse.requestId); 省略 実行するとこんな感じでポータル上にも表示されます。 何か出来てますね。このNode.jsのコード見る限りだとhelloって文字列が入ってそうです。 5Bって書いてるしそれっぽいですね。 いよいよ画像を投稿 長くなってきたのと、この辺は分かってる人は見なくてもよさそうということで、記事を分けて次回に続く...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでアプリを作成しました【7】【パックマン】

はじめに 学習するに至った経緯 2020年より、未経験からエンジニアへの転職を目指し、某プログラミングスクールへ通う。入学後、『Ruby』を未経験から学ぶ人が多いのと『Ruby』の求人が思っていた以上に少ないので、卒業後、フロントエンドのエンジニアを目指す事に。 Javascriptの学習した事を言語化し、認識の深化による備忘録として記載。 【仕様】 参考サイト バニラJavaScript、HTML、CSSでパックマンを構築する|アニア・クボウ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでアプリを作成しました【7】【Space Invaders】

はじめに 学習するに至った経緯 2020年より、未経験からエンジニアへの転職を目指し、某プログラミングスクールへ通う。入学後、『Ruby』を未経験から学ぶ人が多いのと『Ruby』の求人が思っていた以上に少ないので、卒業後、フロントエンドのエンジニアを目指す事に。 Javascriptの学習した事を言語化し、認識の深化による備忘録として記載。 【仕様】 参考サイト Space Invaders in JavaScript (no-nonsense version!)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでアプリを作成しました【7】【パックマン】

はじめに 学習するに至った経緯 2020年より、未経験からエンジニアへの転職を目指し、某プログラミングスクールへ通う。入学後、『Ruby』を未経験から学ぶ人が多いのと『Ruby』の求人が思っていた以上に少ないので、卒業後、フロントエンドのエンジニアを目指す事に。 Javascriptの学習した事を言語化し、認識の深化による備忘録として記載。 【仕様】
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】コントローラーからjavascriptへパラメータを渡す

はじめに ・本記事ではコントローラからjavascriptにパラメータを渡す方法を説明します。 controller→view→javascriptとviewを介してパラメーターを渡すことができます。 まず、コントローラからビューへは、インスタンス変数を使いパラメータを渡します。 /app/controller/books_controller.rb def index #パラメータを、インスタンス変数でビューに渡す @book = 'コントローラからjavascriptへパラメータを渡す方法' end view(ビュー)ではinputタグでtypeをhidden属性にし、valueとして受け取ります。 /app/views/books/index.html.erb <h1>indexページ</h1> <input type="hidden" id="book" value="<%= @book %>"> javascriptで、htmlの要素をdocument.getElementByIdで取得します。 /app/assets/javascripts/application.js window.onload = function () { const book = document.getElementById('book').value alert(book); } ブラウザで確認すると、コントローラから渡した@bookのパラメータが、javascriptのalertログで出力されていることが分かります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む