diff --git a/docs/src/docs/features/theming.md b/docs/src/docs/features/theming.md
index ccd0e792..61db4746 100644
--- a/docs/src/docs/features/theming.md
+++ b/docs/src/docs/features/theming.md
@@ -82,12 +82,33 @@ class ProductController extends AbstractController
}
```
-Last but not least, you can also overwrite the themes of the data table inside a template:
+## Applying themes in Twig
+
+Similar to forms, you can set the data table themes directly in the Twig template, by using the `data_table_theme` tag:
```twig
-
- {{ data_table(products, { themes: ['@KreyuDataTable/themes/bootstrap_5.html.twig'] }) }}
-
+{% data_table_theme products 'themes/data_table.html.twig' %}
+
+{{ data_table(products) }}
+```
+
+If you wish to use multiple themes, pass an array using the `with` keyword:
+
+```twig
+{% data_table_theme products with [
+ 'themes/data_table.html.twig',
+ '@KreyuDataTable/themes/bootstrap_5.html.twig',
+] %}
+
+{{ data_table(products) }}
+```
+
+If you wish to disable currently configured themes for the data table and **only** use given ones, add the `only` keyword after the list of data table themes:
+
+```twig
+{% data_table_theme products with ['themes/data_table.html.twig'] only %}
+
+{{ data_table(products) }}
```
## Customizing existing theme
diff --git a/src/Twig/DataTableExtension.php b/src/Twig/DataTableExtension.php
index 0a7ba6b0..6168bf2b 100755
--- a/src/Twig/DataTableExtension.php
+++ b/src/Twig/DataTableExtension.php
@@ -64,6 +64,22 @@ public function getFunctions(): array
return $functions;
}
+ public function getTokenParsers(): array
+ {
+ return [
+ new DataTableThemeTokenParser(),
+ ];
+ }
+
+ public function setDataTableThemes(DataTableView $view, array $themes, bool $only = false): void
+ {
+ if ($only) {
+ $view->vars['themes'] = $themes;
+ } else {
+ array_push($view->vars['themes'], ...$themes);
+ }
+ }
+
/**
* @param array $variables
*
diff --git a/src/Twig/DataTableThemeNode.php b/src/Twig/DataTableThemeNode.php
new file mode 100644
index 00000000..fddfbc7f
--- /dev/null
+++ b/src/Twig/DataTableThemeNode.php
@@ -0,0 +1,28 @@
+ $dataTable, 'themes' => $themes], ['only' => $only], $lineno, $tag);
+ }
+
+ public function compile(Compiler $compiler): void
+ {
+ $compiler
+ ->addDebugInfo($this)
+ ->write(sprintf('$this->env->getExtension("%s")->setDataTableThemes(', DataTableExtension::class))
+ ->subcompile($this->getNode('data_table'))
+ ->raw(', ')
+ ->subcompile($this->getNode('themes'))
+ ->raw(sprintf(", %s);\n", $this->getAttribute('only') ? 'true' : 'false'))
+ ;
+ }
+}
diff --git a/src/Twig/DataTableThemeTokenParser.php b/src/Twig/DataTableThemeTokenParser.php
new file mode 100644
index 00000000..eebfcaa3
--- /dev/null
+++ b/src/Twig/DataTableThemeTokenParser.php
@@ -0,0 +1,46 @@
+getLine();
+ $stream = $this->parser->getStream();
+
+ $dataTable = $this->parser->getExpressionParser()->parseExpression();
+ $only = false;
+
+ if ($this->parser->getStream()->test(Token::NAME_TYPE, 'with')) {
+ $this->parser->getStream()->next();
+
+ $themes = $this->parser->getExpressionParser()->parseExpression();
+
+ if ($this->parser->getStream()->nextIf(Token::NAME_TYPE, 'only')) {
+ $only = true;
+ }
+ } else {
+ $themes = new ArrayExpression([], $stream->getCurrent()->getLine());
+
+ do {
+ $themes->addElement($this->parser->getExpressionParser()->parseExpression());
+ } while (!$stream->test(Token::BLOCK_END_TYPE));
+ }
+
+ $stream->expect(Token::BLOCK_END_TYPE);
+
+ return new DataTableThemeNode($dataTable, $themes, $lineno, $this->getTag(), $only);
+ }
+
+ public function getTag(): string
+ {
+ return 'data_table_theme';
+ }
+}
diff --git a/tests/Unit/Twig/DataTableExtensionTest.php b/tests/Unit/Twig/DataTableExtensionTest.php
new file mode 100644
index 00000000..2e7e5af1
--- /dev/null
+++ b/tests/Unit/Twig/DataTableExtensionTest.php
@@ -0,0 +1,42 @@
+vars['themes'] = ['foo'];
+
+ $this->createExtension()->setDataTableThemes($view, ['bar']);
+
+ $this->assertEquals(['foo', 'bar'], $view->vars['themes']);
+ }
+
+ public function testSetDataTableThemesWithOnly(): void
+ {
+ $view = new DataTableView();
+ $view->vars['themes'] = ['foo'];
+
+ $this->createExtension()->setDataTableThemes($view, ['bar'], true);
+
+ $this->assertEquals(['bar'], $view->vars['themes']);
+ }
+
+ private function createExtension(): DataTableExtension
+ {
+ return new DataTableExtension(
+ $this->createStub(ColumnSortUrlGeneratorInterface::class),
+ $this->createStub(FilterClearUrlGeneratorInterface::class),
+ );
+ }
+}
diff --git a/tests/Unit/Twig/DataTableThemeNodeTest.php b/tests/Unit/Twig/DataTableThemeNodeTest.php
new file mode 100644
index 00000000..fc607611
--- /dev/null
+++ b/tests/Unit/Twig/DataTableThemeNodeTest.php
@@ -0,0 +1,111 @@
+assertEquals($dataTable, $node->getNode('data_table'));
+ $this->assertEquals($themes, $node->getNode('themes'));
+ $this->assertFalse($node->getAttribute('only'));
+ }
+
+ #[DataProvider('provideCompileCases')]
+ public function testCompile(DataTableThemeNode $node, array $expected)
+ {
+ $environment = new Environment($this->createMock(LoaderInterface::class));
+
+ $extension = $this->createMock(DataTableExtension::class);
+
+ $environment->addExtension($extension);
+
+ $compiler = new Compiler($environment);
+
+ // In Twig 2, the ["foo", "bar"] is parsed with as [0 => "foo", 1 => "bar"].
+ // In Twig 3, the ["foo", "bar"] is parsed with as ["foo", "bar"].
+ // For compatibility, we are checking whether the compiled value is equal to one of the expected values.
+ $this->assertThat(trim($compiler->compile($node)->getSource()), $this->logicalOr(
+ $this->equalTo($expected[0]),
+ $this->equalTo($expected[1]),
+ ));
+ }
+
+ public static function provideCompileCases(): iterable
+ {
+ yield 'single theme' => [
+ new DataTableThemeNode(
+ new NameExpression('data_table', 0),
+ new ArrayExpression([
+ new ConstantExpression(0, 0),
+ new ConstantExpression('foo', 0),
+ ], 0),
+ 0,
+ 'data_table_theme',
+ false,
+ ), [
+ '$this->env->getExtension("Kreyu\\Bundle\\DataTableBundle\\Twig\\DataTableExtension")->setDataTableThemes(($context["data_table"] ?? null), ["foo"], false);',
+ '$this->env->getExtension("Kreyu\\Bundle\\DataTableBundle\\Twig\\DataTableExtension")->setDataTableThemes(($context["data_table"] ?? null), [0 => "foo"], false);',
+ ],
+ ];
+
+ yield 'multiple themes without only' => [
+ new DataTableThemeNode(
+ new NameExpression('data_table', 0),
+ new ArrayExpression([
+ new ConstantExpression(0, 0),
+ new ConstantExpression('foo', 0),
+ new ConstantExpression(1, 0),
+ new ConstantExpression('bar', 0),
+ ], 0),
+ 0,
+ 'data_table_theme',
+ false,
+ ), [
+ '$this->env->getExtension("Kreyu\\Bundle\\DataTableBundle\\Twig\\DataTableExtension")->setDataTableThemes(($context["data_table"] ?? null), ["foo", "bar"], false);',
+ '$this->env->getExtension("Kreyu\\Bundle\\DataTableBundle\\Twig\\DataTableExtension")->setDataTableThemes(($context["data_table"] ?? null), [0 => "foo", 1 => "bar"], false);',
+ ],
+ ];
+
+ yield 'multiple themes with only' => [
+ new DataTableThemeNode(
+ new NameExpression('data_table', 0),
+ new ArrayExpression([
+ new ConstantExpression(0, 0),
+ new ConstantExpression('foo', 0),
+ new ConstantExpression(1, 0),
+ new ConstantExpression('bar', 0),
+ ], 0),
+ 0,
+ 'data_table_theme',
+ true,
+ ), [
+ '$this->env->getExtension("Kreyu\\Bundle\\DataTableBundle\\Twig\\DataTableExtension")->setDataTableThemes(($context["data_table"] ?? null), ["foo", "bar"], true);',
+ '$this->env->getExtension("Kreyu\\Bundle\\DataTableBundle\\Twig\\DataTableExtension")->setDataTableThemes(($context["data_table"] ?? null), [0 => "foo", 1 => "bar"], true);',
+ ],
+ ];
+ }
+}
diff --git a/tests/Unit/Twig/DataTableThemeTokenParserTest.php b/tests/Unit/Twig/DataTableThemeTokenParserTest.php
new file mode 100644
index 00000000..63b4acee
--- /dev/null
+++ b/tests/Unit/Twig/DataTableThemeTokenParserTest.php
@@ -0,0 +1,84 @@
+createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]);
+ $env->addTokenParser(new DataTableThemeTokenParser());
+
+ $source = new Source($source, '');
+ $stream = $env->tokenize($source);
+ $parser = new Parser($env);
+
+ $expected->setSourceContext($source);
+
+ $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode('0'));
+ }
+
+ public static function provideCompileCases(): iterable
+ {
+ yield 'single theme' => [
+ '{% data_table_theme data_table "foo" %}',
+ new DataTableThemeNode(
+ new NameExpression('data_table', 1),
+ new ArrayExpression([
+ new ConstantExpression(0, 1),
+ new ConstantExpression('foo', 1),
+ ], 1),
+ 1,
+ 'data_table_theme',
+ false,
+ ),
+ ];
+
+ yield 'multiple themes without only' => [
+ '{% data_table_theme data_table with ["foo", "bar"] %}',
+ new DataTableThemeNode(
+ new NameExpression('data_table', 1),
+ new ArrayExpression([
+ new ConstantExpression(0, 1),
+ new ConstantExpression('foo', 1),
+ new ConstantExpression(1, 1),
+ new ConstantExpression('bar', 1),
+ ], 1),
+ 1,
+ 'data_table_theme',
+ false,
+ ),
+ ];
+
+ yield 'multiple themes with only' => [
+ '{% data_table_theme data_table with ["foo", "bar"] only %}',
+ new DataTableThemeNode(
+ new NameExpression('data_table', 1),
+ new ArrayExpression([
+ new ConstantExpression(0, 1),
+ new ConstantExpression('foo', 1),
+ new ConstantExpression(1, 1),
+ new ConstantExpression('bar', 1),
+ ], 1),
+ 1,
+ 'data_table_theme',
+ true,
+ ),
+ ];
+ }
+}