From dd8bc08333eade0258728d8d10c501a738f7b3ec Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 21:17:49 +0000 Subject: [PATCH 1/2] docs: Add comprehensive fragment rendering specification Add detailed specification for implementing fragment rendering feature, which enables multiple ViewContext implementations per ViewComponent with type-based conditional rendering in templates. Key features: - Multiple ViewContext types in single ViewComponent - Type-safe fragment selection via view:context attribute - Support for Thymeleaf, JTE, and KTE template engines - Backward compatible with existing code Includes: - Full technical specification (fragment-rendering-spec.md) - Example implementations (ButtonComponent, AlertComponent) - Usage examples and controller integration - Migration guide and best practices - Implementation roadmap This addresses the need for reduced code duplication when creating component variants (e.g., primary/secondary/danger buttons) while maintaining compile-time type safety. --- docs/specs/examples/AlertComponent.html | 97 ++ docs/specs/examples/AlertComponent.java | 75 ++ docs/specs/examples/ButtonComponent.html | 35 + docs/specs/examples/ButtonComponent.java | 61 ++ docs/specs/examples/ButtonComponent.jte | 32 + docs/specs/examples/ExampleController.java | 68 ++ docs/specs/examples/README.md | 442 ++++++++ docs/specs/fragment-rendering-spec.md | 1150 ++++++++++++++++++++ 8 files changed, 1960 insertions(+) create mode 100644 docs/specs/examples/AlertComponent.html create mode 100644 docs/specs/examples/AlertComponent.java create mode 100644 docs/specs/examples/ButtonComponent.html create mode 100644 docs/specs/examples/ButtonComponent.java create mode 100644 docs/specs/examples/ButtonComponent.jte create mode 100644 docs/specs/examples/ExampleController.java create mode 100644 docs/specs/examples/README.md create mode 100644 docs/specs/fragment-rendering-spec.md 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 @@ + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + 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..5189356 --- /dev/null +++ b/docs/specs/fragment-rendering-spec.md @@ -0,0 +1,1150 @@ +# Fragment Rendering Specification + +**Version:** 1.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. [Current State](#current-state) +6. [Proposed Solution](#proposed-solution) +7. [Technical Design](#technical-design) +8. [Examples](#examples) +9. [Implementation Considerations](#implementation-considerations) +10. [Migration Path](#migration-path) +11. [Alternatives Considered](#alternatives-considered) +12. [Open Questions](#open-questions) + +--- + +## Overview + +This specification proposes adding **fragment rendering** capabilities to Spring View Component, inspired by Thymeleaf's fragment system. The goal is to enable: + +- **Multiple ViewContext implementations** for a single ViewComponent +- **Type-based conditional rendering** within templates +- **Fragment scoping** based on ViewContext type +- **Reduced code duplication** for components with variations + +--- + +## 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. **No Template-Level Conditional Rendering**: Conditional logic must be: + - Implemented in template engine syntax (`th:if`, JTE conditionals) + - Spread across multiple template files + - Difficult to type-check at compile time + +3. **Verbose Nesting**: Complex layouts require many nested ViewComponent calls: + ```java + layoutComponent.render( + headerComponent.render(), + contentComponent.render(), + footerComponent.render() + ) + ``` + +### Real-World Use Cases + +#### Use Case 1: Button Component with Variants +```java +// Current approach: Separate components or complex conditionals +@ViewComponent +public class PrimaryButtonComponent { ... } + +@ViewComponent +public class SecondaryButtonComponent { ... } + +@ViewComponent +public class DangerButtonComponent { ... } +``` + +**Desired:** One `ButtonComponent` with multiple contexts for variants. + +#### Use Case 2: Layout with Different Headers +```java +// Current: Multiple layout components or conditional logic in templates +@ViewComponent +public class AdminLayoutComponent { ... } + +@ViewComponent +public class UserLayoutComponent { ... } + +@ViewComponent +public class GuestLayoutComponent { ... } +``` + +**Desired:** One `LayoutComponent` with context-based header selection. + +#### Use Case 3: Form Fields with Validation States +```java +// Current: Complex template logic +
+ + +
+
+ +
+``` + +**Desired:** Type-safe fragments for error/normal states. + +--- + +## Goals + +1. **Enable Multiple ViewContext Implementations**: Allow a single ViewComponent to return different ViewContext types +2. **Type-Based Fragment Selection**: Render template fragments based on the ViewContext type +3. **Compile-Time Safety**: Leverage Java/Kotlin type system for fragment selection +4. **Template Engine Agnostic**: Support JTE, KTE, and Thymeleaf +5. **Backward Compatibility**: Existing ViewComponents continue to work unchanged +6. **Natural Templates**: Maintain Thymeleaf's "natural templating" philosophy where possible + +--- + +## Non-Goals + +1. **Dynamic Fragment Selection**: Runtime string-based fragment selection (use template engine features) +2. **Cross-Component Fragments**: Sharing fragments across different ViewComponents (separate feature) +3. **Fragment Composition**: Nesting fragments within fragments (can be added later) +4. **Fragment Parameters**: Passing parameters to fragments (can be added later) + +--- + +## Current State + +### ViewContext Architecture + +```kotlin +// Core interface +interface IViewContext { + companion object { + fun getViewComponentTemplateWithoutSuffix(context: IViewContext): String + fun getViewComponentName(viewContext: Class): String + } +} + +// Template-specific interfaces +interface ViewContext : IViewContext { } // Thymeleaf +interface ViewContext : Content, IViewContext { } // JTE/KTE +``` + +### Current Template Resolution + +Template path is resolved from the ViewContext's enclosing class: +```kotlin +val componentName = context.javaClass.enclosingClass.simpleName +val componentPackage = context.javaClass.enclosingClass.`package`.name.replace(".", "/") +return "$componentPackage/$componentName" +``` + +Example: `de.example.ButtonComponent.PrimaryButton` → `de/example/ButtonComponent.html` + +### Template Rendering Flow + +1. Controller returns `ViewContext` from ViewComponent render method +2. `ViewComponentAspect` intercepts call, sets `ApplicationContext` +3. `ViewContextMethodReturnValueHandler` resolves template path +4. Template engine renders with ViewContext as model attribute + +--- + +## Proposed Solution + +### Core Concept + +**One ViewComponent → Multiple ViewContext Implementations → Fragment-Based Template** + +```java +@ViewComponent +public class ButtonComponent { + + // Multiple ViewContext implementations + 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 confirmMessage) + implements ViewContext {} + + // Render methods return different context types + public PrimaryButton renderPrimary(String label, String action) { + return new PrimaryButton(label, action); + } + + public SecondaryButton renderSecondary(String label, String action) { + return new SecondaryButton(label, action); + } + + public DangerButton renderDanger(String label, String action, String confirmMessage) { + return new DangerButton(label, action, confirmMessage); + } +} +``` + +### Template with Fragments + +#### Thymeleaf Syntax + +```html + + + + + + + + + + +``` + +#### JTE Syntax + +```java +@import de.example.ButtonComponent.* + +@if(model instanceof PrimaryButton primaryButton) + +@elseif(model instanceof SecondaryButton secondaryButton) + +@elseif(model instanceof DangerButton dangerButton) + +@endif +``` + +#### Alternative Thymeleaf Syntax (Simpler) + +```html + +
+ + + + + + + + + + +
+``` + +--- + +## Technical Design + +### 1. ViewContext Type Resolution + +Extend `IViewContext` to support fragment/context type resolution: + +```kotlin +interface IViewContext { + companion object { + // Existing methods + fun getViewComponentTemplateWithoutSuffix(context: IViewContext): String + fun getViewComponentName(viewContext: Class): String + + // NEW: Get ViewContext simple name for fragment matching + fun getViewContextSimpleName(context: IViewContext): String { + return context.javaClass.simpleName + } + + // NEW: Get fully qualified ViewContext name + fun getViewContextTypeName(context: IViewContext): String { + return context.javaClass.canonicalName + } + } +} +``` + +### 2. Thymeleaf Integration + +#### Option A: Custom Attribute Processor (`view:context`) + +```kotlin +class ThymeleafViewContextFragmentProcessor( + dialectPrefix: String, + private val applicationContext: ApplicationContext +) : AbstractAttributeTagProcessor( + TemplateMode.HTML, + dialectPrefix, + null, // Any element + false, + "context", // Attribute name + true, + PRECEDENCE, + true // Remove attribute +) { + + override fun doProcess( + context: ITemplateContext, + tag: IProcessableElementTag, + attributeName: AttributeName, + attributeValue: String, // e.g., "PrimaryButton" + structureHandler: IElementTagStructureHandler + ) { + val webContext = context as WebEngineContext + + // Find the ViewContext in the model + val viewContext = findViewContextInModel(webContext) + ?: throw ViewComponentException("No ViewContext found in model") + + // Get the ViewContext simple name + val contextTypeName = IViewContext.getViewContextSimpleName(viewContext) + + // Match fragment + if (contextTypeName != attributeValue) { + // Don't render this fragment + structureHandler.removeElement() + } + // Otherwise, render normally (just remove the view:context attribute) + } + + private fun findViewContextInModel(context: WebEngineContext): IViewContext? { + // Search model for IViewContext implementation + for (variableName in context.variableNames) { + val value = context.getVariable(variableName) + if (value is IViewContext) { + return value + } + } + return null + } +} +``` + +#### Option B: Custom Dialect with `view:context-root` + +```kotlin +class ThymeleafViewContextRootProcessor( + dialectPrefix: String, + private val applicationContext: ApplicationContext +) : AbstractAttributeTagProcessor( + TemplateMode.HTML, + dialectPrefix, + null, + false, + "context-root", + true, + PRECEDENCE, + true +) { + + override fun doProcess( + context: ITemplateContext, + tag: IProcessableElementTag, + attributeName: AttributeName, + attributeValue: String, + structureHandler: IElementTagStructureHandler + ) { + val webContext = context as WebEngineContext + val viewContext = findViewContextInModel(webContext) + ?: throw ViewComponentException("No ViewContext found in model") + + val contextTypeName = IViewContext.getViewContextSimpleName(viewContext) + + // Process children and remove non-matching fragments + // This requires more complex processing of child elements + structureHandler.setLocalVariable("_viewContextType", contextTypeName) + } +} +``` + +### 3. JTE/KTE Integration + +JTE/KTE already support type-based conditionals via `instanceof`: + +```java +@import de.example.ButtonComponent.* + +@if(model instanceof PrimaryButton primaryButton) + +@elseif(model instanceof SecondaryButton secondaryButton) + +@endif +``` + +**Enhancement:** Provide utility templates or macros for fragment selection: + +```java +@import static de.example.utils.ViewContextFragments.* + +${fragment(model, + PrimaryButton.class, () -> renderPrimary(model), + SecondaryButton.class, () -> renderSecondary(model), + DangerButton.class, () -> renderDanger(model) +)} +``` + +### 4. Template Resolution Changes + +**Current:** Template path based on enclosing class name +**Proposed:** Same behavior (backward compatible) + +Template fragments are selected **within** the resolved template, not via different template files. + +### 5. Model Attribute Naming + +**Current:** ViewContext added to model with auto-generated variable name +**Proposed:** Support both: +- Auto-generated name (backward compatible) +- Simple name based on ViewContext type (e.g., `PrimaryButton` → `primaryButton`) + +```kotlin +override fun handleReturnValue( + returnValue: Any?, + returnType: MethodParameter, + mavContainer: ModelAndViewContainer, + webRequest: NativeWebRequest +) { + val viewContext = returnValue as IViewContext + + // Resolve template path (unchanged) + mavContainer.view = IViewContext.getViewComponentTemplateWithoutSuffix(viewContext) + + // Add with auto-generated name (backward compatible) + mavContainer.addAttribute(viewContext) + + // NEW: Also add with ViewContext simple name + val contextSimpleName = IViewContext.getViewContextSimpleName(viewContext) + val variableName = contextSimpleName.replaceFirstChar { it.lowercase() } + mavContainer.addAttribute(variableName, viewContext) +} +``` + +--- + +## Examples + +### Example 1: Alert Component + +#### ViewComponent + +```java +@ViewComponent +public class AlertComponent { + + public record InfoAlert(String message) implements ViewContext {} + public record WarningAlert(String message, String details) implements ViewContext {} + public record ErrorAlert(String message, String stackTrace) implements ViewContext {} + 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); + } +} +``` + +#### Thymeleaf Template (`AlertComponent.html`) + +```html +
+ +
+ + Info message +
+ +
+ + Warning message +
+ Details +
Warning details
+
+
+ +
+ + Error message +
+ Stack Trace +
Stack trace
+
+
+ +
+ + Success message +
+ +
+``` + +#### JTE Template (`AlertComponent.jte`) + +```java +@import de.example.AlertComponent.* + +@if(model instanceof InfoAlert infoAlert) +
+ + ${infoAlert.message()} +
+@elseif(model instanceof WarningAlert warningAlert) +
+ + ${warningAlert.message()} +
+ Details +
${warningAlert.details()}
+
+
+@elseif(model instanceof ErrorAlert errorAlert) +
+ + ${errorAlert.message()} +
+ Stack Trace +
${errorAlert.stackTrace()}
+
+
+@elseif(model instanceof SuccessAlert successAlert) +
+ + ${successAlert.message()} +
+@endif +``` + +#### Controller Usage + +```java +@Controller +public class NotificationController { + + @Autowired + private AlertComponent alertComponent; + + @GetMapping("/success") + ViewContext showSuccess() { + return alertComponent.success("Operation completed successfully!"); + } + + @GetMapping("/error") + ViewContext showError() { + return alertComponent.error( + "An error occurred", + "java.lang.RuntimeException: Database connection failed..." + ); + } +} +``` + +### Example 2: Form Field Component + +#### ViewComponent + +```java +@ViewComponent +public class FormFieldComponent { + + public record TextField(String name, String label, String value) + implements ViewContext {} + + public record TextFieldWithError(String name, String label, String value, String error) + implements ViewContext {} + + public record TextArea(String name, String label, String value, int rows) + implements ViewContext {} + + public record Select(String name, String label, String value, List