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

[MAJOR FEAT] Decoupled template logic #465

Closed
danielfernandez opened this Issue Feb 22, 2016 · 18 comments

Comments

Projects
None yet
6 participants
@danielfernandez
Member

danielfernandez commented Feb 22, 2016

Thymeleaf 3.0 will introduce the possibility to completely decouple the template's markup from its logic, allowing the creation of completely logic-less markup templates in the HTML and XML template modes.

The main idea is that template logic will be defined in a separate logic file (more exactly a logic resource, as it doesn't need to be a file). By default that logic resource will be an additional file living by the template file, with the same name but .th.xml extension:

/templates
+->/home.html
+->/home.th.xml

Such home.html file can be completely logic-less. It might look like this:

<!DOCTYPE html>
<html>
  <body>
    <table id="usersTable">
      <tr>
        <td class="username">Jeremy Grapefruit</td>
        <td class="usertype">Normal User</td>
      </tr>
      <tr>
        <td class="username">Alice Watermelon</td>
        <td class="usertype">Administrator</td>
      </tr>
    </table>
  </body>
</html>

Absolutely no Thymeleaf code there. This is a template file that a designer with no Thymeleaf or templating knowledge could have created, edited and/or understood. Or a fragment of HTML provided by some external system with no Thymeleaf hooks at all.

Let's now turn that home.html template into a perfectly working Thymeleaf template by creating our additional home.th.xml file like this:

<?xml version="1.0"?>
<thlogic>
  <attr sel="#usersTable" th:remove="all-but-first">
    <attr sel="/tr[0]" th:each="user : ${users}">
      <attr sel="td.username" th:text="${user.name}" />
      <attr sel="td.usertype" th:text="#{|user.type.${user.type}|}" />
    </attr>
  </attr>
</thlogic>

Here we can see a lot of <attr> tags inside a thlogic block. Those <attr> tags perform attribute injection on nodes of the original template selected by means of their sel attributes, which contain Thymeleaf markup selectors (actually AttoParser markup selectors).

Also note that <attr> tags can be nested so that their selectors are appended. That sel="/tr[0]" above, for example, will be processed as sel="#usersTable/tr[0]". And the selector for the user name's <td> will be processed as sel="#usersTable/tr[0]//td.username".

So once merged, both files seen above will be completely equivalent to:

<!DOCTYPE html>
<html>
  <body>
    <table id="usersTable" th:remove="all-but-first">
      <tr th:each="user : ${users}">
        <td class="username" th:text="${user.name}">Jeremy Grapefruit</td>
        <td class="usertype" th:text="#{|user.type.${user.type}|}">Normal User</td>
      </tr>
      <tr>
        <td class="username">Alice Watermelon</td>
        <td class="usertype">Administrator</td>
      </tr>
    </table>
  </body>
</html>

This looks more familiar, and is indeed less verbose than creating two separate files. But the advantage of decoupled templates is that we can achieve for our templates total independence from Thymeleaf, and therefore better maintainability from the design standpoint.

Of course some contracts between designers or developers will still be needed --like e.g. the fact that the users' <table> will need an id="usersTable"--, but in many scenarios a pure-HTML template will be a much better communication artifact between design and development teams.

Enabling decoupled templates

Decoupled logic will not be expected for every template by default. Instead, the configured template resolvers (implementations of ITemplateResolver) will need to specifically mark the templates they resolve as using decoupled logic.

Except for StringTemplateResolver (which does not allow decoupled logic), all other out-of-the-box implementations of ITemplateResolver will provide a flag called useDecoupledLogic that will mark all templates resolved by that resolver as potentially having all or part of its logic living in a separate resource:

final ServletContextTemplateResolver templateResolver = 
        new ServletContextTemplateResolver(servletContext);
...
templateResolver.setUseDecoupledLogic(true);

Mixing coupled and decoupled logic

Decoupled template logic, when enabled, is not a requirement. Its being enabled for a specific resolved template will only mean that the engine will look for a resource containing decoupled logic, parsing and merging it with the original template if it exists. No error will be issued if the decoupled logic resource does not exist.

Also, in the same template we can mix both coupled and decoupled logic, for example by adding some Thymeleaf attributes at the original template file but leaving others for the separate decoupled logic file. The most common case for this is using the new (in v3.0) th:ref attribute.

Using th:ref

th:ref is only a marker attribute (see #196). It does nothing from the processing standpoint and simply disappears when the template is processed, but its usefulness lies in the fact that it acts as a markup reference, i.e. it can be resolved by name from a markup selector just like a tag name or a fragment (th:fragment).

So if we have a selector like:

  <attr sel="whatever" .../>

This will match:

  • Any <whatever> tags.
  • Any tags with a th:fragment="whatever" attribute.
  • Any tags with a th:ref="whatever" attribute.

What is the advantage of th:ref against, for example, using a pure-HTML id attribute? merely the fact that we might not want to add so many id and class attributes to our tags to act as logic anchors, which might end up polluting a bit our output.

And in the same sense, what is the disadvantage of th:ref? well, obviously that we'd be adding a bit of Thymeleaf logic ("logic") to our templates.

Note this applicability of the th:ref attribute does not only apply to decoupled logic template files: it works the same in other types of scenarios like e.g. in fragment expressions (~{...}).

Performance impact

Extremely small. When a resolved template is marked to use decoupled logic and it is not cached, the template logic resource will be resolved first, parsed and processed into a secuence of instructions in-memory: basically a list of attributes to be injected to each markup selector.

But this is the only additional step required because, after this, the real template will be parsed, and while it is parsed these attributes will be injected on-the-fly by the parser itself, thanks to the advanced capabilities for node selection in AttoParser. So parsed nodes will come out of the parser as if they had their injected attributes written in the original template file, no difference at all.

The biggest advantage of this? when a template is configured to be cached, it will be cached already containing the injected attributes. So the overhead of using decoupled templates for cacheable templates, once they are cached, will be absolutely zero.

Resolution of the decoupled logic

The way Thymeleaf resolves the decoupled logic resources corresponding to each template is configurable by the user. It is determined by an extension point, the org.thymeleaf.templateparser.markup.decoupled.IDecoupledTemplateLogicResolver, for which a default implementation is provided: StandardDecoupledTemplateLogicResolver.

What does this standard implementation do?

  • First, it applies a prefix and a suffix to the base name of the template resource (obtained by means of its ITemplateResource#getBaseName() method). Both prefix and suffix can be configured and, by default, the prefix will be empty and the suffix will be .th.xml.
  • Second, it asks the template resource to resolve a relative resource with the computed name by means of its ITemplateResource#relative(String relativeLocation) method.

See #419 for more info on the ITemplateResource interface.

The specific implementation of IDecoupledTemplateLogicResolver to be used can be configured at the TemplateEngine easily:

final StandardDecoupledTemplateLogicResolver decoupledresolver = 
        new StandardDecoupledTemplateLogicResolver();
decoupledResolver.setPrefix("../viewlogic/");
...
templateEngine.setDecoupledTemplateLogicResolver(decoupledResolver);
@danielfernandez

This comment has been minimized.

Show comment
Hide comment
@danielfernandez

danielfernandez Feb 22, 2016

Member

This is already in 3.0.0-SNAPSHOT

Member

danielfernandez commented Feb 22, 2016

This is already in 3.0.0-SNAPSHOT

@pciiitmk

This comment has been minimized.

Show comment
Hide comment
@pciiitmk

pciiitmk Apr 13, 2016

This sounds great. Still to get my hands dirty it was something like this in Thymeleaf that I wanted from a long time. Thanks for doing this.

pciiitmk commented Apr 13, 2016

This sounds great. Still to get my hands dirty it was something like this in Thymeleaf that I wanted from a long time. Thanks for doing this.

@ochsA

This comment has been minimized.

Show comment
Hide comment
@ochsA

ochsA May 4, 2016

Does it also work with th:text and replace statements?

ochsA commented May 4, 2016

Does it also work with th:text and replace statements?

@danielfernandez

This comment has been minimized.

Show comment
Hide comment
@danielfernandez

danielfernandez May 4, 2016

Member

@ochsA it works with any processor from any dialect.

Member

danielfernandez commented May 4, 2016

@ochsA it works with any processor from any dialect.

@arminnaderi

This comment has been minimized.

Show comment
Hide comment
@arminnaderi

arminnaderi Aug 21, 2016

Great feature! Is there a list of all possible selectors? I read the AttoParser docs but it was mostly internal documentation. Also, is it possible to make a ~1:1 xml file that maps directly to each line in the html?

arminnaderi commented Aug 21, 2016

Great feature! Is there a list of all possible selectors? I read the AttoParser docs but it was mostly internal documentation. Also, is it possible to make a ~1:1 xml file that maps directly to each line in the html?

@danielfernandez

This comment has been minimized.

Show comment
Hide comment
@danielfernandez

danielfernandez Aug 23, 2016

Member

The JavaDoc for the org.attoparser.select package is currently the only authoritative source on selectors. Note it is not internal documentation, but actually stable, fully-valid one. The AttoParser website itself has not been updated to AttoParser 2.0 though, so we are still lacking proper website documentation for that.

Also, is it possible to make a ~1:1 xml file that maps directly to each line in the html?

Could you elaborate a bit on this? what do you mean with "maps directly to each line in the html"?

Member

danielfernandez commented Aug 23, 2016

The JavaDoc for the org.attoparser.select package is currently the only authoritative source on selectors. Note it is not internal documentation, but actually stable, fully-valid one. The AttoParser website itself has not been updated to AttoParser 2.0 though, so we are still lacking proper website documentation for that.

Also, is it possible to make a ~1:1 xml file that maps directly to each line in the html?

Could you elaborate a bit on this? what do you mean with "maps directly to each line in the html"?

@arminnaderi

This comment has been minimized.

Show comment
Hide comment
@arminnaderi

arminnaderi Aug 24, 2016

What I meant to say is if it is possible to match every single tag in a corresponding html file simply by the order in which the tags appeared in the html.

For example, if line one of the html file was <div class="row">, I could just write <attr th:text="${user.name}" /> on line one of the xml file and it would map correctly. Of course, this means that I would have a lot of empty <attr/> tags, which may not be desirable.

My main concern is when the html changes, and having to modify the xml files with child selectors that depend on the parents. Do you suggest using the most specific selectors whenever possible so that refactoring of the html does not easily break the xml?

arminnaderi commented Aug 24, 2016

What I meant to say is if it is possible to match every single tag in a corresponding html file simply by the order in which the tags appeared in the html.

For example, if line one of the html file was <div class="row">, I could just write <attr th:text="${user.name}" /> on line one of the xml file and it would map correctly. Of course, this means that I would have a lot of empty <attr/> tags, which may not be desirable.

My main concern is when the html changes, and having to modify the xml files with child selectors that depend on the parents. Do you suggest using the most specific selectors whenever possible so that refactoring of the html does not easily break the xml?

@danielfernandez

This comment has been minimized.

Show comment
Hide comment
@danielfernandez

danielfernandez Aug 24, 2016

Member

No, you need a selector in your <attr> tags. There is no 1:1 correspondence mechanism.

For example, if line one of the html file was <div class="row">, I could just write <attr th:text="${user.name}" /> on line one of the xml file and it would map correctly. Of course, this means that I would have a lot of empty <attr/> tags, which may not be desirable.

I understand this, but that would make you end up with very large XMLs with, as you say, tons of empty <attr> tags. And your XML would be 100% sensitive to changes in the (now-hardly-coupled) HTML. So IMHO the coupling situation between markup and logic would have gone worse, not better...

My main concern is when the html changes, and having to modify the xml files with child selectors that depend on the parents. Do you suggest using the most specific selectors whenever possible so that refactoring of the html does not easily break the xml?

That's when carefully choosing the selectors shows its value. If you make selectors depending on the structure of the HTML (e.g. div/div/p[2]) then changes in the HTML will heavily affect the decoupled logic XML and you'll have to continuously modify your selectors at the XML side. But if you have an id="content" somewhere in your markup and you select it with the #content selector, then you will be able to move it around easily without minding changes in your HTML structure...

Member

danielfernandez commented Aug 24, 2016

No, you need a selector in your <attr> tags. There is no 1:1 correspondence mechanism.

For example, if line one of the html file was <div class="row">, I could just write <attr th:text="${user.name}" /> on line one of the xml file and it would map correctly. Of course, this means that I would have a lot of empty <attr/> tags, which may not be desirable.

I understand this, but that would make you end up with very large XMLs with, as you say, tons of empty <attr> tags. And your XML would be 100% sensitive to changes in the (now-hardly-coupled) HTML. So IMHO the coupling situation between markup and logic would have gone worse, not better...

My main concern is when the html changes, and having to modify the xml files with child selectors that depend on the parents. Do you suggest using the most specific selectors whenever possible so that refactoring of the html does not easily break the xml?

That's when carefully choosing the selectors shows its value. If you make selectors depending on the structure of the HTML (e.g. div/div/p[2]) then changes in the HTML will heavily affect the decoupled logic XML and you'll have to continuously modify your selectors at the XML side. But if you have an id="content" somewhere in your markup and you select it with the #content selector, then you will be able to move it around easily without minding changes in your HTML structure...

@arminnaderi

This comment has been minimized.

Show comment
Hide comment
@arminnaderi

arminnaderi Sep 6, 2016

Great advice, thank you. I have been using your decoupled templates for a few files and have begun to appreciate how invaluable they are! It can make working with front end developers much easier.

By the way, what are your suggestions for fragments? If a front end developer views the html file that was just converted from full coupling, he would have a hard time developing if there are a lot of fragments in the html file.

I use fragments for my head and script tags, but solved that issue by pasting the minimal amount of CSS or JavaScript required to properly render the page for the front end developer.

Is it normal to have to copy/paste all the required fragments into the html file, or is there a better way to do this? It seems like a lot of duplication and confusion for other developers, who may end up editing the html twice or more.

Thanks for reading

arminnaderi commented Sep 6, 2016

Great advice, thank you. I have been using your decoupled templates for a few files and have begun to appreciate how invaluable they are! It can make working with front end developers much easier.

By the way, what are your suggestions for fragments? If a front end developer views the html file that was just converted from full coupling, he would have a hard time developing if there are a lot of fragments in the html file.

I use fragments for my head and script tags, but solved that issue by pasting the minimal amount of CSS or JavaScript required to properly render the page for the front end developer.

Is it normal to have to copy/paste all the required fragments into the html file, or is there a better way to do this? It seems like a lot of duplication and confusion for other developers, who may end up editing the html twice or more.

Thanks for reading

@garyhellman

This comment has been minimized.

Show comment
Hide comment
@garyhellman

garyhellman Sep 9, 2016

Hello
We use a header/footer/ assets server with "pure" HTML (but named "*.inc" not *.html)
ala "jsp c:import tag"

Can I see an example of using UrlTemplateResolver like this?
Would the *.th.xml files need to exist on that same assets server?
OR would just UrlTemplateResolver accomplish this?

basically in order to do this:

...

Thanks!

garyhellman commented Sep 9, 2016

Hello
We use a header/footer/ assets server with "pure" HTML (but named "*.inc" not *.html)
ala "jsp c:import tag"

Can I see an example of using UrlTemplateResolver like this?
Would the *.th.xml files need to exist on that same assets server?
OR would just UrlTemplateResolver accomplish this?

basically in order to do this:

...

Thanks!

@danielfernandez

This comment has been minimized.

Show comment
Hide comment
@danielfernandez

danielfernandez Sep 9, 2016

Member

@garyhellman the UrlTemplateResolver will try to look for the th.xml file in the same path as the template (also retrieving it as an URL). It does this because that's what the UrlTemplateResource#relative(String relativeLocation) does.

And the reason UrlTemplateResource#relative(...) is called is because that is what StandardDecoupledLogicTemplateResolver does. But this is only the default implementation of IDecoupledTemplateResolver, which is an extension point. So you can change this behaviour.

Member

danielfernandez commented Sep 9, 2016

@garyhellman the UrlTemplateResolver will try to look for the th.xml file in the same path as the template (also retrieving it as an URL). It does this because that's what the UrlTemplateResource#relative(String relativeLocation) does.

And the reason UrlTemplateResource#relative(...) is called is because that is what StandardDecoupledLogicTemplateResolver does. But this is only the default implementation of IDecoupledTemplateResolver, which is an extension point. So you can change this behaviour.

@garyhellman

This comment has been minimized.

Show comment
Hide comment
@garyhellman

garyhellman Sep 9, 2016

Wow thanks for fast response -
I am trying to do this:

<div th:include="http://www.thymeleaf.org :: p.notice" >...</div>

am I making it more complicated that necessasry?

garyhellman commented Sep 9, 2016

Wow thanks for fast response -
I am trying to do this:

<div th:include="http://www.thymeleaf.org :: p.notice" >...</div>

am I making it more complicated that necessasry?

@danielfernandez

This comment has been minimized.

Show comment
Hide comment
@danielfernandez

danielfernandez Sep 9, 2016

Member

@garyhellman that could be enough (though I'd recommend th:insert in Thymeleaf 3.0). However, note that if that <p class="notice">...</p> fragment you are selecting there needs to be applied no Thymeleaf logic (because of being a static asset, not a template or anything you want to apply logic on), you'd need no th.xml file at all. Simply insert your .inc and you'd be done...

Member

danielfernandez commented Sep 9, 2016

@garyhellman that could be enough (though I'd recommend th:insert in Thymeleaf 3.0). However, note that if that <p class="notice">...</p> fragment you are selecting there needs to be applied no Thymeleaf logic (because of being a static asset, not a template or anything you want to apply logic on), you'd need no th.xml file at all. Simply insert your .inc and you'd be done...

@garyhellman

This comment has been minimized.

Show comment
Hide comment
@garyhellman

garyhellman Sep 9, 2016

I am not configured I supposed - I get " [templates/http://www.thymeleaf.org.html] cannot be opened because it does not exist"
I am using Spring Boot with Thymelaf 3.1
can we use both URL and html templates? (yes - as in the example in the docs)

garyhellman commented Sep 9, 2016

I am not configured I supposed - I get " [templates/http://www.thymeleaf.org.html] cannot be opened because it does not exist"
I am using Spring Boot with Thymelaf 3.1
can we use both URL and html templates? (yes - as in the example in the docs)

@danielfernandez

This comment has been minimized.

Show comment
Hide comment
@danielfernandez

danielfernandez Sep 9, 2016

Member

@garyhellman yes. You need to configure your UrlTemplateResolver. Have a look: http://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#template-resolvers (especially at the Chaining Template Resolvers section)

Member

danielfernandez commented Sep 9, 2016

@garyhellman yes. You need to configure your UrlTemplateResolver. Have a look: http://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#template-resolvers (especially at the Chaining Template Resolvers section)

@garyhellman

This comment has been minimized.

Show comment
Hide comment
@garyhellman

garyhellman Sep 16, 2016

Daniel (and all),
I am interested in using Spring Boot, Thymeleaf(2 or 3) and URLTemplate (to pull in header/footer snippets from a different host.
I probably should read more/all of the code in Thymeleaf and Spring Boot
but ... if anyone already knows how to do this, I would appreciate the help ...

Also please just let me know if it makes more sense to ask for help from Spring Boot .. forum
Thanks!

1 - I saw the Spring example by Raphael - using Thymeleaf 2 (it is pulling in a notice from the www.Thymeleaf.org/ page - but it is currently failing dues to the "defer" attributes in the "<script>" tags.

<head>
...
    <script src="scripts/dumbqueryselector.js" defer></script>
    <script src="scripts/prism.js" defer data-manual></script>
    <script src="scripts/thymeleaf.js" defer></script>
</head>

```I could get that to work fine if I used a different web site (not having "defer"   )
2 - I do not know "yet" how to config that (v 2) in a Spring Boot application
3 - I would like to use Thymeleaf 3 in the same way - if I can understand how to configure URLTemplates in a SpringBoot application
Thanks again for any help or suggfestions

garyhellman commented Sep 16, 2016

Daniel (and all),
I am interested in using Spring Boot, Thymeleaf(2 or 3) and URLTemplate (to pull in header/footer snippets from a different host.
I probably should read more/all of the code in Thymeleaf and Spring Boot
but ... if anyone already knows how to do this, I would appreciate the help ...

Also please just let me know if it makes more sense to ask for help from Spring Boot .. forum
Thanks!

1 - I saw the Spring example by Raphael - using Thymeleaf 2 (it is pulling in a notice from the www.Thymeleaf.org/ page - but it is currently failing dues to the "defer" attributes in the "<script>" tags.

<head>
...
    <script src="scripts/dumbqueryselector.js" defer></script>
    <script src="scripts/prism.js" defer data-manual></script>
    <script src="scripts/thymeleaf.js" defer></script>
</head>

```I could get that to work fine if I used a different web site (not having "defer"   )
2 - I do not know "yet" how to config that (v 2) in a Spring Boot application
3 - I would like to use Thymeleaf 3 in the same way - if I can understand how to configure URLTemplates in a SpringBoot application
Thanks again for any help or suggfestions
@garyhellman

This comment has been minimized.

Show comment
Hide comment
@garyhellman

garyhellman Sep 21, 2016

This link was a help - I think it is working for me now...
spring-projects/spring-boot#6500
(for v2 and v3) - the "order" was apparently very important

Thanks again Daniel!

garyhellman commented Sep 21, 2016

This link was a help - I think it is working for me now...
spring-projects/spring-boot#6500
(for v2 and v3) - the "order" was apparently very important

Thanks again Daniel!

@lilalinux

This comment has been minimized.

Show comment
Hide comment
@lilalinux

lilalinux commented Feb 23, 2017

How do you inject a th:block? I would prefer not to introduce divs
http://stackoverflow.com/questions/42748663/decoupled-template-logic-how-do-you-insert-thblock

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