【Python特化】おすすめのオンラインプログラミングスクール

【Flask】Webアプリ開発におけるTemplateの役割

flask-mvt-template
本記事の要点
  • FlaskにおけるTemplateの役割を理解する
  • Viewとの連携を理解する
  • 具体的にTemplateへ記述するコードを理解する

上記をまとめ、具体的なコードやTemplateの基本知識が習得できます。

目次

FlaskにおけるTemplateの役割とは

Flaskは、UIを持つアプリを実装するデザインパターンとしてMVTモデル(Model/View/Template)を採用してます。

Model/View/Templateは以下の役割を持ちます。

MVTの役割
  • Model:ロジックを担当
  • View:入力を受け取りModelとTemplateを制御
  • Template:入出力を担当

一般的に、MVCモデル(Model/View/Controller)が有名ですが、MVTのViewはMVCのControllerに相当し、MVTのTemplateはMVCのViewに相当します。

具体的なルーティングとして図解を記載しておきます。

flask-mvt-template
flask-mvt-template

また、FlaskにおいてTemplateは以下の役割を持ちます。

Templateの具体的な役割
  • ViewからTemplateへのデータの受け渡し
  • ルーティング情報に沿ったテンプレート作成(各ページ)
  • <a>タグ, <form>タグ, <button>タグが持つルートデザイン設計
  • CSSによるUI/UXを意識したデザイン調整

つまり、Templateはユーザー行動を意識したデザインを考慮することになります。

Templateを実装する際に必要な知識が以下の内容になります。

Templateの具体的な役割
  • ルーティングの理解
  • テンプレートエンジン(jinja2)の理解
  • 共通テンプレートと継承
  • データ出力のロジック設計

本記事は、規模の小さいToDoアプリを想定しディレクトリ構成しています。

FlaskにおけるModelの役割

FlaskにおいてModelは以下の役割を持ちます。

Modelの具体的な役割
  • データベースを作成する
  • データベースを操作する

つまり、Modelはデータベースのテーブルを定義することになります。

データベースを構築する際に必要な知識が以下の内容になります。

Modelに必要な基本知識
  • flask-sqlalchemy
  • flask-migrate
  • SQLite

FlaskにおけるModelの役割について詳細に知りたい人は、「【Flask】Webアプリ開発におけるModelの役割」を一読ください。

FlaskにおけるViewの役割

FlaskにおいてViewは以下の役割を持ちます。

Viewの具体的な役割
  • データベースを操作する
  • PRGに基づいてルーティングとテンプレートを制御する

つまり、ViewはModelとTemplateを制御することになります。

Viewを実装する際に必要な知識が以下の内容になります。

Viewに必要な基本知識
  • CRUD機能の理解
  • ルーティングの理解
  • PRG(Post/Redirect/Get)パターン
  • render_template
  • redirect
  • url_for
  • request

Viewの作成方法やデータベース/テンプレート制御を具体的に知りたい人は、「【Flask】Webアプリ開発におけるViewの役割」を一読ください。

ルーティングとは

ルーティングとは、リクエスト先のURIと実際に処理する関数を紐づけることを指します。

Flaskでは、関数の先頭にデコレータと呼ばれる関数@app.route()を追加することでルートを追加できます。

Flaskにてルーティングを利用する場合、以下の基本的な使い方を理解しましょう。

Flaskにおけるルーティングの使い方
  • ルーティングを設定する
  • flask routesコマンドでルーティング情報を確認する
  • FlaskのEndpoint(エンドポイント)を設定する
  • Ruleに変数を設定する

そのため、Template側はルーティング情報に適切なHTMLファイルを作成する必要があります。

サンプルをもとに詳しくルーティングを知りたい人は、「【Python】Flaskによるルーティングの利用方法とテンプレート活用」を一読ください。

テンプレートエンジン(jinja2)とは

テンプレートエンジンは、テンプレートと呼ばれるひな形とデータを合成してファイルを出力します。

Flaskのデフォルトテンプレートエンジンは、jinja2になります。

Flaskをインストールすると、同時にjinja2もインストールされます。

以下はテンプレートエンジンであるjinja2のイメージです。

共通テンプレートと継承とは

テンプレートエンジンであるjinja2は、共通テンプレートファイルを継承させる機能があります。

共通テンプレートを作成し継承することで、HTMLファイルに重複したHTMLを記述する必要が無くなります。

本記事で例にするToDoアプリの各ページは以下になります。

テンプレートディレクトリにあるHTMLファイル群
  • base.html
  • index.html
  • todo_create.html
  • todo_edit.html
  • todo_done.html
  • todo_delete.html

ここでは、base.htmlとindex.htmlを比較してみます。

base.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ToDoApp</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
    <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}" />
    <link rel="stylesheet" href="{{ url_for('todo.static', filename='css/style.css') }}" />
  </head>
  <body>
    <nav class="navbar navbar-dark bg-dark">
      <div class="container-fluid">
        <button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasDarkNavbar" aria-controls="offcanvasDarkNavbar" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>
        <a class="navbar-brand" href="/todo">ToDoApp</a>
        <div class="offcanvas offcanvas-start text-bg-dark" tabindex="-1" id="offcanvasDarkNavbar" aria-labelledby="offcanvasDarkNavbarLabel">
          <div class="offcanvas-header">
            <h5 class="offcanvas-title" id="offcanvasDarkNavbarLabel">ToDoApp</h5>
            <button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
          </div>
          <div class="offcanvas-body">
            <ul class="navbar-nav justify-content-end flex-grow-1 pe-3">
              <li class="nav-item">
                <a class="nav-link d-flex align-items-center" href="/todo">
                  <span class="material-symbols-outlined pe-2">check_box_outline_blank</span>
                  現在のタスク
                </a>
              </li>
              <li class="nav-item">
                <a class="nav-link d-flex align-items-center" href="/todo/done">
                  <span class="material-symbols-outlined pe-2">check_box</span>
                  完了したタスク
                </a>
              </li>
              <li class="nav-item">
                <a class="nav-link d-flex align-items-center" href="/todo/delete">
                  <span class="material-symbols-outlined pe-2">delete</span>
                  ゴミ箱
                </a>
              </li>
            </ul>
          </div>
        </div>
      </div>
    </nav>
    {% block content %}{% endblock %}
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
  </body>
</html>
index.html
{% extends "todo/base.html" %}
{% block content %}
{% for task in tasks %}
{% if task.status == 0 and task.delete == 0 %}
<div class="card mx-2 my-2">
  <div class="card-header align-items-center">
    <h5 class="card-title">{{ task.title }}</h5>
  </div>
  <div class="card-body">
    <p class="card-text" style="white-space:pre-wrap;">{{ task.content }}</p>
    <hr>
    <div class="card-btn-group d-flex align-items-center justify-content-end">
      <a class="btn d-inline-flex align-items-center" href="{{ url_for('todo.edit_tasks', tasks_id=task.id) }}" style="color: #198754;">
        <span class="material-symbols-outlined">edit</span>
      </a>
      <form action="{{ url_for('todo.tasks', tasks_id=task.id) }}" method="POST" class="mx-1">
        <button type="submit" name="flag" value="0" class="btn d-inline-flex align-items-center" style="color: #0d6efd;">
          <span class="material-symbols-outlined">radio_button_unchecked</span>
        </button>
      </form>
      <form action="{{ url_for('todo.tasks', tasks_id=task.id) }}" method="POST" class="mx-1">
        <button type="submit" name="flag" value="1" class="btn d-inline-flex align-items-center">
          <span class="material-symbols-outlined">delete</span>
        </button>
      </form>
    </div>
  </div>
</div>
{% endif %}
{% endfor %}
<div class="fab-container">
  <a href="/todo/create" class="pushcircle">
    <span class="material-symbols-outlined">add</span>
  </a>       
</div>
{% endblock %}

共通テンプレートを作成する際、継承元であるbase.htmlに以下を挿入します。

{% block [block name] %}{% endblock %}

また、代表的なブロックは以下になります。

スクロールできます
ブロック名記述方法
タイトル{% block title %}{% endblock %}
ヘッダー{% block header %}{% endblock %}
ボディ{% block body %}{% endblock %}
フッター{% block footer %}{% endblock %}
スクリプト{% block script %}{% endblock %}
コンテンツ{% block content %}{% endblock %}
共通テンプレートで利用する代表的なブロック

共通テンプレートを利用したい継承先がある場合は、以下を挿入します。

{% extends "継承元のファイルパス" %}

jinja2におけるデータ出力方法

jinja2は、Python構文に似たコードを記述できます。

スクロールできます
記法説明
{% %}ブロックやPython構文を記述する
{{ }}オブジェクト/変数など値を持つものを記述
{# #}コメント
テンプレートで利用できる代表的な記法

MVTモデルを考慮したディレクトリ構成

本記事では、サンプルとしてFlaskによるMVTモデルに沿ったToDoアプリを想定します。

Flaskは自由度が高いため、様々なディレクトリ構成を組めますが一例として参考にしてください。

flask-project
├── .env
├── apps
│   ├── app.py
│   ├── static
│   │   └── bootstrap.min.css
│   └── todo
│       ├── __init__.py
│       ├── forms.py
│       ├── models.py
│       ├── static
│       │   └── style.css
│       ├── templates
│       │   └── todo
│       │       ├── base.html
│       │       ├── index.html
│       │       ├── todo_create.html
│       │       ├── todo_edit.html
│       │       ├── todo_done.html
│       │       └── todo_delete.html
│       └── views.py
├── local.sqlite
└── migrations

様々なディレクトリやファイルがありますが、.htmlを中心に解説します。

CRUD機能の実装における各テンプレート作成

本記事は、ToDoアプリを例に各テンプレートを作成していきます。

ToDoアプリのページ構成
  • ベースファイル(base.html)
  • TOPページ(index.html)
  • タスク作成ページ(todo_create.html)
  • タスク編集ページ(todo_edit.html)
  • タスク完了ページ(todo_done.html)
  • ゴミ箱ページ(todo_delete.html)
BootStrapについて

実践的なアプリを作成するにあたって、CSSフレームワークであるBootStrapを利用します。

以下のサイトからBootStrapをダウンロードし、staticディレクトリ直下にあるcssディレクトリに追加しています。

style.cssについて

BootStrap以外にも、右下のフローティングボタンを設置しているため、ToDoアプリのstaticに以下のCSSを格納しています。

.fab-container {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    align-items: center;
    user-select: none;
    position: fixed;
    bottom: 30px;
    right: 30px;
}

.pushcircle {
    /*周囲の線の起点とするためrelativeを指定*/
    position: relative;
    /*円の形状*/
    width:70px;
    height:70px;
    box-sizing: border-box;
    padding: 0 10px;
    text-align: center;
    background:#0d6efd;
    border-radius:50%;
    color: #fff;
    text-decoration: none;
    outline: none;
    /*天地中央にテキストを配置*/
    display: flex;
    align-items: center;
    justify-content: center;
}

.pushcircle:after {
	content: "";
    /*絶対配置で線の位置を決める*/
	position: absolute;
	top: 50%;
	left: 50%;
    /*線の形状*/
    width: 85%;
	height: 85%;
	border: 2px solid #0d6efd;
	border-radius: 50%;
	transform: translate(-50%, -50%) scale(1.1);
    /*アニメーションの指定*/
    transition: .3s ease; 
}

/*hoverをしたら枠線が小さくなる*/
.pushcircle:hover:after {
	transform: translate(-50%, -50%) scale(1);
    border-color:#fff;
}

.pushcircle-delete {
    /*周囲の線の起点とするためrelativeを指定*/
    position: relative;
    /*円の形状*/
    width:70px;
    height:70px;
    box-sizing: border-box;
    padding: 0 10px;
    text-align: center;
    background:#d63384;
    border-radius:50%;
    border: none;
    color: #fff;
    text-decoration: none;
    outline: none;
    /*天地中央にテキストを配置*/
    display: flex;
    align-items: center;
    justify-content: center;
}

.pushcircle-delete:after {
	content: "";
    /*絶対配置で線の位置を決める*/
	position: absolute;
	top: 50%;
	left: 50%;
    /*線の形状*/
    width: 85%;
	height: 85%;
	border: 2px solid #d63384;
	border-radius: 50%;
	transform: translate(-50%, -50%) scale(1.1);
    /*アニメーションの指定*/
    transition: .3s ease; 
}

.pushcircle-delete:hover:after {
	transform: translate(-50%, -50%) scale(1);
    border-color:#fff;
}

.pushcircle-disabled {
    /*周囲の線の起点とするためrelativeを指定*/
    position: relative;
    /*円の形状*/
    width:70px;
    height:70px;
    box-sizing: border-box;
    padding: 0 10px;
    text-align: center;
    background:#adb5bd;
    border-radius:50%;
    color: #fff;
    text-decoration: none;
    outline: none;
    /*天地中央にテキストを配置*/
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: .5;
}

.pushcircle-disabled .material-symbols-outlined {
    color: #495057;
    font-size: 35px;
}

.fab-container .pushcircle .material-symbols-outlined {
    color: white;
    font-size: 35px;
}

.form-create-btn {
    /*周囲の線の起点とするためrelativeを指定*/
    position: absolute;
    /*円の形状*/
    width: 70px;
    height:70px;
    border-radius:50%;
    border: none;
    background-color: transparent;
    font-size: 35px;
    z-index: 1;
}

.form-delete-btn {
    /*周囲の線の起点とするためrelativeを指定*/
    position: absolute;
    /*円の形状*/
    width: 70px;
    height:70px;
    border-radius:50%;
    border: none;
    background-color: transparent;
    font-size: 35px;
    z-index: 2;
    /*天地中央にテキストを配置*/
    display: flex;
    align-items: center;
    justify-content: center;
}

.form-delete-btn .material-symbols-outlined {
    color: white;
    font-size: 35px;
}

TOPページ(index.html)の作成

以下、Viewによる処理に基づいたテンプレート(TOPページ)の処理になります。

全ページ共通テンプレートのナビゲーションバーを設置したbase.htmlを継承し、index.htmlを作成しています。

ナビゲーションバーのトグルをクリックすると、以下のサイドバーが表示されます。

また、タスク作成後のタスクカード表示画面は以下になります。

base.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ToDoApp</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
    <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}" />
    <link rel="stylesheet" href="{{ url_for('todo.static', filename='css/style.css') }}" />
  </head>
  <body>
    <nav class="navbar navbar-dark bg-dark">
      <div class="container-fluid">
        <button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasDarkNavbar" aria-controls="offcanvasDarkNavbar" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>
        <a class="navbar-brand" href="/todo">ToDoApp</a>
        <div class="offcanvas offcanvas-start text-bg-dark" tabindex="-1" id="offcanvasDarkNavbar" aria-labelledby="offcanvasDarkNavbarLabel">
          <div class="offcanvas-header">
            <h5 class="offcanvas-title" id="offcanvasDarkNavbarLabel">ToDoApp</h5>
            <button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
          </div>
          <div class="offcanvas-body">
            <ul class="navbar-nav justify-content-end flex-grow-1 pe-3">
              <li class="nav-item">
                <a class="nav-link d-flex align-items-center" href="/todo">
                  <span class="material-symbols-outlined pe-2">check_box_outline_blank</span>
                  現在のタスク
                </a>
              </li>
              <li class="nav-item">
                <a class="nav-link d-flex align-items-center" href="/todo/done">
                  <span class="material-symbols-outlined pe-2">check_box</span>
                  完了したタスク
                </a>
              </li>
              <li class="nav-item">
                <a class="nav-link d-flex align-items-center" href="/todo/delete">
                  <span class="material-symbols-outlined pe-2">delete</span>
                  ゴミ箱
                </a>
              </li>
            </ul>
          </div>
        </div>
      </div>
    </nav>
    {% block content %}{% endblock %}
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
  </body>
</html>
index.html
{% extends "todo/base.html" %}
{% block content %}
{% for task in tasks %}
{% if task.status == 0 and task.delete == 0 %}
<div class="card mx-2 my-2">
  <div class="card-header align-items-center">
    <h5 class="card-title">{{ task.title }}</h5>
  </div>
  <div class="card-body">
    <p class="card-text" style="white-space:pre-wrap;">{{ task.content }}</p>
    <hr>
    <div class="card-btn-group d-flex align-items-center justify-content-end">
      <a class="btn d-inline-flex align-items-center" href="{{ url_for('todo.edit_tasks', tasks_id=task.id) }}" style="color: #198754;">
        <span class="material-symbols-outlined">edit</span>
      </a>
      <form action="{{ url_for('todo.tasks', tasks_id=task.id) }}" method="POST" class="mx-1">
        <button type="submit" name="flag" value="0" class="btn d-inline-flex align-items-center" style="color: #0d6efd;">
          <span class="material-symbols-outlined">radio_button_unchecked</span>
        </button>
      </form>
      <form action="{{ url_for('todo.tasks', tasks_id=task.id) }}" method="POST" class="mx-1">
        <button type="submit" name="flag" value="1" class="btn d-inline-flex align-items-center">
          <span class="material-symbols-outlined">delete</span>
        </button>
      </form>
    </div>
  </div>
</div>
{% endif %}
{% endfor %}
<div class="fab-container">
  <a href="/todo/create" class="pushcircle">
    <span class="material-symbols-outlined">add</span>
  </a>       
</div>
{% endblock %}
index.htmlの流れ
  1. 共通テンプレートbase.htmlからナビゲーションバー継承
  2. タスク作成ボタンの設置とルーティング
  3. タスクカードの表示処理
  4. 編集ボタンの設置とルーティング/データの受け渡し
  5. 未完了/完了ボタンの設置とルーティング/データの受け渡し
  6. 元に戻す/削除ボタンの設置とルーティング/データの受け渡し

Viewから渡されたデータをfor文のループ処理で表示します。

また、if文を利用することで指定した条件のデータのみ表示されるよう処理しています。

編集ボタンに関しては、url_forによってURIパラメータを取得できるようタスクidを返す処理を実装してます。

完了・削除ボタンは<form>タグを利用しflagとして値を持たせ、POST時にクエリパラメータとしてタスクidを返す処理を実装しています。

URIパラメータとクエリパラメータの渡し方における記述が同じであるため、注意してください。

タスク作成ページ(todo_create.html)の作成

以下、Viewによる処理に基づいたテンプレート(タスク作成ページ)の処理になります。

こちらも同様に、base.htmlを継承しています。

バリデーションチェックによって入力がない時、エラーが発生した場合は以下の画面が表示されます。

formに関する解説は割愛しています。

todo_create.html
{% extends "todo/base.html" %}
{% block content %}
<form action="{{ url_for('todo.create_tasks') }}" method="POST" novalidate="novalidate">
  {{ form.hidden_tag() }}
  <div class="card mx-2 my-2">
    <div class="card-header align-items-center">
      <h5 class="card-title">
        {{ form.title(class="form-control", placeholder="タスク名") }}
      </h5>
      {% for error in form.title.errors %}
      <span style="color: red;">{{ error }}</span>
      {% endfor %}
    </div>
    <div class="card-body">
      {{ form.content(class="form-control", placeholder="タスク内容", style="white-space:pre-wrap;") }}
      {% for error in form.content.errors %}
      <span style="color: red;">{{ error }}</span>
    {% endfor %}
    </div>
  </div>
  <div class="fab-container">
    <div class="pushcircle">
      {{ form.submit(class="form-create-btn material-symbols-outlined") }}
    </div>
  </div>
</form>
{% endblock %}
todo_create.htmlの流れ
  1. タイトルの入力とフォームによるバリデーションチェック
  2. タスク内容の入力フォームによるバリデーションチェック
  3. チェックボタン押下時のPOST処理

タイトル・タスク内容に不備(入力なしの場合)があれば、バリデーションチェックによってエラーが発生します。

また、チェックボタン押下時はPOST処理としてデータベースにタスクが作成され、TOPページに遷移します。

タスク編集ページ(todo_edit.html)の作成

以下、Viewによる処理に基づいたテンプレート(タスク編集ページ)の処理になります。

こちらも同様に、base.htmlを継承しています。

todo_edit.html
{% extends "todo/base.html" %}
{% block content %}
<form action="{{ url_for('todo.edit_tasks', tasks_id=tasks.id) }}" method="POST" novalidate="novalidate">
  {{ form.hidden_tag() }}
  <div class="card mx-2 my-2">
    <div class="card-header align-items-center">
      <h5 class="card-title">
        {{ form.title(class="form-control", placeholder="タスク名", value=tasks.title) }}
      </h5>
      {% for error in form.title.errors %}
      <span style="color: red;">{{ error }}</span>
      {% endfor %}
    </div>
    <div class="card-body">
      {% set f = form.content.process_data(tasks.content) %}
      {{ form.content(class="form-control", placeholder="タスク内容") }}
      {% for error in form.content.errors %}
      <span style="color: red;">{{ error }}</span>
    {% endfor %}
    </div>
  </div>
  <div class="fab-container">
    <div class="pushcircle">
      {{ form.submit(class="form-create-btn material-symbols-outlined") }}
    </div>
  </div>
</form>
{% endblock %}
todo_edit.htmlの流れ
  1. タイトルの入力とフォームによるバリデーションチェック
  2. タスク内容の入力フォームによるバリデーションチェック
  3. チェックボタン押下時のPOST処理

TOPページにてタスクカードの編集アイコンをクリックすると、タスク編集ページに遷移します。

タスク編集ページでは、すでに登録されているタイトルとタスク内容を表示させ編集可能になっています。

また、url_forによってURIパラメータをViewへ渡しています。

タスク完了ページ(todo_done.html)の作成

以下、Viewによる処理に基づいたテンプレート(タスク完了ページ)の処理になります。

こちらも同様に、base.htmlを継承しています。

todo_done.html
{% extends "todo/base.html" %}
{% block content %}
{% for task in tasks %}
{% if task.status == 1 and task.delete == 0 %}
<div class="card mx-2 my-2">
  <div class="card-header align-items-center">
    <h5 class="card-title">{{ task.title }}</h5>
  </div>
  <div class="card-body">
    <p class="card-text" style="white-space:pre-wrap;">{{ task.content }}</p>
    <hr>
    <div class="card-btn-group d-flex align-items-center justify-content-end">
      <a class="btn d-inline-flex align-items-center disabled" href="{{ url_for('todo.edit_tasks', tasks_id=task.id) }}" style="border: none; color: #198754;">
        <span class="material-symbols-outlined">edit</span>
      </a>
      <form action="{{ url_for('todo.done_tasks', tasks_id=task.id) }}" method="POST" class="mx-1">
        <button type="submit" name="flag" value="0" class="btn d-inline-flex align-items-center" style="color: #d63384;">
          <span class="material-symbols-outlined">radio_button_checked</span>
        </button>
      </form>
      <form action="{{ url_for('todo.done_tasks', tasks_id=task.id) }}" method="POST" class="mx-1">
        <button type="submit" name="flag" value="1" class="btn d-inline-flex align-items-center">
          <span class="material-symbols-outlined">delete</span>
        </button>
      </form>
    </div>
  </div>
</div>
{% endif %}
{% endfor %}
<div class="fab-container">
  <a tabindex="-1" class="pushcircle-disabled">
    <span class="material-symbols-outlined">add</span>
  </a>       
</div>
{% endblock %}
todo_done.htmlの流れ
  1. タスクカードの表示処理
  2. 編集ボタンの無効
  3. 未完了/完了ボタンの設置とルーティング/データの受け渡し
  4. 元に戻す/削除ボタンの設置とルーティング/データの受け渡し
  5. タスク作成ボタンの無効

完了したタスクページでは、完了ボタンを押下されたタスクのみを表示させる処理として、if文による条件分岐を記述しています。

また、現在のタスクページ(TOPページ)のみ編集ボタンを許可したいため、完了したタスクページの編集ボタンは無効にするためdisabledしています。

未完了/完了ボタンを押下すると、未完了に戻せるよう値を持たせています。

元に戻す/削除ボタンは、現在のタスクページ(TOPページ)と同様の処理になります。

タスク作成ボタンはTOPページのみ有効にしたかったため、ボタン(特に処理なし)のみ設置しています。

本来、ボタンを作る必要はありませんが、アプリ全体のUIを統一したいため、デザインの観点から設置しています。

ゴミ箱ページ(todo_delete.html)の作成

以下、Viewによる処理に基づいたテンプレート(ゴミ箱ページ)の処理になります。

こちらも同様に、base.htmlを継承しています。

ゴミ箱ボタンを押下すると、モーダルウィンドウにて確認画面を用意しています。

ゴミ箱ページにタスクが存在しない場合、ボタンをdisabledによって無効にしています。

todo_delete.html
{% extends "todo/base.html" %}
{% block content %}
{% if not tasks == [] %}
{% for task in tasks %}
{% if task.delete == 1 %}
<div class="card mx-2 my-2">
  <div class="card-header align-items-center">
    <h5 class="card-title">{{ task.title }}</h5>
  </div>
  <div class="card-body">
    <p class="card-text" style="white-space:pre-wrap;">{{ task.content }}</p>
    <hr>
    <div class="card-btn-group d-flex align-items-center justify-content-end">
      <a class="btn d-inline-flex align-items-center disabled" href="{{ url_for('todo.edit_tasks', tasks_id=task.id) }}" style="border: none; color: #198754;">
        <span class="material-symbols-outlined">edit</span>
      </a>
      {% if task.status == 0 %}
      <button type="submit" class="btn d-inline-flex align-items-center disabled" style="border: none; color: #0d6efd;">
        <span class="material-symbols-outlined">radio_button_unchecked</span>
      </button>
      {% elif task.status == 1 %}
        <button type="submit" class="btn d-inline-flex align-items-center disabled" style="border: none; color: #d63384;">
          <span class="material-symbols-outlined">radio_button_checked</span>
        </button>
        {% endif %}
      <form action="{{ url_for('todo.delete_tasks', tasks_id=task.id) }}" method="POST" class="mx-1">
        <button type="submit" name="flag" value="0" class="btn d-inline-flex align-items-center" style="border: none;">
          <span class="material-symbols-outlined">undo</span>
        </button>
      </form>
    </div>
  </div>
</div>
{% endif %}
{% endfor %}
<!-- Button trigger modal -->
<div class="fab-container">
  <div class="pushcircle-delete">
    <button type="button"class="form-delete-btn" data-bs-toggle="modal" data-bs-target="#deleteModal">
      <span class="material-symbols-outlined">delete</span>
    </button>
  </div>
</div>
<!-- Modal -->
<div class="modal fade" data-bs-backdrop="false" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="false">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="deleteModalLabel">完全に削除</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        ゴミ箱を完全に空にしますか?<br>
        削除の操作は取り消しできません。
      </div>
      <div class="modal-footer">
        <button type="button" class="btn" style="color: #495057;"data-bs-dismiss="modal">キャンセル</button>
        <form action="{{ url_for('todo.delete_tasks') }}" method="POST">
          <button type="submit" name="flag" value="1" class="btn" style="color: #d63384;"><span>削除</span></button>
        </form>  
      </div>
    </div>
  </div>
</div>
{% else %}
<div class="fab-container">
  <div tabindex="-1" class="pushcircle-disabled">
    <span class="material-symbols-outlined">delete</span>
  </div>
</div>
{% endif %}
<script>
    const deleteModal = document.getElementById('deleteModal')
    const deleteInput = document.getElementById('myInput')
  
    deleteModal.addEventListener('shown.bs.modal', () => {
      deleteInput.focus()
    })
</script>
{% endblock %}
todo_delete.htmlの流れ
  1. タスクカードの表示処理
  2. 編集ボタンの無効
  3. 未完了/完了ボタンの無効
  4. 元に戻す/削除ボタンの設置とルーティング/データの受け渡し
  5. ゴミ箱ボタンによる確認処理
  6. モーダルウィンドウによる削除処理
  7. ゴミ箱ボタンの無効(タスクデータなし)

タスクカードのゴミ箱アイコンを押下すると、deleteカラムの値が更新されゴミ箱ページに表示されます。

編集ボタン・未完了/完了ボタンはdisabledによって無効化しています。

元に戻すボタンを押下すると、ゴミ箱ページから消えるよう処理を追加しています。

ゴミ箱ボタンを押下すると、いきなり削除されず確認用モーダルウィンドウを表示する処理を加えています。

モーダルウィンドウで削除ボタンを押下すると、url_forにてクエリパラメータであるタスクidとflagの値をPOSTします。

タスクデータが存在しない場合は、if文の条件分岐でdisabledによる無効化を表示するよう処理しています。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

sugiのアバター sugi SUGI

【経歴】玉川大学工学部卒業→新卒SIer企業入社→2年半後に独立→プログラミングスクール運営/受託案件→フリーランスエンジニア&SEOコンサル→Python特化のコンテンツサイトJob Code運営中

コメント

コメントする

目次