Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What is the scope of fragment expression selector? #626

Closed
gavenkoa opened this issue Jul 20, 2017 · 6 comments
Closed

What is the scope of fragment expression selector? #626

gavenkoa opened this issue Jul 20, 2017 · 6 comments
Assignees
Milestone

Comments

@gavenkoa
Copy link

gavenkoa commented Jul 20, 2017

I am new to Thymeleaf, heard about it 4 years ago (so v2.x) that it has layout support. During learning of Thymeleaf 3 features I began to think that it is possible to implement layout design with Fragment expression.

What is desired is so called "Mashup" (forgot precise name) layout style when fragments are enriched with surrounding elements, not just inserted in place. One of such example provided in official docs:

http://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#flexible-layouts-beyond-mere-fragment-insertion

where fragment expression ~{::link} selects CSS style references from page and pass them to fragment:

<head th:fragment="common_header(title,links)">
    <th:block th:replace="${links}" />

In addition to CSS/JS I want to add <nav> to each page.

What is the problem with fragment expressions?

  • Each type of thing (like title/CSS/JS) require adding argument to th:fragment and all of them should be passed or rendering will fail.
  • Each type of thing (like title/CSS/JS) require repeated selectors over all pages.
  • head and body usually are processed separately in most examples on the Web. So you have a lot of th:replace instead of single.

So I asked if it is possible to pass html via fragment expression and extract parts from that object:

https://stackoverflow.com/questions/45183075/what-type-of-operation-and-syntax-for-them-are-possible-on-thymeleaf-3-fragment/

That is single th:replace with single fragment expression argument ~{::html}. But it seems that you can't do anything with that object in fragment because there is no Thymleaf language syntax and there is no support in API (I believe TemplateModel is actual holder of page DOM and its API allow only to pass visitor or write to stream).

I came across https://stackoverflow.com/questions/18896915/thymeleaf-templates-is-there-a-way-to-decorate-a-template-instead-of-including and tried that technique (here it is https://stackoverflow.com/a/45187766/173149 ):

template:

<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <body>
       <nav></nav>
       <div th:replace="this :: body"/>
    </body>
</html>

and page:

<html lang="en" xmlns:th="http://www.thymeleaf.org"
      th:replace="thymeleaf/layout/default :: html">
    <body>
       XXX
    </body>
</html>

Page is rendered into infinite sequence of <body><nav>. That because ~{this::body} select template body with has recursive link to itself.

But what is interested that expression fragment allows to match original page DOM! So I guarded selector with valid class attribute:

<html lang="en" xmlns:th="http://www.thymeleaf.org" class="htmlFrag">
    <body>
       <nav></nav>
       <div th:replace="this :: html[!class]/body"/>
    </body>
</html>

Now selector captures page html!

That allows to have rich layout support without needs to place many th:replace and many arguments to fragment expressions and even maintain fragment expressions on pages!

For example "smart template" with navigation and extensible CSS/JS can be defined as:

<html lang="en" xmlns:th="http://www.thymeleaf.org" class="htmlFrag">
    <head>
       <meta charset="UTF-8">
       <meta name="viewport" content="width=device-width, initial-scale=1.0">

       <title th:text="~{::html[!class]/head/title/text()}"></title>

       <link rel='stylesheet' href='/webjars/...">
       <div th:replace="this :: html[!class]/head/link"/>

       <script src="/webjars/..."></script>
       <div th:replace="this :: html[!class]/head/script"/>
    </head>
    <body>
       <nav></nav>
       <div th:replace="this :: html[!class]/body"/>
    </body>
</html>

Note: I don't define name for fragment in template. It is not necessary for example to work.

What I am worry about is if it not a bug in current implementation. How can fragment selector in template capture itself and external page?

@gavenkoa
Copy link
Author

Of cause I see recommendationы to name fragments differently from HTML tags to avoid ambiguity and my discovery shows problems in identifying what DOM is captured.

It is better to have named argument in fragment and functions/operations/syntax that allows getting part of DOM via CSS/XPath selector syntax. Is it possible?

@danielfernandez
Copy link
Member

Each type of thing (like title/CSS/JS) require adding argument to th:fragment and all of them should be passed or rendering will fail.

Not necessarily. Only if these arguments are added to the fragment's signature. But it is possible to add arguments to a fragment call specifying a name for them, even if they are not in the signature, and then let them be used. This allows therefore for optional fragment parameters:

<head th:fragment="common_header(title)">
    <th:block th:replace="${links} ?: ~{}" />

And then:

<head th:replace="temp :: common_header(title='Nice page',links=~{::links})">

Note the links is added to the fragment call, but it is not declared as required by the fragment itself. It's simply a local variable therefore, as it if were defined with th:with at the beginning of the fragment. Also note we are adding a default operator (?:) to th:replace so that it doesn't fail if the links fragment argument is not passed, and in such case simply writes nothing (empty fragment, ~{}).

Alternatively, you could leave your links argument at the fragment signature and fill it with the empty fragment (~{}) when calling it from pages that you don't want to add any links.

Each type of thing (like title/CSS/JS) require repeated selectors over all pages.

head and body usually are processed separately in most examples on the Web. So you have a lot of th:replace instead of single.

I understand in this case you are enunciating a preference for a different type of layout architecture: a hierarchical one instead of a pull-based or fragment-inclusion-based one like Thymeleaf uses by default. Hierarchical layout is not the preferred default option in Thymeleaf for a number of reasons (mostly related to how it negatively affects HTML design), but it is anyway a possibility using extensions like the Layout Dialect (have look at the ecosystem page).

What I am worry about is if it not a bug in current implementation. How can fragment selector in template capture itself and external page?

As far as I understand the situation here, the behaviour is the expected. If you have:

<!-- thymeleaf/default/layout.html -->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <body>
       <nav></nav>
       <div th:replace="this :: body"/>
    </body>
</html>

<!-- thymeleaf/page.html -->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      th:replace="thymeleaf/layout/default :: html">
    <body>
       XXX
    </body>
</html>

...and you render thymeleaf/page, when the <html> tag in page.html is processed, it will replace the entire <html> in page.html with the <html> in layout.html, so we will have:

<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <body>
       <nav></nav>
       <div th:replace="this :: body"/>
    </body>
</html>

And the this selector in that th:replace will apply (as it does always) to the original template where the code comes from (if you wanted otherwise, you'd have to pass a fragment as an argument), so the expected result here is that the <body> from layout.html will be recursively inserted in an infinite loop manner.

But what is interested that expression fragment allows to match original page DOM!

But that's not how fragment expressions work. When the template in them is specified as this, they refer to the specific template file they are on. As said above, if this is not the intended, then fragments from other pages can be pased as fragment arguments.

Or alternatively, you could use the name of the template being processed (meaning "the one for which the template engine was originally called") and use it in the fragment expression at your layout.html, something like:

<!-- thymeleaf/default/layout.html -->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <body>
       <nav></nav>
       <div th:replace="${#execInfo.processedTemplateName} :: body"/>
    </body>
</html>

<!-- thymeleaf/page.html -->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      th:replace="thymeleaf/layout/default :: html">
    <body>
       XXX
    </body>
</html>

At runtime, when processing the <div> in layout.html , the #execInfo.processedTemplateName variable will have value "thymeleaf/page". Alternatively, #execInfo.templateName will have value "thymeleaf/default/layout", but that's precisely what you are not interested on.

Have a look at this #execInfo utility object here: http://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#execution-info

Of cause I see recommendationы to name fragments differently from HTML tags to avoid ambiguity and my discovery shows problems in identifying what DOM is captured.

This ambiguity is advised against because the expression th:replace="::body" matches both the <body> tag and also any fragments defined with th:fragment="body". The way to break such ambiguity is to use th:replace=::%body", which will exclusively apply to th:fragment="body".


So in summary, I believe that your needs should be covered by the Layout Dialect and your questions answered above. If so, please go ahead and close this ticket.

@gavenkoa
Copy link
Author

But that's not how fragment expressions work. When the template in them is specified as this, they refer to the specific template file they are on. As said above, if this is not the intended, then fragments from other pages can be passed as fragment arguments.

By marking template body with class attribute <div th:replace="this :: html[!class]/body"/> inside template capture body of original page.

I can match any parts from original page if Xpath/CSS selector can't find match inside template. How that can be?

I was afraid using provided template in production, because it is not clear why that happen.

Though, you provided much safer way to refer to original page via: ${#execInfo.processedTemplateName} :: body avoiding ambiguity.

And there is an option to use Layout Dialect.

@gavenkoa
Copy link
Author

This page https://stackoverflow.com/a/20449189/173149 also refer to:

<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org"
  th:include="templates/layout :: page">
<head>
    <title></title>
</head>
<body>
    <div th:fragment="content">
        my page content
    </div>
</body>
</html>

from template via:

so original page content become available via this in template...

@danielfernandez
Copy link
Member

I can match any parts from original page if Xpath/CSS selector can't find match inside template. How that can be?

Because it works up in the stack of calls. If your specified fragment (with this) is not found in the leaf template (the one the expression is in), it goes up in the stack of template calls looking for something that matches such definition. So by discarding the <body> from one level, you end up matching the next level up.

However, in your case –if I understood correctly– that would be a completely unintended effect, because you don't want to match some <body> tag at whichever level of the template call hierarchy, but specifically the one from the original template. So that's why I suggested you using the #execInfo.processedTemplateName expression.

@gavenkoa
Copy link
Author

Wow! I miss that part (about lookup in template hierarchy stack).

Now I see that my questionable code is legitimate but it is safer to stick to suggested ${#execInfo.processedTemplateName}. In this way Layout Template dialect can be partially implemented with built-in power of Thymeleaf.

Thanks for support.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants