WEBシステム、WEBプログラム開発において昨今ではjQueryではなくVue.js、React、AngularなどのJSフレームワークが主流となってきています。ただ、これらの活用は学習コストが高いといわれています。その原因はフロントエンドありきで話が進みすぎていたからだと考えています。したがって、自分の投稿記事は、jQueryを多用するWEBシステムエンジニアに向けた、フォーム操作をメインに置いた半備忘録兼自分なりに解釈した解説です。
ちなみに自分はサーバ構築からバックエンドまでこなしているワンオペエンジニア(フリーランス、非正規雇用に非ず)です。
-
コンポーネント、ルーティング、スタイル周りの続編を作成しました(編集が困難になるほど容量圧迫してたので、それぞれで独立させています)。また、Vueは3.2から採用されたscript setup、Angularは14から採用されたスタンドアロンコンポーネントにも触れています(Reactも18から採用されたトランジションに触れようとは考えています)。また、当記事ではVue.js、ReactではTypeScriptには触れていませんが、続編では第8章で採り上げています。
昨今で流行りつつあるSvelteでも動きを追ってみました。
§1:Vue.jsとReact、そしてAngular
その前に、vue.jsとReact、そしてAngularとはどういったもので、どんな意図で開発されたものかを知っておく必要があります。そして、その理念を知っていたら、スクリプト記法の理屈もわかりやすいからです。
ひとまず、共通のスローガンがあります。それはいずれも素早くデータの同期をとりたいために実現させている技術であることです。
Vue.js
Vue.js(ヴュージェイエス)はもともとGoogleが開発したAngularJSの開発者の一人、エヴァン・ヨーが、個人で開発を始めたJSフレームワークで、柔軟に開発できるというコンセプトで生み出されたプログレッシブ・フレームワークという概念を持っています。そのため、小規模の開発に向いた柔軟な利用も可能です。そのVue.jsとは一言でどんな技術かというと
html内の部品に対して、Vueディレクティブという魔法をかけ、多種多様な呪文(プロパティ)でデータを加工していく
こういうものです。VueディレクティブとはVue.js内で定義されるv-hogeという各種コマンドのことで、データのインプットとアウトプットをリアルタイムで行うことができたりする同期(バインド)操作のことです(訂正指摘感謝します)。このVue.jsでのバインドを、Reactではリアクティブ、AngularJSなどでは双方向バインディングなどと呼んでおり、今のAngularでもその技術はある程度継承されています。そして、AngularJSはhtml側でngというプロパティやメソッドを使ってバックエンドの処理をしており、主にフォーム周りの制御をより円滑にするために開発されたものでした。したがって、かなり明解だった反面、開発が進むとかなりhtmlソースが汚されてしまうので、技術をある程度継承し、Angularというパッケージ単位のフレームワークを作ったわけです。しかし、このAngularJSの双方向バインディングと簡潔さを捨てるのはもったいないと、飛躍的に発展させたのがVue.jsでありVueディレクティブというもので、これはソースが汚れる原因となったhtml側でのバックエンド処理を、スクリプト側(コンポーネントを作成して管理)で処理させるようにしました。つまり
- AngularJS: 双方向バインディング処理をhtml側で処理できたために、ソースが汚れてしまい、敬遠されてしまった。
- Vue.js : バインド(双方向バインディングとほぼ同定義)処理をスクリプト側に記載させるようにし、html側は基本、メソッドとプロパティだけ入出力させるようにした(つまり、JavaSrciptの基本に返った)
この理念を覚えておけば、今後の学習にも役立ちます。また、既存の書き方をoptions APIと呼びますが、Vue3よりcomposition API(コンポジションAPI)を用いた記法が可能となっています。ほかにJSX対応の記法やVue3.2からはSvelteのようなsetup記法もできますが、基本は上記の2つの記法です。
React
React(リアクト)はMeta社(当時はFacebook社)が自社ツール(Facebook、Instagram)のデータ管理のために開発したコンポーネント型のJSフレームワークで、その鍵となるのはJSX
という記法です。そのJSXとはなにかというと、スクリプトの中に直接埋め込める、html言語とほぼそっくりそのままの処理用言語で、それを作った理由は後述するように完全なるデザイン部分とプログラム部分の切り分け(すなわち部品管理の徹底化)のためです。ですが、その独特の記法によって技術者や入門者に違和感を与え敬遠されてしまっていたのも事実で、そのために何度も記法が変更されてきており、昨今ではだいぶ見やすく、そして記述しやすくなっています。それでもReactの基本的な理念と動きは変わっておらず、一言でいうと
html内に魔法結社(JSXの製造拠点、つまりはコンポーネント)を作り、そこでリアクティブな部品(JSX)を錬金する
こういうものです。つまり、Vue.jsだといわゆるリアクティブな部品(Vueディレクティブ)自体はhtml上にあったのに対し、Reactの場合は、リアクティブな部品はhtmlになく、バックエンド上のコンポーネントで作成されることになります。そして、リアクティブな処理を行う際もそのバックエンド内だけで処理するので、非常に動きが高速で、部品もバックエンドにしか存在しないことで、開発を分担しやすく小~中規模の開発に向いています。
- React :部品の管理を徹底するために、リアクティブなデータ処理をhtml側で一切できないようにしている。部品の調達と管理はすべてJSXに則ったコンポーネント内で行う。
これが基本です。あと、コンポーネントの記法もいろいろあって、しかもしょっちゅう変更されていた(2021年現在は16.7以前の記法《クラスコンポーネント》と16.8以降《関数コンポーネント》で落ち着いている)ので、それが却って敬遠させていた(初心者がどこから手を付けたらいいのかわからない)気もしますが、基本となる部分は全くぶれていないことを踏まえておいてください。
2023年現在は関数コンポーネントでの記述が主流なので、新たに学習する場合は関数コンポーネントだけにするといいでしょう。
Angular
Angular(アンギュラー)はGoogle社が開発したAngularJSでの失敗を教訓に、その失敗の原因となったソースの汚れを解消させたものです。そしてvue.js、Reactと違い、RubyのRailsやPHPのLaravel、PythonのDjangoのような、それ一つでパッケージとなっているフルスタックフレームワークとなっており、TypeScriptがベースです。そして、これも一言で表すと
パッケージ自体がAngularという魔法世界(フレームワーク)であり、その中でバインディングという魔法の扉でデータをやりとりできる
なので、そのパッケージ内では自由自在にAngularの技術を利用できます(VueやReactのように、どこからどこまでという範囲指定が不要)。ですが、前述したようにソースの汚れを反省して作ったフレームワークなので、いわゆるリアクティブな部品は各アーキテクチャ内で実行するようになっていて、バインディングというものでデータや処理のやりとりができるようになっています。なので、上記2つと比較するといろいろなメソッドでデータの同期を取るというよりは、Ajaxでやりとりしているようなものです。ただ、基本はAngularJS時代とそこまで変わっていないためにあまり難しくなく、どちらかというとVue.jsに(特にoptionsAPI時代のVue.js)近いですが、JavaScriptでの記述法がそのまま応用できるためVue.jsよりデータのやりとりの理屈は解りやすいので、フレームワーク開発に慣れているバックエンドエンジニアなら、上記2つより学習は楽でTypeScriptの癖さえ押さえておけば簡単にプログラムを作れます。また、けっこう容量があるので、大規模開発にも対応できます(ただ、普通に軽量システムに用いることも可能ですし、敬遠の原因の一つともいわれるRxJSも、ルーティング(続編の第6章参照)でデータを受け渡しするまでは覚える必要ありません。要はスペックが揃っているので、小規模~中規模システムには少しばかり性能を持て余しますが、感覚としてはRailsやDjangoで小規模システムを構築するようなものです)。
- Augular:バインディング処理を行う部品はhtml上にあるが、処理は外部のアーキテクチャ内で行う。言語はTypeScriptを用いる。
そしてこのTypeScriptですが、JavaScriptと互換性を持っているため、そこまで難しく考えることはないと思います。
なお、本記事でAngularJSは採り上げません。よく勘違いしている記事を見かけますがAngularJSとAngularは全く別物で、互換性は全くない上にAngularJSは2021年12月を以てサポート終了しています。
また、Angularは15よりスタンドアロンコンポーネントでの記述が主流となってきています。当記事はそれ以前の記述となっています。
§2:バックエンドのための基本文法
前述したように、これらのJSフレームワークの学習コストが肥大化してしまった原因は、フロントエンドありきで解説していることが多かったためでしょう。そのため、バックエンドエンジニアが基礎の基礎である文法もわからずに、そっちばかりに目が行ってしまっていることで混乱を招き、これらの技術に手を出す気力を与えず、比較的記述が簡単なjQuery依存から脱皮できなかったのではないかと考えています(自分もそうでした)。
したがって、自分はjQueryで操作してきたフォーム操作をVue.js、React、Angularではどう記述するのかを重点において解説していきたいと思います。また、基礎の学習のため、サーバは極力構築しないようにしておきます(本来ならVue.jsはVue CLIやNuxt、ReactはReact NativeやNextというサーバ、フレームワークを用いて開発していきます。Angularはローカルでテスト操作できないので、Angular-CLI上で動かしています)。
演習1:フォーム操作:テキスト文字を表示させる
フォーム操作の基本の基本です。ですが、この基本だけでかなり記法の根本が理解できるのも事実です。ただ、単純に表示させるだけなのも面白くないので、jQueryでフォームに対し、キーの打鍵ごとに値を表示させるようにしましょう。
<body>
<input type="text" id="f_inp">
<p>入力された文字<span id="mes"></span></p>
<script>
$(function(){
$('#f_inp').on("keyup",function(){
let mes = $(this).val(); //値を取得する
$("#mes").text(mes); //打ち込んだ値を反映させる
})
})
</script>
</body>
これで、打ち込んだ文字をそのまま下の#mesに表示させることができます。これとほぼ同じ挙動をVue.js、React、Angularで再現してみます。
Vue.jsで再現
Vue.jsで記述するとこうなります。そしてVue.jsですが、こちらはコンテンツの後に制御部分を記述する必要があります。なぜなら、DOMが作成されてからでないと、データの同期が始まらないからです。
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="mes">
<p>入力された文字<span>{{ mes }}</span></p>
</div>
<!-- 制御部分はコンテンツの後 -->
<script>
new Vue({
el: '#app',
data:{
mes: '',
},
});
</script>
</body>
目の付け所はelというプロパティとv-model
というVueディレクティブであり、これこそがまさしくVue.jsの要であるバインディング技術(ディレクティブ)になります。そして、それが適用される部品は任意のIDで囲んだ単一の親要素のみとなります。これをエレメントと呼び、el(elementの略)プロパティで指定した範囲のみ、ディレクティブを適用できます。
もっと詳しく文法を解説する
Vue.jsの場合は、html部分とスクリプト部分は区別しない方が説明もしやすいです。もう一度さっきのhtmlファイルを確認してみましょう。そして、コメントを付与します。
<body>
<div id="app"><!--#app内のブロック要素にVueディレクティブを適用(1) -->
<input type="text" v-model="mes"><!-- v-modelはフォームの値を監視するVueディレクティブ(2) -->
<p>入力された文字<span>{{ mes }}</span></p><!-- {{hoge}}はマスタッシュ -->
</div>
<script>
new Vue({
el: '#app', //適用対象となる要素名(1)
//dataはVue.jsの操作に必要な変数を格納するオブジェクト
data:{
mes: '', //今回は変数mesを使うので定義しておく(中は空白)。(2)
},
});
</script>
</body>
このようになります。まずは、部品をバインドするために(1)のように、どこまで適用するのか、その場所を定義し、そこから具体的な動作(2)が行われます。v-modelはフォーム部品の動きを監視する働きを持っているので、inputの動きに変化(新たに文字が入力された)があると即座に反応し、そしてその結果を{{ mes }}に返すようになっています。
■補足1:dataプロパティ
dataプロパティはVueディレクティブで作業するための変数置き場で、これを定義しておかないと、xxxx is not definedと処理中に未定義のエラーが起きます。今回はmesという変数を使用しているので、これを定義しておき、そして初期値は何も打ち込まれていないので、空っぽにしておきます。
■補足2:マスタッシュ
マスタッシュとは英語で口髭のことで、{{ }}
という記号です。そしてVue.jsではVueディレクティブの外側(v-modelなどv-xxxxというプロパティ)に値を適用する場合は{{変数名}}
とすることで、その値を受けとることができます。ここでは{{mes}}はVueディレクティブの外側なのでマスタッシュで記述しています(エレメントの外側ではないので注意。エレメントの外側にマスタッシュを記述しても、そこはVue.jsの適用外なので、ただの文字列として認識されるだけです)。
■※ Vue3のcomposition APIを、ローカル環境で表示させる
結論からいえば、応用性のない記述が求められることになります。なので、Vue3のcompositionAPIから始めたい場合は、最初からvue CLIでテストした方がいい気がします。
演習1ならばこんな感じの書き方になります。基本的にはsetupメソッドに変数を記述して、それをreturnで返すというものです(またcomposition API用の処理用スクリプトも多数ありますが、ローカル環境で表示させる場合は、それらもすべてVueクラスから呼び出すという記述が必要になります)。
一方、テンプレート側は記述にほぼほぼ変更はありません。
<head>
<script src="https://unpkg.com/vue@next"></script><!-- CDN -->
</head>
<body>
<div id="app">
<input type="text" v-model="mes">
<p>入力された文字<span>{{ mes }}</span></p>
</div>
<script>
Vue.createApp({
setup(){
const mes = Vue.ref("") //refは単数の変数を処理するために必要
return{
mes
}
},
}).mount("#app")
</script>
</body>
とりあえず、CDNで使う方法のリンクを貼っておきます。
cdnを使ってHello World
※Vue3.2(script setup記法)のCDNは2023年5月時点ではまだないようです。
Reactで再現
では、全く同じ動作をReactでも再現してみます。ただ、Reactは少し説明をわかりやすくするために、敢えて最短の記述にしていません(関数コンポーネント以降はもっとスリム化した記法も使えるのですが、まずはクラスコンポーネントで記述してみます)。
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.10.3/babel.min.js"></script>
<script type="text/babel">
class App extends React.Component{
constructor(){
super();
this.state = {
mes: ''
};
this.changeText = this.changeText.bind(this);
}
changeText(e){
this.setState({ mes :e.target.value });
}
render(){
return(
<div>
<input type="text" onChange={this.changeText} />
<p>入力された文字<span>{ this.state.mes }</span></p>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
</script>
</head>
<body>
<div id="root"></div>
</body>
初見の人が見たら、「なんだこりゃ?」となること請け合いです。スクリプトの中にhtmlタグ(厳密にはJSX)が入っているのが生理的に受け付けないのでしょう。しかし、Reactも初期と比較すると、バージョン15以降、だいぶ記述はスリム化しており、そして解りやすくなりました。では、この処理の流れがわかるようにコメントを付与してみます。なお、Vue.jsでは、スクリプトをbodyタグの内側、コンテンツの後に書くのがセオリーでしたが、Reactはコンテンツの前、headタグの中にスクリプトを記述するのがセオリーです(普通は外部ファイル化します)。なぜなら、JSXはDOMの生成前から作成されていくからで、完成されたパーツをDOMに組み込むような読み込みをするからです。
また、JSXのコメント欄は{/*ほげほげ*/}
とすると干渉されずに打ち込むことができます。
<script type="text/babel">
//Reactコンポネントを作成するクラス(1)
class App extends React.Component{
//A:定義部分。簡単にいえば、処理前に準備する変数やメソッドなど。
constructor(){
super();
this.state = {
mes: ''
};
this.changeText = this.changeText.bind(this); //(3)値をバインドさせる準備
}
//B:処理関数部分。renderの外側に書く方が見やすい。
//入力された文字を返す関数(2)
changeText(e){
this.setState({ mes :e.target.value }); //setStateは値をバインドさせる処理(3)
}
//C:レンダリング部分。JSX記法のhtmlタグを描画する。
render(){
//JSX記法で返す部品(2)
return(
{/*onChangeハンドラ以下はReactでバインディング処理を行う関数(3)*/}
<div>
<input type="text" onChange={this.changeText} />
<p>入力された文字<span>{ this.state.mes }</span></p>
</div>
);
}
}
//コンポーネントの反映
ReactDOM.render(
<App />, {/*コンポネント作成を行う部品(1)。記述方法にルールがある。*/}
document.getElementById("root") {/*//外側に部品を表示させる(4)*/}
);
</script>
</head>
<body>
<div id="root"><!-- ここに処理部品が入る(4)--></div>
</body>
だいたいこのような流れになっています。16.8の関数コンポーネントからはもっと簡略化した記法も使えるのですが、ひとまず一般的に知られているクラスコンポーネントの記法を試してみてからの方がいいでしょう。見た目は複雑に見えますが、まずは次のように分類しましょう。
- React.Component() //コンポーネントを作成します
- ReactDOM.render() //作成したコンポネントを反映させます
renderとはいろいろな意味がありますが、レンダリングという言葉通り、描写という言葉で覚えればいいでしょう。そして、その名の通り、ReactDOM.render内ではJSX(htmlそっくりの言語)を描写しているのです。そしてその部品を作成するのがReactDOM.Componentであり、いわば、前者がシステムの納入、後者がシステムの開発や保守管理だと思えばいいでしょう。したがって、htmlタグ側にはリアクティブ処理されたJSXしか存在していなく、スクリプトの処理部分は、Vue.jsと違って部品すら見えません。
では、今度はReact.Componentの中身を3つに分類してみます。
- constructer() //定義部分
- 処理用の関数 //処理部分
- render() //レンダリング部分
このように分類するとわかりやすいですし、関数コンポーネントでも同じように区分できます。そして、その作業の流れを文章化してみると
- Appという部品を作成する命令を出す(1)
- 命令を受けたコンポネントはコンストラクタから部品を用意してレンダリング領域にある通り、タグを作成し、ReactDOM.renderを使用して処理の外側に返す(2)(4)
- onChangeイベントが発火したら、用意していた関数を使って処理を行う(3)
- 処理を行った状態で再度レンダリングを行い、ReactDOM.renderを使用して処理の外側に返す(4)
それを踏まえた上で処理を追ってみると、動作はJavaScriptとそこまで相違ないと気づくはずです。
ただ、決定的な違いとして処理関数内にreturnが存在しない(レンダリング部分ではない)、そしてreturn処理を行う代わりにsetStateで制御することです。そして、これによって同期処理が可能になります。
オブジェクト、変数の利用
Reactでもう一つ躓きがちなのが、オブジェクトや変数の利用に際しては、同じコンポーネント内であっても、クラスオブジェクトで制御しているために外部の存在として扱わないといけないということです(オブジェクト指向におけるthisのやりとりを連想してください、それとほぼ同じ理屈です)。具体的に言うと、thisという代名詞を置いているのがそれで、それぞれ、定義部分、レンダリング部分、関数部分で変数や関数をやりとりする際には、thisという代名詞を使っているのがわかると思います。
- 関数部分のchangeTextをレンダリング部分で使用する場合、this.changeTextとして呼び出し
- レンダリング部分でmesを使用するために、定義部分のstate.mesをthis.state.mesとして呼び出し
このような具合です。またsetStateのようにコンポーネントで用意された部品を使用する際も、外部から借用することになるので、this.setStateと代名詞を付与しています。
補足1:バインディングのルール
定義部分で
this.changeText = this.changeText.bind(this);
とあると思いますが、これは別におまじないではなく、利用する関数において値をバインド(同期)したいときに記述するルールで、これを記述しないと値をバインドできません。そして、基本は同じオブジェクト名にしておくだけで、左側はコールバック関数を代入しているだけですので、定義さえしておけば、別名でも大丈夫です(したところでメリットが薄いので同名にしおくべき※ですが)。
※同一階層のコンポーネント処理の場合、親子階層になった場合は、区別するためにコールバック関数側にhogeStateとしている場合が多いです。
また、setStateはReactにおいて極めて重要性の高いメソッドですが、簡単にいえば、元の値と新しい値をバインドさせたいときに設定するものです。具体的には先程入力した文字と新たに入力した文字が異なる場合、随時setStateメソッドが実行されることになります。
また、e.target.valueはJSX内にある部品の、任意のフォームに対して取得した値(value)を受けとるもので、e(jQueryではelemと書くことが多い)とは、任意のオブジェクト変数に過ぎません。そして、受け取った値を先程のsetStateメソッドを使って、値の変化を処理しているわけです。
補足2:JSXの記述ルール
JSXは共通して以下の決まりがあります。
-
単一の親要素しか生成してはいけない(中に子要素、孫要素があるのは問題ない)
もし、複数のタグを使いたい場合は、次の例のようにFlagmentオブジェクトを使ったり、タグごとにオブジェクト化する方法があります(React-Nativeなら<>
という空タグを用いることもできます)。 -
<input>
タグのようにそれ一つで完結するタグは必ず<input />
のようにスラッシュを入れること。これを入れないとJSXがどこまで適用したらいいのかわからずにエラーが起きます。これはテンプレートタグ<App />
などでも同様です。 - JSXは必ず丸括弧(...)で囲って値を返すこと。これは次章のmapメソッドでも活用します。
- プロパティに特殊な記述ルールがある
たとえば、classプロパティはclassが予約語であるため、ReactではclassNameと記述する必要があります。
関数コンポーネントでの記述
上記の方法でも新たに導入されたクラスオブジェクトを使用するクラスコンポーネントでかなり簡潔になりました(ECMA5時代はコンポネントも逐一作成する必要がありました)が、それでも至るところにthisばかりあったりと、色々と無駄があるようにも見えました。React16.8からは関数コンポーネントで記述することができます。
<script type="text/babel">
//定義部分
const { useState } = React; //ローカルの場合で、useStateを使用するための定義
const App =()=>{
//処理部分
const [mes,setMes] = useState(true); //バインディング処理
//一度メソッドで準備する
const changeText = (e)=>{
setMes( e.target.value ); //値の比較
}
//レンダー部分
return(
<div>
<input onChange={changeText} />
<p>入力された文字<span>{ mes }</span></p>
</div>
);
}
//レンダリング処理
ReactDOM.render(
<App />,
document.getElementById("root")
);
</script>
代名詞のthisが消えてかなり明白になったと思いますが、基本的な動作は変わっていません。ただ、変数定義部分、処理部分、レンダリング部分が一つの関数に収まったので、すごくすっきりしています。
Angularで再現
Angularは他のフレームワークと異なり、ローカル上で手軽に動かして…ということができないので、一旦はAngular-CLI上で動いているシステムからのかんたんな説明から始めます。
<div>
<input type="text" [(ngModel)] ="mes" />
<p>入力された文字{{mes}}</p>
</div>
これだけで入力データを反映できます。尤も、データ部分は色々と呼び出さなければいけないライブラリはありますが、最低限覚えておく必要があるのが3種類のバインディングで、この3つを頭に入れておくだけで最低限の同期操作が可能になります。
◆Angularのバインディング
基本的な文法だけを説明すると[(ngModel)] = "mes"
がデータの双方向バインディング部分であり、{{mes}}にバインドされたデータが表示されます。[(hoge)]
はかなり珍妙な括弧に見えますが、実はこれはちゃんと意味があり、(fuga)の丸括弧はビューからデータへの偏方向(イベントバインディング)※3章で説明、[piyo]の角括弧はデータからビューへの偏方向(プロパティバインディング)※2章で説明、を意味しているので、[(hoge)]の場合はビューからデータ、データからビューの双方向(双方向バインディング)を意味しています。また、options API時代のVue.jsと非常に表記が似ていますが、前述したようにVue.js自体、AngularJSを飛躍的に発展させたものなので、記法は非常によく似ている部分があります。
◆ビューの呼び出し
さて、Angularでビューとデータをやりとりする場合に最低限必要なものを解説していきます。
◆モジュール
Angularでデータを同期させるためにはngModelというディレクティブでの制御が必須となりますが、ただビューに記述するだけでは機能しません。モジュールにFormsModule
の呼び出しを記述する必要があります。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; //ここを追記しないとngModelを認識しない
import { AppComponent } from './app.component'; //データの呼出
@NgModule({
declarations: [
AppComponent //シンボルの定義
],
imports: [
BrowserModule,
FormsModule //ここにも追記
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
また、自動作成を行った場合はapp.component.htmlというビューが作成されますが、これを任意に変更することも可能です。ただし、色々と記述ルールが公式ガイドに記載されており、たとえば、上記のモジュールの場合はインポート部分、宣言部分での書き換えが必要になります。これについては次項のデータの部分で説明します(初歩学習のうちはそのままのビューを使用することを強く推奨します)。
◆データ
引き続いて、データ部分ですが、最低限これだけの記述が必要です。ビュー用のテンプレートファイルとシンボルは対応させておきましょう。ちなみに、命名規則があり、テンプレートファイルはドットで接続すること(間にハイフンを挟んだりするのは可)、シンボルはパスカルケースで記述します(間にハイフンが挟まった場合も同様:例sample1-angular.component →Sample1AngularComponent)。あと、ファイル名もcomponentやdirectiveなどといった慣例的な接尾辞を付与することを推奨しているようです(ファイル名もそれに従うように変えました)。
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './sample1-angular.component.html', //ビューテンプレートはドットで接続すること
styleUrls: ['./sample1-angulart.component.css']
})
//データ処理を行わない場合でも必ずクラス(公式ガイドではシンボルと表記)は定義しておくこと。
export class Sample1AngularComponent {
}
そして、独自定義したシンボルを使用する場合は、モジュール部分も随時書き換えが必要になってきます。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; //ここを追記しないとngModuleを認識しない
import { Sample1AngularComponent } from './sample1.angular.component'; //データも独自で定義する場合はファイル名に合わせておく
@NgModule({
declarations: [
Sample1AngularComponent //シンボルの定義
],
imports: [
BrowserModule,
FormsModule //ここにも追記
],
providers: [],
bootstrap: [Sample1AngularComponent]
})
export class AppModule { }
演習2:プルダウンメニュー(ループ処理)
では、今度は繰り返し処理の実例としてプルダウンメニューの生成とイベント処理をそれぞれ記述していきます。そして、共通の動作としてプルダウンメニューをオブジェクトから生成し、選択した値を表示させるという動きをそれぞれjQuery、Vue.js、React、Angularで再現してみます。
そうすれば、共通点と押さえどころが見えてくるはずです。
jQuery
まずはjQueryで記述してみます。ループ式の記述方法は色々ありますが、今回は敢えて$.each
メソッドを使います。なお、jQueryはあくまでJavaScriptライブラリなので、ECMA6でも普通に記述できます(jQueryでは敢えて見慣れた記法にしています)。
<script>
$(function(){
//配列の値
let ary_data = [
{id:0,name: "cakePHP"},
{id:1,name: "Laravel"},
{id:2,name: "CodeIgniter"},
{id:3,name: "Symfony"},
{id:4,name: "Zend Framework"},
{id:5,name: "Yii"},
];
let sel = $("#sel");
//プルダウンメニューの生成
$.each(ary_data,function(key,data){
opt = $('<option>').val(item.key).text(item.name);
sel.append(opt);
})
sel.appendTo("#f_sel");
//プルダウンの操作
$("#sel").on("change",function(){
let selkey = $(this).val();
$("#txt").text(ary_data[selkey].name);
})
})
</script>
</head>
<body>
<div id="f_sel">
<select id="sel">
<option value="">選択</option>
</select>
</div>
<p>選択された値:<span id="txt"></span></p>
</body>
このシステムのポイントは前述した通りで、
$.each(ary_data,function(key,item){
opt = $('<option>').val(item.key).text(item.name);
})
このループ文の部分です。これはary_dataという配列をitemという変数に格納しながら順番に展開させ、item.keyをvalueプロパティに、item.nameをoptionタグのテキスト値に代入している工程になりますが、実はこれと全く同じ工程が次のVue.jsとReactとAngularにも現れます。
Vue.js
同じ動作をVue.jsで記述するとこうなります。
<body>
<div id="app">
<select v-model="sel">
<option value="">選択</option>
<option v-for="(data,idx) in ary_data" :value="data.name" :key="idx">{{ data.name }}
</option>
</select>
<p>選択された値:<span id="txt" >{{ sel }}</span></p>
</div>
<script>
new Vue({
el: "#app",
data:{
sel: "",
ary_data :[
{id:0,name: "cakePHP"},
{id:1,name: "Laravel"},
{id:2,name: "CodeIgniter"},
{id:3,name: "Symfony"},
{id:4,name: "Zend Framework"},
{id:5,name: "Yii"},
]
}
})
</script>
非常にすっきりした内容になりました。後述するReactと比較してもVue.jsはフォーム操作に向いており、何よりv-modelプロパティを使用するだけで値を一発で同期できるのが魅力的です。
さて、先程申し上げたように、この記述での目の付け所はv-for="(data,idx) in ary_data"
という部分であり、これは先程のjQueryの$.eachと同様に配列分のoptionタグに対してvalueプロパティとテキスト値を付与するループ処理となっています。forはPHPのfor文などでもお馴染みですが、よく知られる「~のために」という意味のほかにも 「~している間」 という意味があり、その名のとおり配列ary_dataを展開している間はdataという変数に格納しているわけです。しかもjQueryでは逐一スクリプト文で記述していましたが、Vue.jsはこのVueディレクティブの魔法の力(?)でスクリプトに数式を記述することなくループ処理が行えるので、かなり記述が楽になります。なお、ループできるのは何もoptionタグだけでなく、リスト作成に用いるliタグやテーブル作成のtdタグ、はたまたテンプレートタグを用いれば複数のタグをひとまとまりでループさせることも可能です。
また、ループさせる際には:key(v-bind:keyの省略形。後述)が必須となり、これを忘れると警告が出ます。
■補足:Vueディレクティブの省略記法
-
v-bind:xxxx → :xxxx
上記のoptionにあるvalueと:valueは別物で、:valueはVueディレクティブにおけるv-bind:valueの省略記法です。v-bindは非常に使用頻度が高いので、このように省略記法を使うことが多く、慣れてきたら使ってみるといいでしょう。なお、v-bindとはhtmlタグ内のプロパティに、任意の値を同期させるプロパティで、この場合v-forを展開している間に、valueプロパティにその値を格納させる働きを持っています(あくまでhtmlの内側です。外側でvueディレクティブは使えないので、そちらでは前述したように {{ hoge }} (マスタッシュ)を使用します。 -
v-on:xxxx → @xxxx
もう一つ使用頻度が高い省略記法が上記のもので、これはイベントを実行するときに使用し、同じく使用頻度が高いv-onを省略させるものでイベンドハンドラと呼びます。これは次章で採り上げます。
■Vue3のcoposition APIで記述した場合
以下のようになります。今度はVue.reactiveというメソッドを代入していますが、前述のVue.refとは異なり、一度に複数の変数を定義できます。ただ、欠点もあり、逐一stateという予約変数に代入する必要があるので、テンプレートに展開する際もstateオブジェクトから書き始める必要があります。
ちなみに直書きしたオブジェクトはVue.jsの仕様によって同期が取れなくなります。
<body>
<div id="app">
<select v-model="state.sel">
<option value="">選択</option>
<option v-for="(data,idx) in state.ary_data" :value="data.name" :key="idx">{{ data.name }}
</option>
</select>
<p>選択された値:<span id="txt" >{{ state.sel }}</span></p>
</div>
<script>
Vue.createApp({
setup(){
//同期を取りたいオブジェクトはreactiveかrefを使用する
const state = Vue.reactive({
sel: "",
ary_data :[
{id:0,name: "cakePHP"},
{id:1,name: "Laravel"},
{id:2,name: "CodeIgniter"},
{id:3,name: "Symfony"},
{id:4,name: "Zend Framework"},
{id:5,name: "Yii"},
]
})
return{
state
}
},
}).mount("#app")
</script>
</body>
React
同じようにプルダウンメニューの生成をReactでも記述してみます。記述は関数コンポーネントを使っています。
<script type="text/babel">
const {useState } = React;
const App =()=>{
//変数定義
const [sel,setMenu] = useState(true);
const ary_data = [
{id: 1,name: "cakePHP"},
{id: 2,name: "Laravel"},
{id: 3,name: "Code Igniter"},
{id: 4,name: "Symfony"},
{id: 5,name: "Zend Framework"},
{id: 6,name: "Yii"},
];
//値の設定処理(いわばVue.jsの算出プロパティに当たる部分)
const setList = ary_data.map((data,idx)=>(
<option key={idx} value={data.id}>{data.name}</option>
))
//イベント処理
const changeMenu = (e)=>{
setMenu(e.target.value ); //フォームから取得した値
}
//レンダリング
return(
<React.Fragment>
<select onChange={changeMenu}>
<option>選択</option>
{ setList }
</select>
<p>選択された値:<span>{sel}</span></p>
</React.Fragment>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
</script>
</head>
<body>
<div id="root"></div>
</body>
Vue.jsと比較すると色々とややこしくは見えますが、それでも前述したReactとそこまで動作は変わっていませんし、やはりこれも同様にループ式によって配列を展開しています。その箇所がmapメソッドで、このmapメソッドはどちらかというとjQueryの$.eachメソッドに近く、コールバック関数を使って値を展開する働きを持っています。後は、他の2例と同様にvalueプロパティやテキスト値にidやnameを配列順に代入しているだけです。
※Reactもkeyプロパティを忘れると警告が出ます。必ずユニークな値(他と被らないもの)を格納するようにしてください。
//設定処理 ary_dataを順々に展開し、data変数に格納。該当するプロパティをそれぞれ当てはめていく。
const setList = ary_data.map((data,idx)=>(
<option key={idx} value={data.id}>{data.name}</option>
))
●オブジェクトリテラルについて
さて、先程までは触れていませんでしたが、Reactの記法を見ていているとJavaScriptやVue.jsで見られたクォート("")はどこにも書かれておらず、代わりに**{ ... }という波括弧があちこちに存在し、その役割が気になったと思います。これはオブジェクトリテラル**というもので、JSX内において外側に置いている変数(オブジェクト、メソッドなども含む)を展開するために用いるものです。それを踏まえて動きを追ってみるとJSXのselectタグの内部にsetListという変数を展開し、子要素のoptionタグを代入していることがわかります。また、changeイベントの実行時のメソッド、changeMenuはプルダウンメニューの値が変更した際に行うイベント処理を展開しており、それによって先程と同様、直前と現在の値を比較し、値を出力させるようになっています。
補足
Reactでは、JSXにおけるレンダリングのルールとして、単一の親要素しか作れないというものがあります。ですが、今回のようにプルダウンメニューとPタグなど複数のタグを生成したい場合は、上述のように<React.Fragment>
というテンプレートで括る方法があります(オブジェクトに格納して返す方法も昔は活用していました)。
※React Native、Next.jsだと<>という空タグを活用することもできるので、プロパティの格納が不要な場合はこれを用いることがセオリー化しています。
Angular
Angularの場合もVue.jsと非常によく似た記述となります。
<select [(ngModel)]="sel">
<option>選択</option>
<option *ngFor="let data of ary_data" [value]="data.id">{{data.name}}</option>
</select>
<p>選択された値:<span>{{sel}}</span></p>
◆Angularのディレクティブについて
*ngXxxxとして接頭にアスタリスクを付与しているのは、これはAngularにおけるディレクティブであるという構文上の約束であり、ngForディレクティブとはその名の通り、ループ処理を意味します。
*ngFor="let item of array"
Vue.jsと違い、let
という使役文(JavaScriptのletと似たようなもの)が付与されており、前置詞もof
なので注意してください。また、無駄なブロック要素を省きたい場合は
<ng-container *ngFor="let item of array">
</ng-container>
のような記述もできます(ng-containerタグはDOMに反映されません)。
※実はVueのループもinではなくofを使うこともできるようです。
◆プロパティバインディング
optionタグのvalueに[value]="data.key"としているのはプロパティバインディングといってデータからビューへの偏方向を操作するために用います。また、このプロパティバインディングで文字列の中に変数を埋め込みたい場合は、下のサンプルのように、ダブルクォートの中に文字列を結合させる形となります。
//imagesrcが変数
<img [src]="'assets/'+imagesrc">
◆データ部分の記述
ちなみに、データ部分の記述はこうなりますが、詳細は次章で採り上げます。
import { Component } from '@angular/core';
//型の定義
export class Arydata{
id: Number;
name: string;
constructor(id: Number, name: string){
this.id = id;
this.name = name;
}
}
//変数定義
const ARYDATA: Arydata[] = [
new Arydata(1,"モスクワ"),
new Arydata(2,"サンクトペテルブルク"),
new Arydata(3,"エカテリンブルク"),
new Arydata(4,"ムンバイ"),
new Arydata(5,"ベンガルール"),
new Arydata(6,"コルカタ"),
new Arydata(7,"サンパウロ"),
new Arydata(8,"リオデジャネイロ"),
new Arydata(9,"ブラジリア"),
];
const ary_data = [];
@Component({
selector: 'app-root',
templateUrl: './sample2-angular.component.html',
})
export class Sample2AngularComponent {
public ary_data = ARYDATA;
}
演習3:検索フォーム(値の連動)
さて、今までの演習ではjQueryとその他の技術でそこまで工数に差が発生していませんでした。それはイベントがほとんど起きておらず、処理もあまり行われていないからです。しかし、次の演習に用意した検索システムだとどうでしょうか。
では、テキストボックスに入力した文字をもとに、リスト一覧から該当する要素を出力するという機能をそれぞれjQuery、Vue.js、ReactそしてAngularで実装してみます。なお、この一見単純な動きですが、これがよくある写真共有サイトの基礎となる動きです。
jQuery
まずは、あえてjQueryで記述してみます。構築の仕方は色々ありますが、結論から言って面倒です。
<script>
$(function(){
//配列の値
ary_data =[
{id: 1,name: "モスクワ",},
{id: 2,name: "サンクトペテルブルク"},
{id: 3,name: "エカテリンブルク"},
{id: 4,name: "ムンバイ"},
{id: 5,name: "ベンガルール"},
{id: 6,name: "コルカタ"},
{id: 7,name: "サンパウロ"},
{id: 8,name: "リオデジャネイロ"},
{id: 9,name: "ブラジリア"},
];
let ul = $("#list");
let li;
let opt;
//プルダウンメニューの生成
$.each(ary_data,function(key,item){
opt = $('<li>').text(item.name);
ul.append(opt);
})
//プルダウンの操作
$("#word").on("change",function(){
let word = $(this).val();
li = ul.find('li');
li.remove(); //リストの初期化
$.each(ary_data,function(key,item){
//部分一致検索
if(item.name.indexOf(word) !== -1 ){
opt2 = $('<li>').text(item.name);
ul.append(opt2);
}
})
$("#len").text(li.length);
})
})
</script>
</head>
<body>
<div id="app">
<input type="text" id="word">
<p>検索結果<span id="len"></span>件</p>
<ul id="list"></ul>
</div>
</body>
一度リストを形成しているのに、イベントが起きるごとに再度DOMを再構築しているので、動作に無駄があるように思われます。このように検索機能など、何度もレンダリングが必要になる動作はjQueryはあまり向いていません(detachで隔離して不要なデータだけ除去するという裏技も別の記事で紹介してますが…)。
Vue.js
同じ動作をするシステムをVue.jsで書くとこのようになります。そしてここから算出プロパティ、メソッド、そして監視プロパティにも触れていきます。
<body>
<div id="app">
<input type="text" v-model="word">
<p>検索結果{{filterData.length}}件</p>
<ul>
<li v-for="(data,idx) in filterData" :key="idx">{{ data.name }}</li>
</ul>
</div>
<script>
new Vue({
el: "#app",
data:{
word: "",
ary_data :[
{id: 1,name: "モスクワ",},
{id: 2,name: "サンクトペテルブルク"},
{id: 3,name: "エカテリンブルク"},
{id: 4,name: "ムンバイ"},
{id: 5,name: "ベンガルール"},
{id: 6,name: "コルカタ"},
{id: 7,name: "サンパウロ"},
{id: 8,name: "リオデジャネイロ"},
{id: 9,name: "ブラジリア"},
]
},
//算出プロパティ
computed:{
filterData: function(){
let word = this.word
let ary_data = this.ary_data
let lists = []
if(word != ""){
ary_data.filter(function(elem,idx){
if(elem.name.indexOf(word) !== -1){
lists.splice(idx,1,elem)
}
})
return lists;
}
return ary_data
}
}
})
</script>
■算出プロパティ(computed)
今まではあまりjQueryとVue.jsでそこまで動きや記述に差はありませんでしたが、動的なイベントが増えてくるとその本領が発揮されてきます。特に画期的な機能がcomputedプロパティによって機能する算出プロパティでしょう。これはv-modelなどでリアクティブに依存している値が更新された場合のみ、即座にその更新情報をキャッチして処理を行い同期をとるものです。裏返せば、値が不変の場合は何も反応しないという優れた性能を持っています。
具体的には検索用のテキストボックスはv-modelディレクティブで値をバインドしているため、逐次検索文字が変わるごとにfilterData
メソッドが実行され、その結果を返します。当該メソッド内ではindexOf()
メソッドによって値のマッチングを調べているので-1(検索結果なし)以外は反応するようにし、それに従いsplice
メソッドによって検索結果を新たなlistオブジェクトに代入するようにしています。そしてその結果をv-forディレクティブの初期値に設定し、値をループさせているのです。そして、その説明でお気づきだと思いますが、この場合には算出プロパティに引数は不要どころか無用です。また、この算出プロパティは値がキャッシュされるので、それによって高速な値のバインド処理が可能になっています(それによる弊害もあるのですが、それは次の説明)。
※なお、getterとsetterを用いて算出プロパティでも引数を用いる場合があります。プルダウンを連動させたい場合などがその最たる例で、ここでは選択した値と選択によって連動して取得したオブジェクトの値の双方が必要になります。ただし、その場合はgetterとsetterを用いる必要があります。また、returnで値を返すときは配列に格納したり、メソッドに振り分けたりします。
<select v-model="sel">
<option v-for="opt in options">
{{ opt.name}}
</option>
</select>
new Vue({
//省略
computed:{
sel:{
get: function(){
let sel = this.sel
options= objects.filter(function(item,idx){
return 処理
})
return sel //選択したプルダウンの値を返す
this.getvalue(options) //値格納用のメソッド
},
set: function(sel){
this.sel = sel
}
},
},
methods:{
getValue:function(value){
this.options = value
}
}
})
なお、イベント自体を監視するwatch(監視プロパティ:後述)やcreated(ライフサイクルフック:4章で解説)などもありますが、まずはcomputedと次に挙げるmethodsだけに話を絞ります。
■メソッド(methods)
対してメソッドというのものがあり、methodsプロパティ以下に記述します。それが一般にJavaScriptで使われてきたonXxxxxというプロパティで制御するメソッドのように思いがちです。ちなみに、これをVueディレクティブではv-on:xxxxと記述し、前章で説明した通り@xxxxと省略することができます。ところが、Vue.jsではフォーム部分の制御は概ねv-modelでバインドしてくれるので、そこに記述する必要はありません。では、どのようなときに使用するかといえば
- (1)リアルタイムな値のバインドが不要なイベント制御
- (2)画像ファイルなどブラウザに値をキャッシュされては困るものの制御
- (3)算出プロパティなど関数内での、内部で再計算などの処理が必要な場合
これらのケースで使用することが多いです。具体的な例を挙げましょう。
<body>
<div id="app">
<input type="text" v-model="word">
<p>検索結果{{filterData.length}}件</p>
<button @click="clear">文字のクリア</button><!-- clearはmethodsのケース1-->
<ul>
<li v-for="(data,idx) in filterData">
<dl>
<dt>{{ data.name }}</dt>
<dd ><img :src="imagePath(data.img)"></dd>
</dl>
</li>
</ul>
</div>
<script>
new Vue({
el: "#app",
data:{
word: "",
ary_data :[
{id: 1,name: "モスクワ",img:"Moscow.jpg"},
{id: 2,name: "サンクトペテルブルク",img:"Sankt.jpg"},
{id: 3,name: "エカテリンブルク",img:"Yekaterin.jpg"},
{id: 4,name: "ムンバイ",img:"Mumbai.jpg"},
{id: 5,name: "ベンガルール",img:"Bengaluru.jpg"},
{id: 6,name: "コルカタ",img:"Kolkata.jpg"},
{id: 7,name: "サンパウロ",img:"SaoPaulo.jpg"},
{id: 8,name: "リオデジャネイロ",img:"Rio.jpg"},
{id: 9,name: "ブラジリア",img:"Brasilia.jpg"},
]
},
computed:{
filterData: function(){
let word = this.word
let ary_data = this.ary_data
let hit_data = this.hit_data
let lists = []
word = this.hiraToKata(word) //平仮名をカタカナに変換(methodsのケース3)
if(word != ""){
ary_data.filter(function(elem,idx){
if(elem.name.indexOf(word) !== -1){
lists.splice(idx,1,elem)
}
})
return lists; //検索結果が返される
}
return ary_data //初期に全値を出力したい場合はこのようにしておく
}
},
//イベントメソッド
methods:{
//値のクリアはボタンが押されたときだけ反応すればよい(リアクティブに対応しない)
clear: function(){
this.word = '' //returnを使わない場合最終行が評価される
},
//画像のパスを読み込む(画像はキャッシュされると同じ画像を返してしまうため、イベントメソッドで制御する。methodsのケース2)
imagePath: function(imgsrc){
if(imgsrc != ''){
return `./image/${imgsrc}`
}
},
//入力された平仮名をカタカナに変換する
hiraToKata: function(word){
return word.replace(/[\u3041-\u3096]/g, function(match) {
chr = match.charCodeAt(0) + 0x60;
return String.fromCharCode(chr);
})
}
}
})
</script>
</body>
ここでメソッドを使用しているのは以下の3箇所です。
- 検索文字のクリア
- 画像の呼び出し
- 入力文字のカタカナ変換
まず、検索値のクリアを実行するにはボタンのクリックが必要ですが、クリックに際して@clickイベントは、あくまでクリックされた場合のみ処理が実行されればいいものです。そこでこのメソッドに@click(v-on:clickの略称)ディレクティブを記述しておき、そのメソッドclear
内には検索文字を空白化しておきます(dataに値を返すのを忘れないで下さい)。
また、画像の呼び出しはブラウザによって値がキャッシュされてしまうので、次別の値を呼び出そうとしても同じ画像キャッシュを保持する現象が起きてしまい、画像更新ができなくなってしまいます。そこで算出プロパティではなくメソッドを:src(v-bind:srcの略)に記述しておくことで画像のソースに対して値をバインドし、キャッシュを保持させることなく、それぞれの画像を表示できます。このように、メソッドは必ずしもv-onディレクティブ(イベント制御)上で起動するものとは限りません。
※requireはNode.js依存のメソッドなので通常のローカル環境では起動しない上に処理エラーが発生します。ローカルで動かす方法はかなり困難です。
そして入力文字のカタカナ変換ですが、そのまま入力しても平仮名のままでは検索に引っかかるわけないので、各入力かなに対し、カタカナに変換する処理関数hiraToKata
を算出プロパティfilteredList
内で呼び出しています。そして結果を元の算出プロパティに返し、それを検索条件としているので、ひらがなを入力してもカナ検索ができます。
ですが、これだけだと入力文字が平仮名のままなので、もう少し改良を加えてみましょう。
■監視プロパティ(watch)
現在、かな検索で表示されるのは平仮名ですが、工夫すれば入力文字もカタカナで表示させることができます。それを不自然なく処理するために監視プロパティを使ってみましょう。ちょうどdataとcomputedの間に以下のメソッドを書き足してください。
data:{
},
watch:{
word:function(word){
chr = this.hiraToKata(word)
this.word = chr
}
},
computed: {
これは監視プロパティというもので、名の通り、イベントそのものを監視する機能です。今回はv-model="word"
イベントそのものを監視しているので、このv-modelに動きがあった場合(computedの違いは内部の同期データを監視しているものではないということ)、内部でカナ変換メソッドhiraToKana
を呼び出し、変換されたカタカナを返す変数chr
を返すようにしています。また、その受け皿として:value
には変数chrを代入しています(これを設定しないと値がクリアされません)。
※補足:こんな記述に注意
なお、次のような記述は警告が出ます。
<input @change="overwork(sel)" v-model="sel">
v-modelで既に値を監視しているので、そこにメソッドを入れると想定外の動きを起こすことがあるからです。テキストボックスなどでこういう冗長なメソッド設定はやめましょう。
■v-ifとv-else
ここで、エレメント部分をこのように書き換えてみてください。
<div id="app">
<input type="text" v-model="word">
<p v-if="filterData.length > 0">検索結果{{filterData.length}}件</p>
<p v-else>検索結果なし</p>
<ul>
<li v-for="data in filterData">{{ data.name }}</li>
</ul>
</div>
このv-if文とv-else文は同一エレメント内で分岐処理を行いたい場合に用います。ここでは判定文として検索結果を返しており、filter.lengthで検索件数が表示されるので、それが存在しない場合はv-else以下のタグが表示されます。
ただ、範囲としては単一のタグとなってしまうので、複数のタグを分岐させたい場合はtemplate
タグが役立ちます。
<template v-if="filterData.length > 0">
<p>検索結果{{filterData.length}}件</p>
</template>
<template v-else>
<p>検索結果なし</p>
<p>※全角カナを入力してください</p>
</template>
Vue3のcomposition APIで記述
この演習3もVue3で記述するとこうなります。refプロパティで展開した場合はクラス化されるので、値を呼び出したい場合はhoge.valueとする必要があります。ちなみに、算出プロパティも監視プロパティもVueクラスから利用します。そして、メソッドはプロパティ自体が不要となっており、JavaScriptと同じ感覚で記述することができます。
<body>
<div id="app">
<input type="text" v-model="word">
<p>検索結果{{filterData.length}}件</p>
<button @click="clear">文字のクリア</button><!-- clearはmethodsのケース1-->
<ul>
<li v-for="(data,idx) in filterData">
<dl>
<dt>{{ data.name }}</dt>
<dd ><img :src="imagePath(data.img)"></dd>
</dl>
</li>
</ul>
</div>
<script>
Vue.createApp({
setup(){
let word = Vue.ref("")
let state = Vue.reactive({
ary_data:[
{id: 1,name: "モスクワ",img:"Moscow.jpg"},
{id: 2,name: "サンクトペテルブルク",img:"Sankt.jpg"},
{id: 3,name: "エカテリンブルク",img:"Yekaterin.jpg"},
{id: 4,name: "ムンバイ",img:"Mumbai.jpg"},
{id: 5,name: "ベンガルール",img:"Bengaluru.jpg"},
{id: 6,name: "コルカタ",img:"Kolkata.jpg"},
{id: 7,name: "サンパウロ",img:"SaoPaulo.jpg"},
{id: 8,name: "リオデジャネイロ",img:"Rio.jpg"},
{id: 9,name: "ブラジリア",img:"Brasilia.jpg"},
]
})
const filterData = Vue.computed(()=>{
let ary_data = state.ary_data
let lists = []
if(word.value != ""){
ary_data.filter((elem,idx)=>{
if(elem.name.indexOf(word.value) !== -1){
lists.splice(idx,1,elem)
}
})
return lists
}
return ary_data
})
//メソッド(関数として記述するだけ)
//値のクリアはボタンが押されたときだけ反応すればよい(リアクティブに対応しない)
const clear = ()=>{
word.value = '' //returnを使わない場合最終行が評価される
}
//画像のパスを読み込む(画像はキャッシュされると同じ画像を返してしまうため、イベントメソッドで制御する。methodsのケース2)
const imagePath = (imgsrc)=>{
if(imgsrc != ''){
return `./image/${imgsrc}`
}
}
//入力された平仮名をカタカナに変換する
Vue.watch([word],()=>{
let chr = hiraToKata(word.value)
word.value = chr
})
const hiraToKata = (chr)=>{
return chr.replace(/[\u3041-\u3096]/g, function(match) {
chr = match.charCodeAt(0) + 0x60;
return String.fromCharCode(chr);
})
}
return{
word,state,filterData,clear,imagePath,hiraToKata
}
}
}).mount("#app")
</script></body>
■Vue3で算出プロパティを制御する
Vue3のcomposition APIを用いた場合の算出プロパティの記述は以下のようになります。また、getterだけの場合でもエラーは表示されません。
setup(){
//算出プロパティ(getterとsetterがある場合)
const selCountry = computed({
get:()=>{}
set:()=>{}
})
//算出プロパティ(getterだけの場合)
const filterAreas = computed(()=>{
})
}
そして、同様にreturn{}で処理結果を返していくのですが、ここでVue3の大きな変更点があります。算出プロパティの値をメソッドで呼び出したい場合は、returnで返した変数
を代入して呼び出す必要があります。具体的には、次のような記述になり、算出プロパティfilterAreasで最後に返している変数areasを呼び出す(stateに返しているので、実際はstate.areasとして呼び出し)ことで処理が可能になります。
//検索候補となる州を集約。算出プロパティfilterAreasで最後に返している変数areasを呼び出すことで可能になる。
state.areas.map((elem,idx)=>{
targetareas.splice(idx,1,elem.code);
})
■監視プロパティについて
Vue3における監視プロパティは以下のような記述形式になります。おさらいですが、監視できるのはデータ上の変数ではなく、算出プロパティやディレクティブなどの各種イベントです。ここでは検索文字wordを監視対象としています。また、お後述するref関数で通した変数はステータスも保持するため、監視対象とすることができます。
watch(監視対象のイベント,()=>{
//処理を行う
})
また、複数のイベントを監視したい場合は監視対象をオブジェクト化すれば対応できます。
watch([イベント1,イベント2],()=>{
//処理を行う
})
React
さて、Reactも関数コンポーネントで同じ処理を記述してみます。そして、それにしたがい、関数コンポーネントの最大の特長であるフックを活用しています。
<script type="text/babel">
const { useState, useEffect } = React; //使用するフックの定義
const App =()=>{
//変数定義
const [word,setWord] = useState(""); //フックの使用(検索文字のバインド)
const [searched ,setSearched] = useState([]); //フックの使用(検索結果のバインド)
const ary_data = [
{id: 1,name: "モスクワ",img:"Moscow.jpg"},
{id: 2,name: "サンクトペテルブルク",img:"Sankt.jpg"},
{id: 3,name: "エカテリンブルク",img:"Yekaterin.jpg"},
{id: 4,name: "ムンバイ",img:"Mumbai.jpg"},
{id: 5,name: "ベンガルール",img:"Bengaluru.jpg"},
{id: 6,name: "コルカタ",img:"Kolkata.jpg"},
{id: 7,name: "サンパウロ",img:"SaoPaulo.jpg"},
{id: 8,name: "リオデジャネイロ",img:"Rio.jpg"},
{id: 9,name: "ブラジリア",img:"Brasilia.jpg"},
];
//ループ処理の分離
const setList = searched.map((data,idx)=>{
return(
<li key={idx}>
<dl>
<dt>{ data.name }</dt>
<dd ><img src={data.img}/></dd>
</dl>
</li>
)
})
//useEffectフックによって、検索値がバインドされたときに処理が実行される
useEffect( ()=>{
const searched = ary_data.filter((item,idx)=>{
return item.name.search(word)!== -1;
});
setSearched(searched);
},[word]);
//検索文字のバインド
const bindWord = (e)=>{
let word = e.target.value
word = word.replace(/[\u3041-\u3096]/g, function(match) {
let chr = match.charCodeAt(0) + 0x60;
return String.fromCharCode(chr);
})
setWord(word); //検索文字のバインド
}
//検索文字のクリア
const clear = ()=>{
let word = ''
setWord(word);
}
//レンダリング
return(
<React.Fragment>
<input type="text" onChange={bindWord} value={word}/>
<p>検索結果{searched.length}件</p>
<button onClick={ clear }>文字のクリア</button>
<ul>
{ setList }
</ul>
</React.Fragment>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
</script>
</head>
<body>
<div id="root"></div>
</body>
非常に複雑に見えますが、流れとしてはこうなっています。
- JSX内のsetListオブジェクトにリストの値を代入する。
- テキストに文字が入力されたら、bindWordメソッドが実行され、setWordメソッドによって検索値がバインドされる。
- 検索値に変化があったら、それをキャッチしたuseEffect(フックの一つ、後述)が働き、リストの値(ary_data)に対してマッチングを行いながら、検索値にマッチした値のみsearchedオブジェクトに格納される。
- 検索結果が格納されたsearchedはsetSearchedメソッドによってバインドされ、リストの値が更新される。
- もし、クリアボタンが押された場合は、検索値が初期化され、それに対しバインドが行われるため、リストは初期化される。
以上の流れになります。ここで鍵となり、そして関数コンポーネントから新たに導入されたuseStateフックとuseEffectフックについて説明しようと思いますが、Vue.jsで言うところのv-modelディレクティブと算出プロパティのような働きだと認識しています。
●フック(hooks)
フックはReact16.8から導入された新機能で、日本語に直すと接続のことです。そして今までクラスオブジェクト依存していた記述に代わり関数で処理できるようになったことで、それに伴いよりデータの同期処理をより円滑に行うために開発された機能的なメソッドです(というよりコンストラクタやsetStateが普通だと使えなくなったので、その代替の手段です)。
●useState
useStateは実は演習1にも登場していましたが、全く触れませんでした。これはどういうものかというと、値の変更前後で同期をとるためのメソッドで、以下のような記述ルールがありますが、これを把握していないと引っかかります(前後の変数を代入するものではありません)。
const [バインドさせたい値,バインドさせるためのメソッド] = useState(初期値)
つまり、検索値を処理する場合、初期値は何もないのでuseState("")と空白を代入し、1つ目の戻り値は検索値を格納するのは変数word
、それに対して2つめの戻り値に代入するのはそれをバインド処理するためのメソッドsetWord
の戻り値であるということです。同様に、検索結果に対しても同じように記述しますが、注意点として初期値がオブジェクトになる場合は引数にuseState([])
と空オブジェクトを代入する必要があります。混乱しないためにも、奇を衒わずバインドを行うメソッド名はsetXxxxx
(Xxxxx
はバインドしたい値と対応)とした方が迷いが起こらずいいでしょう。
※このuseStateの戻り値はJavascriptの分割代入です。分割代入というのは[元の値,追加したい値]とすることで、オブジェクトの値を分解して、個別の変数を戻り値に代入していくものなので、useStateメソッドが実行される度に、オブジェクトの中身を分解し、更新されたデータを処理する仕組みとなっています。
●useEffect
名の通りエフェクトを与えるフックで、バインドした値が変化したときに処理を行う、いわばVue.jsのv-modelと算出プロパティ(computed)の関係と同じようなものだと思えばいいでしょう。なお、公式サイトではeffectを副作用と物々しげな(日本語では問題解決の方法によって起きた、新たな弊害を表すマイナスの言葉)和訳を使ってそれが一般化してしまっていますが、正直言って波及とかの方がふさわしい気がします(相乗効果といった意味合いで捉えてもいいでしょう)。
それはおいといて、今回の演習では検索結果を返す処理に対してこのuseEffectを使用しています。なぜなら、検索値をバインドさせても、それはあくまで検索値のバインドに過ぎないので、それだけでは検索結果までバインドできないからです。そこでこのuseEffectを使い、検索値をバインドさせ、その値に変化が与えられた時のみ、その連動で、検索結果も処理するようにしているわけです。そしてその検索結果もまた、useStateでバインド処理することで、随時検索結果をフィルタリングすることができます。
このuseEffectの記述方法ですが、このようになります。
useEffect(()=>{...元となる値によって連動させたい処理...},[連動させたい元となる値])
※元となる値は必ずオブジェクトに格納してください。もし、今回のように単発の値であっても、オブジェクトに格納しないと処理エラーが起きます。
※フックは他に頻用されるものとしてuseContextフック、useReducerフック、useCallbackフック、useMemoフック、useRefフックなどがありますが、これらは動作が複雑なので、上記2つのフックを理解してからのほうがいいでしょう(いずれのフックも続編で解説しています)。
さらっと説明すると
- useContextフック (子孫関係のコンポーネントに値を受け渡す)
- useReducerフック (類似の処理を一元化する・複数のオブジェクトを同時に扱う)
- useCallbackフック (繰り返し処理を行うメソッドを記憶させ、高速化、省力化を図る)
- useMemoフック (特定の変数を記憶し、変更があった場合のみ処理を実施する)
- useRefフック (特定のフォームの値を参照する)
こんな場合に使うことが多いです。
●Reactで分岐処理する場合
ReactはVue.js、Angularのようにif構文を埋め込んだりはできないのですが、大きく分類して3種類の方法があるようです。
1.関数コンポーネント
2.即時関数(説明は省略)
3.三項演算子
※即時関数は拡張性がなく見づらい上、使用している人もほとんどないので説明を省略します。
●関数コンポーネントで分岐
//前略
//ループ処理の分離
const setList = searched.map((data,key)=>{
return(
<li key={data.key}>
<dl>
<dt>{ data.name }</dt>
<dd ><img src={data.img}/></dd>
</dl>
</li>
)
})
//分岐処理
const Result = ()=>{
let result
if(searched.length > 0){
result = <p>検索結果{searched.length}件</p>
}else{
result = <p>検索結果なし</p>
}
return result
}
//中略
//レンダリング
return(
<React.Fragment>
<input type="text" onChange={bindWord} value={word}/>
<Result />
<button onClick={ clear }>文字のクリア</button>
<ul>
{ setList }
</ul>
</React.Fragment>
);
//後略
JSX内に<Result />
という見慣れないタグが描かれていますが、実はこれが関数コンポーネントで、その定義として分岐処理を記述しています。そして、JSXで再描写が行われる毎に分岐処理が行われるので、そこにはResultメソッド内で処理された変数resultが随時返されることになります。
ただ、入れ子にならない場合は次に説明する記述法の方が楽です。
●三項演算子で分岐
上記のような、入れ子構造なしの単純なif判定だけならば、三項演算子の方が表記が楽です。そして、この三項演算子は複数行に跨っていても使用可能です。
//前略
//レンダリング
return(
<React.Fragment>
<input type="text" onChange={bindWord} value={word}/>
{ searched.length > 0? <p>検索結果{searched.length}件</p>:<p>検索結果なし</p> }
<button onClick={ clear }>文字のクリア</button>
<ul>
{ setList }
</ul>
</React.Fragment>
);
//後略
●(参考)&&接続
※なお、if判定がtrueの場合のみ、&&で接続することもできます(trueといってもbool型でないと評価されないというものではなく、要は条件一致の場合のみ処理が可能というものです)
return(
<React.Fragment>
<input type="text" onChange={bindWord} value={word}/>
<p>検索結果{searched.length}件</p>
{searched.length == 0 && <p>別の検索条件でやり直してください</p>}
<button onClick={ clear }>文字のクリア</button>
<ul>
{ setList }
</ul>
</React.Fragment>
);
Angular
では、これをAngularで記述してみますが、驚くほど単純です。その前にAngularですが、他のフレームワークとは異なり、Typescriptがベースとなっています。そのため、型指定が厳格となりますので、一見複雑そうには見えるのですが、データ処理部分がある程度自由が利くため、他のフレームワークより理屈は理解しやすいと思います。
まず、ビューの部分になりますが、たったのこれだけです。
<div id="app">
<input type="text" [(ngModel)]="word" (change)="filterData(word)" />
<p *ngIf="lists.length > 0">検索結果{{lists.length}}件</p>
<p *ngIf="lists.length == 0">検索結果なし</p>
<ul>
<li *ngFor="let data of lists">{{ data.name }}</li>
</ul>
</div>
では、そのデータ制御部分ですが、このようになります。
import { Component } from '@angular/core';
import { Arydata } from './class/search'; // コンストラクタの呼出
const chr = '';
const ARYDATA: Arydata[] = [
new Arydata(1,"モスクワ"),
new Arydata(2,"サンクトペテルブルク"),
new Arydata(3,"エカテリンブルク"),
new Arydata(4,"ムンバイ"),
new Arydata(5,"ベンガルール"),
new Arydata(6,"コルカタ"),
new Arydata(7,"サンパウロ"),
new Arydata(8,"リオデジャネイロ"),
new Arydata(9,"ブラジリア"),
];
const lists = [];
@Component({
selector: 'app-root',
templateUrl: './sample3.angular.html',
})
export class Sample3Angular{
public ary_data = ARYDATA;
public lists = [];
public chr = '';
filterData(word: string){
word = this.hiraToKata(word); //メソッドを呼び出し
lists = this.lists;
lists = []; //検索結果を初期化する
if(word != ""){
this.ary_data.filter(function(elem,idx){
if(elem.name.indexOf(word) !== -1){
lists.splice(idx,1,elem)
}
})
this.lists = lists;
}
this.word = word;
}
//検索のひらがなをカタカナに直す。
hiraToKata(word: string){
return word.replace(/[\u3041-\u3096]/g, function(match) {
chr = match.charCodeAt(0) + 0x60;
return String.fromCharCode(chr);
})
}
}
このようにVue.jsの演習用に使ったプログラムを使い回すだけで同じ動作を実現できました。ただ、注意点は戻り値を返す場合で、ここではthis.xxxxxとまた、メソッドから元のクラス内の定義部分に値を返してやるだけで大丈夫です。例でいえば、元のビュー部分にデータを返したいのは検索された文字と検索結果なので、それぞれthis.word = word、this.lists = listsとして、処理された値をビュー部分に返しています。ちなみに変数wordをわざわざ返しているのは、検索文字をカタカナに変換しているためで、この変数を元のクラスに返す処理を行わないとビュー上にカタカナで表示されません。
◆イベントバインディング
ここでイベント部分が(change)
となっていますが、(hoge)は前述したようにビューからデータに偏方向でデータを受け渡すので、Angularにおけるイベントトリガーはこのように記述するだけで作動します。これをAngularではイベントバインディングと呼んでおり、引数を渡すこともできます。そして、ここでは検索文字wordを[(ngModel)]でバインドしているので、この値に変化が与えられるごとに、メソッドfiilterData(word)が作動することなります。そして、それによって得られた検索結果がオブジェクト変数lists
に返され、ngForディレクティブによってループされます。
◆分岐について
また、Angularにおけるif分岐ですが、実はngIfディレクティブのみを使用し、ngElseというディレクティブは存在しません。なので、elseの場合も上記のように判定結果を逆転させて記述させたりするほか、else条件を付与したい場合は、ngテンプレートとテンプレートタグのidで制御します(ngテンプレート以外に記述するとエラーとなります)。
<div id="app">
<input type="text" [(ngModel)]="word" (change)="filterData(word)" />
<p *ngIf="lists.length > 0; else noResult">検索結果{{lists.length}}件</p>
<ng-template #noResult>
<p>検索結果なし</p>
</ng-template>
<ul>
<li *ngFor="let data of lists">{{ data.name }}</li>
</ul>
</div>
記述ルールはこのようになっています
<div *ngIf="判定文; else hogehoge"><!-- 肯定の処理 --></div>
<ng-template #hogehoge>
<!-- elseの場合の処理 -->
</ng-template>
なお、判定結果がtrueの場合(肯定)もidで返したい場合はこのように記述できます。肯定の場合はthen
前置詞を用いれば、idの呼び出しが可能です。なお、then文とelse文の間にコロンを挟んでいる記述とそうでない記述が見られますが、どっちでも処理されます。
<ng-template *ngIf="lists.length > 0 ; then someResults else noResult"></ng-template>
<ng-template #someResults>
<p>検索結果{{lists.length}}件</p>
</ng-template>
<ng-template #noResult>
<p>検索結果なし</p>
</ng-template>
注意点としてngIfディレクティブとngテンプレートは入れ子構造ではありません。下手に入れ子にしてしまうと、ngIfディレクティブの判定結果にかかわらず、何も表示されなくなってしまいます(しかもコンソールでエラー表示されないのでバグフィクスが大変です)。
※また、id指定で制御できるテンプレートは一箇所だけです。複数箇所を記述しても判別できるのは直近のテンプレートだけとなります。
◆コンストラクタによる型の定義
ただ、Typescript経験者ならおわかりかと思われますが、この言語は非常に型に厳格なので、このデータ処理前にコンストラクタを呼び出す必要があります。それが上にある
import{ Arydata } from './class/search';
の部分で、その中身はこうなっています。このように定義された変数に対し、一つ一つ型を定義していかないと、エラーが検出され処理が中断されてしまいます。逆にそこさえ押さえておけば、ある程度操作に自由が利くので上記2つより至極単純に作れます。
※TypeScriptは型だけのエクスポートもできるようになっているので、昨今では以下のように記述することが多いです。
export Type Arydata{
id: number; //型の定義
name: string; //型の定義
}
Angularの学習にあたって参考にしたページ
演習4: データリストの追加、削除、修正(DOMの再生成)
いわゆるCRUD(Create、Read、Update、Delete)と呼ばれる操作です。これに至っては今の所Vue.js、React、Angularのバインド(同期)が足かせになることもあり、I/Oのタイミングがわかりやすく、オブジェクト操作がしやすいjQuery+Ajaxの方が便利ではないかと考えていた(PHPやRailsなどと合わせてDB操作の補助的な機能としてjQueryを重宝していた理由です)部分ですが、これについても各フレームワークの癖も掴みながら解説していきたいと思います。
jQuery
まずは、データの新規登録と削除について制御していきたいと思います。記述方法は色々あるとは思いますが、まずはオーソドックなやり方で制御していきます。
<head>
<script src="https://code.jquery.com/jquery-3.5.1.js"></script>
<script>
$(function(){
//配列の値
ary_data =[
{id: 1,name: "モスクワ",},
{id: 2,name: "サンクトペテルブルク"},
{id: 3,name: "エカテリンブルク"},
{id: 4,name: "ムンバイ"},
{id: 5,name: "ベンガルール"},
{id: 6,name: "コルカタ"},
{id: 7,name: "サンパウロ"},
{id: 8,name: "リオデジャネイロ"},
{id: 9,name: "ブラジリア"},
];
let ul = $("#list");
let li;
//プルダウンメニューの生成
$.each(ary_data,function(idx,item){
key = idx + 1;
li = $('<li>').text(item.name).attr("id","l"+key);
bt_del = $('<button>').attr({"type":"button","id":"d"+item.key}).text("削除");
li.append(bt_del);
ul.append(li);
})
$('button').each(function(){
$(this).on("click",function(){
sel_id = $(this).attr("id");
//編集
if(sel_id.indexOf("d") != -1 ){
idx = sel_id.slice(1);
key = "l"+idx;
$("ul").find("li").filter("#"+key).remove();
}else if(sel_id.indexOf("ins") != -1){
item = $("#data_ins").val();
key = ul.find("li").length +1;
li = $('<li>').text(item).attr("id","l"+key);
bt_del = $('<button>').attr({"type":"button","id":"d"+item.key}).text("削除");
li.append(bt_del);
ul.append(li);
}
})
})
})
</script>
</head>
<body>
<input type="text" id="data_ins">
<button type="button" id="ins">新規</button>
<div id="app">
<p>検索結果</p>
<ul id="list"></ul>
</div>
</body>
ここでふと気付いたことがあるとは思われますが、実際、新規作成、削除しているのは配列そのものではなくて、配列を基準に作成したDOM要素(ここではliタグ)になります(array.apliceなどを用いて中の配列そのものを操作する必要がない)。
なので、新規作成(insert)と削除(delete)処理のタイミングがはっきりしており、非常に操作が明解です。では、これをVue.js、React、Angularで操作した場合はどうなるか見ていくことにしましょう。
※また、データ修正においては本来、データ修正、更新という二段階の作業を行うので、一旦置いておきます。
※Vue.jsとReactとの違い
Vue.jsでもReactでも、jQueryと大きな違いは、DOM要素そのものではなく、オブジェクトの値を操作することになります。なぜなら、jQueryと違い、VueのディレクティブやReactのリアクティブというものは、元になるデータソースがあり、そこからリアルタイムにDOM要素を再生成しているからで、処理を行ったDOM要素を操作したところで意味を成さないどころか、データの整合性が取れなくなってしまいます(なので、そういう操作はできないようになっている)。
そして、オブジェクトを操作するにあたって、それぞれに適したプロパティやメソッドを用いることになります。ですが、新規作成や削除という操作は必ずしもリアルタイムに操作する必要がなく、それどころかワンクッション置いておかないと「いつこのデータを挿入したのか?」あるいは「いつ削除したのか?」と問題を提起しかねません。なので、もしデータの検索や修正を伴わずに、単に新規作成や削除だけをしたい場合はそれこそJavascriptやJQueryなどで十分処理ができる上、**元のオブジェクト内データを破壊しなくて済む(Vue.jsの場合は破壊しないように工夫が必要)**ので、それらの方が使いやすいと考えていたわけです(まあ、今はJavascriptだけで色々できるようにはなってきているので、jQueryのメリットはフォーム制御の簡潔さぐらいになってますね)。
ちなみに、Angularは上記2つとは異なる挙動をします。
Vue.js
Vue.jsを用いて新規作成と削除を実装するとこのようになります。Reactとの違いは、元のオブジェクトの値も操作できる点があります。ただ、それだと元のオブジェクトの値を破壊してしまうので、移し替えておくようにしましょう。ただ、それにあたって新たな技術、ライフサイクルフックが必要となります。
<!doctype>
<html lang="ja">
<head>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="ins_item">
<button type="button" @click="ins">新規</button>
<ul>
<li v-for="(data,idx) in filterData">
<dl>
<dt>{{ data.name }}</dt>
<dd ><button type="button" @click="del(idx)">削除</button></dd>
</dl>
</li>
</ul>
</div>
<script>
new Vue({
el: "#app",
data:{
ins_item: "",
datalist: [],
ary_data :[
{id: 1,name: "モスクワ"},
{id: 2,name: "サンクトペテルブルク"},
{id: 3,name: "エカテリンブルク"},
{id: 4,name: "ムンバイ"},
{id: 5,name: "ベンガルール"},
{id: 6,name: "コルカタ"},
{id: 7,name: "サンパウロ"},
{id: 8,name: "リオデジャネイロ"},
{id: 9,name: "ブラジリア"},
]
},
//ライフサイクルフック(後述)…ロード時に操作する
created:function(){
let datalist = this.datalist
let ary_data = this.ary_data
datalist = ary_data;
this.datalist = datalist
},
//算出プロパティ
computed:{
filterData: function(){
let datalist = this.datalist
return datalist //初期に全値を出力したい場合はこのようにしておく
}
},
//イベントメソッド
methods:{
ins: function(){
let ins_item = this.ins_item
let datalist = this.datalist
let key = datalist.length;
let items = {
"key": key,
"name": ins_item
}
this.$set(datalist, key,items ) //データの挿入
},
del: function(idx){
let datalist = this.datalist
this.$delete(datalist,idx) //データの削除
}
}
})
</script>
</body>
</html>
新規作成も削除もリアルタイムに値を取得する必要がないので、メソッドで処理します(例のmethods内関数insと関数del)。また、今回は事前にオブジェクトの値を空オブジェクトdatalistに移し替える動作が必要になるので、ライフサイクルフックの一種、createdプロパティを用いています。
■ライフサイクルフック
ライフサイクルフックは演習3でも軽く触れましたが、簡潔に答えるとシステムを動かす事前に操作するためのものです。たとえば、オブジェクトary_dataの値を変数datalistに移し替えるのは、最初だけ作業すればいいものです。これを下手に算出プロパティで作業すると、初期作動こそはいいですが、操作をするたびにary_dataの値をdatalistに代入してしまうので、編集作業が全く反映されなくなります。
//※これは処理の失敗例です
computed:{
filterData: function(){
let datalist = this.datalist
let ary_data = this.ary_data
datalist = ary_data //ここで毎回初期化されてしまう
return datalist //初期化された値を算出プロパティに返してしまう
}
},
※createdとmounted
ライフサイクルフックはもっと種類があるのですが、頻繁に使用されるのはこの2種類です。そして、この2つの違いを簡潔に説明すると
created ⇒ DOM生成 ⇒ mounted
DOMが生成される前(created)か後(mounted)の違いになります。要はJavascriptでもheadタグ内に記述するものが、createdプロパティと同じ働き、bodyタグの末尾に記述するものがmountedプロパティと同じ働きと意識すればいいでしょう。したがって、今回はオブジェクトの値をdatalistに代入しておくだけなので、createdプロパティで十分作業を行えます。また、算出プロパティは初期作動時には必ず実行されるので、変数datalistの値が反映されていくことになります。
※createdに当たる処理はVue3で廃止され、setupメソッド内に直接記述することで同様の動作を実現できるようです。
■登録の場合
this.$set(array,idx,item) //itemは任意の値。オブジェクトも可。idxの値を絶対忘れないこと
これだけで新規追加されていきます。また、お気づきだと思いますが、メソッドを実行し、データを追加するということは結果的に配列datalistの値も操作することになるので、datalistの値が変更されたタイミングで算出プロパティfilterDataが自動で実行され、DOMを再生成することになります。また、array.push(item)でもできないことはないですが、連想配列の場合に適応できなくなりますし、間に挿入することもできないので使わない方がいいでしょう。
■削除の場合
任意の値をリストから削除する場合は同様にメソッドを用います。また、対象のインデックスはループ式から取得するのですが、従来の記述だと値だけしかループされていないので、インデックスも取得できるようにするので、ループの記法は次のようになります。
(data,idx) in ary_data
※JQueryの$.each(array,function(idx,item){…}とインデックスを示す引数が逆になっているので、間違わないようにしましょう。
また、削除処理はthis.$deleteメソッドを用います。これでオブジェクトから削除してしまいます。すると、また同じように算出プロパティが自動的に実行されるので、DOMの再生成が行われます。なお、arra.splice(idx,1)も同様、連想配列の場合に対応できません。
this.$delete(array,idx)
※削除の際の注意点
間違ってもKeyの値を取得して、それを元に削除しないでください。そんなことをやると、インデックス番号がずれてしまうので大きなトラブルの元となります。
■修正の場合
修正は基本、編集画面と更新処理にわけて行うものですが、当初からテキストボックスで表示し、変更したら処理が実行できるようにしてみます(あくまで演習用で、実用的な処理ではないです)。
<!doctype>
<html lang="ja">
<head>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="ins_item">
<button type="button" @click="ins">新規</button>
<ul>
<li v-for="(data,idx) in filterData" :key="idx">
<dl>
<dt>
<input type="text " v-model="data.name" >
</dt>
<dd ><button type="button" @click="upd(idx,data.name)">修正</button></dd>
<dd ><button type="button" @click="del(idx)">削除</button></dd>
</dl>
</li>
</ul>
</div>
<script>
new Vue({
el: "#app",
data:{
ins_item: "",
upd_item: "",
isEdited: false,
datalist: [],
ary_data :[
{id: 1,name: "モスクワ"},
{id: 2,name: "サンクトペテルブルク"},
{id: 3,name: "エカテリンブルク"},
{id: 4,name: "ムンバイ"},
{id: 5,name: "ベンガルール"},
{id: 6,name: "コルカタ"},
{id: 7,name: "サンパウロ"},
{id: 8,name: "リオデジャネイロ"},
{id: 9,name: "ブラジリア"},
]
},
created:function(){
let datalist = this.datalist
let ary_data = this.ary_data
let isEdited = this.isEdited
datalist = ary_data;
this.datalist = datalist
this.isEdited = isEdited
},
computed:{
filterData: function(){
let datalist = this.datalist
return datalist //初期に全値を出力したい場合はこのようにしておく
},
},
//イベントメソッド
methods:{
ins: function(){
let ins_item = this.ins_item
let datalist = this.datalist
let key = datalist.length;
let items = {
"key": key,
"name": ins_item
}
this.$set(datalist, key,items )
},
del: function(idx){
let datalist = this.datalist
this.$delete(datalist,idx)
},
upd: function(idx,name){
let datalist = this.datalist
let items = {
"key": idx,
"name": name
}
this.$set( datalist,idx, items )
}
}
})
</script>
</body>
</html>
これで、テキストボックスで修正されたエリアが反映されていくことになります。さて、その場合の注意点ですが、テキストボックスに入力する値は前後で変更されます。たとえばデータのベンガルールをバンガロールに書き換えると、メソッドupd実行時にはバンガロールの値が送られないといけません。ですが、実際イベントを操作するのはテキストボックスではなく修正ボタンなので、一見テキストボックスには何も値を入力しなくてもよい、あるいは値を取得する:valueを使うように思われますが、実はそのように操作すると修正後の値が反映されません。正解は同期(バインド)させたい値はv-modelで同期させておくのが正しく、この場合も以下のようにテキストボックスでv-modelディレクティブで値を同期させておかないと、変更後の値が保持されなくなります。
<input type="text" :value="data.name"><!-- こっちは値が同期されない -->
<input type="text " v-model="data.name" ><!-- 値が同期される -->
このようにv-modelディレクティブは必ずしも算出プロパティで用いるとは限らず、このように値を変化させるタイミングとメソッドを実行するタイミングが異なる場合にも用いることもあります。
また、値の修正を反映させるには新規挿入と同様にthis.$setメソッドを用います。その際、メソッドの引数には監視した値も一緒に代入することを忘れないでください。
this.$set("任意のオブジェクト",idx, item) //idxは任意のインデックス
※v-forを用いる場合の注意点
一瞬、v-ifとv-elseの各ディレクティブを用いて値を制御したくなるでしょうが、それはやってはいけません。マニュアルにはv-forディレクティブの中にv-ifやv-elseを用いると想定外の動作が起きることを警告しています。
■Vue3のcomposition APIで記述
では、このCRUDについてもVue3のcomposition APIで記述してみます。ですが、注意しなければいけないのは、composition APIからは分割代入を用いてデータの更新処理を行うので、今までの知識を一新する必要があります。よって、 Vue.setメソッドとVue.deleteメソッドは使用できません 。
また、ライフサイクルフックでDOM生成前に処理を実行するcreatedメソッドも廃止(mountedメソッドは残っている)となり、setupメソッドに組み込まれています。
それから、reactiveは以下のように空オブジェクトを代入しておく方法もあります。refはv-modelディレクティブを直に参照できるので便利なのですが、値を展開する際にはvalueプロパティを付与しないといけません。対してreactiveは以下のように空オブジェクトを代入しておくだけで参照渡しになるので、変数を同期することができます。
しかし、そのままでは分割代入ができないので、解決の方法を探っているとライフサイクルフックでObject.assignという方法があるみたいです。
Vue.js 3 - replace/update reactive object without losing reactivity
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<script src="https://unpkg.com/vue@next"></script><!-- CDN -->
</head>
<body>
<div id="app">
<input type="text" v-model="ins_item">
<button type="button" @click="ins">新規</button>
<ul>
<li v-for="(data,idx) in datalist">
<dl>
<dt><input type="text" v-model="data.name"></dt>
<dd ><button type="button" @click="upd(idx,data.name)">修正</button></dd>
<dd ><button type="button" @click="del(idx)">削除</button></dd>
</dl>
</li>
</ul>
</div>
<script>
Vue.createApp({
setup(){
let ins_item = Vue.ref('');
let datalist = Vue.reactive([])
const ary_data =[
{id: 1,name: "モスクワ",img:"Moscow.jpg"},
{id: 2,name: "サンクトペテルブルク",img:"Sankt.jpg"},
{id: 3,name: "エカテリンブルク",img:"Yekaterin.jpg"},
{id: 4,name: "ムンバイ",img:"Mumbai.jpg"},
{id: 5,name: "ベンガルール",img:"Bengaluru.jpg"},
{id: 6,name: "コルカタ",img:"Kolkata.jpg"},
{id: 7,name: "サンパウロ",img:"SaoPaulo.jpg"},
{id: 8,name: "リオデジャネイロ",img:"Rio.jpg"},
{id: 9,name: "ブラジリア",img:"Brasilia.jpg"},
]
//ライフサイクルフック
Vue.onMounted(()=>{
Object.assign(datalist,ary_data) //空オブジェクトで分割代入する方法
}
//イベントメソッド
const ins = ()=>{
let key = datalist.length;
let items = {
"key": key,
"name": ins_item.value
}
datalist[key] = items //データの挿入
}
const del = (idx)=>{
datalist.splice(idx,1)
}
const upd = (idx,name)=>{
const items = [...datalist]
const item = {...items[idx]}
item.name = name
datalist[idx] = item //修正は分割代入で対応する
}
return{
ins_item,datalist,ins,del,upd
}
}
}).mount("#app")
</script></body>
</html>
React
ReactもVue.jsと同様にオブジェクトを操作します。ただし、Vue.jsは元のオブジェクトの値を操作できた(compostion APIはできません)のに対し、Reactの場合は、元のオブジェクトの値を変更できないので、フックに用意された変数に代入して操作することになります。ただ、リアクティブなオブジェクト操作は色々と癖があり、注意しなければいけない部分が非常に多いです。また、ここでもReact16.8以降、関数コンポーネントでの記法を用いています。
※なぜ、変更できないかというとReactで同期を取っているオブジェクトは常に参照渡しとなっているからです。
<!doctype>
<html lang="ja">
<head>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.10.3/babel.min.js"></script>
<script type="text/babel">
const { useState, useEffect } = React; //使用するフックの定義
const App =()=>{
//変数定義
const [insitem, setInsitem] = useState('');
const [bind ,setBind] = useState([]); //フックの使用(データリストの更新)
const ary_data = [
{id: 1,name: "モスクワ"},
{id: 2,name: "サンクトペテルブルク"},
{id: 3,name: "エカテリンブルク"},
{id: 4,name: "ムンバイ"},
{id: 5,name: "ベンガルール"},
{id: 6,name: "コルカタ"},
{id: 7,name: "サンパウロ"},
{id: 8,name: "リオデジャネイロ"},
{id: 9,name: "ブラジリア"},
];
//新規作成用のメソッド
const ins = ()=>{
setBind([
...bind,
{
id: bind.length + 1,
name: insitem
}
]) //新規作成
}
//削除用のメソッド
const del = (idx)=>{
setBind(bind.filter((_, i) => i !== idx)) //削除処理
}
//ループ処理の分離
const taglist = bind.map((data,idx)=>{
return(
<li key={idx}>
<dl>
<dt>{ data.name }</dt>
<dd ><button type="button" onClick={ ()=> del(idx) }>削除</button></dd>
</dl>
</li>
)
})
//useEffectフックでコンストラクタ的な働きを行う
useEffect( ()=>{
setBind(...bind,ary_data) //空オブジェクトに代入
},[]);
//レンダリング
return(
<React.Fragment>
<label>新規<input type="text" onChange={ (e)=>{ setInsitem(e.target.value) } } value={insitem}/></label>
<button type="button" onClick={ ins }>新規</button>
<ul>
{ taglist }
</ul>
</React.Fragment>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
</script>
</head>
<body>
<div id="root"></div>
</body>
●ロード時の制御
意外と引っかかりがちなのが、読込直後の制御で、いわばオブジェクト指向のコンストラクタっぽいことをしたい場合にどう記述するがですか、実はこれもuseEffectフックを利用します。そして初期値に空オブジェクトを入れておけば、読込直後にフックが働き、setBind変数の働きによって、空の変数bindにary_dataのオブジェクトが代入されることになります。
また、記述方法ですがJavascriptの分割代入を用います。ここで注意しなければいけない、Vue.jsとの相違点として元のオブジェクトは一切値を変化させてはいけないようになっています(代入しようとするとread-onlyという警告が出ます)。あくまでオブジェクトを操作し、タグの再生成を行っていくのは変数bindに代入された値となります。
なので、空のオブジェクトbindに対し、オブジェクト変数ary_dataの値を代入することで、リアクティブなリストの順位ができます。
●登録
登録において厄介なのは、新規挿入する値はinputタグのテキストの値なのに対し、メソッドの実行はbuttonタグのonCickイベント内の関数insになります。したがって、リアクティブにテキスト操作は行いません。だとするとonChangeイベントの付記は不要かと思われますが、それをするとReactのルール違反になってしまい、警告が出ます。なので、テキストを直接操作しない場合でも、フックを実装しておきましょう(readOnlyと記述する方法もありますが、そうするとテキストへの入力ができなくなってしまいます)。
メソッドinsの中身についてですが、これはロード時の制御と同じ理屈で、リアクティブに制御しているオブジェクトbindに対し、随時分割代入で、オブジェクトを追加しているだけです。そして、テキストの値はvalueプロパティでリアクティブ制御しているので、直接取得することができます。
ちなみにフックを用いたリアクティブなオブジェクトの場合はarray.push()を使用できません。また、array.concat()も使用できません。
●削除
この削除における制御も色々と注意点があります。
●コンポネントの内側で関数を呼び出す場合
削除ボタンに実装しているonClickイベントに着目してください。このように、オブジェクトリテラル内に無名関数を入れてメソッドを呼び出していますが、こうしないと何度もイベントを呼び出してしまい、React limits the number of renders to prevent an infinite loop
というエラーが発生してしまいます。なので、コンポネントの内側のイベントでメソッドを呼び出す場合は無名関数(インライン関数)を用いて、メソッドを制御しておきましょう。
●削除の仕組み
削除用のメソッドはdelですが、この中ではarray.filter関数(任意のオブジェクトで引数と一致するものだけを抽出するメソッド)を用いています。そして
bind.filter((_,i) => i !== idx ))
とだけ記述されていますが、これは
bind.filter(function(item,i){ i !== idx }))
と同様で、i !== idx に当てはまる(つまり、削除しようとしたインデックスidxとループしているインデックス番号iが一致している)もの以外をデータとして残す動きをしています。そして、ここでもVue.jsと同様に間違ってもkeyを元に削除操作をしないようにしてください。そんなことをすると値がどんどんずれてきて、誤操作を招きます。
●修正
表示データをテキストボックスに変更し修正できるようにしてみました。ところが、リアクティブに変更させるということは、データの値を書き換えた時点でリストを再生成することになるので、結局更新ボタンの有無に関係なく処理が行われることになります(手前の値を削除すれば更新されているかはわかります)。
<!doctype>
<html lang="ja">
<head>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.10.3/babel.min.js"></script>
<script type="text/babel">
const { useState, useEffect } = React; //使用するフックの定義
const App =()=>{
//変数定義
const [insitem, setInsitem] = useState('');
const [upditem, setUpditem] = useState('');
const [bind ,setBind] = useState([]); //フックの使用(データリストの更新)
const ary_data = [
{id: 1,name: "モスクワ"},
{id: 2,name: "サンクトペテルブルク"},
{id: 3,name: "エカテリンブルク"},
{id: 4,name: "ムンバイ"},
{id: 5,name: "ベンガルール"},
{id: 6,name: "コルカタ"},
{id: 7,name: "サンパウロ"},
{id: 8,name: "リオデジャネイロ"},
{id: 9,name: "ブラジリア"},
];
const ins = ()=>{
setBind([
...bind,
{
id: bind.length + 1,
name: insitem
}
])
}
const del = (idx,name)=>{
setBind(bind.filter((_, i) => i !== idx))
}
const upd = (idx,name)=>{
changeValue(idx,name)
}
const changeValue = (idx,item)=>{
let edit = []
bind.map((elem,i)=>{
if(i !== idx){
edit = [...edit,elem]
}else{
edit = [...edit,{id:i + 1, name: item}]
}
})
setBind(edit)
}
//ループ処理の分離
const taglist = bind.map((data,idx)=>{
return(
<li key={idx}>
<dl>
<dt><input type="text" onChange={ (e)=>{ changeValue(idx,e.target.value) } } value={ data.name } /></dt>
<dd><button type="button" onClick={ ()=> upd(idx,data.name) }>修正</button></dd>
<dd><button type="button" onClick={ ()=> del(idx) }>削除</button></dd>
</dl>
</li>
)
})
//useEffectフックでコンストラクタ的な働きを行う
useEffect( ()=>{
setBind(...bind,ary_data)
},[]);
//レンダリング
return(
<React.Fragment>
<label>新規<input type="text" onChange={ (e)=>{ setInsitem(e.target.value) } } value={insitem}/></label>
<button type="button" onClick={ ins }>新規</button>
<ul>
{ taglist }
</ul>
</React.Fragment>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
</script>
</head>
<body>
<div id="root"></div>
</body>
注意点はオブジェクトの値を入れ替えるときで、次のような分割代入を用いれば、対象箇所だけ修正がかんたんにできます。
bind.map((elem,i)=>{
const edit = [...bind]
if(i === idx){
elem.name = item
edit[i] = elem
setBind(edit)
}
})
分割代入について押さえておきたい大事なポイントは...hogeという表記ですが、これはスプレッド構文と呼び、更新対象でないオブジェクト内のデータを補完する役割を持ちます。具体的には、更新したい情報はelem.nameだけですが、更新情報にいちいち他のプロパティも書いていられません。そこで、このスプレッド構文を分割代入に用いることで、他の更新対象でない値も一度に同期を取りたいオブジェクトに格納してくれるわけです。
Angular
では、これをAngularで制御してみましょう。これも驚くほど簡単だったりします。
<div id="app">
<input type="text" [(ngModel)]="ins_item" />
<button type="button" (click)="ins(ins_item)">新規</button>
<ul>
<li id="li" *ngFor="let data of lists; index as idx">
<input type="text" [(ngModel)]="data.name">
<button type="button" (click)="upd(idx,data.name)">修正</button>
<button type="button" (click)="del(idx)">削除</button>
</li>
</ul>
</div>
ここでngForディレクティブにて任意のインデックス記号を渡す場合にはindex as hogeと追記します。そして、同期を取りたいデータは双方向バインディングを、イベントを実行させたい場合はイベントバインディングをセットしておきます。
では、引き続いてTypeScriptによるデータ部分です。
import { Component } from '@angular/core';
import { Arydata } from './class/search'; // 演習3の型定義と同じ
const next_num = '';
const ARYDATA:Arydata = [
{id: 1,name: "モスクワ",},
{id: 2,name: "サンクトペテルブルク"},
{id: 3,name: "エカテリンブルク"},
{id: 4,name: "ムンバイ"},
{id: 5,name: "ベンガルール"},
{id: 6,name: "コルカタ"},
{id: 7,name: "サンパウロ"},
{id: 8,name: "リオデジャネイロ"},
{id: 9,name: "ブラジリア"},
];
const lists = [];
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent{
public lists = ARYDATA;
console.log(lists)
//新規
ins(ins_item: string){
next_num = this.next_num;
next_num = this.lists.length + 1;
this.lists.push({id:next_num,name:ins_item});
this.ins_item = '';
}
//修正
upd(idx: number,upd_item: string){
this.lists.splice(idx,1,{id:next_num,name:upd_item});
}
//削除
del(idx: number){
this.lists.splice(idx,1);
}
}
これで問題なく動きます。そしてVue.jsやReactでは使用を控えていたarray.push
メソッドとarray.splice
メソッドを使用していますが、Angularの場合は、これで何も問題は発生しません。なぜなら、Angularでは第1章で軽く説明したように同期データはあくまでバインディングで処理しているため、その処理部分ではJavaScriptでオブジェクトを処理しているのと同じ操作だからです。したがって挿入はarray.push、修正と削除はarray.spliceで事足りるわけです。
また、一度空オブジェクトlistsに移し替えているため、操作しているオブジェクトはこの移し替えたリストを随時操作していることになり、オブジェクトの再生成が不要なので、オブジェクト自体同期を取る必要がないのです。それこそ、見かけ上では同期を取っているようですが、実質Ajaxでhtmlと制御プログラムをやりとりしているようなもので、アーキテクチャーを使ってビュー部分とデータ部分で変数のやりとりをしているだけです。
ちなみにAngularも分割代入は可能です。続編では分割代入で記述しています。
※なお、連想配列の場合はReactのようにmapメソッドやfilterメソッドなどを使って制御するといいでしょう。
演習5予行演習編 コンポーネント演習
さて、演習4ではデータの新規作成と削除だけ採り上げましたが、修正は基本、編集画面→修正画面という2画面で制御するものなので、単一のファイルだけで演習するには限度があります。したがって、この演習ではファイルの分割化とコンポーネント化について記述できたらと思います。
また、jQueryとの比較は行いません(あまり、jQueryを用いてデータの修正は行わないので)。
Vue.js
では、その分割作業の第一歩として演習4のシステムをそのままコンポーネント化してみます。また、コンポーネントには
- ローカルコンポーネント
- グローバルコンポーネント
この2種類が存在します。ローカルコンポーネントはその名の通り、ローカル環境でしか使用できないコンポーネントです。
※コンポーネントのやりとりの条件
コンポーネントのやりとりにはルールがあり、テンプレートはケバブケース、コンポーネントはパスカルケースで書かないとやりとりできません。
- ケバブケース。トルコ料理のケバブのように、ハイフンでつながっている記法です。例のようにvue_componentをケバブケースにするとvue-componentとなります。
- パスカルケース。先頭を大文字にする記法です。なので、VueComponentという書き方になります。
■ローカルコンポーネント
<body>
<div id="app"><vue-component></vue-component></div>
<script>
const VueComponent = {
data(){
return{
ins_item: "",
datalist: [],
ary_data :[
{id: 1,name: "モスクワ"},
{id: 2,name: "サンクトペテルブルク"},
{id: 3,name: "エカテリンブルク"},
{id: 4,name: "ムンバイ"},
{id: 5,name: "ベンガルール"},
{id: 6,name: "コルカタ"},
{id: 7,name: "サンパウロ"},
{id: 8,name: "リオデジャネイロ"},
{id: 9,name: "ブラジリア"},
]
}
},
template:
`<div>
<input type="text" v-model="ins_item">
<button type="button" @click="ins">新規</button>
<ul>
<li v-for="(data,idx) in filterData">
<dl>
<dt>{{ data.name }}</dt>
<dd ><button type="button" @click="del(idx)">削除</button></dd>
</dl>
</li>
</ul>
</div>`
,
created:function(){
let datalist = this.datalist
let ary_data = this.ary_data
datalist = ary_data;
this.datalist = datalist
},
computed:{
filterData: function(){
let datalist = this.datalist
return datalist //初期に全値を出力したい場合はこのようにしておく
}
},
//イベントメソッド
methods:{
ins: function(){
let ins_item = this.ins_item
let datalist = this.datalist
let key = datalist.length;
let items = {
"key": key,
"name": ins_item
}
this.$set(datalist, key,items )
},
del: function(idx){
let datalist = this.datalist
this.$delete(datalist,idx)
}
}
}
new Vue({
el : "#app",
components:{
'vue-component': VueComponent
}
})
</script>
</body>
対して、グローバルコンポーネントの記述方法はこのようになります。記述としてはこっちの方がすっきりしており、しかも登録後の全インスタンスで流用できるので、非常に使い勝手がいいです。ただ、その分容量を取ってしまうので、単一ページ内だけで制御すればいい場合は、ローカルコンポーネントの方がいいでしょう。
■グローバルコンポーネント
<body>
<div id="app"><vue-component><!-- ケバブケース --></vue-component></div>
<script>
Vue.component('vue-component',{
data(){
return{
ins_item: "",
datalist: [],
ary_data :[
{id: 1,name: "モスクワ"},
{id: 2,name: "サンクトペテルブルク"},
{id: 3,name: "エカテリンブルク"},
{id: 4,name: "ムンバイ"},
{id: 5,name: "ベンガルール"},
{id: 6,name: "コルカタ"},
{id: 7,name: "サンパウロ"},
{id: 8,name: "リオデジャネイロ"},
{id: 9,name: "ブラジリア"},
]
}
},
template:
`<div>
<input type="text" v-model="ins_item">
<button type="button" @click="ins">新規</button>
<ul>
<li v-for="(data,idx) in filterData">
<dl>
<dt>{{ data.name }}</dt>
<dd ><button type="button" @click="del(idx)">削除</button></dd>
</dl>
</li>
</ul>
</div>`
,
created:function(){
let datalist = this.datalist
let ary_data = this.ary_data
datalist = ary_data;
this.datalist = datalist
},
computed:{
filterData: function(){
let datalist = this.datalist
return datalist //初期に全値を出力したい場合はこのようにしておく
}
},
//イベントメソッド
methods:{
ins: function(){
let ins_item = this.ins_item
let datalist = this.datalist
let key = datalist.length;
let items = {
"key": key,
"name": ins_item
}
this.$set(datalist, key,items )
},
del: function(idx){
let datalist = this.datalist
this.$delete(datalist,idx)
}
}
})
new Vue({
el : "#app"
})
</script>
</body>
React
Reactの場合、コンポーネントの分割以前に、まずコンポーネントの基本形について知っておく必要があると思います。いろいろなサイトを巡回してみると、どうも様々な記法が紛れてしまっているようなので整理しておきましょう。
###Reactを構成するコンポーネントについて
- クラスコンポーネント
- 関数コンポーネント
大きく分けて、この2種類が使われています。クラスコンポーネントはこのような書式で書かれているコンポーネントで、これはフック使用以前のReact16.7以前で主流だった書き方です(本記事ではほとんど採り上げませんでしたが、双方覚えた方がいいです)。
export default class Xxxxx extends React.Component{…}
あるいは
class Xxxxx extends React.Component{…}
export default Xxxxx
と記述されたもので、このクラスコンポーネント記法だとフックは使えません。
対して、関数コンポーネントとはこの演習用プログラムで挙げているような記述方法で、平たく言えば一つの大きな関数にまとめているだけです。
const Xxxxx = ()=>{…} //ECMA6以降
function Xxxxx(){…} //ECMA5以前の書き方も同じ
つまりは、上記のような書き方で、React16.8以降でフックを使用する場合、この記法以外は使いません。
###外部ファイル化
ではReact16.8の記法で作成した演習用プログラムを外部ファイル化してみると、このようになります。ただ、ここでは単純にレンダリング部分と関数コンポーネントを分割しただけです。
<!doctype>
<html lang="ja">
<head>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.10.3/babel.js"></script>
<script type="text/babel" src="react-sample5.js"></script><!-- コンポーネント呼び出し -->
<script type="text/babel">
ReactDOM.render(
<App />,
document.getElementById("root")
)
</script>
</head>
<body>
<div id="root"></div>
</body>
</html>
注意点として外部ファイルで切り離す場合は、フックを定義する場合にオブジェクトリテラル{}
を使用してください(エラーが表示されて、正しく表示されません)。
const { useState, useEffect } = React; //使用するフックの定義
const App =()=>{
//変数定義
const [insitem, setInsitem] = useState('');
const [bind ,setBind] = useState([]); //フックの使用(データリストの更新)
const ary_data = [
{id: 1,name: "モスクワ"},
{id: 2,name: "サンクトペテルブルク"},
{id: 3,name: "エカテリンブルク"},
{id: 4,name: "ムンバイ"},
{id: 5,name: "ベンガルール"},
{id: 6,name: "コルカタ"},
{id: 7,name: "サンパウロ"},
{id: 8,name: "リオデジャネイロ"},
{id: 9,name: "ブラジリア"},
];
const ins = ()=>{
setBind([
...bind,
{
id: bind.length + 1,
name: insitem
}
])
}
const del = (idx)=>{
setBind(bind.filter((_, i) => i !== idx))
}
//ループ処理の分離
//ry_data.set(state.datalist);
const taglist = bind.map((data,idx)=>{
return(
<li key={idx}>
<dl>
<dt>{ data.name }</dt>
<dd ><button type="button" onClick={ ()=> del(idx) }>削除</button></dd>
</dl>
</li>
)
})
//useEffectフックでコンストラクタ的な働きを行う
useEffect( ()=>{
setBind(...bind,ary_data)
},[]);
//レンダリング
return(
<React.Fragment>
<label>新規<input type="text" onChange={ (e)=>{ setInsitem(e.target.value) } } value={insitem}/></label>
<button type="button" onClick={ ins }>新規</button>
<ul>
{ taglist }
</ul>
</React.Fragment>
);
}
では、これらを踏まえ、リストページに編集機能を実装し、編集ページを作成し、修正後リストページに遷移するようにしないといけないのですが、ここまで来ると単一のページだけで制御はできず、Vue-CLIやReact Nativeなどが必要となってきます。この辺りフロントエンドあたりの知識も必要になってくる上にこの記事も容量が切迫してきたため、続編(コンポーネント処理、ルーティング処理、スタイル処理、TypeScript)を作成してますので、それを参照してもらえればと思います。
ただ、ここまで履修された方なら、たぶんソースの読解力、理解力は格段に上がり、サーバサイドでの開発も抵抗なく付いていけると思いますので、ご健闘お祈りします。また、昨今ではVueもReactもTypeScriptを用いた記述が主流になってきている部分もありますが、基本はやはりJavaScriptでの記述をこなしてからです。
追伸:まさかここまで再生数と評価数が上がるとは思いませんでした。嬉しさと同時に大凡な記事は作れないな、という責任感と少しばかりの重圧も抱いております。至らないところもあるかも知れませんが、ご指導ご鞭撻の程を宜しくお願い致します。
なお、自分はこのように知らない言語は、知っている言語と知らないもの同士最低2セットを並行しながら学習していき、共通点と相違点を洗い出していくということをよくします。ですので、今回の場合はjQueryと並行させ、Vue.jsとReactを敢えて並行し、そして2つに慣れたところでAngularも加えて演習していくという手段をとっており、今はSvelteでも同じような実践を行っております。
慣れてきたら、この4種類のフレームワークで全く同じ働きをするプログラムを作ってみるのも面白いと思いますし、更に理解力が高まると思います。solidJSももっと普及率が上がってきたら挑戦してみようと思います。