【JavaScript】ページ遷移せずに画面遷移する

# 0. 広告: 【僕は男だから戦場に閉じ込められた】ウクライナの男性出国禁止措置はジェンダー平等に反しています。
男性差別に反対するため署名にご協力ください。
https://change.org/MensRights18

# 1. やりたいこと
有向グラフで表される任意の状態遷移をブラウザの画面上に実現したい。
具体的には、画面上に、「現在のノード」を表示し、さらに現在のノードを始点とする全エッジを、  
ボタンとして描画する。ボタンをクリックすると、そのエッジに従って次のノードへ移動する。  
但しこの際の移動では、ページ遷移を行わない。あくまでも1ファイルで完結する。

# 2. 仕様
- `head`タグで画面シングルトンを定義し、`body`タグで呼び出す。
- 画面シングルトンは、DOMを用いて`document.body`を書き換える機能を持つ。
- 有向グラフは`head`タグにて書いておく。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319477/50411f80-d4f9-351f-d2b4-faa456f94411.png)

																									
																									
																									
																									
																									
							
																									
																									
																									
																																													


# 3. コーディング
次の順番で行う。
1. ノードクラスの定義
2. 有向グラフクラスの定義
3. 有向グラフ(とノード)のテスト
4. 画面シングルトンの定義
5. 画面シングルトンのテスト
					





## 3-1. ノードクラスの定義

```javascript:ノード.js
export var ノード = {
    new: function(内容, 子たち){
        return {
            __子たち: 子たち,
            __内容: 内容,
            __型: "ノード",
            子: function(エッジ名){
                try{
                    if(this.__子たち[エッジ名].__型 == "ノード")
                        return this.__子たち[エッジ名];
                    else
                        throw "";
                }catch(error){
                    throw "エッジ名が見つからないか、または子ノードに異常がありました";
                }
            },
            内容: function(){return this.__内容;}
        };
    }
};
```



## 3-2. 有向グラフクラスの定義

`有向グラフ`クラスのコンストラクタに、次の文法でコードを与えると、有向グラフが表現されるものとする。

```xml
<コード> ::= (<ノード> ( <ノード> "to" <ノード> "as" <文字列 except="{{{">)*)?
<ノード> ::= "{{{" <文字列 except="}}}"> "}}}"
```

没(始ノードが異なるなら、エッジ名の衝突を許可すべき)
```javascript:有向グラフ.js
import {ノード} from "./ノード.js";
export var 有向グラフ = {
    new: function(コード){
        コード = コード.replaceAll("\r", "").replaceAll("\n", "");
        var 開始ノード名, 始点ノード名, 終点ノード名, エッジ名; 
        [開始ノード名, コード] = 開始ノード名を取得する(コード);
        var エッジ情報たち = {};
        while(コード.match("{{{"))
        {
            [始点ノード名, 終点ノード名, エッジ名, コード] = エッジ名を取得する(コード);
            エッジ情報たち[エッジ名] = [始点ノード名, 終点ノード名];
        }
        var ノードオブジェクトたち = ノードオブジェクトたちを求める(開始ノード名, エッジ情報たち); 

        return {
            __開始ノード: ノードオブジェクトたち[開始ノード名],
            最初のノード: function(){return this.__開始ノード;}
        };
    }
};
var 開始ノード名を取得する = function(コード){
    var arr = コード.match(/^[ \t\r\n]*{{{(.*?)}}}(.*)$/);
    arr.shift();
    return arr;
};
var エッジ名を取得する = function(コード){
    var arr = コード.match(/^[ \t\r\n]*{{{(.*?)}}}to{{{(.*?)}}}as[ \t\r\n]*(.+?)[ \t\r\n]*({{{.*)?$/i);
    arr.shift();
    if (arr[3] == undefined) arr[3] = "";
    return arr;
};
var ノードオブジェクトたちを求める = function(開始ノード名, エッジ名から始ノード名と終ノード名への辞書){

    // ①ノード名をすべて集める
    var ノード名たち = [開始ノード名];
    for (var [エッジ名, [始ノード名, 終ノード名]] of Object.entries(エッジ名から始ノード名と終ノード名への辞書))
    {
        if(!(ノード名たち.includes(始ノード名)))
            ノード名たち.push(始ノード名);
        if(!(ノード名たち.includes(終ノード名)))
            ノード名たち.push(終ノード名);
    }

    // ②重複除去
        // (処理不要)

    // ③「ノード名から子たちへの辞書」を作る
    var ノード名から子たちへの辞書 = {};
    var ノードたち = {};
    for(var ノード名 of ノード名たち)
        ノードたち[ノード名] = ノード.new(ノード名, {});
    for(var ノード名 of ノード名たち)
    {
        ノード名から子たちへの辞書[ノード名] = {};
        for (var [エッジ名, [始ノード名, 終ノード名]] of Object.entries(エッジ名から始ノード名と終ノード名への辞書))
            if(始ノード名 == ノード名)             
                ノード名から子たちへの辞書[ノード名][エッジ名] = ノードたち[終ノード名];
    }

    // ④「ノード名からノードオブジェクトへの辞書」を作る
    var ノード名からノードオブジェクトへの辞書 = {};
    for (var [ノード名, 子たち] of Object.entries(ノード名から子たちへの辞書))
    {
        var ノードobj = ノードたち[ノード名];
        ノードobj.__子たち = 子たち;
        ノード名からノードオブジェクトへの辞書[ノード名] = ノードobj;
    }
        
    // ⑤ノードオブジェクトたちを返却する
    return ノード名からノードオブジェクトへの辞書;
};
```

## 3-3.有向グラフ(とノード)のテスト
3-1節および3-2節の通りノード.jsと有向グラフ.jsを作成して同じディレクトリに格納し、さらに次のようなindex.htmlを作って同じディレクトリに格納しよう。そしてそのディレクトリはサーバにアップロードする。
ローカルで同じ実験をするとCORSポリシーというのに引っかかって動作してくれないので注意。

```html:index.html
<script type="module">
import {有向グラフ} from "./有向グラフ.js";
var じゃんけん = 有向グラフ.new(`{{{グー}}}
{{{グー}}}to{{{パー}}}as 勝つ
{{{グー}}}to{{{チョキ}}}as 負ける
{{{チョキ}}}to{{{グー}}}as 勝つ
{{{チョキ}}}to{{{パー}}}as 負ける
{{{パー}}}to{{{チョキ}}}as 勝つ
{{{パー}}}to{{{グー}}}as 負ける
`);
;

console.log(じゃんけん);
</script>
```

サーバにアップロードしたら、ブラウザから `サーバ/ディレクトリ/index.html` にアクセスし、
ブラウザの開発者ツールから開いてコンソールを開く。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319477/0091c521-badc-c43f-a7ad-33e56107b6fb.png)




































chromeブラウザでやると、上図のように表示されるはずだ。
「▶」をクリックすると、メンバを一覧で見ることが出来る。この機能を使って、じゃんけんの有向グラフがちゃんと表現されていることを確認できる。
例えば、「勝つ」をたどって、「グー」→「パー」→「チョキ」→「グー」→...の循環が表現されていることを確認したのが次の図である。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319477/c32108d0-a621-c1de-ce40-4797fa3ed82d.png)





































あるいは、`console.log(じゃんけん);`の行にブレークポイントを設置してブラウザを一時停止させたのち、
次のように対話方式でjavascriptコードを実行することにより確認することもできる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319477/fbd972f2-9cd0-de9a-8573-8637a78ce481.png)


















## 3-4. 画面シングルトンの定義
`index.html`と同じディレクトリに次のような`画面.js`を置く。
```javascript:画面.js
export var 画面 = {
    __現在のノード: undefined,
    表示する: function(ノードobj){
        try{
            if(!(ノードobj.__型 == "ノード"))
                throw "";
        }catch(thrown)
        {
            throw "ノードが異常です。";
        }
        this.__現在のノード = ノードobj;
        document.body = document.createElement("body");
            var span_node = document.createElement("span");
            span_node.setAttribute("id", "node");
            span_node.innerHTML = ノードobj.内容();

            var span_edge = document.createElement("span");
            span_edge.setAttribute("id", "edge"); 
                var button;
                for (var [エッジ名, 子ノードobj] of Object.entries(ノードobj.__子たち))
                {
                    button = document.createElement("input");
                    button.setAttribute("type", "button");
                    button.setAttribute("value",  エッジ名);
                    button.onclick = function(){画面.表示する(ノードobj.子(this.value))};
                    span_edge.appendChild(button);
                    子ノードobj = null;
                }
        document.body.appendChild(span_node);
        document.body.appendChild(document.createElement("hr"));
        document.body.appendChild(span_edge);
    }   
};
```
ちなみに上記コード中の`button.onclick = function(){画面.表示する(ノードobj.子(this.value))};`における`this`は、押されたボタンに対応する`input`要素オブジェクトを指す。
したがって、この文脈で`this.value`はエッジ名を指す。

## 3-5. 画面シングルトンのテスト
`index.html`を次のように書き換える
```html:index.html
<head><script type="module">
import {有向グラフ} from "./有向グラフ.js";
import {画面} from "./画面.js";
var じゃんけん = 有向グラフ.new(`{{{グー}}}
{{{グー}}}to{{{パー}}}as 勝つ
{{{グー}}}to{{{チョキ}}}as 負ける
{{{チョキ}}}to{{{グー}}}as 勝つ
{{{チョキ}}}to{{{パー}}}as 負ける
{{{パー}}}to{{{チョキ}}}as 勝つ
{{{パー}}}to{{{グー}}}as 負ける
`);


画面.表示する(じゃんけん.最初のノード());
</script></head>
```







その後ブラウザから`index.html`にアクセスすると、次のように表示、画面遷移が起き、有向グラフオブジェクトに従った画面遷移が実現したことを確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319477/530f7480-a9ff-5b07-cb10-0117111ba1df.png)