良くあるアレ
以下のようにformBuilder
にaddするだけで勝手に↑のようになってくれるFormTypeを作ります。
$formBuilder()->add('password', ShowHidePasswordType::class);
作り方
プロジェクト作成
# sensio/framework-extra-bundleはsymfony最新版に対応していないのに何故かデフォルトでインストールされるので削除
$ symfony new show_hide_password --full
$ composer remove sensio/framework-extra-bundle
ベースのフォーム画面作成
以下のコマンドでControllerを作成します。
$ bin/console make:controller
Choose a name for your controller class (e.g. TinyGnomeController):
> home
created: src/Controller/HomeController.php
created: templates/home/index.html.twig
Controllerとtemplateを以下のように書き換えてフォームを作成します。
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Routing\Annotation\Route;
class HomeController extends AbstractController
{
/**
* @Route("/home", name="home")
*/
public function index()
{
$form = $this->createFormBuilder()
->add('username', TextType::class)
->add('password', PasswordType::class)
->add('submit', SubmitType::class)
->getForm()
;
return $this->render('/home/index.html.twig', [
'form' => $form->createView()
]);
}
}
{% extends 'base.html.twig' %}
{% block title %}Hello HomeController!{% endblock %}
{% block body %}
<div class="container mt-5">
<div class="row">
<div class="col-8 offset-2">
<div class="card">
<div class="card-body">
{{ form_start(form) }}
{{ form_rest(form) }}
{{ form_end(form) }}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
また、bootstrapのフォームテーマを使用するので、有効化の設定 & templateにCDNのリンクを追加します。
twig:
default_path: '%kernel.project_dir%/templates'
form_theme: ['bootstrap_4_layout.html.twig'] # 追加
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}
// ここから
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
// ここまで
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}
// ここから
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
// ここまで
{% endblock %}
</body>
</html>
以下のコマンドでサーバーを起動し、https://127.0.0.1:8000 へアクセスすると画像のようなページが表示されます。
$ symfony server:start -d
[OK] Web server listening
The Web server is using PHP FPM 7.4.9
https://127.0.0.1:8000
例のアレを作る
まず、FormTypeを作成します。名前は適当です(笑)。こいつはPasswordTypeの子Typeにします。
ControllerではPasswordTypeの代わりに、このShowHidePasswordTypeを使用するようにします。
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
class ShowHidePasswordType extends AbstractType
{
public function getParent()
{
return PasswordType::class;
}
}
...
use App\Form\ShowHidePasswordType; // 追加
...
$form = $this->createFormBuilder()
->add('username', TextType::class)
//->add('password', PasswordType::class)
->add('password', ShowHidePasswordType::class) // 追加
->add('submit', SubmitType::class)
->getForm()
;
...
次に、カスタムフォームテーマを作成し、このテーマを使用するように設定を変更します。bootstrap_4_layout.html.twig
を継承し、ShowHidePasswordTypeの表示方法だけ上書きします。
{% extends 'bootstrap_4_layout.html.twig' %}
twig:
default_path: '%kernel.project_dir%/templates'
# form_theme: ['bootstrap_4_layout.html.twig']
form_theme: ['form_theme.html.twig'] # 追加
Symfonyのフォームテーマでは、デフォルトでは{% block form_row %} ... {% endblock %}
ブロックがレンダリングに用いられますが、ShowHidePasswordType
なら{% block show_hide_password %} ... {% endblock %}
というブロックが定義されていれば、そちらが優先的にレンダリングに用いられます。命名規則などについての詳しい解説はドキュメント1を参照ください。
bootstrap_4_layout.html.twig
の{% block form_row %} ... {% endblock %}
は以下の通りです。この中の{{- form_widget(form, widget_attr) -}}
という箇所が実際にinput要素をレンダリングしている部分です。なので、ここをラップし、cssやJavaScriptでゴニョゴニョすることで例のアレの表示を試みます。
```twig:bootstrap_4_layout.html.twig`
{% block form_row -%}
{%- if compound is defined and compound -%}
{%- set element = 'fieldset' -%}
{%- endif -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
<{{ element|default('div') }}{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group')|trim})} %}{{ block('attributes') }}{% endwith %}>
{{- form_label(form) -}}
{{- form_widget(form, widget_attr) -}} # ココ
{{- form_help(form) -}}
{{ element|default('div') }}>
{%- endblock form_row %}
最終的に完成したのがこちら。input要素と目のアイコンをdivで包み、`position:absolute`でいい感じに表示しているだけですので、大した事はしていません。目のアイコンがクリックされた時にJavaScriptでinputのtypeを変更しています。
目のアイコンはfontawesomeのリソースを利用しているので、`base.html.twig`にCDNを追加してください。ブラウザを再読み込みすれば、冒頭のような良くあるアレが表示されます。
```twig:templates/form_theme.html.twig
{% extends 'bootstrap_4_layout.html.twig' %}
{% block show_hide_password_row %}
<style>
.showHiddenPassword-wrapper {
position: relative;
}
.showHiddenPassword-wrapper .is-invalid {
background-image: none!important;
background-size: 0!important;
}
.showHiddenPassword-toggle {
position: absolute;
top: 50%;
right: 1.5em;
transform: translateY(-50%);
}
</style>
<script>
function __togglePassword__{{ form.vars.id }}() {
const _passwordField = document.querySelector('#{{ form.vars.id }}');
const _showHideToggle = document.querySelector('#showHideToggle-{{ form.vars.id }}');
if (_showHideToggle.classList.contains('fa-eye-slash')) {
_showHideToggle.classList.remove('fa-eye-slash')
_showHideToggle.classList.add('fa-eye')
_passwordField.type = 'text'
} else {
_showHideToggle.classList.remove('fa-eye')
_showHideToggle.classList.add('fa-eye-slash')
_passwordField.type = 'password'
}
}
</script>
{%- if compound is defined and compound -%}
{%- set element = 'fieldset' -%}
{%- endif -%}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help", 'class': 'showHiddenPassword-widget'}} -%} {# class 追加 #}
{%- endif -%}
<{{ element|default('div') }}{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group')|trim})} %}{{ block('attributes') }}{% endwith %}>
{{- form_label(form) -}}
{# ここから #}
<div class='showHiddenPassword-wrapper'>
{{- form_widget(form, widget_attr) -}}
<span class='showHiddenPassword-toggle'
onclick='__togglePassword__{{ form.vars.id }}()'
>
<i id='showHideToggle-{{ form.vars.id }}' class="fa fa-eye-slash"></i>
</span>
</div>
{# ここまで #}
{{- form_help(form) -}}
</{{ element|default('div') }}>
{% endblock %}
...
{% block stylesheets %}
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
// ここから
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css" rel="stylesheet" crossorigin="anonymous">
// ここまで
{% endblock %}
...
当たり前ですが、CSSやJavaScriptは別ファイルに定義した方がいいです。が、その場合そのファイルを読み込む手間がかかりますので、FormTypeだけで完結したい場合は敢えてこのようにするのも個人的にはアリなのかなと思います。(そこんとこどうなんでしょうか)
今回のソースコード