diff --git a/docs/architecture.md b/docs/architecture.md index 6633f1d..0951301 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1240,6 +1240,24 @@ Example: }) }} ``` +### Additional rendering CSS classes + +Applications can append custom CSS classes to the rendered datatable structure through runtime options. + +Supported options: + +```twig +{{ zhortein_datatable('users', { + rootClass: 'my-datatable', + tableWrapperClass: 'my-table-wrapper', + tableClass: 'my-table' +}) }} +``` + +These options append classes and do not replace the bundle's default Bootstrap classes. + +This allows host applications to apply project-specific styling without overriding templates. + --- ## 9. Action rendering layer diff --git a/docs/basic-usage.md b/docs/basic-usage.md index 16260a4..06d1bc4 100644 --- a/docs/basic-usage.md +++ b/docs/basic-usage.md @@ -636,10 +636,33 @@ For a complete minimal example without Doctrine, see [`examples/array-datatable. For a complete Doctrine-backed example, see [`examples/doctrine-datatable.md`](examples/doctrine-datatable.md). +--- + +## 19. Additional CSS classes + +You can append custom CSS classes to the generated datatable markup: + +```twig +{{ zhortein_datatable('users', { + rootClass: 'my-datatable', + tableWrapperClass: 'my-table-wrapper', + tableClass: 'my-table' +}) }} +``` + +Available options: + +| Option | Target | +|---|---| +| `rootClass` | Root datatable container | +| `tableWrapperClass` | Table responsive wrapper | +| `tableClass` | `` element | + +Classes are appended to existing Bootstrap classes. --- -## 19. Current limitations +## 20. Current limitations The bundle is still under active development. diff --git a/docs/theming.md b/docs/theming.md index 126739d..0a2d5fb 100644 --- a/docs/theming.md +++ b/docs/theming.md @@ -307,6 +307,19 @@ The following controls use Bootstrap dropdown markup: Without Bootstrap JavaScript, these controls may render but not open. +## Additional CSS classes + +For project-specific styling, applications can append CSS classes without overriding templates. + +```twig +{{ zhortein_datatable('users', { + rootClass: 'datatable datatable-users', + tableWrapperClass: 'datatable-wrapper', + tableClass: 'datatable-table' +}) }} +``` + +The bundle keeps its default Bootstrap classes and appends the provided classes. ## Future direction diff --git a/templates/bootstrap/datatable.html.twig b/templates/bootstrap/datatable.html.twig index 788947b..0095b02 100644 --- a/templates/bootstrap/datatable.html.twig +++ b/templates/bootstrap/datatable.html.twig @@ -12,6 +12,9 @@ {% set table_small = options.tableSmall is defined ? options.tableSmall : false %} {% set table_responsive = options.tableResponsive is defined ? options.tableResponsive : true %} {% set table_classes = ['table', 'align-middle', 'mb-0'] %} +{% set root_class = options.rootClass is defined ? options.rootClass|trim : '' %} +{% set table_wrapper_class = options.tableWrapperClass is defined ? options.tableWrapperClass|trim : '' %} +{% set table_class = options.tableClass is defined ? options.tableClass|trim : '' %} {% if table_striped %} {% set table_classes = table_classes|merge(['table-striped']) %} @@ -33,9 +36,23 @@ {% set table_classes = table_classes|merge(['table-sm']) %} {% endif %} +{% if table_class is not empty %} + {% set table_classes = table_classes|merge([table_class]) %} +{% endif %} + +{% set table_wrapper_classes = [] %} + +{% if table_responsive %} + {% set table_wrapper_classes = table_wrapper_classes|merge(['table-responsive']) %} +{% endif %} + +{% if table_wrapper_class is not empty %} + {% set table_wrapper_classes = table_wrapper_classes|merge([table_wrapper_class]) %} +{% endif %} +
{{ 'zhortein_datatable.loading'|trans({}, 'zhortein_datatable') }}
- +
{% include '@ZhorteinDatatable/bootstrap/_header.html.twig' %} diff --git a/tests/Unit/Renderer/DatatableRendererAdditionalCssClassesTest.php b/tests/Unit/Renderer/DatatableRendererAdditionalCssClassesTest.php new file mode 100644 index 0000000..3877ba3 --- /dev/null +++ b/tests/Unit/Renderer/DatatableRendererAdditionalCssClassesTest.php @@ -0,0 +1,110 @@ +createRenderer()->render($this->createDefinition(), [ + 'rootClass' => 'datatable--compact my-root-class', + 'columnVisibility' => false, + 'export' => false, + ]); + + self::assertStringContainsString('class="zhortein-datatable datatable--compact my-root-class"', $html); + } + + public function test_it_appends_additional_table_wrapper_class(): void + { + $html = $this->createRenderer()->render($this->createDefinition(), [ + 'tableWrapperClass' => 'my-table-wrapper', + 'columnVisibility' => false, + 'export' => false, + ]); + + self::assertStringContainsString('class="table-responsive my-table-wrapper"', $html); + } + + public function test_it_appends_wrapper_class_even_when_responsive_wrapper_is_disabled(): void + { + $html = $this->createRenderer()->render($this->createDefinition(), [ + 'tableResponsive' => false, + 'tableWrapperClass' => 'my-table-wrapper', + 'columnVisibility' => false, + 'export' => false, + ]); + + self::assertStringContainsString('class="my-table-wrapper"', $html); + self::assertStringNotContainsString('table-responsive my-table-wrapper', $html); + } + + public function test_it_appends_additional_table_class_without_removing_defaults(): void + { + $html = $this->createRenderer()->render($this->createDefinition(), [ + 'tableClass' => 'my-table table-sm-custom', + 'columnVisibility' => false, + 'export' => false, + ]); + + self::assertStringContainsString('table', $html); + self::assertStringContainsString('align-middle', $html); + self::assertStringContainsString('mb-0', $html); + self::assertStringContainsString('table-striped', $html); + self::assertStringContainsString('table-hover', $html); + self::assertStringContainsString('my-table table-sm-custom', $html); + } + + public function test_it_ignores_empty_additional_classes(): void + { + $html = $this->createRenderer()->render($this->createDefinition(), [ + 'rootClass' => ' ', + 'tableWrapperClass' => '', + 'tableClass' => ' ', + 'columnVisibility' => false, + 'export' => false, + ]); + + self::assertStringContainsString('class="zhortein-datatable"', $html); + self::assertStringContainsString('class="table-responsive"', $html); + self::assertStringContainsString('class="table align-middle mb-0 table-striped table-hover"', $html); + } + + private function createDefinition(): DatatableDefinition + { + $definition = new DatatableDefinition('users'); + $definition->addColumn('e.email', label: 'Email'); + + return $definition; + } + + private function createRenderer(): DatatableRenderer + { + return new DatatableRenderer($this->createTwigEnvironment()); + } + + 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; + } +}