From 5c8977c014ab4788d0579f48e366b6a5ae36d1fd Mon Sep 17 00:00:00 2001 From: David RENARD Date: Mon, 11 May 2026 10:55:39 +0200 Subject: [PATCH] feat: implement boolean cell display modes --- docs/architecture.md | 25 ++++ docs/cell-templates.md | 57 ++++++++ src/Enum/BooleanDisplayMode.php | 22 +++ src/Renderer/DatatableRenderer.php | 12 ++ templates/bootstrap/_cell.html.twig | 3 +- templates/bootstrap/_row.html.twig | 3 +- templates/bootstrap/cell/boolean.html.twig | 33 ++++- tests/Unit/Enum/BooleanDisplayModeTest.php | 27 ++++ ...atatableRendererBooleanDisplayModeTest.php | 128 ++++++++++++++++++ 9 files changed, 305 insertions(+), 5 deletions(-) create mode 100644 src/Enum/BooleanDisplayMode.php create mode 100644 tests/Unit/Enum/BooleanDisplayModeTest.php create mode 100644 tests/Unit/Renderer/DatatableRendererBooleanDisplayModeTest.php diff --git a/docs/architecture.md b/docs/architecture.md index 2e8268b..d96ba98 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1146,6 +1146,31 @@ The documentation covers: - Doctrine type enrichment; - current limitations. +### Boolean cell display modes + +Boolean cells support configurable display modes. + +Supported modes: + +```text +badge +icon +switch +text +``` + +`badge` remains the default. + +The display mode can be provided through render options: + +```twig +{{ zhortein_datatable('users', { + booleanDisplayMode: 'icon' +}) }} +``` + +The implementation stays display-only. Switch mode renders a disabled Bootstrap switch and does not mutate data. + ### Bootstrap template cleanup Bootstrap templates have been reviewed for readability and consistency. diff --git a/docs/cell-templates.md b/docs/cell-templates.md index 415a661..340e8c0 100644 --- a/docs/cell-templates.md +++ b/docs/cell-templates.md @@ -448,6 +448,63 @@ Custom template existence is not validated when the datatable definition is buil Twig errors remain explicit at render time. +## Boolean display modes + +Boolean cells support several display modes: + +```text +badge +icon +switch +text +``` + +### Badge mode + +Badge mode is the default. + +It renders translated `Yes` / `No` labels as Bootstrap badges. + +```twig +{{ zhortein_datatable('users', { + booleanDisplayMode: 'badge' +}) }} +``` + +### Icon mode + +Icon mode renders decorative check/cross characters with visually hidden translated labels. + +```twig +{{ zhortein_datatable('users', { + booleanDisplayMode: 'icon' +}) }} +``` + +No icon library is required. + +### Switch mode + +Switch mode renders a disabled Bootstrap switch. + +```twig +{{ zhortein_datatable('users', { + booleanDisplayMode: 'switch' +}) }} +``` + +The switch is display-only and does not update data. + +### Text mode + +Text mode renders the translated label only. + +```twig +{{ zhortein_datatable('users', { + booleanDisplayMode: 'text' +}) }} +``` + ## Related documentation - [`templates.md`](templates.md) diff --git a/src/Enum/BooleanDisplayMode.php b/src/Enum/BooleanDisplayMode.php new file mode 100644 index 0000000..602b855 --- /dev/null +++ b/src/Enum/BooleanDisplayMode.php @@ -0,0 +1,22 @@ + $this->readColumnValue($row, $column), 'template' => $this->resolveCellTemplate($column), 'className' => $this->resolveCellClassName($column), + 'booleanDisplayMode' => $this->resolveBooleanDisplayMode($options)->value, ]; } @@ -399,4 +401,14 @@ private function createHtmlId(DatatableDefinition $definition): string return 'zhortein-datatable-'.strtolower(trim($name, '-')); } + + /** + * @param array $options + */ + private function resolveBooleanDisplayMode(array $options): BooleanDisplayMode + { + $mode = $options['booleanDisplayMode'] ?? null; + + return BooleanDisplayMode::fromNullableString(is_string($mode) ? $mode : null); + } } diff --git a/templates/bootstrap/_cell.html.twig b/templates/bootstrap/_cell.html.twig index c23b1cb..4984d38 100644 --- a/templates/bootstrap/_cell.html.twig +++ b/templates/bootstrap/_cell.html.twig @@ -1,6 +1,7 @@ {% include template with { column: column, - value: value + value: value, + boolean_display_mode: boolean_display_mode } only %} diff --git a/templates/bootstrap/_row.html.twig b/templates/bootstrap/_row.html.twig index 570953d..42d044a 100644 --- a/templates/bootstrap/_row.html.twig +++ b/templates/bootstrap/_row.html.twig @@ -4,7 +4,8 @@ column: cell.column, value: cell.value, template: cell.template, - class_name: cell.className + class_name: cell.className, + boolean_display_mode: cell.booleanDisplayMode } only %} {% endfor %} diff --git a/templates/bootstrap/cell/boolean.html.twig b/templates/bootstrap/cell/boolean.html.twig index d2974bd..ba0b881 100644 --- a/templates/bootstrap/cell/boolean.html.twig +++ b/templates/bootstrap/cell/boolean.html.twig @@ -1,5 +1,32 @@ -{% if value %} - {{ 'zhortein_datatable.boolean.yes'|trans({}, 'zhortein_datatable') }} +{% set display_mode = boolean_display_mode ?? 'badge' %} +{% set yes_label = 'zhortein_datatable.boolean.yes'|trans({}, 'zhortein_datatable') %} +{% set no_label = 'zhortein_datatable.boolean.no'|trans({}, 'zhortein_datatable') %} + +{% if display_mode == 'icon' %} + {% if value %} + + {{ yes_label }} + {% else %} + + {{ no_label }} + {% endif %} +{% elseif display_mode == 'switch' %} + + + +{% elseif display_mode == 'text' %} + {{ value ? yes_label : no_label }} {% else %} - {{ 'zhortein_datatable.boolean.no'|trans({}, 'zhortein_datatable') }} + {% if value %} + {{ yes_label }} + {% else %} + {{ no_label }} + {% endif %} {% endif %} diff --git a/tests/Unit/Enum/BooleanDisplayModeTest.php b/tests/Unit/Enum/BooleanDisplayModeTest.php new file mode 100644 index 0000000..603dddc --- /dev/null +++ b/tests/Unit/Enum/BooleanDisplayModeTest.php @@ -0,0 +1,27 @@ +renderBoolean(true); + + self::assertStringContainsString('badge text-bg-success', $html); + self::assertStringContainsString('Yes', $html); + } + + public function test_it_renders_boolean_false_as_badge_by_default(): void + { + $html = $this->renderBoolean(false); + + self::assertStringContainsString('badge text-bg-secondary', $html); + self::assertStringContainsString('No', $html); + } + + public function test_it_renders_boolean_as_icon(): void + { + $html = $this->renderBoolean(true, BooleanDisplayMode::Icon); + + self::assertStringContainsString('text-success', $html); + self::assertStringContainsString('✓', $html); + self::assertStringContainsString('visually-hidden', $html); + self::assertStringContainsString('Yes', $html); + self::assertStringNotContainsString('badge text-bg-success', $html); + } + + public function test_it_renders_boolean_false_as_icon(): void + { + $html = $this->renderBoolean(false, BooleanDisplayMode::Icon); + + self::assertStringContainsString('text-danger', $html); + self::assertStringContainsString('×', $html); + self::assertStringContainsString('No', $html); + } + + public function test_it_renders_boolean_as_switch(): void + { + $html = $this->renderBoolean(true, BooleanDisplayMode::Switch); + + self::assertStringContainsString('form-check form-switch', $html); + self::assertStringContainsString('role="switch"', $html); + self::assertStringContainsString('disabled', $html); + self::assertStringContainsString('checked', $html); + self::assertStringContainsString('aria-label="Yes"', $html); + } + + public function test_it_renders_boolean_false_as_switch(): void + { + $html = $this->renderBoolean(false, BooleanDisplayMode::Switch); + + self::assertStringContainsString('form-check form-switch', $html); + self::assertStringContainsString('aria-label="No"', $html); + self::assertStringNotContainsString('checked', $html); + } + + public function test_it_renders_boolean_as_text(): void + { + $html = $this->renderBoolean(true, BooleanDisplayMode::Text); + + self::assertStringContainsString('Yes', $html); + self::assertStringNotContainsString('badge', $html); + self::assertStringNotContainsString('form-check', $html); + } + + private function renderBoolean(bool $value, ?BooleanDisplayMode $mode = null): string + { + $definition = new DatatableDefinition('users'); + + $definition->addColumn( + name: 'enabled', + label: 'Enabled', + type: 'boolean', + ); + + $options = []; + + if (null !== $mode) { + $options['booleanDisplayMode'] = $mode->value; + } + + return new DatatableRenderer($this->createTwigEnvironment())->renderBody( + $definition, + new DatatableResult( + rows: [ + [ + 'enabled' => $value, + ], + ], + totalItems: 1, + ), + $options, + ); + } + + private function createTwigEnvironment(): Environment + { + $loader = new FilesystemLoader(); + $loader->addPath(__DIR__.'/../../../templates', 'ZhorteinDatatable'); + + $twig = new Environment($loader, [ + 'strict_variables' => true, + 'autoescape' => 'html', + ]); + + $this->addTranslationExtension($twig); + + return $twig; + } +}