🥥

Symfony/formのChoiceTypeをJavaScriptで動的に選択不可にする方法

2023/04/10に公開

対象者

  • セレクトボックスの選択肢の状態を動的に変化させたい人

この記事でやること

選択肢1つがランダムでdisabledになるセレクトボックスを作ります。

動作環境・前提

バージョン
MacOS 12.6.3
Symfony 4.x

本題

次の手順で実装していきます。

  1. Controllerを作成する
  2. twigファイルを編集する
  3. disabledにする選択肢をランダムに指定する
  4. JavaScriptで<option>を動的にdisabledにする

1. Controllerを作成する

次のコマンドでControllerを作成します。
コマンドを実行すると「Controllerの名前を決めてね」と言われるので、適当な名前を入力してください。以下の例ではIndexControllerとしています。

$ php bin/console make:controller

Choose a name for your controller class (e.g. AgreeableGnomeController):
 > IndexController

成功したら、Controllerとtwigファイルが作成されます。

作成されたController(パスはsrc/Controller/IndexController.php)を編集して、フォームの実装を行います。
ここでは、シンプルに3つの選択肢を持つセレクトボックスを実装します。プレースホルダーなどはお好みで追加してください。

/**
 * @Route("/index", name="index")
 */
public function index(): Response
{
    // フォームの実装
    // 3つの選択肢を持つセレクトボックス
    $form = $this->createFormBuilder()
        ->add('select_box', ChoiceType::class, [
            'choices' => [
                'select A' => 1,
                'select B' => 2,
                'select C' => 3,
            ],
        ])
        ->getForm();

    return $this->render('index/index.html.twig', [
        'form' => $form->createView(),
    ]);
}

2. twigファイルを編集する

1で作成されたtwig(パスはtemplates/index/index.html.twig)ファイルを編集して、フォームを表示できるようにします。
bodyブロックの中にmainブロックを作成し、フォームを表示するコードを追加していきます。

{% block body %}
    {% block main %}
        {# フォームを表示するコードを追加 #}
        {{ form_start(form) }}
        {{ form_widget(form) }}
        {{ form_end(form) }}
    {% endblock %}
{% endblock %}

これで、フォームを表示できるようになりました。
http://localhost:8001/indexなどにアクセスし、画面を確認します。以下のようにセレクトボックスが表示されればOKです。

3. disabledにする選択肢をランダムに指定する

random_int()を使ってdisabledにする選択肢をランダムに指定するようにします。
Controllerでランダムな数字を生成して、twigファイルに渡します。returnで返す要素に次を加えてください。

return $this->render('index/index.html.twig', [
    'form' => $form->createView(),
    // 以下を追加
    'disabled_select' => random_int(0, 2),
]);

今回は3つの選択肢を持つセレクトボックスを実装しているので、0~2の間でランダムな数字を生成しています。
あとは、この数字を使って選択肢をdisabledにするコードをJavaScriptで実装するだけです。

4. JavaScriptで<option>を動的にdisabledにする

手順2で編集したtwigファイルに、JavaScriptのコードを追加していきます。
bodyブロックの中にjavascriptブロックを作成し、以下のコードを追加してください。

{% block body %}
    {% block javascript %}
        <script>
            window.addEventListener('load', function() {
                const $disabledSelectNum = '{{disabled_select}}';
                $selectBox = document.getElementById('form_select_box');
                $options = $selectBox.options;
                $options[$disabledSelectNum].disabled = true;
            }, false);
        </script>
    {% endblock javascript %}

    {% block main %}
    {# 以下略... #}

<script>タグの中で何をしているか説明します。

まず初めに、addEventListener()メソッドが出てきます。これは、ターゲットに対して第一引数で指定した何らかのイベントが配信される度に呼び出され、第二引数で指定した関数を実行するメソッドです。
今回はターゲットがwindow、第一引数が'load'ですので、画面がロードされる度に第二引数で定義した関数が実行されることになりますね。そして、この第二引数で定義しているのが今回のテーマである「選択肢を動的に選択不可にする」部分です。
以下は、addEventListener()メソッドの第二引数だけを取り出したものです。

function() {
    const $disabledSelectNum = '{{disabled_select}}';
    $selectBox = document.getElementById('form_select_box');
    $options = $selectBox.options;
    $options[$disabledSelectNum].disabled = true;
}

2行目では、IndexControllerindex()メソッドから返したrandom_int(0, 2)の値をJSの変数$disabledSelectNumに格納しています。この$disabledSelectNumの値を使って、選択不可にする選択肢を指定します。
3行目では、getElementById()メソッドで取得したドキュメント要素を変数$selectBoxに格納しています。ここでは、セレクトボックス(<select>)のidを指定する必要がありますので、デベロッパーツールなどでidを確認して、適切なものを指定してください。
4行目は、3行目で取得したセレクトボックスの選択肢(options)を取り出して、変数$optionsに格納するコードです。$optionsは配列になっており、配列の要素として<option>のドキュメント要素が1つずつ格納されています。
最後に5行目で上記までのコードで取得した<option>のドキュメント要素を使って、選択肢を非表示にします。$disabledSelectNum$optionsの配列要素の1つを指定し、disabledをtrueにすることで、<option>の1つを非表示にすることができます。

これでコードは完成です。
http://localhost:8001/indexなどにアクセスし、画面を確認してみてください。選択肢のうち1つがdisabledになっているはずです。
また、画面をリロードするとdisabledになる選択肢がランダムに切り替わると思います。

まとめ

以下は、ここまで説明してきたコードの全容をです。
手順通りに作ると、大体こんな感じのコードになっているはずです。

IndexController.php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

class IndexController extends AbstractController
{
    /**
     * @Route("/index", name="index")
     */
    public function index(): Response
    {
        $form = $this->createFormBuilder()
            ->add('select_box', ChoiceType::class, [
                'choices' => [
                    'select A' => 1,
                    'select B' => 2,
                    'select C' => 3,
                ],
            ])
            ->getForm();

        return $this->render('index/index.html.twig', [
            'form' => $form->createView(),
            'disabled_select' => random_int(0, 2),
        ]);
    }
}
index.html.twig
{% extends 'base.html.twig' %}

{% block title %}Hello Issue2Controller!{% endblock %}

{% block body %}
    {% block javascript %}
        <script>
            window.addEventListener('load', function() {
                const $disabledSelectNum = '{{disabled_select}}';
                $selectBox = document.getElementById('form_select_box');
                $options = $selectBox.options;
                $options[$disabledSelectNum].disabled = true;
            }, false);
        </script>
    {% endblock javascript %}

    {% block main %}
        {{ form_start(form) }}
        {{ form_widget(form) }}
        {{ form_end(form) }}
    {% endblock %}
{% endblock %}

実用例

たとえば、

  • 予約フォームを作る際、既に予約がいっぱいになっている時間は選択できないようにする
  • 画面を見ているユーザのロールに応じて選べる要素が変わる

などが挙げられると思います。
(どちらかサンプルを作ってみて、Githubにでも上げようと思ってます。)

最後に

ユーザのロールに応じて選択肢の1つを選択不可にするコードを書こうとした時に、選択肢1つだけをdisabledにする方法が分からずとても苦戦していました。どうやってもセレクトボックス全体にdisabledがかかるか、どこにもdisabledがかからないかのどちらかにしかならなかったんです。
悩んでいた当時は、SymfonyのaddEventListener()とかでできるんじゃないかと格闘していましたが、結局できずにJSでやる方法に落ち着きました。
公式ドキュメントのFormEvents::PRE_SET_DATAの説明には

イベントはメソッドFormEvents::PRE_SET_DATAの開始時に送出されます Form::setData()。次の用途に使用できます。

・事前入力中に指定されたデータを変更します。
・事前入力されたデータに応じてフォームを変更します (フィールドを動的に追加または削除します)。

とあったので、Symfonyの機能だけでやる方法もありそうと思ったんですが。。。

いつかまた今回のような機能が必要になったら、この辺りをもう少し調べてみたいですね。
もし、Symfonyの機能だけでやる方法をご存知の方がここまで読んでくださっていたなら、是非コメントでご教示いただけると嬉しいです。

参考にさせてもらった記事の紹介

https://zenn.dev/ttskch/articles/54867c7ba79e92

カラビナテクノロジー デベロッパーブログ

Discussion