|
1 | 1 | # Symfony UX TogglePassword
|
2 | 2 |
|
| 3 | +> [!WARNING] |
| 4 | +> **Deprecated**: This package has been **deprecated** in 2.x and will be removed in the next major version. |
| 5 | +
|
| 6 | +To keep the same functionality in your Symfony application, follow these migration steps: |
| 7 | + |
| 8 | +1. Remove the `symfony/ux-toggle-password` package from your project: |
| 9 | +```bash |
| 10 | +composer remove symfony/ux-toggle-password |
| 11 | +``` |
| 12 | + |
| 13 | +2. Create the following files in your project: |
| 14 | + |
| 15 | +> [!NOTE] |
| 16 | +> These files are provided as a reference. |
| 17 | +> You can customize them to fit your needs, and even simplify the implementation if you don't need all the features. |
| 18 | +
|
| 19 | + - `src/Form/Extension/TogglePasswordTypeExtension.php` |
| 20 | +```php |
| 21 | +<?php |
| 22 | + |
| 23 | +namespace App\Form\Extension; |
| 24 | + |
| 25 | +use Symfony\Component\Form\AbstractTypeExtension; |
| 26 | +use Symfony\Component\Form\Extension\Core\Type\PasswordType; |
| 27 | +use Symfony\Component\Form\FormInterface; |
| 28 | +use Symfony\Component\Form\FormView; |
| 29 | +use Symfony\Component\OptionsResolver\Options; |
| 30 | +use Symfony\Component\OptionsResolver\OptionsResolver; |
| 31 | +use Symfony\Component\Translation\TranslatableMessage; |
| 32 | +use Symfony\Contracts\Translation\TranslatorInterface; |
| 33 | + |
| 34 | +final class TogglePasswordTypeExtension extends AbstractTypeExtension |
| 35 | +{ |
| 36 | + public function __construct(private readonly ?TranslatorInterface $translator) |
| 37 | + { |
| 38 | + } |
| 39 | + |
| 40 | + public static function getExtendedTypes(): iterable |
| 41 | + { |
| 42 | + return [PasswordType::class]; |
| 43 | + } |
| 44 | + |
| 45 | + public function configureOptions(OptionsResolver $resolver): void |
| 46 | + { |
| 47 | + $resolver->setDefaults([ |
| 48 | + 'toggle' => false, |
| 49 | + 'hidden_label' => 'Hide', |
| 50 | + 'visible_label' => 'Show', |
| 51 | + 'hidden_icon' => 'Default', |
| 52 | + 'visible_icon' => 'Default', |
| 53 | + 'button_classes' => ['toggle-password-button'], |
| 54 | + 'toggle_container_classes' => ['toggle-password-container'], |
| 55 | + 'toggle_translation_domain' => null, |
| 56 | + 'use_toggle_form_theme' => true, |
| 57 | + ]); |
| 58 | + |
| 59 | + $resolver->setNormalizer( |
| 60 | + 'toggle_translation_domain', |
| 61 | + static fn (Options $options, $labelTranslationDomain) => $labelTranslationDomain ?? $options['translation_domain'], |
| 62 | + ); |
| 63 | + |
| 64 | + $resolver->setAllowedTypes('toggle', ['bool']); |
| 65 | + $resolver->setAllowedTypes('hidden_label', ['string', TranslatableMessage::class, 'null']); |
| 66 | + $resolver->setAllowedTypes('visible_label', ['string', TranslatableMessage::class, 'null']); |
| 67 | + $resolver->setAllowedTypes('hidden_icon', ['string', 'null']); |
| 68 | + $resolver->setAllowedTypes('visible_icon', ['string', 'null']); |
| 69 | + $resolver->setAllowedTypes('button_classes', ['string[]']); |
| 70 | + $resolver->setAllowedTypes('toggle_container_classes', ['string[]']); |
| 71 | + $resolver->setAllowedTypes('toggle_translation_domain', ['string', 'bool', 'null']); |
| 72 | + $resolver->setAllowedTypes('use_toggle_form_theme', ['bool']); |
| 73 | + } |
| 74 | + |
| 75 | + public function buildView(FormView $view, FormInterface $form, array $options): void |
| 76 | + { |
| 77 | + $view->vars['toggle'] = $options['toggle']; |
| 78 | + |
| 79 | + if (!$options['toggle']) { |
| 80 | + return; |
| 81 | + } |
| 82 | + |
| 83 | + if ($options['use_toggle_form_theme']) { |
| 84 | + array_splice($view->vars['block_prefixes'], -1, 0, 'toggle_password'); |
| 85 | + } |
| 86 | + |
| 87 | + $controllerName = 'toggle-password'; |
| 88 | + $view->vars['attr']['data-controller'] = trim(\sprintf('%s %s', $view->vars['attr']['data-controller'] ?? '', $controllerName)); |
| 89 | + |
| 90 | + if (false !== $options['toggle_translation_domain']) { |
| 91 | + $controllerValues['hidden-label'] = $this->translateLabel($options['hidden_label'], $options['toggle_translation_domain']); |
| 92 | + $controllerValues['visible-label'] = $this->translateLabel($options['visible_label'], $options['toggle_translation_domain']); |
| 93 | + } else { |
| 94 | + $controllerValues['hidden-label'] = $options['hidden_label']; |
| 95 | + $controllerValues['visible-label'] = $options['visible_label']; |
| 96 | + } |
| 97 | + |
| 98 | + $controllerValues['hidden-icon'] = $options['hidden_icon']; |
| 99 | + $controllerValues['visible-icon'] = $options['visible_icon']; |
| 100 | + $controllerValues['button-classes'] = json_encode($options['button_classes'], \JSON_THROW_ON_ERROR); |
| 101 | + |
| 102 | + foreach ($controllerValues as $name => $value) { |
| 103 | + $view->vars['attr'][\sprintf('data-%s-%s-value', $controllerName, $name)] = $value; |
| 104 | + } |
| 105 | + |
| 106 | + $view->vars['toggle_container_classes'] = $options['toggle_container_classes']; |
| 107 | + } |
| 108 | + |
| 109 | + private function translateLabel(string|TranslatableMessage|null $label, ?string $translationDomain): ?string |
| 110 | + { |
| 111 | + if (null === $this->translator || null === $label) { |
| 112 | + return $label; |
| 113 | + } |
| 114 | + |
| 115 | + if ($label instanceof TranslatableMessage) { |
| 116 | + return $label->trans($this->translator); |
| 117 | + } |
| 118 | + |
| 119 | + return $this->translator->trans($label, domain: $translationDomain); |
| 120 | + } |
| 121 | +} |
| 122 | +``` |
| 123 | + - `template/form_theme.html.twig`, and follow the [How to work with form themes](https://symfony.com/doc/current/form/form_themes.html) documentation to register it. |
| 124 | +```twig |
| 125 | +{%- block toggle_password_widget -%} |
| 126 | + <div class="{{ toggle_container_classes|join(' ') }}">{{ block('password_widget') }}</div> |
| 127 | +{%- endblock toggle_password_widget -%} |
| 128 | +``` |
| 129 | + - `assets/controllers/toggle_password_controller.js` |
| 130 | +```javascript |
| 131 | +import { Controller } from '@hotwired/stimulus'; |
| 132 | +import '../styles/toggle_password.css'; |
| 133 | + |
| 134 | +export default class extends Controller { |
| 135 | + static values = { |
| 136 | + visibleLabel: { type: String, default: 'Show' }, |
| 137 | + visibleIcon: { type: String, default: 'Default' }, |
| 138 | + hiddenLabel: { type: String, default: 'Hide' }, |
| 139 | + hiddenIcon: { type: String, default: 'Default' }, |
| 140 | + buttonClasses: Array, |
| 141 | + }; |
| 142 | + |
| 143 | + isDisplayed = false; |
| 144 | + visibleIcon = `<svg xmlns="http://www.w3.org/2000/svg" class="toggle-password-icon" viewBox="0 0 20 20" fill="currentColor"> |
| 145 | +<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" /> |
| 146 | +<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" /> |
| 147 | +</svg>`; |
| 148 | + hiddenIcon = `<svg xmlns="http://www.w3.org/2000/svg" class="toggle-password-icon" viewBox="0 0 20 20" fill="currentColor"> |
| 149 | +<path fill-rule="evenodd" d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z" clip-rule="evenodd" /> |
| 150 | +<path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.669-.105 2.454-.303z" /> |
| 151 | +</svg>`; |
| 152 | + |
| 153 | + connect() { |
| 154 | + if (this.visibleIconValue !== 'Default') { |
| 155 | + this.visibleIcon = this.visibleIconValue; |
| 156 | + } |
| 157 | + |
| 158 | + if (this.hiddenIconValue !== 'Default') { |
| 159 | + this.hiddenIcon = this.hiddenIconValue; |
| 160 | + } |
| 161 | + |
| 162 | + const button = this.createButton(); |
| 163 | + |
| 164 | + this.element.insertAdjacentElement('afterend', button); |
| 165 | + this.dispatchEvent('connect', { element: this.element, button }); |
| 166 | + } |
| 167 | + |
| 168 | + /** |
| 169 | + * @returns {HTMLButtonElement} |
| 170 | + */ |
| 171 | + createButton() { |
| 172 | + const button = document.createElement('button'); |
| 173 | + button.type = 'button'; |
| 174 | + button.classList.add(...this.buttonClassesValue); |
| 175 | + button.setAttribute('tabindex', '-1'); |
| 176 | + button.addEventListener('click', this.toggle.bind(this)); |
| 177 | + button.innerHTML = `${this.visibleIcon} ${this.visibleLabelValue}`; |
| 178 | + return button; |
| 179 | + } |
| 180 | + |
| 181 | + /** |
| 182 | + * Toggle input type between "text" or "password" and update label accordingly |
| 183 | + */ |
| 184 | + toggle(event) { |
| 185 | + this.isDisplayed = !this.isDisplayed; |
| 186 | + const toggleButtonElement = event.currentTarget; |
| 187 | + toggleButtonElement.innerHTML = this.isDisplayed |
| 188 | + ? `${this.hiddenIcon} ${this.hiddenLabelValue}` |
| 189 | + : `${this.visibleIcon} ${this.visibleLabelValue}`; |
| 190 | + this.element.setAttribute('type', this.isDisplayed ? 'text' : 'password'); |
| 191 | + this.dispatchEvent(this.isDisplayed ? 'show' : 'hide', { element: this.element, button: toggleButtonElement }); |
| 192 | + } |
| 193 | + |
| 194 | + dispatchEvent(name, payload) { |
| 195 | + this.dispatch(name, { detail: payload, prefix: 'toggle-password' }); |
| 196 | + } |
| 197 | +} |
| 198 | +``` |
| 199 | + - `assets/styles/toggle_password.css` |
| 200 | +```css |
| 201 | +.toggle-password-container { |
| 202 | + position: relative; |
| 203 | +} |
| 204 | +.toggle-password-icon { |
| 205 | + height: 1rem; |
| 206 | + width: 1rem; |
| 207 | +} |
| 208 | +.toggle-password-button { |
| 209 | + align-items: center; |
| 210 | + background-color: transparent; |
| 211 | + border: none; |
| 212 | + column-gap: 0.25rem; |
| 213 | + display: flex; |
| 214 | + flex-direction: row; |
| 215 | + font-size: 0.875rem; |
| 216 | + justify-items: center; |
| 217 | + height: 1rem; |
| 218 | + line-height: 1.25rem; |
| 219 | + position: absolute; |
| 220 | + right: 0.5rem; |
| 221 | + top: -1.25rem; |
| 222 | +} |
| 223 | +``` |
| 224 | + |
| 225 | +You're done! |
| 226 | + |
| 227 | +--- |
| 228 | + |
3 | 229 | Symfony UX TogglePassword is a Symfony bundle providing visibility toggle for password inputs
|
4 | 230 | in Symfony Forms. It is part of [the Symfony UX initiative](https://ux.symfony.com/).
|
5 | 231 |
|
|
0 commit comments