{"action":"created","issue":{"url":"https://api.github.com/repos/thymeleaf/thymeleaf/issues/451","labels_url":"https://api.github.com/repos/thymeleaf/thymeleaf/issues/451/labels{/name}","comments_url":"https://api.github.com/repos/thymeleaf/thymeleaf/issues/451/comments","events_url":"https://api.github.com/repos/thymeleaf/thymeleaf/issues/451/events","html_url":"https://github.com/thymeleaf/thymeleaf/issues/451","id":128657099,"number":451,"title":"[MAJOR FEAT] Fragment Expressions","user":{"login":"danielfernandez","id":1492299,"avatar_url":"https://avatars.githubusercontent.com/u/1492299?v=3","gravatar_id":"","url":"https://api.github.com/users/danielfernandez","html_url":"https://github.com/danielfernandez","followers_url":"https://api.github.com/users/danielfernandez/followers","following_url":"https://api.github.com/users/danielfernandez/following{/other_user}","gists_url":"https://api.github.com/users/danielfernandez/gists{/gist_id}","starred_url":"https://api.github.com/users/danielfernandez/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/danielfernandez/subscriptions","organizations_url":"https://api.github.com/users/danielfernandez/orgs","repos_url":"https://api.github.com/users/danielfernandez/repos","events_url":"https://api.github.com/users/danielfernandez/events{/privacy}","received_events_url":"https://api.github.com/users/danielfernandez/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/thymeleaf/thymeleaf/labels/status:pending","name":"status:pending","color":"e0d8c7"},{"url":"https://api.github.com/repos/thymeleaf/thymeleaf/labels/tracker:feature","name":"tracker:feature","color":"005dc5"}],"state":"closed","locked":false,"assignee":{"login":"danielfernandez","id":1492299,"avatar_url":"https://avatars.githubusercontent.com/u/1492299?v=3","gravatar_id":"","url":"https://api.github.com/users/danielfernandez","html_url":"https://github.com/danielfernandez","followers_url":"https://api.github.com/users/danielfernandez/followers","following_url":"https://api.github.com/users/danielfernandez/following{/other_user}","gists_url":"https://api.github.com/users/danielfernandez/gists{/gist_id}","starred_url":"https://api.github.com/users/danielfernandez/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/danielfernandez/subscriptions","organizations_url":"https://api.github.com/users/danielfernandez/orgs","repos_url":"https://api.github.com/users/danielfernandez/repos","events_url":"https://api.github.com/users/danielfernandez/events{/privacy}","received_events_url":"https://api.github.com/users/danielfernandez/received_events","type":"User","site_admin":false},"milestone":{"url":"https://api.github.com/repos/thymeleaf/thymeleaf/milestones/11","html_url":"https://github.com/thymeleaf/thymeleaf/milestones/Thymeleaf%203.0","labels_url":"https://api.github.com/repos/thymeleaf/thymeleaf/milestones/11/labels","id":213281,"number":11,"title":"Thymeleaf 3.0","description":"","creator":{"login":"danielfernandez","id":1492299,"avatar_url":"https://avatars.githubusercontent.com/u/1492299?v=3","gravatar_id":"","url":"https://api.github.com/users/danielfernandez","html_url":"https://github.com/danielfernandez","followers_url":"https://api.github.com/users/danielfernandez/followers","following_url":"https://api.github.com/users/danielfernandez/following{/other_user}","gists_url":"https://api.github.com/users/danielfernandez/gists{/gist_id}","starred_url":"https://api.github.com/users/danielfernandez/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/danielfernandez/subscriptions","organizations_url":"https://api.github.com/users/danielfernandez/orgs","repos_url":"https://api.github.com/users/danielfernandez/repos","events_url":"https://api.github.com/users/danielfernandez/events{/privacy}","received_events_url":"https://api.github.com/users/danielfernandez/received_events","type":"User","site_admin":false},"open_issues":11,"closed_issues":119,"state":"open","created_at":"2012-11-14T23:08:17Z","updated_at":"2016-01-25T22:21:25Z","due_on":null,"closed_at":null},"comments":0,"created_at":"2016-01-25T22:21:05Z","updated_at":"2016-01-25T22:21:25Z","closed_at":"2016-01-25T22:21:25Z","body":"Building on the idea explained at #180, Thymeleaf 3.0 will introduce a new type of *simple expressions* for the Thymeleaf Standard Expression engine. These expressions are called **Fragment Expressions** and their main idea is that they *generalize* the syntax that was already being used in `th:insert`/`th:replace`/`th:include` in order to be able to use *resolved fragments* as any other kind of objects in the template execution context.\r\n\r\nTheir syntax is similar to that of other simple expressions, using the *tilde* (`~`) as a selector char. So expressions such as these:\r\n\r\n```html\r\n~{commons::main}\r\n```\r\n...will now not only be usable in `th:insert`/`th:replace`/`th:include` like:\r\n```html\r\n<div th:insert=\"~{commons :: main}\">...</div>\r\n```\r\n...but also as a part of other more complex expressions like:\r\n```html\r\n<div th:insert=\"${user.admin}? ~{adm :: admintools} : ~{common :: basictools}\">...</div>\r\n```\r\n...or even:\r\n```html\r\n<div th:with=\"frag=~{footer :: #main/text()}\">\r\n  <p th:insert=\"${frag}\">\r\n</div>\r\n```\r\n\r\nAll features available before Thymeleaf 3.0 in fragment insertion syntax are also available in *fragment expressions*, including:\r\n\r\n  * Same-template fragments: `~{::frag}`, `~{this::frag}`, ...\r\n  * Complete-template fragments: `~{main}`, `~{usermain}`, ...\r\n  * Variable-based resolution, `~{${templateName} :: ${fragmentName}}`\r\n  * Fragment parameters, both named and unnamed, like `~{common::details(${user.name})`, `~{::main(title=#{corp.title})}`\r\n\r\nBesides, Thymeleaf 3.0 fully embraces the new *Markup Selector* syntax provided by AttoParser 2.0, which builds on the previously existing syntax and largely extends it, providing new selection capabilities (see below).\r\n\r\n**Fragments as parameters (layout)**\r\n\r\nNow that *fragments* (objects of class `org.thymeleaf.standard.expression.Fragment`) can be put into context just as any other variables, we can use them as parameters to another fragments, effectively turning fragment expressions into a **complete layout mechanism**.\r\n\r\nFor example, we might develop a *common template* called `base.html`, containing the `<head>` tag we want to use for most of our application's pages, containing all the style and script imports we will need but allowing the specification of a *page title* and some additional `<link>`s:\r\n\r\n```html\r\n<head th:fragment=\"common_header(title,links)\">\r\n\r\n  <title th:replace=\"${title}\">The awesome application</title>\r\n\r\n  <!-- Common styles and scripts -->\r\n  <link rel=\"stylesheet\" type=\"text/css\" media=\"all\" th:href=\"@{/css/awesomeapp.css}\">\r\n  <link rel=\"shortcut icon\" th:href=\"@{/images/favicon.ico}\">\r\n  <script type=\"text/javascript\" th:src=\"@{/sh/scripts/codebase.js}\"></script>\r\n\r\n  <!--/* Per-page placeholder for additional links */-->\r\n  <th:block th:replace=\"${links}\" />\r\n\r\n</head>\r\n```\r\n\r\nThen we could have a `main.html` template making use of the fragment above:\r\n```html\r\n...\r\n<head th:replace=\"base :: common_header(~{::title},~{::link})\">\r\n\r\n  <title>Awesome - Main</title>\r\n\r\n  <link rel=\"stylesheet\" th:href=\"@{/css/bootstrap.min.css}\">\r\n  <link rel=\"stylesheet\" th:href=\"@{/themes/smoothness/jquery-ui.css}\">\r\n\r\n</head>\r\n...\r\n```\r\n\r\nNote how those `~{::title}` and `~{::links}` parameters we are specifying in our `th:replace` will effectively *pass* parts of the original template markup (`main.html`) on to the inserted fragment (`base.html`), where these parts can be themselves inserted into specific placeholders. So the end result will look like:\r\n\r\n```html\r\n...\r\n<head>\r\n\r\n  <title>Awesome - Main</title>\r\n\r\n  <!-- Common styles and scripts -->\r\n  <link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"/awe/css/awesomeapp.css\">\r\n  <link rel=\"shortcut icon\" href=\"/awe/images/favicon.ico\">\r\n  <script type=\"text/javascript\" src=\"/awe/sh/scripts/codebase.js\"></script>\r\n\r\n  <link rel=\"stylesheet\" href=\"/awe/css/bootstrap.min.css\">\r\n  <link rel=\"stylesheet\" href=\"/awe/themes/smoothness/jquery-ui.css\">\r\n\r\n</head>\r\n...\r\n```\r\n\r\n**Compatibility with fragment insertion in older versions**\r\n\r\nEven if the new fragment expressions bring a lot of flexibility to fragment insertion operations in Thymeleaf, this does not mean that old `th:include` or `th:replace` attributes need to be changed. So even if Thymeleaf 3.0 *recommends* using specifying fragment insertion like this:\r\n```html\r\n<div th:replace=\"~{commons :: main}\">...</div>\r\n```\r\n...this does not mean that the previous, *unwrapped* way of expressing fragments doesn't work anymore. So this will still work perfectly:\r\n```html\r\n<div th:replace=\"commons :: main\">...</div>\r\n```\r\n\r\n\r\n**Writing fragments as text, not as... fragments**\r\n\r\nA new possibility that fragment expressions bring is using fragment expressions outside *fragment insertion* attributes (`th:insert`/`th:replace`/`th:include`). Specifically, they can be useful in `th:text` and `th:utext` attributes:\r\n\r\n```html\r\n<span th:utext=\"~{::title/text()}\">...</span>\r\n```\r\nNote there are important differences between `th:insert`, `th:text` and `th:utext` when used with a fragment expression:\r\n\r\n  * `th:insert` will *process* the inserted fragment (i.e. any `th:*` attributes in the fragment will get processed) while the others won't.\r\n  * `th:text` will *escape* the inserted fragment (after converting it to a `String` if needed) while the others won't.\r\n  * `th:utext` will therefore be almost equivalent to `th:insert`, but nothing inside the fragment will ever get processed.\r\n\r\nSo we can revisit the example above and rewrite the *title* of our `base.html` template, using a more elegant `th:utext` instead of the previous `th:replace`:\r\n```html\r\n<head th:fragment=\"common_header(title,links)\">\r\n\r\n  <title th:utext=\"${title}\">The awesome application</title>\r\n\r\n  <!-- Common styles and scripts -->\r\n  <link rel=\"stylesheet\" type=\"text/css\" media=\"all\" th:href=\"@{/css/awesomeapp.css}\">\r\n  <link rel=\"shortcut icon\" th:href=\"@{/images/favicon.ico}\">\r\n  <script type=\"text/javascript\" th:src=\"@{/sh/scripts/codebase.js}\"></script>\r\n\r\n  <!--/* Per-page placeholder for additional links */-->\r\n  <th:block th:replace=\"${links}\" />\r\n\r\n</head>\r\n```\r\nBut note this will input the `<title>` tag coming from `main.html` inside the `<title>` tag in `base.html`, so we need to modify the expression by which we call the fragment from `main.html` in order to include only the title's *text body* (`/text()`):\r\n```html\r\n...\r\n<head th:replace=\"base :: common_header(~{::title/text()},~{::link})\">\r\n\r\n  ...\r\n\r\n</head>\r\n...\r\n```\r\n\r\n**Fragment existence**\r\n\r\nFragment expressions do in general check the existence of the fragment they represent. But they do this in two different ways depending on when they are used:\r\n\r\n   * For fragment expressions used directly (i.e. not as a part of a complex expression) in attributes such as `th:insert`/`th:replace`/`th:include`/`th:text`/`th:utext`/... these expressions will **raise an exception** if the resolved fragment does not exist. This includes both the fragment's template not existing and also the specified fragment not existing in an existing template.\r\n\r\n   * For fragment expressions used as a part of complex expressions (conditionals, defaults, etc.) or also as parameters of other fragment expressions, these expressions will simply **evaluate to `null`** whenever the fragment or its container template do no exist. Note that this *nullity* means that fragment expressions used as the condition of a conditional expression will evaluate to `false`.\r\n\r\nLet's see some examples. These will raise an exception if template or fragment do not exist:\r\n```html\r\n<div th:insert=\"~{commons :: main}\">...</div>\r\n<div th:insert=\"commons :: main\">...</div>\r\n<div th:text=\"~{commons :: main}\">...</div>\r\n```\r\nHowever, when used as a fragment expression parameter, it will simply evaluate to `null`:\r\n```html\r\n<!-- Call the fragment with a \"title\" parameter that does not exist: title=null -->\r\n<div th:insert=\"~{commons :: header(title=~{::#maintitle})}\">...</div>\r\n...\r\n<!-- Once \"title\" is null th:utext's behaviour will be to write nothing -->\r\n<span th:utext=\"${title}\">...</span>\r\n...\r\n<!-- But note that, with the same null, th:insert's behaviour will be to fail -->\r\n<span th:insert=\"${title}\">...</span>\r\n```\r\nAlso, when used in a conditional or default expression the fragment will evaluate to `false` (because `null` is `false` in conditionals). Let's see a quite elaborate expression inserting a `base_{TYPE}` fragment depending on the type of user, but knowing that not all types actually exist in our template:\r\n```html\r\n<div th:insert=\"~{commons :: |#base_${user.type}|} ?: ~{commons :: #default}\">...</div>\r\n```\r\n\r\n**The EMPTY fragment**\r\n\r\nIf a nonexisting fragment will either fail or just return null, how can we make a fragment insertion expression *insert nothing*? In such case we can use the *empty fragment* expression: `~{}`.\r\n\r\nSo we can modify the expression above to specify that, if no *base* for the user type exists, we should simply insert *nothing*:\r\n```html\r\n<div th:insert=\"~{commons :: |#base_${user.type}|} ?: ~{}\">...</div>\r\n```\r\nNote that *inserting nothing* is not the same as *doing nothing*. In this case, the `...` in the body of the `<div>` tag will actually get removed and the result will be:\r\n\r\n```html\r\n<div></div>\r\n```\r\nWith this, we can revisit our header template and take into account the possibility that the calling template might not have any `<link>`s to add:\r\n```html\r\n<head th:fragment=\"common_header(title,links)\">\r\n\r\n  <title th:utext=\"${title}\">The awesome application</title>\r\n\r\n  <!-- Common styles and scripts -->\r\n  <link rel=\"stylesheet\" type=\"text/css\" media=\"all\" th:href=\"@{/css/awesomeapp.css}\">\r\n  <link rel=\"shortcut icon\" th:href=\"@{/images/favicon.ico}\">\r\n  <script type=\"text/javascript\" th:src=\"@{/sh/scripts/codebase.js}\"></script>\r\n\r\n  <!--/* Per-page placeholder for additional links */-->\r\n  <th:block th:replace=\"${links} ?: ~{}\" />\r\n\r\n</head>\r\n```\r\n\r\n**The new fragment syntax**\r\n\r\nFragment expressions in Thymeleaf 3.0 allow the use of the powerful *markup selector* syntax provided by AttoParser 2.0, Thymeleaf's new parsing library. This syntax builds on (and importantly extends) the fragment specification syntax available in Thymeleaf version 2.1, so that backwards compatibility is ensured.\r\n\r\nFor more detail on this syntax, read AttoParser's documentation for this syntax at http://www.attoparser.org/apidocs/attoparser/2.0.0.BETA3/org/attoparser/select/package-summary.html\r\n\r\nAs a Thymeleaf-specific adaptation, note that what AttoParser generically calls *reference-based matching* is applied by Thymeleaf by considering the `th:fragment` and `th:ref` attributes as *references*. So the `%whatever` and `whatever` expressions will match any tags with `th:fragment=\"whatever\"` or `th:ref=\"whatever\"` (see #196 on `th:ref`).\r\n\r\nSome very quick examples:\r\n\r\n  * `whatever` or `//whatever`: match any `<whatever>` tags, or any `whatever` references (`th:fragment=\"whatever\"`, `th:ref=\"whatever\"`) at any level in the markup hierarchy.\r\n  * `/whatever`: same as above, but only at the root level of the markup hierarchy.\r\n  * `foo/whatever`: same as above, but only as a direct child of an element/reference called `foo`.\r\n  * `foo//whatever`: same as above, but at any hierarchy level inside an element/reference called `foo`.\r\n  * `.whatever`: any tags with `class=\"whatever\"`.\r\n  * `div.whatever`: any `<div>` tags with `class=\"whatever\"`.\r\n  * `#whatever`: any tags with `id=\"whatever\"`.\r\n  * `div#whatever`: any `<div>` tags with `id=\"whatever\"`.\r\n  * `div#whatever/text()`: any text nodes contained as direct children of a `<div class=\"whatever\">`.\r\n  * `input[type='text']: any `<input>` tags with `type=\"text\"`.\r\n  * ...\r\n"},"comment":{"url":"https://api.github.com/repos/thymeleaf/thymeleaf/issues/comments/174697330","html_url":"https://github.com/thymeleaf/thymeleaf/issues/451#issuecomment-174697330","issue_url":"https://api.github.com/repos/thymeleaf/thymeleaf/issues/451","id":174697330,"user":{"login":"danielfernandez","id":1492299,"avatar_url":"https://avatars.githubusercontent.com/u/1492299?v=3","gravatar_id":"","url":"https://api.github.com/users/danielfernandez","html_url":"https://github.com/danielfernandez","followers_url":"https://api.github.com/users/danielfernandez/followers","following_url":"https://api.github.com/users/danielfernandez/following{/other_user}","gists_url":"https://api.github.com/users/danielfernandez/gists{/gist_id}","starred_url":"https://api.github.com/users/danielfernandez/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/danielfernandez/subscriptions","organizations_url":"https://api.github.com/users/danielfernandez/orgs","repos_url":"https://api.github.com/users/danielfernandez/repos","events_url":"https://api.github.com/users/danielfernandez/events{/privacy}","received_events_url":"https://api.github.com/users/danielfernandez/received_events","type":"User","site_admin":false},"created_at":"2016-01-25T22:21:25Z","updated_at":"2016-01-25T22:21:25Z","body":"Already in `3.0.0-SNAPSHOT`"}}