Flaskでトグルボタンと確認ダイアログの実装

本記事では、PythonのWebアプリケーションフレームワークであるFlaskでwebアプリ等を作る際に、プログラムのパラメータに即時反映されるトグルボタンの機能を実装する方法について記述します。
トグルボタンとは、ユーザーがON／OFFを切り替える用途で使われることの多いボタンで、身近な例だとiPhoneの各アプリケーションの通知機能の有無やwifi接続の有無を切り替える際のボタンとしてよく用います。
今回はwebページにトグルボタンを実装し、トグルボタンの切り替えと連動してバックサイドのプログラムの1パラメータの2値を切り替える機能を設ける簡易手順を示します。

以下画像が今回作成したトグルボタンです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2709441/3f47c458-44af-dd23-1f85-1fa9ae535fca.png)
対応するパラメータとして今回は __config.yaml__ というYAMLファイルの __notification__ キーにonとoffのどちらかを設定することとしました。上画像のようにトグルボタンがOFFの時は __config.yaml__ の __notification__ 値が常にoffとなるように、ONの時はonとなるように実装します。
```config.yaml
notification: 'off'
```

<br>
また今回はもう1つの機能として、以下のようにトグルボタンを押した際に確認用のダイアログが表示される機能を実装しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2709441/bf55937a-75ef-34af-0eb9-d12f3d8a7e16.png)


# 環境
+ MacBook Pro
+ Python3.10.6
+ Flask 2.2.2
+ Bootstrap 4

# 実装方法

### ディレクトリ構成
ディレクトリは以下の様に構成します。今回はBootstrapというCSSのフレームワークを用います。
Flaskで作るWebアプリケーションにBootstrapを適用する場合は以下のように __static__ ディレクトリと __css__ ディレクトリの中にそれぞれ対応したcssファイルとjsファイルを配置しておく必要があります。
.
├─ app.py
├─ config
│       └config.yaml
├─ config_utils.py 
├─ static
│   ├─css
│   │  　　 └ bootstrap.min.css
│   └─ js
│    　　　├ bootstrap.bundle.min.js
│    　　　 └ jquery-3.5.1.min.js
└─ templates
         　　　　 └ test_toggle.html


### コード

__app.py__ と __test_toggle.html__ の内容について、今回はトグルボタンと確認ダイアログの書き方のみを説明するためその他は極めて間便なものとします。
まず __app.py__ は以下のように作成しました。Bootstrapを適用するといっても特別なモジュールをimportする必要はありません。
引数に __config.yaml__ を与え、 __app.py__ の中から__config.yaml__ の値の読み取り、編集を行います。そのための関数として __update_notify__ 関数と __update_notify__ 関数を設けます。
ページングはデフォルトのページとwebページからPOSTが行われた際の処理を行うページを用意します。
```app.py
import argparse
from flask import Flask, render_template, redirect, url_for, request
from config_utils import ConfigUtils

parser = argparse.ArgumentParser()
parser.add_argument('--config',default='config/config.yaml',type=str)
args = parser.parse_args()

CONFIG_FILEPATH = args.config

def update_notify(yamlfile):
    status = ConfigUtils.load(args.config)['notification']
    if status == "on":
        ConfigUtils.update(args.config,'notification', 'off')
    elif status == "off":
        ConfigUtils.update(args.config,'notification', 'on')
    else:
        raise Exception('Invalid value in yaml file')  

def read_notify(yamlfile):
    config = ConfigUtils.load(args.config)
    return config['notification']

app = Flask(__name__)

@app.route("/")
def home():
    return render_template('test_toggle.html', notify=read_notify(CONFIG_FILEPATH))

@app.route('/', methods=['POST'])
def post():
    if request.form.get('confirm'):
        update_notify(CONFIG_FILEPATH)
        return redirect(url_for('test'))

    return redirect(url_for('test'))

if __name__ == "__main__":
    app.run(port=5000)
```
<br>
 
 __ConfigUtils__ クラスは以下の __config_utils.py__ にて作成してimportします。
yamlモジュールの __safe_load__ 関数を用いてYAMLファイルの読み取りと書き込みを行うという極めてシンプルなものとなっています。

```config_utils.py
import yaml

class ConfigUtils:
    @staticmethod
    def load(filename):
        with open(filename, 'r', encoding='utf-8') as f:
            dict_ = yaml.safe_load(f)
        return dict_

    def update(filename, key, new_value):
        with open(filename, 'r+', encoding='utf-8') as f:
            dict_ = yaml.safe_load(f)
            dict_[key] = new_value
            f.truncate(0)
            f.seek(0)
            yaml.safe_dump(dict_, f, indent=4, sort_keys=False)
```


<br>

次に __test_toggle.html__ は以下のように作成しました。


```test_toggle.html
<!doctype html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="/static/css/bootstrap.min.css">
    <title>toggle_test</title>
  </head>
  <body>
    <nav class="navbar navbar-expand-lg navbar-light p-1" style="background-color: #e3f2fd;">
        <div>
          <ul>
            <form class="form-inline" method="post" action="/">
            <div style="display: inline-block" class="custom-control custom-switch">
              {% if notify=="on" %}
              <input type="checkbox" checked name="notification" class="custom-control-input" id="customSwitch1" data-toggle="modal" data-target="#confirm-change2">
              {% else %}
              <input type="checkbox" name="notification" class="custom-control-input" id="customSwitch1" data-toggle="modal" data-target="#confirm-change2">
              {% endif %}
              <label class="custom-control-label" for="customSwitch1">通知　</label>
            </div>
            <div style="display: inline-block" class="custom-control custom-switch">
            </div>
            <div class="modal fade" id="confirm-change2" data-backdrop="static" data-keyboard="false" tabindex="-1" role="dialog">
            <div class="modal-dialog">
              <div class="modal-content">
                <div class="modal-header">
                  <h5 class="modal-title">確認</h5>
                  <span aria-hidden="true">&times;</span>
                  </button>
                </div>
                <div class="modal-body">
                  {% if notify=="on" %}
		  通知をOFFにしてもよろしいですか？
                  {% else %}
		  通知をONにしてもよろしいですか？
                  {% endif %}
                </div>
                <div class="modal-footer">
                  {% if notify=="on" %}
                  <button id="dataConfirmCancel_on" type="button" class="btn btn-secondary" data-dismiss="modal">キャンセル</button>
		          {% else %}
                  <button id="dataConfirmCancel_off" type="button" class="btn btn-secondary" data-dismiss="modal">キャンセル</button>
                  {% endif %}
          <button type="submit" class="btn btn-success" name="confirm" value="{{ notify }}">OK</button>
                </div>
              </div>
            </div>
          </div>
          </form>
          </ul>
        </div>
      </div>
    </nav>
    <script src="/static/js/jquery-3.5.1.min.js"></script>
    <script src="/static/js/bootstrap.bundle.min.js"></script>
  </body>
  <script>
  $('#dataConfirmCancel_on').click(function() {
      $('input[name=notification]').prop('checked', true);
  });
  </script>
  <script>
  $('#dataConfirmCancel_off').click(function() {
      $('input[name=notification]').prop('checked', false);
  });
  </script>
</html>
```
最初の __form__ タグの中でボタンの切り替えの処理が記述されてます。
トグルボタンは __input__ タグの __checkbox__ にて描画されます。その際 __app.py__ の中の __home__ 関数にてページを表示する際に __notify__ という引数を設け、 __read_notify__ 関数で読み取った現時点でのconfigの値を常にhtmlに渡しています。
 __test_toggle.html__ の中ではその __notify__ 引数の値を場合分けしてONの状態のトグルボタンかOFFの状態のトグルボタンかのどちらを描画するかが場合分けされています。ONかOFFかはinputタグの中で __checked__ を入れるか入れないかの違いとなっています。

```test_toggle.html（抜粋）
{% if notify=="on" %}
  <input type="checkbox" checked name="notification" class="custom-control-input" id="customSwitch1" data-toggle="modal" data-target="#confirm-change2">
{% else %}
  <input type="checkbox" name="notification" class="custom-control-input" id="customSwitch1" data-toggle="modal" data-target="#confirm-change2">
{% endif %}
```

またここで __input__ タグの中の __data-target__ に __#confirm-change2__ を記入し、以降の確認ダイアログ処理とトグルボタンの処理を連結します。これによりトグルボタンが押下されたら確認ダイアログの処理に移るようになります。
確認ダイアログはそれぞれ __div__ タグの __modal-dialog__ クラス、 __modal-content__ クラス、 __modal-header__ クラス、 __modal-body__ クラスで挟むようにして記述します。今回は既に通知ボタンがONだったらOFFにして良いかの確認、既に通知ボタンがOFFだったらONにして良いかの確認を行う都合上、同じように __notify__ の値で場合分けを行なって表示される文章を変更します。

```test_toggle.html（抜粋）
<div class="modal-body">
  {% if notify=="on" %}
通知をOFFにしてもよろしいですか？
  {% else %}
通知をONにしてもよろしいですか？
  {% endif %}
</div>
```

トグルボタンによってconfigの値の変更が送信されてるように見えてますが、実際は以下で作成した確認ダイアログのOKボタンを押すことで変更がバックサイドにsubmitされconfigの値の更新処理に移ることができます。

```test_toggle.html（抜粋）
<button type="submit" class="btn btn-success" name="confirm" value="{{ notify }}">OK</button>
```
ここでsubmitする際にボタンの __name__ を指定することで __app.py__ 側では __post()__ 内で __request.form.get()__ を行うことで、submitされたかどうかを常に確認することができます。今回は2値なので今のconfigの値に関わらず、確認ダイアログのOKボタンが押下された時点で今のconfigの値を逆に更新することで期待通りの挙動を実現することができます

<br>
最後にtest_toggle.html最後の以下部分について解説します。


```test_toggle.html（抜粋）
  <script>
  $('#dataConfirmCancel_on').click(function() {
      $('input[name=notification]').prop('checked', true);
  });
  </script>
  <script>
  $('#dataConfirmCancel_off').click(function() {
      $('input[name=notification]').prop('checked', false);
  });
  </script>
```

上記までで内部のconfigと連動したトグルボタンと確認ダイアログは実装することができましたが、1つ問題点としてこのままだと例えば通知OFFの状態からトグルボタンを押して確認ダイアログを表示した後にOKでなくキャンセルボタンを押した場合、config自体はoffからoffのままで変更ありませんが、web上のトグルボタンは押下された状態で見た目がONになったままとなってしまいます。これは確認ダイアログの画面にてキャンセルをして処理を完了した際にwebのページ更新処理が行われないためです。確認ダイアログ画面のキャンセルを押下した後に逐一ブラウザのリフレッシュボタンを押すなどすればトグルボタンの見た目が反映されますが、それだと煩雑になってしまうので今回は以下の __script__ タグを入れました。
これは確認ダイアログ画面のキャンセルボタンにあらかじめ __id__ を割り振っておき、キャンセルボタンが押下された際に指定の __function__ が実行されるようにしたものです。前述した通りトグルボタンのONOFFの見た目は __input__ タグの中の __checked__ のステータスに依存します。
そこで元々通知ONの状態から確認ダイアログを押してキャンセルした場合に指定の __input__ タグの __checked__ ステータスをtrueに戻すようにすることでキャンセル時に見た目もONの状態を維持するようにしました。元々通知OFFの状態も同様に記載することで対応しました。

# 結果

以下の状態からトグルボタンを押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2709441/3f47c458-44af-dd23-1f85-1fa9ae535fca.png)
```config.yaml
notification: 'off'
```

<br>

確認ダイアログのOKを押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2709441/bf55937a-75ef-34af-0eb9-d12f3d8a7e16.png)

トグルボタンがONに切り替わりconfigの内容も更新されていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2709441/47ad19ef-ceca-5394-9177-14a6bfd249f5.png)

```config.yaml
notification: 'on'
```
# 参考文献
[input要素でON/OFFのスイッチを作成してみよう](https://reffect.co.jp/html/toggle-switch)
[Webアプリ初心者のFlaskチュートリアル](https://qiita.com/kiyokiyo_kzsby/items/0184973e9de0ea9011ed)