Skip to content

Commit c75106f

Browse files
committed
Add a Twig component to render switches/toggles
1 parent b928fb3 commit c75106f

20 files changed

Lines changed: 391 additions & 87 deletions

File tree

UPGRADE.md

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1-
UPGRADE FROM EASYADMIN 4.X to 5.X
2-
=================================
1+
Upgrade Guide
2+
=============
33

4-
## Pretty URLs
4+
## EasyAdmin 5.0.14
5+
6+
The HTML markup of the boolean switch has changed. Before, the element wrapping
7+
the switch applied the `.form-switch` CSS class and the checkbox used the
8+
`.form-check-input` class. Now, the switch is rendered with the new
9+
`<twig:ea:Switch>` component: the wrapping element uses the `.ea-switch` class and
10+
the checkbox uses the `.ea-switch-input` class. This affects both the switch shown
11+
in the `index` page and the one displayed in the `edit`/`new` forms. Update any
12+
custom CSS or JavaScript that targeted the old selectors.
13+
14+
## Upgrading from Symfony 4.x to 5.x
15+
16+
### Pretty URLs
517

618
Using pretty URLs is now mandatory. They are created with a custom route loader
719
that must be enabled in your application. If you use Symfony Flex, this file is
@@ -14,7 +26,7 @@ easyadmin:
1426
type: easyadmin.routes
1527
```
1628
17-
## Admin Context
29+
### Admin Context
1830
1931
The global `ea` variable injected in all templates is removed in favor of the
2032
equivalent `ea()` Twig function, which returns the current context of the
@@ -28,7 +40,7 @@ EasyAdmin application:
2840
{{ ea().i18n.translationDomain }}
2941
```
3042

31-
## Main Menus
43+
### Main Menus
3244

3345
The `linkToCrud()` method used to link to CRUD controllers from the main menu of the
3446
dashboard was removed in favor of the new `linkTo()` method:
@@ -45,7 +57,7 @@ yield MenuItem::linkTo(BlogPostCrudController::class, 'Blog Posts', 'fa fa-file-
4557
yield MenuItem::linkTo(CommentCrudController::class);
4658
```
4759

48-
## Custom CRUD Actions
60+
### Custom CRUD Actions
4961

5062
Custom CRUD actions now require to apply the `#[AdminRoute]` attribute to them.
5163
Otherwise, they are ignored when generating routes for the backend and code
@@ -115,7 +127,7 @@ class CommentCrudController extends AbstractCrudController
115127
}
116128
```
117129

118-
## Actions
130+
### Actions
119131

120132
Some methods related to actions have been removed in favor of equivalent
121133
methods with better names:
@@ -132,7 +144,7 @@ $action->renderAsButton()->...
132144
$action->renderAsForm()->...
133145
```
134146

135-
## Referrers
147+
### Referrers
136148

137149
EasyAdmin URLs no longer include the `referrer` query parameter, and the
138150
`AdminContext:getReferrer()` method was removed.
@@ -152,7 +164,7 @@ return $this->redirect($batchActionDto->getReferrer());
152164
return $this->redirect($adminContext->getRequest()->headers->get('referer'));
153165
```
154166

155-
## Forms
167+
### Forms
156168

157169
Form panels are now called Form fieldsets and the `FormField::addPanel()` method
158170
was removed:
@@ -165,16 +177,16 @@ yield FormField::addPanel('...');
165177
yield FormField::addFieldset('...');
166178
```
167179

168-
## Attributes
180+
### Attributes
169181

170182
The `#[AdminCrud]` and `#[AdminAction]` attributes have been removed in favor
171183
of the `#[AdminRoute]` attribute.
172184

173-
## Contracts
185+
### Contracts
174186

175187
The following contract interfaces changed:
176188

177-
### `Contracts\Context\AdminContextInterface`
189+
#### `Contracts\Context\AdminContextInterface`
178190

179191
```php
180192
// Before (4.x)
@@ -186,7 +198,7 @@ public function getAdminControllers(): AdminControllerRegistry;
186198

187199
The `getSignedUrls()` and `getReferrer()` methods are removed.
188200

189-
### `Contracts\Controller\CrudControllerInterface`
201+
#### `Contracts\Controller\CrudControllerInterface`
190202

191203
```php
192204
// Before (4.x)
@@ -196,7 +208,7 @@ public function createEntity(string $entityFqcn);
196208
public function createEntity(string $entityFqcn): object;
197209
```
198210

199-
### `Contracts\Orm\EntityPaginatorInterface`
211+
#### `Contracts\Orm\EntityPaginatorInterface`
200212

201213
```php
202214
// Before (4.x)
@@ -206,7 +218,7 @@ public function getResultsAsJson(): string;
206218
public function getResultsAsJson(?callable $callback = null, ?string $twigTemplate = null, bool $renderAsHtml = false): string;
207219
```
208220

209-
### `Contracts\Provider\AdminContextInterface`
221+
#### `Contracts\Provider\AdminContextInterface`
210222

211223
```php
212224
// Before (4.x)
@@ -217,13 +229,13 @@ public function hasContext(): bool;
217229
// alternative: check if getContext() return value is null
218230
```
219231

220-
### `Contracts\Menu\MenuItemMatcherInterface`
232+
#### `Contracts\Menu\MenuItemMatcherInterface`
221233

222234
The `isSelected()` and `isExpanded()` methods were removed. A new
223235
`markSelectedMenuItem(array<MenuItemDto> $menuItems, Request $request)` method
224236
has been added.
225237

226-
### `Contracts\Router\AdminRouteGeneratorInterface`
238+
#### `Contracts\Router\AdminRouteGeneratorInterface`
227239

228240
```php
229241
// Before (4.x)
@@ -235,7 +247,7 @@ public function findRouteName(string|null $dashboardFqcn = null, string|null $cr
235247

236248
The `usesPrettyUrls()` method was removed.
237249

238-
## Static Analysis
250+
### Static Analysis
239251

240252
In 5.x, PHPStan will report an error if a class extends
241253
`AbstractCrudController` without specifying the entity type:

assets/css/easyadmin-theme/datagrids.css

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -367,17 +367,6 @@ table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions.actions-as-dro
367367
padding: 6px 8px;
368368
}
369369

370-
.datagrid .field-boolean .form-switch {
371-
display: inline-flex;
372-
justify-content: center;
373-
margin-block-end: 0;
374-
padding-inline-start: 0;
375-
}
376-
.datagrid .field-boolean .form-switch input {
377-
margin-block-start: 0;
378-
position: relative;
379-
inset-block-start: 3px;
380-
}
381370
@media (max-width: 992px) {
382371
.datagrid .field-country {
383372
text-align: left !important;
Lines changed: 101 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,112 @@
11
/* Switches (toggles)
22
/* ------------------------------------------------------------------------- */
3+
/* <twig:ea:Switch> component. A native checkbox is layered (invisible) on top
4+
/* of a track + thumb, so the thumb can slide with a CSS transform like the
5+
/* shadcn/ui switch. The colors are themed via the --switch-* design tokens
6+
/* defined in variables-theme.css (light + dark schemes). */
37

4-
.form-switch .form-check-input {
5-
background-color: var(--form-switch-bg);
6-
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='rgba(148, 163, 184, 0.8)'/%3E%3C/svg%3E");
7-
border-color: var(--form-switch-border-color);
8+
.ea-switch {
9+
--ea-switch-width: 32px;
10+
--ea-switch-height: 18px;
11+
--ea-switch-thumb-size: 14px;
12+
--ea-switch-thumb-inset: 2px;
13+
14+
position: relative;
15+
display: inline-flex;
16+
align-items: center;
17+
vertical-align: middle;
18+
inline-size: var(--ea-switch-width);
19+
block-size: var(--ea-switch-height);
20+
flex-shrink: 0;
21+
}
22+
23+
.ea-switch-sm {
24+
--ea-switch-width: 24px;
25+
--ea-switch-height: 14px;
26+
--ea-switch-thumb-size: 10px;
27+
}
28+
29+
/* the real checkbox sits on top of the visuals, fully transparent, so it stays
30+
keyboard-focusable, clickable and submittable while the track/thumb render */
31+
.ea-switch-input {
32+
position: absolute;
33+
inset: 0;
34+
inline-size: 100%;
35+
block-size: 100%;
36+
margin: 0;
37+
opacity: 0;
838
cursor: pointer;
9-
block-size: 18px;
10-
inline-size: 32px;
11-
-webkit-appearance: none; /* needed for Safari */
39+
z-index: 1;
40+
}
41+
42+
.ea-switch-track {
43+
position: absolute;
44+
inset: 0;
45+
border-radius: 999px;
46+
background-color: var(--switch-bg);
47+
transition: background-color 0.15s ease-in-out;
48+
pointer-events: none;
49+
}
50+
51+
.ea-switch-thumb {
52+
position: absolute;
53+
inset-block-start: var(--ea-switch-thumb-inset);
54+
inset-inline-start: var(--ea-switch-thumb-inset);
55+
inline-size: var(--ea-switch-thumb-size);
56+
block-size: var(--ea-switch-thumb-size);
57+
border-radius: 50%;
58+
background-color: var(--switch-thumb-bg);
59+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
60+
transition: transform 0.15s ease-in-out;
61+
}
62+
63+
.ea-switch-input:checked ~ .ea-switch-track {
64+
background-color: var(--switch-checked-bg);
1265
}
13-
.form-switch .form-check-input:checked,
14-
.ea-dark-scheme .form-switch .form-check-input:checked {
15-
background-color: var(--form-switch-checked-bg);
16-
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='rgb(255, 255, 255)'/%3E%3C/svg%3E");
17-
border-color: var(--form-switch-checked-bg);
66+
67+
.ea-switch-input:checked ~ .ea-switch-track .ea-switch-thumb {
68+
transform: translateX(calc(var(--ea-switch-width) - var(--ea-switch-thumb-size) - 2 * var(--ea-switch-thumb-inset)));
69+
}
70+
71+
.ea-switch-input:focus-visible ~ .ea-switch-track {
72+
box-shadow: 0 0 0 3px var(--switch-focus-ring);
1873
}
19-
.ea-dark-scheme .form-switch .form-check-input:checked {
20-
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='rgba(255, 255, 255, 0.8)'/%3E%3C/svg%3E");
74+
75+
/* color variants: only the checked track + focus ring change, reusing the same
76+
colors as the button variants. The unchecked track stays neutral gray */
77+
.ea-switch-success {
78+
--switch-checked-bg: var(--switch-success-bg);
79+
--switch-focus-ring: color-mix(in srgb, var(--switch-success-bg) 35%, transparent);
80+
}
81+
.ea-switch-warning {
82+
--switch-checked-bg: var(--switch-warning-bg);
83+
--switch-focus-ring: color-mix(in srgb, var(--switch-warning-bg) 35%, transparent);
2184
}
22-
.ea-dark-scheme .form-switch .form-check-input {
23-
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='rgba(163, 163, 163, 0.8)'/%3E%3C/svg%3E");
85+
.ea-switch-danger {
86+
--switch-checked-bg: var(--switch-danger-bg);
87+
--switch-focus-ring: color-mix(in srgb, var(--switch-danger-bg) 35%, transparent);
2488
}
25-
.form-switch.disabled,
26-
.form-switch .form-check-input[disabled] {
89+
90+
/* disabled: dimmed and non-interactive (the wrapper gets .ea-switch-disabled
91+
from the component, and the input itself is also disabled in forms) */
92+
.ea-switch-disabled,
93+
.ea-switch:has(.ea-switch-input:disabled) {
94+
opacity: 0.5;
95+
}
96+
97+
.ea-switch-disabled .ea-switch-input,
98+
.ea-switch-input:disabled {
2799
cursor: not-allowed;
28100
}
29-
.form-switch .form-check-input:focus {
30-
box-shadow: none;
101+
102+
/* switch inside a form (edit/new): the <twig:ea:Switch> is nested in the field
103+
label, so lay them out as a row with the label text next to the switch */
104+
.ea-switch-check {
105+
padding-inline-start: 0;
106+
}
107+
.ea-switch-check .form-check-label {
108+
display: inline-flex;
109+
align-items: center;
110+
gap: 0.5rem;
111+
cursor: pointer;
31112
}

assets/css/easyadmin-theme/variables-theme.css

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,15 @@
156156
--form-input-text-color: var(--gray-700);
157157
--form-input-group-text-bg: var(--form-control-bg);
158158
--form-input-group-text-border-color: var(--form-input-border-color);
159-
--form-switch-bg: var(--body-bg);
160-
--form-switch-border-color: var(--gray-400);
161-
--form-switch-checked-bg: var(--indigo-500);
159+
--switch-bg: var(--gray-300);
160+
--switch-checked-bg: var(--indigo-500);
161+
--switch-thumb-bg: var(--white);
162+
--switch-focus-ring: rgba(99, 102, 241, 0.35);
163+
/* switch color variants reuse the button variant colors (these forward to
164+
scheme-aware tokens, so they resolve correctly in dark mode too) */
165+
--switch-success-bg: var(--button-success-hover-bg);
166+
--switch-warning-bg: var(--button-warning-hover-bg);
167+
--switch-danger-bg: var(--button-danger-hover-bg);
162168
--form-type-check-input-border-color: var(--gray-400);
163169
--form-type-check-input-box-shadow: 0 1px 2px 0 var(--gray-50);
164170
--form-type-check-input-checked-bg: var(--indigo-500);
@@ -549,9 +555,10 @@
549555
--form-input-text-color: var(--true-gray-200);
550556
--form-input-group-text-bg: var(--true-gray-800);
551557
--form-input-group-text-border-color: var(--true-gray-600);
552-
--form-switch-bg: var(--true-gray-600);
553-
--form-switch-border-color: var(--true-gray-700);
554-
--form-switch-checked-bg: var(--blue-600);
558+
--switch-bg: var(--true-gray-600);
559+
--switch-checked-bg: var(--blue-600);
560+
--switch-thumb-bg: var(--gray-300);
561+
--switch-focus-ring: rgba(90, 168, 252, 0.4);
555562
--form-type-check-input-border-color: var(--true-gray-400);
556563
--form-type-check-input-box-shadow: 0 1px 2px 0 var(--true-gray-800);
557564
--form-type-check-input-checked-bg: var(--blue-600);

assets/js/field-boolean.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ document.addEventListener('DOMContentLoaded', () => {
22
// toggle switches are only created in index page (i.e. in datagrid tables) because
33
// in other pages they act like simple checkboxes or labels. Only in index page
44
// the toggle switch can change the value of an entity propert via Ajax requests
5-
document.querySelectorAll('td.field-boolean .form-switch input[type="checkbox"]').forEach((toggleField) => {
5+
document.querySelectorAll('td.field-boolean .ea-switch input[type="checkbox"]').forEach((toggleField) => {
66
new ToggleSwitch(toggleField);
77
});
88
});
@@ -39,6 +39,6 @@ class ToggleSwitch {
3939
#disableField() {
4040
this.field.checked = !this.field.checked;
4141
this.field.disabled = true;
42-
this.field.closest('.form-switch').classList.add('disabled');
42+
this.field.closest('.ea-switch').classList.add('ea-switch-disabled');
4343
}
4444
}

doc/fields/BooleanField.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,20 @@ If you prefer to not allow changing the property value in this way, use this opt
7171

7272
yield BooleanField::new('...')->renderAsSwitch(false);
7373

74+
Colored Switches
75+
~~~~~~~~~~~~~~~~
76+
77+
By default the switch uses a neutral color when it's turned on. If you want to
78+
highlight its meaning, use one of the following methods to color the "on" state
79+
with the same colors as the ``success``, ``warning`` and ``danger`` buttons.
80+
These methods also enable the switch rendering, so you don't need to call
81+
``renderAsSwitch()`` too::
82+
83+
yield BooleanField::new('...')->renderAsSuccessSwitch(); // green when on
84+
yield BooleanField::new('...')->renderAsWarningSwitch(); // amber when on
85+
yield BooleanField::new('...')->renderAsDangerSwitch(); // red when on
86+
87+
The color is applied everywhere the field is rendered as a switch (the ``index``
88+
toggle and the ``edit``/``new`` forms). The "off" state always uses the neutral color.
89+
7490
.. _`CheckboxType`: https://symfony.com/doc/current/reference/forms/types/checkbox.html
Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/entrypoints.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"entrypoints": {
33
"app": {
44
"css": [
5-
"/app.60a570da.css"
5+
"/app.cec96de7.css"
66
],
77
"js": [
88
"/app.b016c66e.js"
@@ -25,7 +25,7 @@
2525
},
2626
"field-boolean": {
2727
"js": [
28-
"/field-boolean.6eb3e3a7.js"
28+
"/field-boolean.6adeff4e.js"
2929
]
3030
},
3131
"field-code-editor": {
Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)