diff --git a/docs/specs/examples/AlertComponent.html b/docs/specs/examples/AlertComponent.html
new file mode 100644
index 0000000..13746e1
--- /dev/null
+++ b/docs/specs/examples/AlertComponent.html
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Show Details
+
+ Warning details
+
+
+
+
+
+
+
+
+ Show Stack Trace
+
+ Stack trace
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/specs/examples/AlertComponent.java b/docs/specs/examples/AlertComponent.java
new file mode 100644
index 0000000..3ea32c3
--- /dev/null
+++ b/docs/specs/examples/AlertComponent.java
@@ -0,0 +1,75 @@
+package de.tschuehly.example.fragments;
+
+import de.tschuehly.spring.viewcomponent.core.component.ViewComponent;
+import de.tschuehly.spring.viewcomponent.thymeleaf.ViewContext;
+
+/**
+ * Advanced example showing fragment rendering for alert messages.
+ *
+ * Demonstrates:
+ * - Different data requirements for different contexts (e.g., ErrorAlert has stackTrace)
+ * - Compile-time safety (can't create ErrorAlert without stackTrace)
+ * - Single template with multiple presentation variants
+ */
+@ViewComponent
+public class AlertComponent {
+
+ /**
+ * Informational alert - simple message display
+ */
+ public record InfoAlert(String message) implements ViewContext {}
+
+ /**
+ * Warning alert - message with additional details
+ */
+ public record WarningAlert(String message, String details) implements ViewContext {}
+
+ /**
+ * Error alert - message with stack trace
+ */
+ public record ErrorAlert(String message, String stackTrace) implements ViewContext {}
+
+ /**
+ * Success alert - simple message for successful operations
+ */
+ public record SuccessAlert(String message) implements ViewContext {}
+
+ public InfoAlert info(String message) {
+ return new InfoAlert(message);
+ }
+
+ public WarningAlert warning(String message, String details) {
+ return new WarningAlert(message, details);
+ }
+
+ public ErrorAlert error(String message, String stackTrace) {
+ return new ErrorAlert(message, stackTrace);
+ }
+
+ public SuccessAlert success(String message) {
+ return new SuccessAlert(message);
+ }
+
+ /**
+ * Convenience method to create error alerts from exceptions
+ */
+ public ErrorAlert error(String message, Exception exception) {
+ var stackTrace = buildStackTrace(exception);
+ return new ErrorAlert(message, stackTrace);
+ }
+
+ private String buildStackTrace(Exception exception) {
+ var sb = new StringBuilder();
+ sb.append(exception.getClass().getName()).append(": ").append(exception.getMessage()).append("\n");
+
+ for (var element : exception.getStackTrace()) {
+ sb.append(" at ").append(element.toString()).append("\n");
+ }
+
+ if (exception.getCause() != null) {
+ sb.append("Caused by: ").append(buildStackTrace((Exception) exception.getCause()));
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/docs/specs/examples/ButtonComponent.html b/docs/specs/examples/ButtonComponent.html
new file mode 100644
index 0000000..bf4e1a2
--- /dev/null
+++ b/docs/specs/examples/ButtonComponent.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/specs/examples/ButtonComponent.java b/docs/specs/examples/ButtonComponent.java
new file mode 100644
index 0000000..4f4e0dc
--- /dev/null
+++ b/docs/specs/examples/ButtonComponent.java
@@ -0,0 +1,61 @@
+package de.tschuehly.example.fragments;
+
+import de.tschuehly.spring.viewcomponent.core.component.ViewComponent;
+import de.tschuehly.spring.viewcomponent.thymeleaf.ViewContext;
+
+/**
+ * Example ViewComponent demonstrating fragment rendering with multiple ViewContext types.
+ *
+ * This single component can render different button variants (primary, secondary, danger)
+ * based on the ViewContext type returned from the render methods.
+ */
+@ViewComponent
+public class ButtonComponent {
+
+ /**
+ * ViewContext for primary action buttons.
+ */
+ public record PrimaryButton(
+ String label,
+ String action
+ ) implements ViewContext {}
+
+ /**
+ * ViewContext for secondary/alternative action buttons.
+ */
+ public record SecondaryButton(
+ String label,
+ String action
+ ) implements ViewContext {}
+
+ /**
+ * ViewContext for dangerous/destructive action buttons.
+ * Includes a confirmation message that will be shown before execution.
+ */
+ public record DangerButton(
+ String label,
+ String action,
+ String confirmMessage
+ ) implements ViewContext {}
+
+ /**
+ * Renders a primary button for main actions.
+ */
+ public PrimaryButton primary(String label, String action) {
+ return new PrimaryButton(label, action);
+ }
+
+ /**
+ * Renders a secondary button for alternative actions.
+ */
+ public SecondaryButton secondary(String label, String action) {
+ return new SecondaryButton(label, action);
+ }
+
+ /**
+ * Renders a danger button for destructive actions.
+ */
+ public DangerButton danger(String label, String action, String confirmMessage) {
+ return new DangerButton(label, action, confirmMessage);
+ }
+}
diff --git a/docs/specs/examples/ButtonComponent.jte b/docs/specs/examples/ButtonComponent.jte
new file mode 100644
index 0000000..07742ce
--- /dev/null
+++ b/docs/specs/examples/ButtonComponent.jte
@@ -0,0 +1,32 @@
+@import de.tschuehly.example.fragments.ButtonComponent.*
+
+<%--
+ ButtonComponent JTE template with type-based conditional rendering.
+
+ JTE natively supports instanceof checks with pattern matching,
+ providing compile-time type safety.
+--%>
+
+@if(model instanceof PrimaryButton primaryButton)
+
+@elseif(model instanceof SecondaryButton secondaryButton)
+
+@elseif(model instanceof DangerButton dangerButton)
+
+@else
+ <%-- Fallback for unknown context types --%>
+ Unknown button type: ${model.getClass().getSimpleName()}
+@endif
diff --git a/docs/specs/examples/ExampleController.java b/docs/specs/examples/ExampleController.java
new file mode 100644
index 0000000..4e351c4
--- /dev/null
+++ b/docs/specs/examples/ExampleController.java
@@ -0,0 +1,68 @@
+package de.tschuehly.example.fragments;
+
+import de.tschuehly.spring.viewcomponent.core.IViewContext;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+
+/**
+ * Example controller demonstrating fragment rendering usage.
+ *
+ * Different endpoints return different ViewContext types from the same ViewComponent,
+ * causing different fragments to be rendered.
+ */
+@Controller
+public class ExampleController {
+
+ private final ButtonComponent buttonComponent;
+
+ public ExampleController(ButtonComponent buttonComponent) {
+ this.buttonComponent = buttonComponent;
+ }
+
+ /**
+ * Returns a primary button - renders the PrimaryButton fragment
+ */
+ @GetMapping("/button/submit")
+ IViewContext submitButton() {
+ return buttonComponent.primary("Submit Form", "/api/submit");
+ }
+
+ /**
+ * Returns a secondary button - renders the SecondaryButton fragment
+ */
+ @GetMapping("/button/cancel")
+ IViewContext cancelButton() {
+ return buttonComponent.secondary("Cancel", "/api/cancel");
+ }
+
+ /**
+ * Returns a danger button - renders the DangerButton fragment
+ */
+ @GetMapping("/button/delete")
+ IViewContext deleteButton() {
+ return buttonComponent.danger(
+ "Delete Account",
+ "/api/delete-account",
+ "Are you sure you want to delete your account? This action cannot be undone."
+ );
+ }
+
+ /**
+ * Example showing nested component rendering with fragments.
+ *
+ * The page component can receive different button variants
+ * as child components.
+ */
+ @GetMapping("/page/submit-form")
+ IViewContext submitFormPage() {
+ var submitButton = buttonComponent.primary("Submit", "/submit");
+ var cancelButton = buttonComponent.secondary("Cancel", "/cancel");
+
+ // In a real application, you might have a PageComponent that accepts
+ // button components as parameters
+ // return pageComponent.render(submitButton, cancelButton);
+
+ return submitButton; // Simplified for this example
+ }
+}
diff --git a/docs/specs/examples/README.md b/docs/specs/examples/README.md
new file mode 100644
index 0000000..a143fcb
--- /dev/null
+++ b/docs/specs/examples/README.md
@@ -0,0 +1,442 @@
+# Fragment Rendering Examples
+
+This directory contains example implementations demonstrating the proposed fragment rendering feature for Spring View Component.
+
+## Overview
+
+Fragment rendering allows a single ViewComponent to have **multiple ViewContext implementations**, with templates that conditionally render different fragments based on the ViewContext type.
+
+## Examples
+
+### 1. ButtonComponent
+
+**File:** `ButtonComponent.java`, `ButtonComponent.html`, `ButtonComponent.jte`
+
+Demonstrates basic fragment rendering with button variants:
+- `PrimaryButton` - Main action buttons
+- `SecondaryButton` - Alternative action buttons
+- `DangerButton` - Destructive action buttons with confirmation
+
+**Key Concepts:**
+- Multiple ViewContext records in one component
+- Type-safe fragment selection
+- Different template syntax for Thymeleaf vs JTE
+
+### 2. AlertComponent
+
+**File:** `AlertComponent.java`, `AlertComponent.html`
+
+Demonstrates advanced fragment rendering with alert messages:
+- `InfoAlert` - Informational messages
+- `WarningAlert` - Warnings with expandable details
+- `ErrorAlert` - Errors with stack traces
+- `SuccessAlert` - Success messages
+
+**Key Concepts:**
+- Different data requirements per context type
+- Compile-time safety (ErrorAlert requires stackTrace)
+- Complex template structures with expandable sections
+
+### 3. ExampleController
+
+**File:** `ExampleController.java`
+
+Demonstrates controller integration:
+- Different endpoints returning different ViewContext types
+- Same ViewComponent rendering different fragments
+- Nested component composition
+
+## How It Works
+
+### Thymeleaf Syntax
+
+```html
+
+
+
+
+
+```
+
+**Behavior:**
+1. The `view:context-root` marks the fragment container
+2. Each child element with `view:context` is a fragment
+3. Only the fragment matching the current ViewContext type is rendered
+4. Non-matching fragments are removed from the output
+
+### JTE Syntax
+
+```java
+@import de.example.ButtonComponent.*
+
+@if(model instanceof PrimaryButton primaryButton)
+
+@elseif(model instanceof SecondaryButton secondaryButton)
+
+@endif
+```
+
+**Behavior:**
+1. Standard JTE `instanceof` checks with pattern matching
+2. Compile-time type safety
+3. Native Java control flow
+
+## Benefits
+
+### 1. Reduced Code Duplication
+
+**Before:**
+```java
+@ViewComponent
+public class PrimaryButtonComponent { ... }
+
+@ViewComponent
+public class SecondaryButtonComponent { ... }
+
+@ViewComponent
+public class DangerButtonComponent { ... }
+```
+
+**After:**
+```java
+@ViewComponent
+public class ButtonComponent {
+ public record PrimaryButton(...) implements ViewContext {}
+ public record SecondaryButton(...) implements ViewContext {}
+ public record DangerButton(...) implements ViewContext {}
+}
+```
+
+### 2. Type Safety
+
+**Before (error-prone):**
+```java
+public record ButtonContext(String variant, ...) { }
+
+// Easy to make typos
+buttonComponent.render("primry", ...); // No compile error!
+```
+
+**After (compile-time checked):**
+```java
+// Typo caught at compile time
+buttonComponent.primary(...); // ✓ Type-safe
+buttonComponent.primry(...); // ✗ Compile error
+```
+
+### 3. Better IDE Support
+
+- Autocomplete for ViewContext types
+- Refactoring support (rename ViewContext → updates templates)
+- Type checking in templates (JTE)
+
+### 4. Clearer Intent
+
+```java
+// Clear intent: this is a danger button
+var deleteButton = buttonComponent.danger(
+ "Delete",
+ "/delete",
+ "Are you sure?"
+);
+
+// vs unclear intent with generic context
+var deleteButton = buttonComponent.render(
+ "danger", // What does "danger" mean?
+ "Delete",
+ "/delete",
+ "Are you sure?"
+);
+```
+
+## Comparison with Current Approach
+
+| Aspect | Current Approach | Fragment Rendering |
+|--------|-----------------|-------------------|
+| **Components** | One ViewContext per ViewComponent | Multiple ViewContexts per ViewComponent |
+| **Type Safety** | Separate components or string flags | Type-based fragment selection |
+| **Templates** | One template per component OR complex conditionals | One template with multiple fragments |
+| **Refactoring** | Rename component class | Rename ViewContext record |
+| **Testing** | Test each component separately | Test each ViewContext type |
+| **Code Volume** | Higher (more component files) | Lower (one component, multiple contexts) |
+
+## Migration Example
+
+### Before (Multiple Components)
+
+```java
+// Three separate components
+@ViewComponent
+public class PrimaryButtonComponent {
+ public record Context(String label, String action) implements ViewContext {}
+
+ public Context render(String label, String action) {
+ return new Context(label, action);
+ }
+}
+
+@ViewComponent
+public class SecondaryButtonComponent {
+ public record Context(String label, String action) implements ViewContext {}
+
+ public Context render(String label, String action) {
+ return new Context(label, action);
+ }
+}
+
+// Controller
+@GetMapping("/submit")
+ViewContext submitButton() {
+ return primaryButtonComponent.render("Submit", "/submit");
+}
+
+@GetMapping("/cancel")
+ViewContext cancelButton() {
+ return secondaryButtonComponent.render("Cancel", "/cancel");
+}
+```
+
+### After (Fragment Rendering)
+
+```java
+// One component with multiple contexts
+@ViewComponent
+public class ButtonComponent {
+ public record PrimaryButton(String label, String action) implements ViewContext {}
+ public record SecondaryButton(String label, String action) implements ViewContext {}
+
+ public PrimaryButton primary(String label, String action) {
+ return new PrimaryButton(label, action);
+ }
+
+ public SecondaryButton secondary(String label, String action) {
+ return new SecondaryButton(label, action);
+ }
+}
+
+// Controller
+@GetMapping("/submit")
+ViewContext submitButton() {
+ return buttonComponent.primary("Submit", "/submit");
+}
+
+@GetMapping("/cancel")
+ViewContext cancelButton() {
+ return buttonComponent.secondary("Cancel", "/cancel");
+}
+```
+
+## Testing
+
+### Testing Individual Fragments
+
+```java
+@SpringBootTest
+class ButtonComponentTest {
+
+ @Autowired
+ private ButtonComponent buttonComponent;
+
+ @Autowired
+ private TemplateRenderer templateRenderer; // Hypothetical renderer
+
+ @Test
+ void shouldRenderPrimaryButton() {
+ var button = buttonComponent.primary("Submit", "/submit");
+ var html = templateRenderer.render(button);
+
+ assertThat(html).contains("btn-primary");
+ assertThat(html).contains("Submit");
+ assertThat(html).doesNotContain("btn-secondary");
+ }
+
+ @Test
+ void shouldRenderDangerButtonWithConfirmation() {
+ var button = buttonComponent.danger("Delete", "/delete", "Are you sure?");
+ var html = templateRenderer.render(button);
+
+ assertThat(html).contains("btn-danger");
+ assertThat(html).contains("Delete");
+ assertThat(html).contains("confirm('Are you sure?')");
+ }
+}
+```
+
+### Integration Testing
+
+```java
+@SpringBootTest
+@AutoConfigureMockMvc
+class ButtonControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ void shouldRenderPrimaryButtonFragment() throws Exception {
+ mockMvc.perform(get("/button/submit"))
+ .andExpect(status().isOk())
+ .andExpect(content().string(containsString("btn-primary")))
+ .andExpect(content().string(not(containsString("btn-secondary"))));
+ }
+
+ @Test
+ void shouldRenderDangerButtonFragment() throws Exception {
+ mockMvc.perform(get("/button/delete"))
+ .andExpect(status().isOk())
+ .andExpect(content().string(containsString("btn-danger")))
+ .andExpect(content().string(containsString("confirm(")));
+ }
+}
+```
+
+## Advanced Patterns
+
+### 1. Nested Fragment Components
+
+```java
+@ViewComponent
+public class PageComponent {
+ public record Page(ViewContext header, ViewContext content, ViewContext footer)
+ implements ViewContext {}
+
+ public Page render(ViewContext header, ViewContext content, ViewContext footer) {
+ return new Page(header, content, footer);
+ }
+}
+
+// Usage
+pageComponent.render(
+ headerComponent.admin("Admin Panel"),
+ contentComponent.dashboard(),
+ footerComponent.standard()
+)
+```
+
+### 2. Sealed Interfaces (Java 17+)
+
+```java
+@ViewComponent
+public class ButtonComponent {
+
+ // Exhaustiveness checking with sealed types
+ public sealed interface Button extends ViewContext
+ permits PrimaryButton, SecondaryButton, DangerButton {}
+
+ public record PrimaryButton(String label, String action) implements Button {}
+ public record SecondaryButton(String label, String action) implements Button {}
+ public record DangerButton(String label, String action, String confirm) implements Button {}
+
+ // Compiler ensures all cases are handled
+ public Button create(String variant, String label, String action) {
+ return switch (variant) {
+ case "primary" -> new PrimaryButton(label, action);
+ case "secondary" -> new SecondaryButton(label, action);
+ case "danger" -> new DangerButton(label, action, "Are you sure?");
+ // No default needed - compiler knows all cases are covered
+ };
+ }
+}
+```
+
+### 3. Fragment with Shared Logic
+
+```java
+@ViewComponent
+public class FormFieldComponent {
+
+ // Base interface for shared properties
+ sealed interface Field extends ViewContext {
+ String name();
+ String label();
+ String value();
+ }
+
+ public record TextField(String name, String label, String value)
+ implements Field {}
+
+ public record TextFieldWithError(String name, String label, String value, String error)
+ implements Field {}
+
+ public record TextArea(String name, String label, String value, int rows)
+ implements Field {}
+}
+```
+
+## Best Practices
+
+### 1. Keep Fragments Focused
+
+Each fragment should represent a distinct presentation variant, not just minor differences:
+
+**Good:**
+```java
+public record PrimaryButton(...) implements ViewContext {}
+public record DangerButton(...) implements ViewContext {}
+```
+
+**Bad (use template conditionals instead):**
+```java
+public record ButtonWithIcon(...) implements ViewContext {}
+public record ButtonWithoutIcon(...) implements ViewContext {}
+```
+
+### 2. Use Descriptive Names
+
+ViewContext names become part of the template syntax, so use clear, descriptive names:
+
+**Good:**
+```java
+public record ErrorAlert(String message, String stackTrace) implements ViewContext {}
+```
+
+**Bad:**
+```java
+public record Alert1(String message, String stackTrace) implements ViewContext {}
+```
+
+### 3. Leverage Type Safety
+
+Use different fields for different contexts to enforce correct usage:
+
+**Good:**
+```java
+public record DangerButton(String label, String action, String confirmMessage)
+ implements ViewContext {}
+```
+
+**Bad:**
+```java
+public record Button(String label, String action, String confirmMessage /* nullable */)
+ implements ViewContext {}
+```
+
+### 4. Document Fragment Purpose
+
+Add Javadoc to explain when each context should be used:
+
+```java
+/**
+ * Renders a danger button for destructive actions.
+ *
+ * @param confirmMessage Message shown in confirmation dialog before action
+ */
+public DangerButton danger(String label, String action, String confirmMessage) {
+ return new DangerButton(label, action, confirmMessage);
+}
+```
+
+## See Also
+
+- [Fragment Rendering Specification](../fragment-rendering-spec.md) - Full technical specification
+- [Spring View Component Documentation](https://github.com/tschuehly/spring-view-component)
+- [Thymeleaf Fragments](https://www.thymeleaf.org/doc/articles/layouts.html)
+- [JTE Documentation](https://jte.gg/)
diff --git a/docs/specs/fragment-rendering-spec.md b/docs/specs/fragment-rendering-spec.md
new file mode 100644
index 0000000..35751d9
--- /dev/null
+++ b/docs/specs/fragment-rendering-spec.md
@@ -0,0 +1,1229 @@
+# Fragment Rendering Specification
+
+**Version:** 2.0-DRAFT
+**Date:** 2026-01-01
+**Author:** Spring View Component Team
+
+## Table of Contents
+
+1. [Overview](#overview)
+2. [Problem Statement](#problem-statement)
+3. [Goals](#goals)
+4. [Non-Goals](#non-goals)
+5. [Proposed Solution](#proposed-solution)
+6. [Technical Design](#technical-design)
+7. [Examples](#examples)
+8. [Implementation Considerations](#implementation-considerations)
+9. [Migration Path](#migration-path)
+10. [Alternatives Considered](#alternatives-considered)
+11. [Open Questions](#open-questions)
+
+---
+
+## Overview
+
+This specification proposes adding **fragment rendering** capabilities to Spring View Component. The goal is to enable:
+
+- **Multiple ViewContext implementations** for a single ViewComponent
+- **Presence-based conditional rendering** within templates
+- **Flexible composition** with `MultiViewContext`
+- **Reduced code duplication** for components with variations
+
+Two complementary patterns are introduced:
+
+1. **Type-Based Variants** - One ViewContext selected, one fragment renders (e.g., button variants)
+2. **Composition with MultiViewContext** - Multiple ViewContexts, multiple fragments render (e.g., page layouts)
+
+---
+
+## Problem Statement
+
+### Current Limitations
+
+1. **One ViewContext per ViewComponent**: Each ViewComponent currently supports one ViewContext implementation, leading to:
+ - Code duplication when creating similar components with slight variations
+ - Proliferation of ViewComponent classes for related functionality
+ - Difficulty managing component families (e.g., buttons: primary, secondary, danger)
+
+2. **Optional Sections Require Optional<>**: Conditional sections need verbose Optional handling:
+ ```java
+ record Page(ViewContext header, Optional footer) implements ViewContext {}
+ ```
+
+ Template:
+ ```html
+
+ ```
+
+3. **No Template-Level Conditional Rendering**: Conditional logic must be:
+ - Implemented in template engine syntax (`th:if`, JTE conditionals)
+ - Difficult to type-check at compile time
+
+### Real-World Use Cases
+
+#### Use Case 1: Button Component with Variants
+
+**Current approach:**
+```java
+@ViewComponent
+public class PrimaryButtonComponent { ... }
+
+@ViewComponent
+public class SecondaryButtonComponent { ... }
+
+@ViewComponent
+public class DangerButtonComponent { ... }
+```
+
+**Desired:** One `ButtonComponent` with multiple ViewContext types for variants.
+
+#### Use Case 2: Page Layout with Optional Sections
+
+**Current approach:**
+```java
+record Page(
+ ViewContext header,
+ ViewContext content,
+ Optional footer // Verbose!
+) implements ViewContext {}
+```
+
+**Desired:** Clean composition without `Optional<>`.
+
+---
+
+## Goals
+
+1. **Enable Multiple ViewContext Implementations** - Allow a single ViewComponent to return different ViewContext types
+2. **Presence-Based Fragment Selection** - Render fragments based on which ViewContexts are present in the model
+3. **Clean Composition API** - `MultiViewContext` for combining multiple sections
+4. **Type Safety** - Leverage Java/Kotlin type system
+5. **Template Engine Agnostic** - Support JTE, KTE, and Thymeleaf
+6. **Backward Compatibility** - Existing ViewComponents continue to work unchanged
+7. **Runtime Validation** - Clear error messages when constraints are violated
+
+---
+
+## Non-Goals
+
+1. **Cross-Component Fragments** - ViewContexts from different components in one MultiViewContext (use nested components)
+2. **Template Scanning/Startup Validation** - Runtime validation is sufficient
+3. **Compile-Time Validation** - Annotation processor complexity not justified
+4. **Fragment Inheritance** - Can be added in future if needed
+5. **Fragment Parameters** - Use ViewContext properties instead
+
+---
+
+## Proposed Solution
+
+### Pattern 1: Type-Based Variants
+
+**One ViewContext in model → One fragment renders**
+
+```java
+@ViewComponent
+public class ButtonComponent {
+
+ public record PrimaryButton(String label, String action) implements ViewContext {}
+ public record SecondaryButton(String label, String action) implements ViewContext {}
+ public record DangerButton(String label, String action, String confirmMsg) implements ViewContext {}
+
+ public PrimaryButton primary(String label, String action) {
+ return new PrimaryButton(label, action);
+ }
+
+ public SecondaryButton secondary(String label, String action) {
+ return new SecondaryButton(label, action);
+ }
+
+ public DangerButton danger(String label, String action, String confirmMsg) {
+ return new DangerButton(label, action, confirmMsg);
+ }
+}
+```
+
+**Thymeleaf Template:**
+```html
+
+
+
+
+
+
+
+```
+
+**Rendering:**
+- If controller returns `PrimaryButton` → only first fragment renders
+- If controller returns `DangerButton` → only third fragment renders
+
+### Pattern 2: Composition with MultiViewContext
+
+**Multiple ViewContexts in model → Multiple fragments render**
+
+```java
+@ViewComponent
+public class PageComponent {
+
+ public record Header(String title) implements ViewContext {}
+ public record Content(String body) implements ViewContext {}
+ public record Footer(String text) implements ViewContext {}
+
+ public ViewContext withFooter(String title, String body, String footer) {
+ return MultiViewContext.of(
+ new Header(title),
+ new Content(body),
+ new Footer(footer)
+ );
+ }
+
+ public ViewContext withoutFooter(String title, String body) {
+ return MultiViewContext.of(
+ new Header(title),
+ new Content(body)
+ // No Footer - won't render!
+ );
+ }
+}
+```
+
+**Template:**
+```html
+
+
+
+
+
+ Content
+
+
+
+```
+
+**Rendering:**
+- `withFooter()` → all three fragments render
+- `withoutFooter()` → only header and content render (footer removed)
+
+### Pattern 3: Shared Properties with Sealed Interfaces
+
+**Best practice for fragments with common properties:**
+
+```java
+@ViewComponent
+public class AlertComponent {
+
+ // Base interface for shared properties
+ sealed interface Alert extends ViewContext {
+ String message();
+ }
+
+ public record InfoAlert(String message) implements Alert {}
+ public record WarningAlert(String message, String details) implements Alert {}
+ public record ErrorAlert(String message, String stackTrace) implements Alert {}
+
+ public InfoAlert info(String message) {
+ return new InfoAlert(message);
+ }
+
+ public WarningAlert warning(String message, String details) {
+ return new WarningAlert(message, details);
+ }
+
+ public ErrorAlert error(String message, String stackTrace) {
+ return new ErrorAlert(message, stackTrace);
+ }
+}
+```
+
+**Template:**
+```html
+
+
+
+
+
+ Info
+
+
+
+
+
+```
+
+**Benefits:**
+- Shared properties accessible via `${alert.message}` (works in all fragments)
+- Specific properties via `${errorAlert.stackTrace}` (type-safe)
+- Sealed interface enables exhaustiveness checking (Java 17+)
+
+---
+
+## Technical Design
+
+### 1. Core: MultiViewContext
+
+Framework-provided class for composition:
+
+```java
+package de.tschuehly.spring.viewcomponent.core;
+
+public final class MultiViewContext implements IViewContext {
+ private final List contexts;
+
+ private MultiViewContext(IViewContext... contexts) {
+ this.contexts = List.of(contexts);
+ }
+
+ /**
+ * Creates a MultiViewContext from multiple ViewContext instances.
+ *
+ * @param contexts ViewContext instances (nulls are filtered out)
+ * @return MultiViewContext containing all non-null contexts
+ * @throws ViewComponentException if contexts are from different components
+ */
+ public static MultiViewContext of(IViewContext... contexts) {
+ // Filter out nulls for easier conditional composition
+ IViewContext[] filtered = Arrays.stream(contexts)
+ .filter(Objects::nonNull)
+ .toArray(IViewContext[]::new);
+
+ validateSameComponent(filtered);
+ return new MultiViewContext(filtered);
+ }
+
+ public List getContexts() {
+ return contexts;
+ }
+
+ private static void validateSameComponent(IViewContext... contexts) {
+ if (contexts.length == 0) {
+ throw new ViewComponentException(
+ "MultiViewContext requires at least one non-null ViewContext"
+ );
+ }
+
+ Class> expectedComponent = contexts[0].getClass().getEnclosingClass();
+
+ if (expectedComponent == null) {
+ throw new ViewComponentException(
+ "ViewContext " + contexts[0].getClass().getSimpleName() +
+ " must be an inner class of a @ViewComponent. " +
+ "Did you forget to define it as a nested record/class?"
+ );
+ }
+
+ // Validate all contexts are from the same component
+ for (IViewContext ctx : contexts) {
+ Class> actualComponent = ctx.getClass().getEnclosingClass();
+
+ if (actualComponent != expectedComponent) {
+ throw new ViewComponentException(
+ "All ViewContexts in MultiViewContext must be from the same ViewComponent. " +
+ "Expected: " + expectedComponent.getSimpleName() + ", " +
+ "found: " + actualComponent.getSimpleName() + " " +
+ "for ViewContext: " + ctx.getClass().getSimpleName() + ". " +
+ "To compose ViewContexts from different components, use nested components instead."
+ );
+ }
+ }
+ }
+}
+```
+
+**Why same component requirement:**
+- Simple template resolution (one template)
+- Clear ownership and organization
+- Cross-component composition uses existing `view:component` directive
+
+### 2. Template Resolution
+
+**Updated ViewContextMethodReturnValueHandler:**
+
+```kotlin
+@Component
+class ViewContextMethodReturnValueHandler : HandlerMethodReturnValueHandler {
+
+ override fun supportsReturnType(returnType: MethodParameter): Boolean {
+ return IViewContext::class.java.isAssignableFrom(returnType.parameterType)
+ }
+
+ override fun handleReturnValue(
+ returnValue: Any?,
+ returnType: MethodParameter,
+ mavContainer: ModelAndViewContainer,
+ webRequest: NativeWebRequest
+ ) {
+ val viewContext = returnValue as IViewContext
+
+ if (viewContext is MultiViewContext) {
+ // Use first context to resolve template (all from same component)
+ val firstContext = viewContext.getContexts().first()
+ mavContainer.view = IViewContext.getViewComponentTemplateWithoutSuffix(firstContext)
+
+ // Add each context to model with lowercase simple name
+ viewContext.getContexts().forEach { ctx ->
+ val variableName = ctx.javaClass.simpleName
+ .replaceFirstChar { it.lowercase() }
+ mavContainer.addAttribute(variableName, ctx)
+ }
+ } else {
+ // Single context - existing behavior
+ mavContainer.view = IViewContext.getViewComponentTemplateWithoutSuffix(viewContext)
+ val variableName = viewContext.javaClass.simpleName
+ .replaceFirstChar { it.lowercase() }
+ mavContainer.addAttribute(variableName, viewContext)
+ }
+ }
+}
+```
+
+**Model attributes:**
+- `PrimaryButton` → added to model as `primaryButton`
+- `Header`, `Content`, `Footer` → added as `header`, `content`, `footer`
+
+### 3. Thymeleaf Integration
+
+**ThymeleafViewContextFragmentProcessor:**
+
+```kotlin
+class ThymeleafViewContextFragmentProcessor(
+ dialectPrefix: String,
+ private val applicationContext: ApplicationContext
+) : AbstractAttributeTagProcessor(
+ TemplateMode.HTML,
+ dialectPrefix,
+ null, // Any element
+ false,
+ "context", // Attribute name: view:context
+ true,
+ PRECEDENCE,
+ true // Remove attribute
+) {
+
+ override fun doProcess(
+ context: ITemplateContext,
+ tag: IProcessableElementTag,
+ attributeName: AttributeName,
+ attributeValue: String, // e.g., "Header"
+ structureHandler: IElementTagStructureHandler
+ ) {
+ val webContext = context as WebEngineContext
+
+ // Check if a ViewContext of this type exists in the model
+ val variableName = attributeValue.replaceFirstChar { it.lowercase() }
+ val hasContext = webContext.getVariable(variableName) != null
+
+ if (!hasContext) {
+ // No matching context in model, remove this fragment
+ structureHandler.removeElement()
+ }
+ // Otherwise render normally (attribute is removed automatically)
+ }
+}
+```
+
+**Register in dialect:**
+
+```kotlin
+class ThymeleafViewComponentDialect(
+ private val applicationContext: ApplicationContext
+) : AbstractProcessorDialect(NAME, PREFIX, PRECEDENCE) {
+
+ override fun getProcessors(dialectPrefix: String): Set {
+ return setOf(
+ ThymeleafViewComponentTagProcessor(dialectPrefix, applicationContext),
+ ThymeleafViewContextFragmentProcessor(dialectPrefix, applicationContext) // NEW
+ )
+ }
+
+ companion object {
+ const val NAME = "ViewComponent Dialect"
+ const val PREFIX = "view"
+ const val PRECEDENCE = 1000
+ }
+}
+```
+
+### 4. JTE/KTE Integration
+
+JTE already supports type-based conditionals via `instanceof`:
+
+```java
+@import de.example.ButtonComponent.*
+
+@if(model instanceof PrimaryButton primaryButton)
+
+@elseif(model instanceof SecondaryButton secondaryButton)
+
+@elseif(model instanceof DangerButton dangerButton)
+
+@endif
+```
+
+**For MultiViewContext with JTE:**
+
+```java
+@import de.example.PageComponent.*
+
+<%-- Access each context if present --%>
+@if(header != null)
+
+@endif
+
+@if(content != null)
+
+ ${content.body()}
+
+@endif
+
+@if(footer != null)
+
+@endif
+```
+
+**No changes needed to JTE/KTE integrations** - existing features support both patterns.
+
+### 5. Fragment Rendering Rules
+
+**Rule 1: No `view:context` attribute → Always render**
+
+```html
+
+```
+
+**Rule 2: Has `view:context` attribute → Render only if matching ViewContext in model**
+
+```html
+
+ Only renders if errorAlert is in model
+
+```
+
+**Rule 3: Multiple fragments can render**
+
+```html
+Header
+Content
+Footer
+```
+
+With `MultiViewContext.of(new Header(...), new Content(...))`:
+- Header fragment renders
+- Content fragment renders
+- Footer fragment removed (not in model)
+
+---
+
+## Examples
+
+### Example 1: Button Variants
+
+```java
+@ViewComponent
+public class ButtonComponent {
+
+ public record PrimaryButton(String label, String action) implements ViewContext {}
+ public record SecondaryButton(String label, String action) implements ViewContext {}
+ public record DangerButton(String label, String action, String confirmMsg) implements ViewContext {}
+
+ public PrimaryButton primary(String label, String action) {
+ return new PrimaryButton(label, action);
+ }
+
+ public SecondaryButton secondary(String label, String action) {
+ return new SecondaryButton(label, action);
+ }
+
+ public DangerButton danger(String label, String action, String confirmMsg) {
+ return new DangerButton(label, action, confirmMsg);
+ }
+}
+```
+
+**Template (Thymeleaf):**
+```html
+
+
+
+
+
+
+
+```
+
+**Controller:**
+```java
+@Controller
+public class ButtonController {
+
+ @Autowired
+ private ButtonComponent buttonComponent;
+
+ @GetMapping("/button/submit")
+ ViewContext submitButton() {
+ return buttonComponent.primary("Submit", "/submit");
+ }
+
+ @GetMapping("/button/delete")
+ ViewContext deleteButton() {
+ return buttonComponent.danger("Delete", "/delete", "Are you sure?");
+ }
+}
+```
+
+### Example 2: Page Layout with Optional Footer
+
+```java
+@ViewComponent
+public class PageComponent {
+
+ public record Header(String title) implements ViewContext {}
+ public record Content(String body) implements ViewContext {}
+ public record Footer(String text) implements ViewContext {}
+
+ public ViewContext render(String title, String body, boolean includeFooter) {
+ return MultiViewContext.of(
+ new Header(title),
+ new Content(body),
+ includeFooter ? new Footer("© 2026") : null
+ );
+ }
+}
+```
+
+**Template:**
+```html
+
+
+
+
+
+ Content
+
+
+
+```
+
+### Example 3: Alert with Sealed Interface
+
+```java
+@ViewComponent
+public class AlertComponent {
+
+ sealed interface Alert extends ViewContext {
+ String message();
+ }
+
+ public record InfoAlert(String message) implements Alert {}
+ public record WarningAlert(String message, String details) implements Alert {}
+ public record ErrorAlert(String message, String stackTrace) implements Alert {}
+
+ public InfoAlert info(String message) {
+ return new InfoAlert(message);
+ }
+
+ public WarningAlert warning(String message, String details) {
+ return new WarningAlert(message, details);
+ }
+
+ public ErrorAlert error(String message, String stackTrace) {
+ return new ErrorAlert(message, stackTrace);
+ }
+}
+```
+
+**Template:**
+```html
+
+
+
+
+
+
+ Info
+
+
+
+
+
Warning
+
+ Details
+ Details
+
+
+
+
+
+
Error
+
+ Stack Trace
+ Stack trace
+
+
+```
+
+### Example 4: Dashboard with Conditional Sections
+
+```java
+@ViewComponent
+public class DashboardComponent {
+
+ public record Stats(int users, int orders) implements ViewContext {}
+ public record Chart(List data) implements ViewContext {}
+ public record Notifications(List messages) implements ViewContext {}
+
+ public ViewContext render(User user) {
+ var contexts = new ArrayList();
+
+ // Always show stats
+ contexts.add(new Stats(
+ userService.count(),
+ orderService.count()
+ ));
+
+ // Premium users get charts
+ if (user.isPremium()) {
+ contexts.add(new Chart(analyticsService.getData()));
+ }
+
+ // Show notifications if any
+ var messages = notificationService.getUnread(user);
+ if (!messages.isEmpty()) {
+ contexts.add(new Notifications(messages));
+ }
+
+ return MultiViewContext.of(contexts.toArray(ViewContext[]::new));
+ }
+}
+```
+
+**Template:**
+```html
+
+
+
+
+
+
+ 0
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+```
+
+### Example 5: Layout Variants
+
+```java
+@ViewComponent
+public class LayoutComponent {
+
+ public record AdminNav(String username) implements ViewContext {}
+ public record UserNav(String username) implements ViewContext {}
+ public record GuestNav() implements ViewContext {}
+ public record Content(ViewContext body) implements ViewContext {}
+
+ public ViewContext adminLayout(String username, ViewContext body) {
+ return MultiViewContext.of(
+ new AdminNav(username),
+ new Content(body)
+ );
+ }
+
+ public ViewContext userLayout(String username, ViewContext body) {
+ return MultiViewContext.of(
+ new UserNav(username),
+ new Content(body)
+ );
+ }
+
+ public ViewContext guestLayout(ViewContext body) {
+ return MultiViewContext.of(
+ new GuestNav(),
+ new Content(body)
+ );
+ }
+}
+```
+
+**Template:**
+```html
+
+
+
+Application
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## Implementation Considerations
+
+### 1. thymeVar Comments for IDE Autocomplete
+
+**Pattern for Thymeleaf templates:**
+
+```html
+
+
+
+```
+
+**With sealed interfaces:**
+```html
+
+
+
+
+
+```
+
+**Best practice:**
+- Add thymeVar comment for each ViewContext type
+- Use sealed interface variable for shared properties
+- Use specific type variable for unique properties
+
+### 2. Validation Strategy
+
+**Runtime validation only** - no template scanning or compile-time validation.
+
+**Why:**
+- Runtime validation catches the critical issue (mixed components)
+- Template errors are caught during development/testing
+- Template engines validate property access
+- IDE plugins are better place for template validation
+
+**Validation in MultiViewContext.of():**
+- ✅ Validates all ViewContexts from same component
+- ✅ Clear error messages
+- ✅ Filters out nulls automatically
+- ✅ Zero overhead (runs once per request)
+
+### 3. Error Handling
+
+**Clear error messages:**
+
+```java
+// Wrong: mixing components
+MultiViewContext.of(
+ new PageComponent.Header("Title"),
+ new FooterComponent.Footer("Footer") // Different component!
+)
+
+// Error message:
+"All ViewContexts in MultiViewContext must be from the same ViewComponent.
+Expected: PageComponent, found: FooterComponent for ViewContext: Footer.
+To compose ViewContexts from different components, use nested components instead."
+```
+
+```java
+// Wrong: not an inner class
+public record Header(String title) implements ViewContext {} // Top-level!
+
+MultiViewContext.of(new Header("Title"))
+
+// Error message:
+"ViewContext Header must be an inner class of a @ViewComponent.
+Did you forget to define it as a nested record/class?"
+```
+
+### 4. Performance
+
+**Fragment Selection:**
+- Thymeleaf: One model lookup per fragment (`webContext.getVariable(variableName)`)
+- JTE/KTE: Compiled to native if-else statements (zero overhead)
+
+**MultiViewContext:**
+- Validation runs once per MultiViewContext.of() call
+- Model population: one addAttribute per context
+- Negligible overhead
+
+### 5. Template Organization
+
+**Best practices:**
+
+1. **Group related fragments together:**
+ ```html
+
+
+
+
+
+
+ ...
+ ```
+
+2. **Use comments to document fragments:**
+ ```html
+
+
+
+
+
+ ```
+
+3. **Keep always-visible content without view:context:**
+ ```html
+
+ ```
+
+### 6. Testing
+
+**Unit testing fragments:**
+
+```java
+@SpringBootTest
+class ButtonComponentTest {
+
+ @Autowired
+ private ButtonComponent buttonComponent;
+
+ @Test
+ void primaryButtonShouldHavePrimaryClass() {
+ var button = buttonComponent.primary("Submit", "/submit");
+
+ assertThat(button).isInstanceOf(ButtonComponent.PrimaryButton.class);
+ assertThat(button.label()).isEqualTo("Submit");
+ assertThat(button.action()).isEqualTo("/submit");
+ }
+
+ @Test
+ void dangerButtonShouldIncludeConfirmMessage() {
+ var button = buttonComponent.danger("Delete", "/delete", "Sure?");
+
+ assertThat(button.confirmMsg()).isEqualTo("Sure?");
+ }
+}
+```
+
+**Integration testing with MockMvc:**
+
+```java
+@SpringBootTest
+@AutoConfigureMockMvc
+class PageControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ void pageWithFooterShouldRenderFooter() throws Exception {
+ mockMvc.perform(get("/page?footer=true"))
+ .andExpect(status().isOk())
+ .andExpect(content().string(containsString("