This is an example to emulate the benefits of fragments using the Java FreeMarker templating library. The main motivation to demonstrate this feature was to support use cases like htmx fragments.
In order to render conditionally, macros are used in a component-like style (inspired by React). By taking advantage of FreeMarker macros being defined at parse time, not at process time, we can call them before their definition in the file.
Example with macro definitions omitted
<!DOCTYPE html>
<html>
<@Head />
<body>
<@Menu />
<@HeroBanner />
<@Sidebar />
<@MainContent />
<@Footer />
</body>
</html>
<<< macros go here >>>
By using specific configuration,
we can optionally specify a fragment identifier as part of the view name in the Controller.
e.g. return "myView :: MyFragment";
where the template is myView.ftlh
and the macro to invoke is MyFragment
.
Including the fragment identifier will cause the matching macro in that template to be invoked and only the output
generated by that macro will be rendered.
When using the FullyAutomatic
strategy, nothing special is required from the template or the Controller. If a fragment
identifier is specified, then only the model attributes used by the selected macro are required (i.e. we avoid
evaluating the full template).
Optionally (disabled by default), the code can automatically convert kebab-case and snake_case identifiers to match
macros with UpperCamelCase/PascalCase names.
e.g. return "view :: my-fragment";
to invoke the macro MyFragment
.
If you don't want to check the source, then all that is happening is that we optionally add a FRAGMENT attribute to the model in the controller. The template then looks like this:
<#if FRAGMENT! == ''>
<@Page />
<#elseif FRAGMENT == 'fragment1'>
<@Fragment1 />
<#else>
<#stop 'Unknown fragment identifier: "${FRAGMENT}"'>
</#if>
<#macro Fragment1>
... fragment content ...
</#macro>
<#macro Page>
... page content, possibly also invoking Fragment1 macro ...
</#macro>
If you instead want the fragment lookup to be dynamic, rather than multiple if/else, you could do something like:
<#if FRAGMENT?has_content><@.vars[FRAGMENT] /><#else><@Page /></#if>
or if you still want to map the values and dislike if/else:
<#assign FRAGMENT_MACROS = {
'': 'Page',
'article': 'ArticleBlock'
} />
<@.vars[FRAGMENT_MACROS[FRAGMENT!]] />
Perhaps kebab-case to upper-camel-case translation would also be useful?
(i.e. convert "my-fragment" to "MyFragment" so your macros can use the component naming style)
<#assign FRAGMENT = FRAGMENT!?replace('-', ' ')?capitalize?replace(' ', '') />
Fragments only
It may sometimes be desirable to have a template file which is just a collection of fragments which are not part of a larger piece or where the larger piece is defined separately.
<#if FRAGMENT == 'fragment1'>
<@Fragment1 />
<#elseif FRAGMENT == 'fragment2'>
<@Fragment2 />
<#else>
<#stop 'Unknown or missing fragment identifier: "${FRAGMENT}"'>
</#if>
or maybe just:
<@.vars[FRAGMENT] />
Automating
Rather than having to handle calling the correct fragment macro at the top of each page,you could use
Configuration.addAutoInclude
(or the setting auto_include
) to reference a special template to be included at the
beginning of each template. This could use a convention to look for a specifically named macro if a fragment is
not set.
<#if FRAGMENT?has_content><@.vars[FRAGMENT] /><#elseif Page??><@Page /></#if>
Another option for the auto-included template is to contain a single macro which can be manually invoked by templates
which use fragments. If you're considering that, it may be nicer to instead use Configuration.setSharedVariable
and define a custom directive in Java
(see TemplateDirectiveModel).
<#macro autoInvoke primary=Page!>
<#if FRAGMENT?has_content><@.vars[FRAGMENT] /><#else><@primary /></#if>
</#macro>
This is built using Spring Boot and so to start the server, either:
- in your IDE, run the
main
method inFreeMarkerFragmentsApplication
- as standalone:
- either use
./mvnw spring-boot:run
- or, build the project using
./mvnw clean install
and then run the jarjava -jar target/fragments.jar
- either use
Pages
The same pages exist for both implementations under /auto
and /manual
.
Simple page and a fragment of it (the article text):
http://127.0.0.1:8080/auto
http://127.0.0.1:8080/auto/fragment
http://127.0.0.1:8080/manual
http://127.0.0.1:8080/manual/fragment
Page with a table and also a single row as a fragment.
A single row's HTML would be useful when the client wants to add a completely new row.
You can easily get the same HTML as for when you're rendering the whole table, without having to specify it more than
once and then needing to keep both versions in sync.
http://127.0.0.1:8080/auto/table
http://127.0.0.1:8080/auto/table/row
http://127.0.0.1:8080/manual/table
http://127.0.0.1:8080/manual/table/row