Skip to content

Commit 6ea2e59

Browse files
authored
feat: replace inline toggle handlers with nowo-password-toggle (#18)
Drop onclick/onkeydown so the widget is CSP-safe and still works with Live Components via event delegation.
1 parent ed6e798 commit 6ea2e59

13 files changed

Lines changed: 265 additions & 61 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ This bundle is **FrankenPHP worker mode friendly**.
1616

1717
- ✅ Password form type with toggle visibility
1818
- ✅ Customizable icons and labels
19-
-**No Stimulus / no extra asset bundle** — toggle uses **inline** `onclick` / `onkeydown` (see `toggle_password_widget.html.twig`) for compatibility with Live Components
19+
-**Web Component** `<nowo-password-toggle>` — CSP-safe script (no inline `onclick`); event delegation stays compatible with Live Components
2020
- ✅ Icons via **`symfony/ux-icons`** + **`symfony/http-client`** (Flex recipe installs both; graceful fallback + log warning if missing)
2121
- ✅ Fully configurable CSS classes
2222
- ✅ Works with Live Components

demo/symfony8-php85/templates/form/toggle_password_widget.html.twig

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,34 @@
55
Icon visibility uses is-password-visible + toggle_password.css (CSP-safe).
66
#}
77

8-
<div class="input-group input-group-merge form-password-toggle {{ (toggle_container_classes ?? [])|join(' ') }}">
9-
{%- set type = type|default('password') -%}
8+
{%- set type = type|default('password') -%}
9+
{%- set _visible_label = visible_label|default('Show password') -%}
10+
{%- set _hidden_label = hidden_label|default('Hide password') -%}
11+
<nowo-password-toggle
12+
class="input-group input-group-merge form-password-toggle {{ (toggle_container_classes ?? [])|join(' ') }}"
13+
data-nowo-password-toggle
14+
data-nowo-password-toggle-visible-label="{{ _visible_label|e('html_attr') }}"
15+
data-nowo-password-toggle-hidden-label="{{ _hidden_label|e('html_attr') }}"
16+
>
1017
<input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
1118
<span class="input-group-text cursor-pointer"
1219
role="button"
1320
tabindex="0"
14-
aria-label="{{ visible_label|default('Show password') }}"
15-
onclick="
16-
const input = this.previousElementSibling;
17-
18-
if (input.type === 'password') {
19-
input.type = 'text';
20-
this.classList.add('is-password-visible');
21-
this.setAttribute('aria-label', '{{ hidden_label|default('Hide password') }}');
22-
} else {
23-
input.type = 'password';
24-
this.classList.remove('is-password-visible');
25-
this.setAttribute('aria-label', '{{ visible_label|default('Show password') }}');
26-
}
27-
">
21+
data-nowo-password-toggle-target="button"
22+
aria-label="{{ _visible_label }}">
2823
<i class="ti ti-eye-off icon-hidden icon-base"></i>
2924
<i class="ti ti-eye icon-visible icon-base"></i>
3025
</span>
31-
</div>
26+
</nowo-password-toggle>
27+
{%- set _load_toggle_js = true -%}
28+
{%- if app is defined and app.request is defined -%}
29+
{%- if app.request.attributes.get('_nowo_password_toggle_js') -%}
30+
{%- set _load_toggle_js = false -%}
31+
{%- else -%}
32+
{%- set _ = app.request.attributes.set('_nowo_password_toggle_js', true) -%}
33+
{%- endif -%}
34+
{%- endif -%}
35+
{%- if _load_toggle_js -%}
36+
<script src="{{ asset('js/nowo-password-toggle.js', 'nowo_password_toggle') }}" defer></script>
37+
{%- endif -%}
3238
{%- endblock toggle_password_widget -%}

demo/symfony8/templates/form/toggle_password_widget.html.twig

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,34 @@
55
Icon visibility uses is-password-visible + toggle_password.css (CSP-safe).
66
#}
77

8-
<div class="input-group input-group-merge form-password-toggle {{ (toggle_container_classes ?? [])|join(' ') }}">
9-
{%- set type = type|default('password') -%}
8+
{%- set type = type|default('password') -%}
9+
{%- set _visible_label = visible_label|default('Show password') -%}
10+
{%- set _hidden_label = hidden_label|default('Hide password') -%}
11+
<nowo-password-toggle
12+
class="input-group input-group-merge form-password-toggle {{ (toggle_container_classes ?? [])|join(' ') }}"
13+
data-nowo-password-toggle
14+
data-nowo-password-toggle-visible-label="{{ _visible_label|e('html_attr') }}"
15+
data-nowo-password-toggle-hidden-label="{{ _hidden_label|e('html_attr') }}"
16+
>
1017
<input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
1118
<span class="input-group-text cursor-pointer"
1219
role="button"
1320
tabindex="0"
14-
aria-label="{{ visible_label|default('Show password') }}"
15-
onclick="
16-
const input = this.previousElementSibling;
17-
18-
if (input.type === 'password') {
19-
input.type = 'text';
20-
this.classList.add('is-password-visible');
21-
this.setAttribute('aria-label', '{{ hidden_label|default('Hide password') }}');
22-
} else {
23-
input.type = 'password';
24-
this.classList.remove('is-password-visible');
25-
this.setAttribute('aria-label', '{{ visible_label|default('Show password') }}');
26-
}
27-
">
21+
data-nowo-password-toggle-target="button"
22+
aria-label="{{ _visible_label }}">
2823
<i class="ti ti-eye-off icon-hidden icon-base"></i>
2924
<i class="ti ti-eye icon-visible icon-base"></i>
3025
</span>
31-
</div>
26+
</nowo-password-toggle>
27+
{%- set _load_toggle_js = true -%}
28+
{%- if app is defined and app.request is defined -%}
29+
{%- if app.request.attributes.get('_nowo_password_toggle_js') -%}
30+
{%- set _load_toggle_js = false -%}
31+
{%- else -%}
32+
{%- set _ = app.request.attributes.set('_nowo_password_toggle_js', true) -%}
33+
{%- endif -%}
34+
{%- endif -%}
35+
{%- if _load_toggle_js -%}
36+
<script src="{{ asset('js/nowo-password-toggle.js', 'nowo_password_toggle') }}" defer></script>
37+
{%- endif -%}
3238
{%- endblock toggle_password_widget -%}

docs/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3535

3636
## [Unreleased]
3737

38+
### Changed
39+
40+
- **Web Component:** the widget renders `<nowo-password-toggle>` and loads `js/nowo-password-toggle.js` (CSP-safe; no inline `onclick` / `onkeydown`). Event delegation on the host remains compatible with Live Components.
41+
3842

3943
## [2.1.4] - 2026-08-24
4044

docs/INSTALLATION.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ return [
9090
<link rel="stylesheet" href="{{ asset('css/toggle_password.css', 'nowo_password_toggle') }}">
9191
```
9292

93+
The default widget also loads `js/nowo-password-toggle.js` (custom element `<nowo-password-toggle>`). Run `assets:install` so both CSS and JS are published under `public/bundles/nowopasswordtoggle/`.
94+
9395
### AssetMapper
9496

9597
If your app uses [Symfony AssetMapper](https://symfony.com/doc/current/frontend/asset_mapper.html), the bundle registers the `nowo_password_toggle` asset package. Run `assets:install` once so `css/toggle_password.css` is published to `public/bundles/nowopasswordtoggle/`.

docs/UPGRADING.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
## Table of contents
44

5+
- [Unreleased](#unreleased)
56
- [From 2.1.3 to 2.1.4](#from-213-to-214)
67

8+
## Unreleased
9+
10+
The default widget is now `<nowo-password-toggle>` plus `js/nowo-password-toggle.js` (loaded once per request). Run `php bin/console assets:install`. If you copied `toggle_password_widget.html.twig`, remove inline `onclick` / `onkeydown` and wrap the input group in `<nowo-password-toggle>` (see the bundle template). Strict `script-src` hosts no longer need `'unsafe-inline'` for the toggle.
11+
712
## From 2.1.3 to 2.1.4
813

914
No breaking changes. **No application upgrade steps.**

docs/USAGE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ With **UX Icons 3.x**, this command scans Twig templates for `ux_icon()` usage (
7373

7474
- **Option 1:** Include the bundle CSS:
7575
`<link rel="stylesheet" href="{{ asset('css/toggle_password.css', 'nowo_password_toggle') }}">`
76+
- **Web Component script:** the default widget loads `js/nowo-password-toggle.js` once per request. After `assets:install` you can also include it in the layout:
77+
78+
```twig
79+
<script src="{{ asset('js/nowo-password-toggle.js', 'nowo_password_toggle') }}" defer></script>
80+
```
81+
82+
The host tag is `<nowo-password-toggle>` (light DOM: native password input + toggle button). Inline `onclick` / `onkeydown` handlers are no longer used.
7683
- **Option 2:** Import the SCSS in your build (Webpack Encore, Vite, etc.):
7784
`@import '@nowo-tech/password-toggle-bundle/src/Resources/public/css/toggle_password.scss';`
7885
- **Option 3:** Style the classes yourself: `.input-group-text.cursor-pointer`, `.form-password-toggle`, etc.

src/Form/Type/PasswordType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
* - Toggle button with customizable icons (hidden/visible states)
2323
* - Customizable labels for accessibility
2424
* - Configurable CSS classes for styling
25-
* - Native JavaScript implementation for maximum compatibility
25+
* - Native JavaScript custom element (`<nowo-password-toggle>`) for CSP-safe toggling
2626
*
2727
* Default values can be configured in config/packages/nowo_password_toggle.yaml
2828
* and can be overridden when using this form type in a form builder.

src/Resources/public/css/toggle_password.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
* place the toggle button next to the input
66
*/
77

8+
nowo-password-toggle {
9+
display: flex;
10+
align-items: stretch;
11+
width: 100%;
12+
}
13+
814
/* Container with input-group */
915
.input-group.input-group-merge {
1016
/* Already has Bootstrap styles */

src/Resources/public/css/toggle_password.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
* place the toggle button next to the input
66
*/
77

8+
nowo-password-toggle {
9+
display: flex;
10+
align-items: stretch;
11+
width: 100%;
12+
}
13+
814
// Container with input-group
915
.input-group.input-group-merge {
1016
// Already has Bootstrap styles

0 commit comments

Comments
 (0)