Skip to content

scrhartley/freemarker-fragments

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HTML Fragments using FreeMarker Example

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.

Partial rendering

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  >>>

Fully automatic (Spring Boot implementation)

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.

General Implementation

Implementation crux (no automation / manual)

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>

Alternative implementations + ideas

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>

Running

This is built using Spring Boot and so to start the server, either:

  • in your IDE, run the main method in FreeMarkerFragmentsApplication
  • as standalone:
    • either use ./mvnw spring-boot:run
    • or, build the project using ./mvnw clean install and then run the jar java -jar target/fragments.jar

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

About

Demo emulating template fragments using FreeMarker

Topics

Resources

Stars

Watchers

Forks

Languages