Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

jQuery愛好家のためのVue.js、React、Angular入門

まず、ことわっておきますが、jQueryは非常に優秀なライブラリです。自分がメインとするWEBシステムの世界ではかなり重宝していますので、時代の潮流だからといって理由もなくjQueryを切り捨てろとは一言も言いませんし、もっと技術は培養すべきです。

ですが、使用できる選択肢を増やす、武器を増やすためにVue.js、React、Angularなどを学習するのは非常に有効です。ただ、これらの活用は学習コストが高いといわれています。その原因はフロントエンドありきで話が進みすぎているからだと考えています。したがって、自分の投稿記事は、jQueryを多用するWEBシステムエンジニアに向けた、フォーム操作をメインに置いた半備忘録兼自分なりに解釈した解説です。

ちなみに自分はサーバ構築からバックエンドまでこなしているワンオペエンジニア(フリーランス、非正規雇用に非ず)です。

§1:Vue.jsとReact、そしてAngular

その前に、vue.jsとReact、そしてAngularとはどういったもので、どんな意図で開発されたものかを知っておく必要があります。そして、その理念を知っていたら、スクリプト記法の理屈もわかりやすいからです。

ひとまず、共通のスローガンがあります。それはいずれも素早くデータの同期をとりたいために実現させている技術であることです。

Vue.js

Vue.js(ビュージェイエス)はもともとGoogleが開発したAngularJSの開発者の一人が、個人で開発を始めたJSフレームワークです。そのため、小規模の開発に向いた柔軟な利用も可能です(デフォルトでjQueryとの混用も可能)。そのVue.jsとは一言でどんな技術かというと

html内の部品に対して、Vueディレクティブという魔法をかける

こういうものです。ディレクティブとはVue.jsで定義されるv-xxxxというコマンドのことで、データのインプットとアウトプットをリアルタイムで行うことができたりする同期(バインド)操作のことです(訂正指摘感謝します)。このVue.jsでのバインドを、Reactではリアクティブ、AngularJSなどでは双方向バインディングなどと呼んでおり、今のAngularでもその技術はある程度継承されています。そして、AngularJSはhtml側でngというプロパティやメソッドを使ってバックエンドの処理をしており、かなり明解だった反面、開発が進むとかなりhtmlソースが汚されてしまうので、技術をある程度継承し、Angularというパッケージ単位のフレームワークを作ったわけです。しかし、このAngularJSの双方向バインディングと簡潔さを捨てるのはもったいないと、飛躍的に発展させたのがVue.jsでありVueディレクティブというもので、これはソースが汚れる原因となったhtml側でのバックエンド処理を、スクリプト側(コンポーネントを作成して管理)で処理させるようにしました。つまり

  • AngularJS: 双方向バインディング処理をhtml側で処理できたために、ソースが汚れてしまい、敬遠されてしまった。
  • Vue.js  : バインド(双方向バインディングとほぼ同定義)処理をスクリプト側に記載させるようにし、html側は基本、メソッドとプロパティだけ入出力させるようにした(つまり、JavaSrciptの基本に返った)

この理念を覚えておけば、今後の学習にも役立ちます。

React

React(リアクト)はFacebook社が自社ツールのために開発したコンポーネント型のJSフレームワークで、その鍵となるのはJSXという記法です。そのJSXとはなにかというと、スクリプトの中に直接埋め込める、html言語とほぼそっくりそのままの処理用言語で、それを作った理由は後述するように完全なるデザイン部分とプログラム部分の切り分け(すなわち部品管理の徹底化)のためです。ですが、その独特の記法によって技術者や入門者に違和感を与え敬遠されてしまっていたのも事実で、そのために何度も記法が変更されてきており、今日ではだいぶ見やすく、そして記述しやすくなっています。それでもReactの基本的な理念と動きは変わっておらず、一言でいうと

html内に魔法結社(JSXの拠点)を作り、そこでリアクティブな部品(htmlタグ)を錬金する

こういうものです。つまり、Vue.jsだといわゆるリアクティブな部品(Vueディレクティブ)自体はhtml上にあったのに対し、Reactの場合は、リアクティブな部品はhtmlになく、バックエンド上のコンポーネントで作成されることになります。そして、リアクティブな処理を行う際もそのバックエンド内だけで処理するので、非常に動きが高速で、部品もバックエンドにしか存在しないことで、開発を分担しやすく中規模の開発に向いています。

  • React :部品の管理を徹底するために、リアクティブな処理をhtml側で一切できないようにしている。部品の調達と管理はすべてJSXに則ったコンポーネント内で行う。

これが基本です。あと、コンポーネントの記法もいろいろあって、しかもしょっちゅう変更されているので、それが却って敬遠させている(初心者がどこから手を付けたらいいのかわからない)気もしますが、基本となる部分は全くぶれていないことを踏まえておいてください。

Angular

Angular(アンギュラー)はGoogle社が開発したAngularJSでの失敗を教訓に、その失敗の原因となったソースの汚れを解消させたものです。そしてvue.js、Reactと違い、RubyのRailsやPHPのLaravel、PythonのDjangoのような、それ一つでパッケージとなっているフルスタックフレームワークとなっており、TypeScriptがベースです。そして、これも一言で表すと

パッケージ自体がAngularという魔法世界(フレームワーク)であり、その中でバインディングという魔法の扉でデータをやりとりできる

なので、そのパッケージ内では自由自在にAngularの技術を利用できます(VueやReactのように、どこからどこまでという範囲指定が不要)。ですが、前述したようにソースの汚れを反省して作ったフレームワークなので、いわゆるリアクティブな部品は各アーキテクチャ内で実行するようになっていて、バインディングというものでデータや処理のやりとりができるようになっています。ただ、基本はAngularJS時代とそこまで変わっていないためにあまり難しくなく、どちらかというとVue.jsに近いですが、JavaScriptでの記述法がそのまま応用できるためVue.jsより理屈は解りやすいので、フレームワーク開発に慣れているバックエンドエンジニアなら、上記2つより学習は楽でTypeScriptの癖さえ押さえておけば簡単にプログラムを作れます。ただ、けっこう容量があるので、大規模開発向きです(これを省力化、小規模化したJSフレームワークも存在します)。

  • Augular:バインディング処理を行う部品はhtml上にあるが、処理は外部のアーキテクチャ内で行う。言語はTypeScriptを用いる。

そしてこのTypeScriptですが、JavaScriptと互換性を持っているため、そこまで難しく考えることはないと思います。

なお、現在もAngularJSはマイナーチェンジを続けていますが、本来のAngularJSの目的はVue.jsが担っていると考えていますので、本記事でAngularJSは採り上げません。

§2:バックエンドのための基本文法

前述したように、これらのJSフレームワークの学習コストが肥大してしまった原因は、フロントエンドありきで解説していることが多いためでしょう。そのため、バックエンドエンジニアが基礎の基礎である文法もわからずに、そっちばかりに目が行ってしまっていることで混乱を招き、これらの技術に手を出す気力を与えず、比較的記述が簡単なjQuery依存から脱皮できなかったのではないかと考えています(自分もそうでした)。

したがって、自分はjQueryで操作してきたことを、Vue.js、React、Angularではどう記述するのかを重点において解説していきたいと思います。また、基礎の学習のため、サーバは極力構築しないようにしておきます(本来ならVue.jsはVue CLI、ReactはReact Nativeというサーバを用いて開発していきます。Angularはローカルでテスト操作できないので、Angular CLI上で動かしています)。

演習1:フォーム操作:テキスト文字を表示させる

フォーム操作の基本の基本です。ですが、この基本だけでかなり記法の根本が理解できるのも事実です。ただ、単純に表示させるだけなのも面白くないので、jQueryでフォームに対し、キーの打鍵ごとに値を表示させるようにしましょう。

jquery-sample1.html
<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ですが、こちらはコンテンツの後に制御部分を記述してください。

vue-lesson1.html
<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>

jQueryより更に簡潔になっているのは一旦置いといて、目の付け所はelというプロパティとv-modelというVueディレクティブであり、これこそがまさしくVue.jsの要であるバインド技術になります。そして、それが適用される部品は任意のIDで囲んだ単一の親要素のみとなります。これをエレメントと呼び、el(elementの略)プロパティで指定した範囲のみ、ディレクティブを適用できます。

もっと詳しく文法を解説する

Vue.jsの場合は、html部分とスクリプト部分は区別しない方が説明もしやすいです。もう一度さっきのhtmlファイルを確認してみましょう。そして、コメントを付与します。

vue-lesson1.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の適用外なので、ただの文字列として認識されるだけです)。

Reactで再現

では、全く同じ動作をReactでも再現してみます。ただ、Reactは少し説明をわかりやすくするために、敢えて最短の記述にしていません(もっとスリム化した記法も使えるのですが、今日、一番見ることが多い記法です)。

react-lesson1.html
<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タグが入っているのが生理的に受け付けないのでしょう。しかし、Reactも初期と比較すると、だいぶ記述はスリム化しており、そして解りやすくなっています。では、この処理の流れがわかるようにコメントを付与してみます。なお、Vue.jsでは、スクリプトをbodyタグの内側、コンテンツの後に書くのがセオリーでしたが、Reactはコンテンツの前、headタグの中にスクリプトを記述するのがセオリーです(普通は外部ファイル化します)。

react-lesson1.html
<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タグ側にはリアクティブ処理された部品しか存在していなく、スクリプトの処理部分は、Vue.jsと違って部品すら見えません。

では、今度はReact.Componentの中身を3つに分類してみます。

  • constructer() //定義部分
  • 処理用の関数 //処理部分
  • render() //レンダリング部分

だいたいはこのように分類するとわかりやすいです。そして、その作業の流れを文章化してみると

  1. Appという部品を作成する命令を出す(1)
  2. 命令を受けたコンポネントはコンストラクタから部品を用意してレンダリング領域にある通り、タグを作成し、ReactDOM.renderを使用して処理の外側に返す(2)(4)
  3. onChangeイベントが発火したら、用意していた関数を使って処理を行う(3)
  4. 処理を行った状態で再度レンダリングを行い、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);

とあると思いますが、これは別におまじないではなく、利用する関数において値をバインド(同期)したいときに記述するルールで、これを記述しないと値をバインドできません。そして、基本は同じオブジェクト名にしておくだけで、左側は変数を代入しているだけですので、定義さえしておけば、別名でも大丈夫です(したところでメリットが薄いので同名にしおくべきですが)。

また、setStateはReactにおいて極めて重要性の高いメソッドですが、簡単にいえば、元の値と新しい値をバインドさせたいときに設定するものです。具体的には先程入力した文字と新たに入力した文字が異なる場合、随時setStateメソッドが実行されることになります。

また、e.target.valueはJSX内にある部品の、任意のフォームに対して取得した値(value)を受けとるもので、e(jQueryではelemと書くことが多い)とは、任意のオブジェクト変数に過ぎません。そして、受け取った値を先程のsetStateメソッドを使って、値の変化を処理しているわけです。

補足2:JSXの記述ルール

JSXは共通して以下の決まりがあります。

  • 単一の親要素しか生成してはいけない(中に子要素、孫要素があるのは問題ない)。もし、複数のタグを使いたい場合は、次の例のようにFlagmentオブジェクトを使ったり、タグごとにオブジェクト化する方法があります。
  • <input>タグのようにそれ一つで完結するタグは必ず<input />のようにスラッシュを入れること。これを入れないとJSXがどこまで適用したらいいのかわからずにエラーが起きます。これはテンプレートタグ<App / >などでも同様です。
  • JSXで生成したhtmlは必ず丸括弧(...)で囲って値を返すこと。これは次章のmapメソッドでも活用します。

最新の書き方(16.8以降)

上記の方法でも新たに導入されたクラスオブジェクトを使用することでかなり簡潔になりました(ECMA5時代はコンポネントも逐一作成する必要がありました)が、それでも至るところにthisばかりあったりと、色々と無駄があるようにも見えました。React16.8からは関数コンポーネントで記述することができます。

react-renew.html
<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が消えてかなり明白になったと思いますが、基本的な動作は変わっていません。ただ、変数定義部分、処理部分、レンダリング部分が一つの関数に収まったので、すごくすっきりしています。

※なお、以前のバージョンは

html
    const App =()=>{....}
    const.elem = <App />; //ワンクッション置く
    ReactDOM.render(
        elem,
        document.getElementById("root")
    );
    //ダイレクトに呼び出す以下の書き方は推奨されていなかった
    const App =()=>{....}
    ReactDOM.render(
        <App />,
        document.getElementById("root")
    );

この部分で、このようにrenderメソッドに対して、ワンクッション置かないと警告メッセージが表示されていましたが、今はダイレクトに代入しても大丈夫なようです。

Angularで再現

Angularは他のフレームワークと異なり、ローカル上で手軽に動かして…ということができないので、一旦はAngular-CLI上で動いているシステムからのかんたんな説明から始めます。

sampl1-angular.component.html
<div>
  <input type="text" [(ngModel)] ="mes" />
  <p>入力された文字{{mes}}</p>
</div>

◆Angularのバインディング

これだけで入力データを反映できます。尤も、データ部分は色々と呼び出さなければいけないライブラリはありますが、基本的な文法だけを説明すると[(ngModel)] = "mes"がデータの双方向バインディング部分であり、{{mes}}にバインドされたデータが表示されます。[(hoge)]はかなり珍妙な括弧に見えますが、実はこれはちゃんと意味があり、(fuga)の丸括弧はビューからデータへの偏方向(イベントバインディング)、[piyo]の角括弧はデータからビューへの偏方向(プロパティバインディング)を意味しているので、[(hoge)]の場合はビューからデータ、データからビューの双方向(双方向バインディング)を意味しています。また、Vue.jsと非常に表記が似ていますが、前述したようにVue.js自体、AngularJSを飛躍的に発展させたものなので、記法は非常によく似ている部分があります。

◆ビューの呼び出し

さて、Angularでビューとデータをやりとりする場合に最低限必要なものを解説していきます。

◆モジュール

Angularでデータを同期させるためにはngModelというディレクティブでの制御が必須となりますが、ただビューに記述するだけでは機能しません。モジュールにFormsModuleの呼び出しを記述する必要があります。

app.module.ts
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などといった慣例的な接尾辞を付与することを推奨しているようです(ファイル名もそれに従うように変えました)。

sample1-angular.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './sample1-angular.component.html', //ビューテンプレートはドットで接続すること
  styleUrls: ['./sample1-angulart.component.css']
})

//データ処理を行わない場合でも必ずクラス(公式ガイドではシンボルと表記)は定義しておくこと。
export class Sample1AngularComponent {
}

そして、独自定義したシンボルを使用する場合は、モジュール部分も随時書き換えが必要になってきます。

app.module.ts
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では敢えて見慣れた記法にしています)。

jquery-sample2.html
<script>
    $(function(){
        //配列の値
        let ary_data = [
            {key:0,name: "cakePHP"},
            {key:1,name: "Laravel"},
            {key:2,name: "CodeIgniter"},
            {key:3,name: "Symfony"},
            {key:4,name: "Zend Framework"},
            {key: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>

このシステムのポイントは前述した通りで、

html
$.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で記述するとこうなります。

vue-lesson2.html
<body>
    <div id="app">
        <select v-model="sel">
            <option value="">選択</option>
            <option v-for="data in ary_data" :value="data.name" >{{ data.name }}</option>
        </select>
        <p>選択された値:<span id="txt" >{{ sel }}</span></p>
    </div>
<script>
new Vue({
    el: "#app",
    data:{
        sel: "",
        ary_data :[
            {name: "cakePHP"},
            {name: "Laravel"},
            {name: "CodeIgniter"},
            {name: "Symfony"},
            {name: "Zend Framework"},
            {name: "Yii"},
        ]
    }
})
</script>

非常にすっきりした内容になりました。後述するReactと比較してもVue.jsはフォーム操作に向いており、何よりv-modelプロパティを使用するだけで値を一発で同期できるのが魅力的ですね。

さて、先程申し上げたように、この記述での目の付け所はv-for="data in ary_data"という部分であり、これは先程のjQueryの$.eachと同様に配列分のoptionタグに対してvalueプロパティとテキスト値を付与するループ処理となっています。forはPHPのfor文などでもお馴染みですが、よく知られる「~のために」という意味のほかにも「~している間」という意味があり、その名のとおり配列ary_dataを展開している間はdataという変数に格納しているわけです。しかもjQueryでは逐一スクリプト文で記述していましたが、Vue.jsはこのVueディレクティブの魔法の力(?)でスクリプトに数式を記述することなくループ処理が行えるので、かなり記述が楽になります。なお、ループできるのは何もoptionタグだけでなく、リスト作成に用いるliタグやテーブル作成のtdタグ、はたまたテンプレートタグを用いれば複数のタグをひとまとまりでループさせることも可能です。

補足:Vueディレクティブの省略記法

  • v-bind:xxxx → :xxxx
    上記のoptionにあるvalueと:valueは別物で、:valueはVueディレクティブにおけるv-bind:valueの省略記法です。v-bindは非常に使用頻度が高いので、このように省略記法を使うことが多く、慣れてきたら使ってみるといいでしょう。なお、v-bindとはhtmlタグ内のプロパティに、任意の値を同期させるプロパティで、この場合v-forを展開している間に、valueプロパティにその値を格納させる働きを持っています(あくまでhtmlの内側です。外側でvueディレクティブは使えないので、そちらでは前述したように{{}}(マスタッシュ)を使用します。

  • v-on:xxxx → @xxxx
    もう一つ使用頻度が高い省略記法が上記のもので、これはイベントメソッドを実行するときに使用し、同じく使用頻度が高いv-onを省略させるものです。これは次章で採り上げます。

React

同じようにプルダウンメニューの生成をReactでも記述してみます。

react-lesson2.html
<script type="text/babel">
const {useState } = React;
const App =()=>{
    //変数定義
    const [sel,setMenu] = useState(true);
    const ary_data = [
        {key: 1,name: "cakePHP"},
        {key: 2,name: "Laravel"},
        {key: 3,name: "Code Igniter"},
        {key: 4,name: "Symfony"},
        {key: 5,name: "Zend Framework"},
        {key: 6,name: "Yii"},
    ];
    //値の設定処理(いわばVue.jsの算出プロパティに当たる部分)
    const setList = ary_data.map((data)=>(
        <option key={data.key} value={data.name}>{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プロパティやテキスト値にkeyやnameを配列順に代入しているだけです。

//設定処理 ary_dataを順々に展開し、data変数に格納。該当するプロパティをそれぞれ当てはめていく。
    const setList = ary_data.map((data)=>(
        <option key={data.key} value={data.name}>{data.name}</option>
    ))

オブジェクトリテラルについて

さて、先程までは触れていませんでしたが、Reactの記法を見ていているとJavaScriptやVue.jsで見られたクォート("")はどこにも書かれておらず、代わりに{ ... }という波括弧があちこちに存在し、その役割が気になったと思います。これはオブジェクトリテラルというもので、JSX内において外側に置いている変数(オブジェクト、メソッドなども含む)を展開するために用いるものです。それを踏まえて動きを追ってみるとJSXのselectタグの内部にsetListという変数を展開し、子要素のoptionタグを代入していることがわかります。また、changeイベントの実行時のメソッド、changeMenuはプルダウンメニューの値が変更した際に行うイベント処理を展開しており、それによって先程と同様、直前と現在の値を比較し、値を出力させるようになっています。

補足

Reactでは、JSXにおけるレンダリングのルールとして、単一の親要素しか作れないというものがあります。ですが、今回のようにプルダウンメニューとPタグなど複数のタグを生成したい場合は、上述のように<React.Fragment>というテンプレートで括る方法や、オブジェクトに格納して返す方法などがあるようです。

Angular

Angularの場合もVue.jsと非常によく似た記述となります。

sample2-angular.component.html
<select [(ngModel)]="sel">
    <option>選択</option>
    <option *ngFor="let data of ary_data">{{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に反映されません)。

ちなみに、データ部分の記述はこうなりますが、詳細は次章で採り上げます。

sample2-angular.component.ts
import { Component } from '@angular/core';
//型の定義
export class Arydata{
    key: User;
    name: string;
    constructor(key: User, name: string){
        this.key = key;
        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で記述してみます。構築の仕方は色々ありますが、結論から言って面倒です。

jquery-sample3.html
<script>
    $(function(){
        //配列の値
        ary_data =[
            {key: 1,name: "モスクワ",},
            {key: 2,name: "サンクトペテルブルク"},
            {key: 3,name: "エカテリンブルク"},
            {key: 4,name: "ムンバイ"},
            {key: 5,name: "ベンガルール"},
            {key: 6,name: "コルカタ"},
            {key: 7,name: "サンパウロ"},
            {key: 8,name: "リオデジャネイロ"},
            {key: 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で書くとこのようになります。そしてここから算出プロパティ、メソッド、そして監視プロパティにも触れていきます。

vue-lesson3.html
<body>
    <div id="app">
        <input type="text" v-model="word">
        <p>検索結果{{filterData.length}}件</p>
        <ul>
            <li v-for="data in filterData">{{ data.name }}</li>
        </ul>
    </div>
<script>
new Vue({
    el: "#app",
    data:{
        word: "",
        ary_data :[
            {key: 1,name: "モスクワ",},
            {key: 2,name: "サンクトペテルブルク"},
            {key: 3,name: "エカテリンブルク"},
            {key: 4,name: "ムンバイ"},
            {key: 5,name: "ベンガルール"},
            {key: 6,name: "コルカタ"},
            {key: 7,name: "サンパウロ"},
            {key: 8,name: "リオデジャネイロ"},
            {key: 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ディレクティブの初期値に設定し、値をループさせているのです。そして、その説明でお気づきだと思いますが、算出プロパティに引数は不要どころか無用です。また、この算出プロパティは値がキャッシュされるので、それによって高速な値のバインド処理が可能になっています(それによる弊害もあるのですが、それは次の説明で…)。

なお、イベント自体を監視するwatch(監視プロパティ)やcreated(ライフサイクルフック)などもありますが、まずはcomputedと次に挙げるmethodsだけに話を絞ります。

■メソッド(methods)

対してメソッドというのものがあり、methodsプロパティ以下に記述します。それが一般にJavaScriptで使われてきたonXxxxxというプロパティで制御するメソッドのように思いがちです。ちなみに、これをVueディレクティブではv-on:xxxxと記述し、前章で説明した通り@xxxxと省略することができます。ところが、Vue.jsではフォーム部分の制御は概ねv-modelでバインドしてくれるので、そこに記述する必要はありません。では、どのようなときに使用するかといえば

  • (1)リアルタイムな値のバインドが不要なイベント制御
  • (2)画像ファイルなど値をキャッシュされては困るものの制御
  • (3)算出プロパティなど関数内での、内部で再計算などの処理が必要な場合

これらのケースで使用することが多いです。具体的な例を挙げましょう。

vue-lesson3b.html
<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 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 :[
            {key: 1,name: "モスクワ",img:"Moscow.jpg"},
            {key: 2,name: "サンクトペテルブルク",img:"Sankt.jpg"},
            {key: 3,name: "エカテリンブルク",img:"Yekaterin.jpg"},
            {key: 4,name: "ムンバイ",img:"Mumbai.jpg"},
            {key: 5,name: "ベンガルール",img:"Bengaluru.jpg"},
            {key: 6,name: "コルカタ",img:"Kolkata.jpg"},
            {key: 7,name: "サンパウロ",img:"SaoPaulo.jpg"},
            {key: 8,name: "リオデジャネイロ",img:"Rio.jpg"},
            {key: 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 require(`@/assets/img/${imgsrc}`)
            }
        },
        //入力された平仮名をカタカナに変換する
        hiraToKata: function(word){
            return word.replace(/[\u3041-\u3096]/g, function(match) {
                chr = match.charCodeAt(0) + 0x60;
                return String.fromCharCode(chr);
            })
        }

    }
})
</script>
</body>

ここでは検索文字のクリアの場合と、画像の呼び出し、そして入力文字のカタカナ変換にmethodsプロパティを使っています。まず検索値のクリアを実行するにはボタンのクリックが必要ですが、クリックに際して@clickイベントは、あくまでクリックされた場合のみ処理が実行されればいいものです。そこでこのメソッドに@click(v-on:clickの略称)ディレクティブを記述しておき、そのメソッドclear内には検索文字を空白化しておきます(dataに値を返すのを忘れないで下さい)。

また、画像の呼び出しはassetsというものを使って制御していますが、ここでは難しいことは置いておいて、画像は値がキャッシュされてしまうので、次別の値を呼び出そうとしても同じ画像キャッシュを保持し続ける現象が起きてしまい、同じ画像しか表示されなくなってしまいます。そこで算出プロパティではなくメソッドを:src(v-bind:srcの略)に記述しておくことで画像のソースに対して値をバインドし、キャッシュを保持させることなく、それぞれの画像を表示できます。このように、メソッドは必ずしもv-onディレクティブ(イベント制御)上で起動するものとは限りません。

そして入力文字のカタカナ変換ですが、そのまま入力しても平仮名のままでは検索に引っかかるわけないので、各入力かなに対し、カタカナに変換する処理関数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>

React

さて、Reactですが、敢えて他のページと差別化を図るために(…というより2つ書くのが大変だし、他にこことか優秀なページがあるので…)、16.8で再現してみます。そして、それにしたがい、フックを活用しています。

react-lesson3.html
<script type="text/babel">
const { useState, useEffect } = React; //使用するフックの定義
const App =()=>{
    //変数定義
    const [word,setWord] = useState(""); //フックの使用(検索文字のバインド)
    const [searched ,setSearched] = useState([]); //フックの使用(検索結果のバインド)
    const ary_data = [
        {key: 1,name: "モスクワ",img:"Moscow.jpg"},
        {key: 2,name: "サンクトペテルブルク",img:"Sankt.jpg"},
        {key: 3,name: "エカテリンブルク",img:"Yekaterin.jpg"},
        {key: 4,name: "ムンバイ",img:"Mumbai.jpg"},
        {key: 5,name: "ベンガルール",img:"Bengaluru.jpg"},
        {key: 6,name: "コルカタ",img:"Kolkata.jpg"},
        {key: 7,name: "サンパウロ",img:"SaoPaulo.jpg"},
        {key: 8,name: "リオデジャネイロ",img:"Rio.jpg"},
        {key: 9,name: "ブラジリア",img:"Brasilia.jpg"},
    ];
    //ループ処理の分離
    const setList = searched.map((data,key)=>{
            return(
            <li key={data.key}>
                <dl>
                    <dt>{ data.name }</dt>
                    <dd ><img src={data.img}/></dd>
                </dl>
            </li>
            )
    })
    //useEffectフックによって、検索値がバインドされたときに処理が実行される
    useEffect( ()=>{
        const searched = ary_data.filter((item,key)=>{
            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>

非常に複雑に見えますが、流れとしてはこうなっています。

  1. JSX内のsetListオブジェクトにリストの値を代入する。
  2. テキストに文字が入力されたら、bindWordメソッドが実行され、setWordメソッドによって検索値がバインドされる。
  3. 検索値に変化があったら、それをキャッチしたuseEffect(フックの一つ、後述)が働き、リストの値(ary_data)に対してマッチングを行いながら、検索値にマッチした値のみsearchedオブジェクトに格納される。
  4. 検索結果が格納されたsearchedはsetSearchedメソッドによってバインドされ、リストの値が更新される。
  5. もし、クリアボタンが押された場合は、検索値が初期化され、それに対しバインドが行われるため、リストは初期化される。

以上の流れになります。ここで鍵となり、そして16.8から新たに導入されたuseStateフックとuseEffectフックについて説明しようと思いますが、Vue.jsで言うところのv-modelディレクティブと算出プロパティのような働きだと認識しています。

●フック(hooks)

フックはReact16.8から導入された新機能で、日本語に直すと接続のことです。そして今までクラスオブジェクト依存していた記述に代わり関数で処理できるようになったことで、それに伴いよりデータの同期処理をより円滑に行うために開発された機能的なメソッドです(というよりコンストラクタやsetStateが普通だと使えなくなったので、その代替の手段です)。

●useState

useStateは実は演習1にも登場していましたが、全く触れませんでした。これはどういうものかというと、値の変更前後で同期をとるためのメソッドで、以下のような記述ルールがありますが、これを把握していないと引っかかります(前後の変数を代入するものではありません)。
const [バインドさせたい値,バインドさせるためのメソッド] = useState(初期値)
つまり、検索値を処理する場合、初期値は何もないのでuseState(””)と空白を代入し、1つ目の戻り値は検索値を格納するのは変数word、それに対して2つめの戻り値に代入するのはそれをバインド処理するためのメソッドsetWordの戻り値であるということです。同様に、検索結果に対しても同じように記述しますが、注意点として初期値がオブジェクトになる場合は引数にuseState([])と空オブジェクトを代入する必要があります。混乱しないためにも、奇を衒わずバインドを行うメソッド名はsetXxxxxXxxxxはバインドしたい値と対応)とした方が迷いが起こらずいいでしょう。

実はこのuseStateの戻り値はJavascriptの分割代入です。分割代入というのは[元の値,追加したい値]とすることで、オブジェクトの値を個別の変数にして戻り値に代入していくものなので、useStateメソッドが実行される度に、オブジェクトの中身を分解し、更新されたデータを処理する仕組みとなっています。

●useEffect

名の通りエフェクトを与えるフックで、バインドした値が変化したときに処理を行う、いわばVue.jsのv-modelと算出プロパティ(computed)の関係と同じようなものだと思えばいいでしょう。なお、公式サイトでは副作用と物々しげな言葉を使っていますが、どちらかといえば、連動の方がふさわしい気がします(effectにそのような意味はありませんので、相乗効果、波及といった意味合いでもいいでしょう)。

さて、今回の演習では検索結果を返す処理に対してこのuseEffectを使用しています。なぜなら、検索値をバインドさせても、それはあくまで検索値のバインドに過ぎないので、それだけでは検索結果までバインドできないからです。そこでこのuseEffectを使い、検索値をバインドさせ、その値に変化が与えられた時のみ、その連動で、検索結果も処理するようにしているわけです。そしてその検索結果もまた、useStateでバインド処理することで、随時検索結果をフィルタリングすることができます。

このuseEffectの記述方法ですが、このようになります。
useEffect(()=>{...元となる値によって連動させたい処理...},[連動させたい元となる値])

※注意点として元となる値は必ずオブジェクトに格納してください。もし、今回のように単発の値であっても、オブジェクトに格納しないと処理エラーが起きます。

●Reactで分岐処理する場合

ReactはVue.js、Angularのようにif構文を埋め込んだりはできないのですが、大きく分類して3種類の方法があるようです。

1.関数コンポーネント
2.三項演算子
3.即時関数

そのうち、即時関数はあまりオススメできない(JSXの記述が見づらくて拡張性がない)と思うので、関数コンポーネントと三項演算子の解説だけしておきます。

●関数コンポーネントで分岐

react-sample3func.tml
//前略
    //ループ処理の分離
    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内にという見慣れないタグが描かれていますが、実はこれが関数コンポーネントで、その定義として分岐処理を記述しています。そして、JSXで再描写が行われる毎に分岐処理が行われるので、そこにはResultメソッド内で処理された変数resultが随時返されることになります。

ただ、入れ子にならない場合は次に説明する記述法の方が楽です。

●三項演算子で分岐

上記のような、入れ子構造なしの単純なif判定だけならば、三項演算子の方が表記が楽です。

react-sample3tri.html
//前略
    //レンダリング
    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型でないと評価されないというものではなく、要は条件一致の場合のみ処理が可能というものです)

react-sample3tri.html
   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がベースとなっています。そのため、型指定が厳格となりますので、一見複雑そうには見えるのですが、データ処理部分がある程度自由が利くため、他のフレームワークより理屈は理解しやすいと思います。

まず、ビューの部分になりますが、たったのこれだけです。

sample3-angular.component.html
    <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>

◆分岐について

また、Angularにおけるif分岐ですが、実はngIfディレクティブのみを使用し、ngElseというディレクティブは存在しません。なので、elseの場合も上記のように判定結果を逆転させて記述させたりするほか、else条件を付与したい場合は、ngテンプレートとテンプレートタグのidで制御します(ngテンプレート以外に記述するとエラーとなります)。

sample3-angular.component.html
    <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文の間にコロンを挟んでいる記述とそうでない記述が見られますが、どっちでも処理されます。

sample3-angular.component.html
<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ディレクティブの判定結果にかかわらず、何も表示されなくなってしまいます(しかもコンソールでエラー表示されないのでバグフィクスが大変です)。

◆イベントバインディング

ここでイベント部分が(change)となっていますが、(hoge)は前述したようにビューからデータに偏方向でデータを受け渡すので、Angularにおけるイベントトリガーはこのように記述するだけで作動します。これをAngularではイベントバインディングと呼んでおり、引数を渡すこともできます。そして、ここでは検索文字wordを[(ngModel)]でバインドしているので、この値に変化が与えられるごとに、メソッドfiilterData(word)が作動することなります。そして、それによって得られた検索結果がオブジェクト変数listsに返され、ngForディレクティブによってループされます。

では、そのデータ制御部分ですが、このようになります。

sample3-angular.component.ts
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をわざわざ返しているのは、検索文字をカタカナに変換しているためで、この変数を元のクラスに返す処理を行わないとビュー上にカタカナで表示されません。

◆コンストラクタによる型の定義

ただ、Typescript経験者ならおわかりかと思われますが、この言語は非常に型に厳格なので、このデータ処理前にコンストラクタを呼び出す必要があります。それが上にある
import{ Arydata } from './class/search';
の部分で、その中身はこうなっています。このように定義された変数に対し、一つ一つ型を定義していかないと、エラーが検出され処理が中断されてしまいます。逆にそこさえ押さえておけば、ある程度操作に自由が利くので上記2つより至極単純に作れます。

search.ts
export class Arydata{
    key: number;
    name: string;
    constructor(key: number, name: string){
        this.key = key;
        this.name = name;
    }
}

Angularの学習にあたって参考にしたページ

-AngularのngForでチャットコメントを実装する

演習4: データリストの追加、削除、修正(DOMの再生成)

いわゆるCRUD(Create、Read、Update、Delete)と呼ばれる操作です。これに至っては今の所Vue.js、React、Angularのバインド(同期)が足かせになることもあり、I/Oのタイミングがわかりやすく、オブジェクト操作がしやすいjQuery+Ajaxの方が便利ではないかと考えている(PHPやRailsなどと合わせてDB操作の補助的な機能としてjQueryを重宝していた理由です)部分ですが、これについても各フレームワークの癖も掴みながら解説していきたいと思います。

jQuery

まずは、データの新規登録と削除について制御していきたいと思います。記述方法は色々あるとは思いますが、まずはオーソドックなやり方で制御していきます。

jquery-sample4.html
<head>
<script src="https://code.jquery.com/jquery-3.5.1.js"></script>
<script>
    $(function(){
        //配列の値
        ary_data =[
            {key: 1,name: "モスクワ",},
            {key: 2,name: "サンクトペテルブルク"},
            {key: 3,name: "エカテリンブルク"},
            {key: 4,name: "ムンバイ"},
            {key: 5,name: "ベンガルール"},
            {key: 6,name: "コルカタ"},
            {key: 7,name: "サンパウロ"},
            {key: 8,name: "リオデジャネイロ"},
            {key: 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で操作した場合はどうなるか見ていくことにしましょう。

※また、データ修正においては、普通、データ修正、更新という2段階の作業を行うので、一旦置いておきます。

※Vue.jsとReactとの違い

Vue.jsでもReactでも、jQueryと大きな違いは、DOM要素そのものではなく、オブジェクトの値を操作することになります。なぜなら、jQueryと違い、双方向バインディングやリアクティブな値というものは、元になるデータソースがあり、そこからリアルタイムにDOM要素を再生成しているからで、処理を行ったDOM要素を操作したところで意味を成さないどころか、データの整合性が取れなくなってしまいます(なので、そういう操作はできないようになっている)。

そして、オブジェクトを操作するにあたって、それぞれに適したメソッドを用いることになります。ですが、新規作成や削除という操作は必ずしもリアルタイムに操作する必要がなく、それどころかワンクッション置いておかないと「いつこのデータを挿入したのか?」あるいは「いつ削除したのか?」と問題を提起しかねません。なので、もしデータの検索や修正を伴わずに、単に新規作成や削除だけをしたい場合はそれこそJavascriptやJQueryなどで十分処理ができる上、元のオブジェクト内データを破壊しなくて済む(Vue.jsの場合は破壊しないように工夫が必要)ので、それらの方が使いやすいと考えていたわけです(まあ、今はJavascriptだけで色々できるようにはなってきていますが)。

Vue.js

Vue.jsを用いて新規作成と削除を実装するとこのようになります。Reactとの違いは、元のオブジェクトの値も操作できる点があります。ただ、それだと元のオブジェクトの値を破壊してしまうので、移し替えておくようにしましょう。ただ、それにあたって新たな技術、ライフサイクルフックが必要となります。

sample-vue4.html
<!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 :[
            {key: 1,name: "モスクワ"},
            {key: 2,name: "サンクトペテルブルク"},
            {key: 3,name: "エカテリンブルク"},
            {key: 4,name: "ムンバイ"},
            {key: 5,name: "ベンガルール"},
            {key: 6,name: "コルカタ"},
            {key: 7,name: "サンパウロ"},
            {key: 8,name: "リオデジャネイロ"},
            {key: 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に代入してしまうので、作業が全く反映されなくなります。

failed.html
  //※これは処理の失敗例です
    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の値が反映されていくことになります。

■登録の場合

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の値を取得して、それを元に削除しないでください。そんなことをやると、インデックス番号がずれてしまうので大きなトラブルの元となります。

■修正の場合

修正は基本、編集画面と更新処理にわけて行うものですが、当初からテキストボックスで表示し、変更したら処理が実行できるようにしてみます(あくまで演習用で、実用的な処理ではないです)。

vue-sample4-edit.html
<!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 :[
            {key: 1,name: "モスクワ"},
            {key: 2,name: "サンクトペテルブルク"},
            {key: 3,name: "エカテリンブルク"},
            {key: 4,name: "ムンバイ"},
            {key: 5,name: "ベンガルール"},
            {key: 6,name: "コルカタ"},
            {key: 7,name: "サンパウロ"},
            {key: 8,name: "リオデジャネイロ"},
            {key: 9,name: "ブラジリア"},
        ]
    },
        created:function(){
                let datalist = this.datalist
                let ary_data = this.ary_data
                let isEdited = this.isEdited
                datalist = ary_data;
                console.log(isEdited)
                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ディレクティブで値を同期させておかないと、変更後の値が保持されなくなります。

compare.html
    <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を用いると想定外の動作が起きることを警告しています。

v-for と一緒に v-if を使うのを避ける

React

ReactもVue.jsと同様にオブジェクトを操作します。ただし、Vue.jsは元のオブジェクトの値を操作できるのに対し、Reactの場合は、元のオブジェクトの値を変更できないので、フックに用意された変数に代入して操作することになります。ただ、リアクティブなオブジェクト操作は色々と癖があり、注意しなければいけない部分が非常に多いです。また、ここでもReact16.8以降での記法を用いています。

react-sample4.html
<!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 = [
        {key: 1,name: "モスクワ"},
        {key: 2,name: "サンクトペテルブルク"},
        {key: 3,name: "エカテリンブルク"},
        {key: 4,name: "ムンバイ"},
        {key: 5,name: "ベンガルール"},
        {key: 6,name: "コルカタ"},
        {key: 7,name: "サンパウロ"},
        {key: 8,name: "リオデジャネイロ"},
        {key: 9,name: "ブラジリア"},
        ];
    //新規作成用のメソッド
    const ins = ()=>{
            setBind([
                ...bind,
            {
                key:  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を元に削除操作をしないようにしてください。そんなことをすると値がどんどんずれてきて、誤操作を招きます。

●修正

表示データをテキストボックスに変更し修正できるようにしてみました。ところが、リアクティブに変更させるということは、データの値を書き換えた時点でリストを再生成することになるので、結局更新ボタンの有無に関係なく処理が行われることになります。

react-sample4update.html
<!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 = [
        {key: 1,name: "モスクワ"},
        {key: 2,name: "サンクトペテルブルク"},
        {key: 3,name: "エカテリンブルク"},
        {key: 4,name: "ムンバイ"},
        {key: 5,name: "ベンガルール"},
        {key: 6,name: "コルカタ"},
        {key: 7,name: "サンパウロ"},
        {key: 8,name: "リオデジャネイロ"},
        {key: 9,name: "ブラジリア"},
        ];
        const ins = ()=>{
            setBind([
                ...bind,
            {
                key:  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,{key: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>

注意点はオブジェクトの値を入れ替えるときで、色々調べたところフックの場合は随時オブジェクトに値を代入しても無効なようで、下記のようにmapメソッドを用いて値を代入していき、setBindメソッドを実行、随時リストを再生成するようにするのが正しいようです。

let edit = []
bind.map((elem,i)=>{
    if(i !== idx){
    edit = [...edit,elem] //修正の対象でないものはそのまま値を代入
    }else{
    edit =  [...edit,{key:i + 1, name: item}] //修正の対象は変更後の値を代入
    }
})

Angular

では、これをAngularで制御してみましょう。これも驚くほど簡単だったりします。

sample4-angular.component.html
    <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によるデータ部分です。

sample4-angular.component.ts
import { Component } from '@angular/core';
import { Arydata } from './class/search'; // 演習3のコンストラクタと同じ
const next_num = '';
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: './sample4.angular.html',
})
export class Sample4Angular{
    public lists = ARYDATA;
        //新規
    ins(ins_item: string){
        next_num = this.next_num;
        next_num = this.lists.legth + 1;
        this.lists.push(new Arydata(next_num,ins_item));
                this.ins_item = '';
    }
    //修正
    upd(idx: number,upd_item: string){
        this.lists.splice(idx,1,new Arydata(idx,upd_item));
    }
    //削除
    del(idx: number){
        this.lists.splice(idx,1);
    }
}

これで問題なく動きます。そしてVue.jsやReactでは使用を控えていたarray.pushメソッドとarray.spliceメソッドを使用していますが、Angularの場合は、これで何も問題は発生しません。なぜなら、Angularは同期データはあくまでバインディングで処理しているため、そのデータの内部はJavaScriptでオブジェクトを処理しているのと同じ操作だからです。したがって挿入はarray.push、修正と削除はarray.spliceで事足りるわけです。

また、一度空オブジェクトlistsに移し替えているため、操作しているオブジェクトはこの移し替えたリストを随時操作していることになり、オブジェクトの再生成が不要なので、オブジェクト自体同期を取る必要がないのです。見かけは同期を取っていうようですが、実質Ajaxでやりとりしているようなもので、アーキテクチャーを使ってビュー部分とデータ部分で変数のやりとりをしているだけです。

※なお、連想配列の場合はReactのようにmapメソッドやfilterメソッドなどを使って制御するといいでしょう。

演習5 データの修正(ファイルの分割化)

さて、演習4ではデータの新規作成と削除だけ採り上げましたが、修正は基本、編集画面→修正画面という2画面で制御するものなので、単一のファイルだけで演習するには限度があります。したがって、この演習ではファイルの分割化について記述できたらと思います。

また、jQueryとの比較は行いません(あまり、jQueryを用いてデータの修正は行わないので)。

Vue.js

では、その分割作業の第一歩として演習4のシステムをそのままコンポーネント化してみます。また、コンポーネントには

  • ローカルコンポーネント
  • グローバルコンポーネント

この2種類が存在します。ローカルコンポーネントはその名の通り、ローカル環境でしか使用できないコンポーネントです。

※コンポーネントのやりとりの条件

コンポーネントのやりとりにはルールがあり、テンプレートはケバブケース、コンポーネントはパスカルケースで書かないとやりとりできません。

  • ケバブケース。トルコ料理のケバブのように、ハイフンでつながっている記法です。例のようにvue_componentをケバブケースにするとvue-componentとなります。
  • パスカルケース。先頭を大文字にする記法です。なので、VueComponentという書き方になります。

■ローカルコンポーネント

vue-sample5local.html
<body>
        <div id="app"><vue-component></vue-component></div>
<script>
const VueComponent = {
    data(){
        return{
            ins_item: "",
                datalist: [],
            ary_data :[
                {key: 1,name: "モスクワ"},
                {key: 2,name: "サンクトペテルブルク"},
                {key: 3,name: "エカテリンブルク"},
                {key: 4,name: "ムンバイ"},
                {key: 5,name: "ベンガルール"},
                {key: 6,name: "コルカタ"},
                {key: 7,name: "サンパウロ"},
                {key: 8,name: "リオデジャネイロ"},
                {key: 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>

対して、グローバルコンポーネントの記述方法はこのようになります。記述としてはこっちの方がすっきりしており、しかも登録後の全インスタンスで流用できるので、非常に使い勝手がいいです。ただ、その分容量を取ってしまうので、単一ページ内だけで制御すればいい場合は、ローカルコンポーネントの方がいいでしょう。

■グローバルコンポーネント

vue-sample5global.html
<body>
        <div id="app"><vue-component><!-- ケバブケース --></vue-component></div>
<script>
Vue.component('vue-component',{
    data(){
        return{
            ins_item: "",
            datalist: [],
            ary_data :[
                {key: 1,name: "モスクワ"},
                {key: 2,name: "サンクトペテルブルク"},
                {key: 3,name: "エカテリンブルク"},
                {key: 4,name: "ムンバイ"},
                {key: 5,name: "ベンガルール"},
                {key: 6,name: "コルカタ"},
                {key: 7,name: "サンパウロ"},
                {key: 8,name: "リオデジャネイロ"},
                {key: 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以前で主流だった書き方です(本記事では採り上げません)。

js
export default class Xxxxx extends React.Component{}

あるいは

js
class Xxxxx extends React.Component{}
export default Xxxxx

と記述されたもので、このクラスコンポーネント記法だとフックは使えません

対して、関数コンポーネントとはこの演習用プログラムで挙げているような記述方法で、平たく言えば一つの大きな関数にまとめているだけです。

const Xxxxx = ()=>{…} //ECMA6以降
function Xxxxx(){…} //ECMA5以前の書き方も同じ

つまりは、上記のような書き方で、React16.8以降でフックを使用する場合、この記法以外は使いません

外部ファイル化

ではReact16.8の記法で作成した演習用プログラムを外部ファイル化してみると、このようになります。ただ、ここでは単純にレンダリング部分と関数コンポーネントを分割しただけです。

react-sample5export.html
<!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>

注意点として外部ファイルで切り離す場合は、フックを定義する場合にオブジェクトリテラル{}を使用してください(エラーが表示されて、正しく表示されません)。

react-sample5.js
const { useState, useEffect } = React; //使用するフックの定義
const App =()=>{
    //変数定義
        const [insitem, setInsitem] = useState('');
    const [bind ,setBind] = useState([]); //フックの使用(データリストの更新)
        const   ary_data = [
        {key: 1,name: "モスクワ"},
        {key: 2,name: "サンクトペテルブルク"},
        {key: 3,name: "エカテリンブルク"},
        {key: 4,name: "ムンバイ"},
        {key: 5,name: "ベンガルール"},
        {key: 6,name: "コルカタ"},
        {key: 7,name: "サンパウロ"},
        {key: 8,name: "リオデジャネイロ"},
        {key: 9,name: "ブラジリア"},
        ];
        const ins = ()=>{
            setBind([
                ...bind,
            {
                key:  bind.length + 1,
                name: insitem
            }
            ])
        }
        const del = (idx)=>{
            console.log(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>
    );
}

では、これらを踏まえ、リストページに編集機能を実装し、編集ページを作成し、修正後リストページに遷移するようにしないといけないのですが、ここまで来ると単一のページだけで制御はできず、React Nativeなどが必要となってきます。この辺りは他の方が詳しい解説記事を作成してますし、フロントエンドあたりの知識も必要になってきますので、その辺りを参照頂けたらと思います。

ただ、ここまで履修された方なら、たぶんソースの読解力、理解力は格段に上がっていると思いますので、ご健闘お祈りします。

追伸:まさかここまで再生数と評価数が上がるとは思いませんでした。嬉しさと同時に大凡な記事は作れないな、という責任感と少しばかりの重圧も抱いております。至らないところもあるかも知れませんが、ご指導ご鞭撻の程を宜しくお願い致します。

なお、自分はこのように知らない言語は、知っている言語と知らないもの同士最低2セットを並行しながら学習していき、共通点と相違点を洗い出していくということをよくします。ですので、今回の場合はjQueryと並行させ、Vue.jsとReactを敢えて並行し、そして2つに慣れたところでAngularも加えて演習していくという手段をとっています。

BRSF
職業、PG・SE・DBエンジニア。オープン環境のwebプログラムをメインにシステム構築担当。使用言語はPHP(cakePHP、Laravel含)jQuery、JavaScript、ExcelVBA、Perl、Ruby、Python。現在Vue、React、Angular強化中。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away