thymeleaf-layout-dialect icon indicating copy to clipboard operation
thymeleaf-layout-dialect copied to clipboard

Making parent template fragments optional

Open andylaw opened this issue 3 months ago • 5 comments

I have a base template that drives multiple child pages. These pages all share a common header section, but then there are two sections just below that header that may or may not be required in the child.

<div class="header-container">
  <div class="common-header">
    ...
  </div>
  <div class="part1" layout:fragment="optional-1">Optional 1</div>
  <div class="part2" layout:fragment="optional-2">Optional 2</div>
</div>

If my child template does not have a layout:fragment called "optional-1", then I want the div with class "part1" to not be present in the output. So:

  <div class="part2" layout:fragment="optional-2">This from the child template</div>

Should result in:

<div class="header-container">
  <div class="common-header">
    ...
  </div>
   <div class="part2">This from the child template</div>
</div>

... with the "Optional 1" div completely removed.

The only way I can get this to work is to add a th:remove="all" to the base template, and a corresponding th:remove="none" to all of the child templates (because th:remove="all" without the corresponding "none" in the child template blows away the replaced tag completely)

Firstly, am I missing something obvious?

Secondly, am I weird for finding the need to add an effective manual override in all the child templates feels wrong and inefficient?

andylaw avatar Sep 25 '25 17:09 andylaw

Hey there, so given what you've described, I do agree that the workaround is a lot. The reason however is that you're going somewhat against the grain of what the layout dialect provides, which is an inheritance-based approach to creating templates (as opposed to a compositional one). As such, the layout dialect follows how inheritance would work in a language like Java, eg: if you had class A with a method you could override, then create class B extends A but don't override that method, the base method isn't gonna suddenly disappear, so neither will any fragments in the template.

However, I had a play with some other approaches and found something that might be useful to you instead of having to put th:remove="all"/th:remove="none" all over the place.

Thymeleaf comes with a special tag, <th:block>, which acts a lot like th:remove="tag". By replacing the divs with that in your base template, and then removing the "Optional 1" / "Optional 2" text in the tags, I was able to get something like that 'remove if not overridden' behaviour that you were describing. Here's a Thymeleaf test file I was working with which shows the input, template, and result:

%TEMPLATE_MODE HTML

%INPUT
<!DOCTYPE html>
<html layout:decorate="~{layout}">
<body>
  <div class="part2" layout:fragment="optional-2">This from the child template</div>
</body>
</html>

%INPUT[layout]
<!DOCTYPE html>
<html>
<body>
  <div class="header-container">
    <th:block class="part1" layout:fragment="optional-1"></th:block>
    <th:block class="part2" layout:fragment="optional-2"></th:block>
  </div>
</body>
</html>

%OUTPUT
<!DOCTYPE html>
<html>
<body>
  <div class="header-container">
    <div class="part2">This from the child template</div>
  </div>
</body>
</html>

How this works is that the layout dialect will replace the th:block in the base template with the element from the matching fragment in the input file. If no matching fragment exists, then it'll stay a th:block and so Thymeleaf will remove it.

If the text within the elements is important, then maybe writing a custom element like th:block that acts more like th:remove="all" would be the way to go. Anyway, I hope this helps you achieve what you need, or at least point you in a direction towards it.

ultraq avatar Sep 26 '25 08:09 ultraq

Thanks for that. I'd seen the <th:block> tag, and I use it for some other include sections elsewhere in the page. The other includes are Javascript and additional stylesheet elements so they're "hidden". The reason I've been banging my head so hard on this, though, is that one of the things that is really good about Thymeleaf with layout inheritance is that I can modify and check my styling simply using the template. I have my template with the optional sections all wrapped with divs like they would be in the processed page and I can check that my styling works without having to compile the app, fire up the application container, hit a web page, log in, then click through to a sub-page. The <th:block> option doesn't allow for that.

andylaw avatar Sep 26 '25 09:09 andylaw

And to come back on the inheritance point, If I declare class A with method someMethod() that returns a long , complicated string, then I can create class B which extends class A. It can then replace someMethod() with its own version that returns a different long, complicated string, but it could also return null, or an empty string.

The conceptual difference between how I view it drops back to where the output of the "method" fits. Thymeleaf, and the layout dialect, replace the <div> and its content with another <div> and its content. There is no way to specify "null" here, and I think having that ability would make the whole thing more flexible

andylaw avatar Sep 26 '25 09:09 andylaw

There is no way to specify "null" here, and I think having that ability would make the whole thing more flexible

Given <th:block layout:fragment="..."></th:block> is sort of Thymeleaf's version of "null" when put in the child template - because the layout dialect would replace the parent element (th:block removes the entire element) and content (which is empty) - would using that in your child templates be a better workaround than the th:remove's everywhere? (Yes, it is still verbose, but just like your inheritance example of replacing the base method with one that returns null, you still have to write it out in every child class to replace that base method which is similarly verbose.)

Anyway, just offering some more ideas that could work as better analogs of the 'replace base implementation with null' that you're trying to achieve. I'll have a think about adding an option to the layout dialect that replaces unmatched fragments with nothing as a shortcut/convenience for putting empty th:blocks everywhere.

ultraq avatar Sep 26 '25 21:09 ultraq

That option is actually more work, because I'd have to specify a blank <th:block> in every child template where I didn't want the element to appear. Having remove="all" in the parent template and then overriding it with remove="none" means I only have to include the fragment in child templates that need it.

andylaw avatar Sep 29 '25 08:09 andylaw