-
-
Notifications
You must be signed in to change notification settings - Fork 384
Description
Problem Statement
Currently, Symfony UX Twig Components don't support theme switching for different UI frameworks (Bootstrap, Tailwind, Bulma, etc.). Developers need to manually create separate components or use complex conditional logic within templates.
Proposed Solution
Warning
I've used the help of Claude AI to help me draft code suggestions, so bear in mind that these are not specific suggestions since I still don't have a deep understanding of Twig Components internals
1. Core Components
// src/Attribute/WithTheme.php
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class WithTheme
{
public function __construct(
public readonly string $name,
public readonly int $priority = 0
) {}
}
// src/Theme/ComponentThemeRegistry.php
class ComponentThemeRegistry
{
private array $themes = [];
private ?string $activeTheme = null;
public function registerTheme(string $name, array $components): void
{
$this->themes[$name] = $components;
}
public function setActiveTheme(string $theme): void
{
$this->activeTheme = $theme;
}
public function resolveComponent(string $componentName): string
{
if (!$this->activeTheme) {
return $componentName;
}
return $this->themes[$this->activeTheme][$componentName] ?? $componentName;
}
}
// src/Theme/ThemeAwareComponentRenderer.php
class ThemeAwareComponentRenderer extends ComponentRenderer
{
public function __construct(
private ComponentThemeRegistry $themeRegistry,
// ... other dependencies
) {}
public function createComponent(string $name, array $props = []): ComponentInterface
{
$themedName = $this->themeRegistry->resolveComponent($name);
return parent::createComponent($themedName, $props);
}
}
2. Twig Extensions
// src/Twig/ComponentThemeExtension.php
class ComponentThemeExtension extends AbstractExtension
{
public function __construct(
private ComponentThemeRegistry $themeRegistry
) {}
public function getTokenParsers(): array
{
return [
new ComponentThemeTokenParser(),
];
}
public function getFunctions(): array
{
return [
new TwigFunction('component_theme', [$this->themeRegistry, 'setActiveTheme']),
new TwigFunction('themed_component', [$this, 'renderThemedComponent']),
];
}
}
// src/Twig/ComponentThemeTokenParser.php
class ComponentThemeTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$theme = $stream->expect(Token::STRING_TYPE)->getValue();
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideBlockEnd'], true);
$stream->expect(Token::BLOCK_END_TYPE);
return new ComponentThemeNode($theme, $body, $lineno, $this->getTag());
}
public function getTag(): string
{
return 'component_theme';
}
}
3. Configuration
# config/packages/twig_component.yaml
twig_component:
themes:
bootstrap_5:
- 'components-theme/bootstrap_5'
tailwind:
- 'components-theme/tailwind'
default_theme: 'bootstrap_5'
# if it doesn't find a template, still search it in `defaults`
4. Usage Examples
{# Template-level theme switching #}
{% component_theme 'tailwind' %}
{{ component('alert', {message: 'Hello'}) }}
{{ component('button', {text: 'Submit'}) }}
{% endcomponent_theme %}
{# Function-based approach #}
{{ themed_component('alert', 'bootstrap_5', {message: 'Hello'}) }}
// Controller-level theme switching
#[WithTheme('bootstrap_5')]
class UserController extends AbstractController
{
public function index(): Response
{
// All components in this action will use bootstrap_5 theme
return $this->render('user/index.html.twig');
}
}
// Runtime theme switching
class UserController extends AbstractController
{
public function __construct(
private ComponentThemeRegistry $themeRegistry
) {}
public function profile(): Response
{
return $this->withComponentTheme('tailwind', function() {
return $this->render('user/profile.html.twig');
});
}
}
Implementation Steps
Phase 1: Core Infrastructure
- Create
ComponentThemeRegistry
service - Add
WithTheme
attribute - Modify component resolution logic
- Add basic Twig functions
Phase 2: Template Integration
- Implement
{% component_theme %}
tag - Add theme-aware component renderer
- Create configuration system
Phase 3: Advanced Features
- Theme inheritance
- Component variants
- Runtime switching helpers
- Form theme integration
Phase 4: Developer Experience
- Debug toolbar integration
- Configuration validation
- Performance optimizations
- Documentation and examples
Backward Compatibility
- All existing components work without changes
- Theme system is opt-in
- No breaking changes to current APIs
- Graceful fallback when themes aren't configured
Migration Path
Existing projects can migrate gradually:
- Install updated bundle
- Configure themes (optional)
- Start using theme features incrementally
- Existing components continue working unchanged
Performance Considerations
- Theme resolution cached during request
- Minimal overhead when themes not used
- Component registry built once at container compile time
- No impact on existing non-themed components
I would be eager to try to do a prototype to gather how it should work and contribute with a pull request, I would love to have feedback from maintainers to check the viability and to steer the contribution to the right direction.
Thanks for this awesome package and community <3