Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion docs/basic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | `<table>` element |

Classes are appended to existing Bootstrap classes.

---

## 19. Current limitations
## 20. Current limitations

The bundle is still under active development.

Expand Down
13 changes: 13 additions & 0 deletions docs/theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 19 additions & 2 deletions templates/bootstrap/datatable.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -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']) %}
Expand All @@ -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 %}

<div
id="{{ htmlId }}"
class="zhortein-datatable"
class="zhortein-datatable{% if root_class is not empty %} {{ root_class }}{% endif %}"
data-controller="zhortein--datatable-bundle--datatable"
data-zhortein--datatable-bundle--datatable-name-value="{{ definition.name }}"
data-zhortein--datatable-bundle--datatable-fragments-url-value="{{ fragments_url }}"
Expand Down Expand Up @@ -74,7 +91,7 @@
<span>{{ 'zhortein_datatable.loading'|trans({}, 'zhortein_datatable') }}</span>
</div>

<div{% if table_responsive %} class="table-responsive"{% endif %}>
<div{% if table_wrapper_classes is not empty %} class="{{ table_wrapper_classes|join(' ') }}"{% endif %}>
<table class="{{ table_classes|join(' ') }}">
{% include '@ZhorteinDatatable/bootstrap/_header.html.twig' %}

Expand Down
110 changes: 110 additions & 0 deletions tests/Unit/Renderer/DatatableRendererAdditionalCssClassesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

namespace Zhortein\DatatableBundle\Tests\Unit\Renderer;

use PHPUnit\Framework\TestCase;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
use Zhortein\DatatableBundle\Definition\DatatableDefinition;
use Zhortein\DatatableBundle\Renderer\DatatableRenderer;

final class DatatableRendererAdditionalCssClassesTest extends TestCase
{
use TranslatableRendererTestTrait;

public function test_it_appends_additional_root_class(): void
{
$html = $this->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;
}
}