# 6.4 Directives

The Enjoy Template Engine consistently adheres to a minimalist design. With just seven core directives—#if, #for, #switch, #set, #include, #define, #(...)—it achieves nearly all the functionalities of traditional template engines. The learning curve is almost non-existent for users with a foundation in any programming language.

If the built-in directives do not meet your needs, you can easily extend them at the template language level. Under the com.jfinal.template.ext.directive package, there are five extended directives. The Active Record SQL module also extends three directives specifically for SQL management. By studying the code for these extended directives, you can easily understand how to create your own, making the process extremely simple.

Note that the extension of Enjoy template engine directives occurs at the lexical and syntactic analysis levels. This is entirely different from the tag-based extensions of traditional template engines. The former allows for comprehensive and flexible use of the engine's basic infrastructure, enabling versatile functionalities through very simple and straightforward code. To understand its power and convenience, refer to the SQL management module of Active Record.

# 1. Output Directive #(...)

Unlike almost all Java template engines, Enjoy Template Engine eliminates the independent concept of interpolation directives, treating them as one among other directives. This directive uses parentheses as delimiters, just like any other directive, avoiding the need for extra delimiters like curly braces.

Using the output directive #(...) is very simple; just pass any expression described in Section 6.4. The directive will output the evaluation result of these expressions. Specifically, if the expression evaluates to null, there will be no output and no exceptions will be thrown. For example:

#(value)
#(object.field)
#(object.field ??)
#(a > b ? x : y)
#(seoTitle ?? "JFinal Club")
#(object.method(), null)
1
2
3
4
5
6

As shown above, just pass an expression to the output directive. Note that in the first line, the value parameter can be null. If object is null in the second line, an exception will be thrown. To avoid this, use the null-safe access operator object.field ??.

Additionally, note the last line in the example. The directive argument is a comma expression, and the directive will only output the last expression in the comma sequence. If the last expression evaluates to null, no output will be generated.

The output directive can be customized by extending the OutputDirectiveFactory class and overriding its getOutputDirective method. Then you can switch to your custom output directive using me.setOutputDirectiveFactory(...) in the configEngine(Engine me) method.

# 2. #if Directive

Example:

#if(cond)
  ...
#end
1
2
3

As shown, the #if directive requires a cond expression and ends with #end. The cond can be any expression introduced in Section 6.3. When cond evaluates to true, the code inside the if block is executed.

The #if directive naturally supports #else if and #else blocks. Example:

#if(c1)
  ...
#else if(c2)
  ...
#else if(c3)
  ...
#else
  ...
#end
1
2
3
4
5
6
7
8
9

The usage of #else if and #else is identical to Java syntax. (Note: In versions prior to JFinal 3.3, you must write #elseif without any space between else and if, otherwise an exception will be thrown.)

# 3. #for Directive

The Enjoy Template Engine offers an extremely user-friendly extension to the #for directive, allowing iteration over any type of data, including null values. Code example:

// Iterating over List, array, Set structures
#for(x : list)
  #(x.field)
#end

// Iterating over a Map
#for(x : map)
  #(x.key)
  #(x.value)
#end
1
2
3
4
5
6
7
8
9
10

The first #for directive iterates over a list, and the usage is identical to Java syntax.

The second #for directive iterates over a map, using item.key and item.value to access the elements. This is an enhancement by Enjoy to reduce code verbosity. You can also use the traditional Java map iteration method: #for(x : map.entrySet()) #(x.key) #(x.value) #end

Note: When the target of iteration is null, you don't need to check for null values; the #for directive will automatically skip the iteration, avoiding the need for if checks and thereby improving efficiency.

The #for directive also allows you to access its state. Code example:

#for(x : listAaa)
  #(for.index)
  #(x.field)
  
  #for(x : listBbb)
     #(for.outer.index)
     #(for.index)
     #(x.field)
  #end
#end
1
2
3
4
5
6
7
8
9
10

In the code above, #(for.index) and #(for.outer.index) are used to access the current state of the #for directive. The former gets the current iteration index (starting from 0), and the latter allows an inner #for directive to access the state of an outer #for directive.

Note: When #for directives are nested, each has its own variable scope, consistent with Java language rules. For example, the two instances of #(x.field) in the example above are in different #for directive scopes and will correctly access the variable values in their respective scopes.

The #for directive supports the following states:

#for(x : listAaa)
   #(for.size)    Size of the iterated object
   #(for.index)   Index starting from 0
   #(for.count)   Count starting from 1
   #(for.first)   Is it the first iteration?
   #(for.last)    Is it the last iteration?
   #(for.odd)     Is it an odd iteration?
   #(for.even)    Is it an even iteration?
   
   #(for.outer)        References the state of the outer #for directive
   #(for.outer.size)   Size of the iterated object in the outer #for directive
#end
1
2
3
4
5
6
7
8
9
10
11
12

The specific usage is explained in Chinese in the code above.

Apart from Map and List, the #for directive also supports Collection, Iterator, array, Iterable, Enumeration, and null value iteration. The usage is identical in form to the earlier List iteration, all being #for(id : target). For null values, the #for directive will directly skip the iteration.

Additionally, the #for directive supports iteration over any type of object, simply iterating over it once. Example:

#for(x : article)
   #(x.title)
#end
1
2
3

In the example above, article is just a regular Java object, not a collection. The #for loop will iterate over this object just once. The x in the #for expression is the article object itself, so you can use #(x.title) for output

The #for directive also supports the #else branch. When the number of iterations is zero, the #else block is executed. Example:

#for(blog : blogList)
   #(blog.title)
#else

You haven't written any blogs yet. Click here <a href="/blog/add">to start blogging</a>.

#end
1
2
3
4
5
6
7

In the code above, when blogList.size() is 0 or blogList is null, meaning the number of iterations is zero, the #else branch will execute. This scenario is very common in web projects.

Lastly, in addition to the #for directive iteration methods mentioned above, it also supports a more conventional for loop syntax. Code example:

#for(i = 0; i < 100; i++)
   #(i)
#end
1
2
3

The syntax is almost identical to Java, with the only difference being that variable declarations don't need types; direct assignment statements suffice. Variables in the Enjoy Template Engine are dynamically typed.

Note: This form of for loop lacks for.size and for.last states, and only supports the following states: for.index, for.count, for.first, for.odd, for.even, for.outer.

The #for directive also supports #continue and #break commands, and their usage is entirely consistent with Java. Further details are not provided here.

# 4. #switch Directive (Added in version 3.6)

The #switch directive is aligned with Java's switch statement, offering similar basic usage but with some improvements for a better user experience. Usage is as follows:

#switch (month)
  #case (1, 3, 5, 7, 8, 10, 12)
    #(month) months have 31 days
  #case (2)
    #(month) has 28 days in a common year and 29 days in a leap year
  #default
    Invalid month: #(month ?? "null")
#end
1
2
3
4
5
6
7
8

As shown in the code above, the #case directive supports multiple arguments separated by commas, effectively eliminating the need for a #break directive. Therefore, the Enjoy Template Engine does not require the use of #break.

Parameters for the #case directive can be any expression, such as:

#case (a, b, x + y, "abc", "123")
1

In the code above, the comma-separated expressions are first evaluated, and then each is compared to the value in #switch(value). If any of these values match, that #case branch is executed.

The support for multiple parameters in #case, without the need for #break, not only reduces code but also eliminates potential errors from forgetting to include #break. Also, unlike Java syntax, neither #case nor #default uses a colon character.

# 5. #set Directive

The #set directive is used for variable declaration and assignment. It accepts only assignment expressions or a list of comma-separated assignment expressions. Code example:

#set(x = 123)
#set(a = 1, b = 2, c = a + b)
#set(array[0] = 123)
#set(map["key"] = 456)

#(x)  #(c)  #(array[0])  #(map.key)  #(map["key"])
1
2
3
4
5
6

In the above code, the first line simply assigns 123 to x. The second line is a list of assignment expressions executed from left to right. If the right-hand side of the equation has an expression, it will be evaluated before assignment. The last line outputs the values of the variables, and other directives can also access these variables like the output directive.

Note that the #for, #include, and #define directives create new variable scopes. The #set directive first looks for the variable in the current scope; if found, it operates on it, otherwise, it continues to the upper scope. If still not found, it defines the variable in the top-level scope. This design is very beneficial for variable value transmission within templates.

To explicitly specify assignment in the current scope, you can use the #setLocal directive, which has the same parameters and usage as #set but operates only in the current scope. #setLocal is often used within #define and #include to avoid naming conflicts.

Important: Since assignment expressions are essentially expressions, and other directives inherently support any expression, the #set directive is not mandatory for assignments. For example, assignments can be done in the #() output directive:

#(x = 123, y = "abc", array = [1, "a", true], map = {k1:v1}, null)
1

The code above uses multiple assignment expressions in the output directive to achieve the functionality of #set, ending with a null value to avoid any output. Similarly, other directives can also use assignment expressions.

# 6. #include Directive

The #include directive is used to include external template content, which is parsed as part of the current template. Code example:

#include("sidebar.html")
1

The first parameter of the #include directive must be a String constant. If it starts with /, it will look for the file relative to baseTemplatePath; otherwise, it will look for the file relative to the current template's path.

baseTemplatePath can be configured in configEngine(Engine me) through me.setBaseTemplatePath(...).

Additionally, the #include directive supports an unlimited number of assignment expressions, which is very beneficial for modularization. For example, the template file named _hot_list.html is used to display hot projects, hot news, etc.:

<div class="hot-list">
  <h3>#(title)</h3>
  <ul>
    #for(x : list)
    <li>
      <a href="#(url)/#(x.id)">#(x.title)</a>
    </li>
    #end
  </ul>
</div>
1
2
3
4
5
6
7
8
9
10

The variables title, list, and url in the HTML fragment are needed for rendering "Hot Projects" and "Hot News":

#include("_hot_list.html", title="Hot Projects", list=projectList, url="/project")
#include("_hot_list.html", title="Hot News", list=newsList, url="/news")
1
2

In the two lines above, different values for title, list, and url are passed to _hot_list.html, enabling modular reuse of _hot_list.html.

# 7. #render Directive

The #render directive is nearly identical to #include in usage, supporting an unlimited number of assignment expressions. There are two main differences:

  1. #render supports dynamic template parameters, e.g., #render(temp), where temp can be any expression. In contrast, #include can only use string constants like #include("abc.html").

  2. Functions defined in #render using #define are only effective in its sub-template and are not valid in the parent template. This design is very beneficial for modularization.

The core purpose of introducing the #render directive is to support dynamic template parameters.

# 8. #define Directive

The #define directive is one of the main ways to extend the template engine. It allows you to define template functions (Template Functions). With #define, you can define reusable template segments as individual template functions, which can be customized by passing in parameters when called.

Here's how to use #define to implement a layout feature. First, create a layout.html file with the following code:

#define layout()
<html>
  <head>
    <title>JFinal Club</title>
  </head>
  <body>
    #@content()
  </body>
</html>
#end
1
2
3
4
5
6
7
8
9
10

In the code above, a template function named layout is defined using #define layout(). It ends with #end. Inside it, #@content() refers to another template function named content.

Note: When calling a template function, an additional @ symbol is used to differentiate it from directive calls.

Next, create another template file as follows:

#include("layout.html")
#@layout()

#define content()
<div>
  This is the template content, similar to the 'nested' part of traditional template engines.
</div>
#end
1
2
3
4
5
6
7
8

The first line includes the previously created layout.html. The second line calls the layout template function defined in layout.html, which in turn calls the content function defined in the current file. Simply understand this as function definition and function invocation.

To make a layout template reusable across many templates, you can set all the template functions in that file as shared by using me.addSharedFunction("layout.html") in configEngine(Engine me). This eliminates the need for #include, reducing code redundancy and improving reusability.

The Enjoy Template Engine eliminates boring concepts like layout, nested, and macro, thus reducing the learning curve and greatly enhancing extensibility.

Template functions support formal parameters, similar to Java but without specifying the parameter type. Here's a code example:

#define test(a, b, c)
  #(a)
  #(b)
  #(c)
#end
1
2
3
4
5

To call this function, you would use:

#@test(123, "abc", user.name)
1

The number of formal and actual parameters should match. If you need to pass additional, optional parameters, you can use the #set directive beforehand.

# 9. Template Function Calls and #call Directive

To call a template function defined by #define, use #@name(p1, p2…, pn). Note the additional @ symbol to differentiate it from directive calls.

Template functions also support safe invocation, denoted by #@name?(p1, p2…, pn). Safe calls do nothing if the template function is not defined, making them suitable for optional template content. For example:

#define layout()
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="/assets/css/jfinal.css">
    #@css?()
  </head>
  <body>
    <div class="content">
      #@main()
    </div>
    <script type="text/javascript" src="/assets/js/jfinal.js"></script>
    #@js?()
  </body>
</html>
#end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

You can then define additional CSS or JS content through #define css() and #define js() in templates that require them.

The #call directive is new in jfinal 3.6 and allows for dynamic specification of both the template function name and parameters. The usage is as follows:

#call(funcName, p1, p2, ..., pn)
1

To ignore the call if the function doesn't exist, add a constant value true as the first parameter:

#call(true, funcName, p1, p2, ..., pn)
1

This makes the template function calling mechanism much more flexible.

# 10. #date Directive

The #date directive is used for formatting the output of date-type data, including all objects that inherit from the Java Date class, such as Date and Timestamp. The usage is incredibly straightforward:

#date(account.createAt)
#date(account.createAt, "yyyy-MM-dd HH:mm:ss")
1
2

In the first line of code, only one parameter is used, so the date is formatted using the default date pattern, which is "yyyy-MM-dd HH:mm". In the second line, the date is formatted according to the pattern specified in the second parameter.

If you want to change the default output format, you can configure it using engine.setDatePattern().

# keepPara Issue

When a date-type field is submitted to the backend, and the backend uses the Controller's keepPara() method, it converts this date-type data into a String type. In such cases, using #date(...) to output this String will throw an exception. To handle this, you can 'keep' the type as shown below:

// keepPara() keeps all form submission data, converting them to String types.
keepPara();

// Use parameterized keepPara again to specify that the 'createAt' field should be kept as a Date type.
keepPara(Date.class, "createAt");
1
2
3
4
5

As shown above, the second line of code uses the Date.class parameter to specifically keep the 'createAt' field as a Date type. This way, the #date(createAt) directive on the page will not throw an exception. Methods like keepModel(...) and keepBean(...) will maintain the original types, so no additional handling is needed.

# 11. #number Directive

The #number directive is used to format the output of numerical data, including all objects that inherit from the Java Number class, such as Double, Float, Integer, Long, and BigDecimal. The usage is exceedingly simple:

#number(3.1415926, "#.##")
#number(0.9518, "#.##%")
#number(123456789, ",###")
#number(300000, "The speed of light is ### km per second.")
1
2
3
4

In the above examples, the first parameter for the #number directive is of a numerical type, and the second parameter is a String pattern. The pattern parameter works the same way as the pattern used in Java's DecimalFormat class. If you're not sure how to use the pattern, you can search for "DecimalFormat" in a search engine to find plenty of resources.

The two parameters for the #number directive can be variables or complex expressions. The use of constants in the above examples is merely for demonstration purposes.

# 12. #escape Directive

The #escape directive is used for HTML-safe output, helping to mitigate XSS attacks. It escapes characters like '<' and '>' in HTML-formatted data. For example, it would convert '<' to &lt; and a space to &nbsp;.

Used in a similar way to the output command:

#escape(blog.content)
1

# 13. Directive Extension

Thanks to the original DKFF and DLRD algorithms, the Enjoy Template Engine allows for extremely convenient extensions of directives at the language level. The amount of code required is minimal, and the learning curve is virtually non-existent. Here's a code example:

public class NowDirective extends Directive {
  public void exec(Env env, Scope scope, Writer writer) {
    write(writer, new Date().toString());
  }
}
1
2
3
4
5

In the above code, by extending the Directive class and implementing the exec method, a new #now directive is created with just three lines of code. This directive outputs the current date into the template. To use it, you only need to add it to the template engine using me.addDirective("now", NowDirective.class). Here's how to use this directive in a template:

The current date is: #now()
1

In addition to supporting directives without an #end block (i.e., directives without a body), the Enjoy Template Engine also directly supports directives that include an #end and a body. Here's an example:

public class Demo extends Directive {

  // ExprList represents the list of parameter expressions for the directive
  public void setExprList(ExprList exprList) {
    // Custom control of exprList can be done here
    super.setExprList(exprList);
  }
  
  public void exec(Env env, Scope scope, Writer writer) {
    write(writer, "Before executing body");
    stat.exec(env, scope, writer);  // Execute body
    write(writer, "After executing body");
  }
  
  public boolean hasEnd() {
    return true;  // Returning true means this directive has an #end ending tag
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

In the example above, the Demo class extends Directive and overrides the hasEnd method, returning true. This indicates that the extended directive has an #end ending tag. The exec method contains three lines of code. The stat.exec(...) line executes the code within the directive's body. The lines before and after stat.exec(...) output a string, and the final output is a combination of all these. Additionally the parameters of the directive can be controlled by overriding the setExprList(...) method of the parent class, which is not required.

After adding to the engine via me.addDirective("demo", Demo.class), it can be used as in the following code example: `` #demo() Here is the content of the demo body #end

The final output looks like this:
1

body Before execution Here is the content of the demo body After body

The #demo directive body in the above example contains a string of characters that will be executed by stat.exec(...) in the Demo.exec(...) method, and write(...) before and after stat.exec(...). The results produced by the two method calls and the results produced by body generate the final result.
**Important**: Properties declared in directives are globally shared, so they must be thread-safe. For instance, consider a snippet from `com.jfinal.template.ext.directive.DateDirective`:

```java
public class DateDirective extends Directive {
	
   private Expr valueExpr;
   private Expr datePatternExpr;
   private int paraNum;
 
   ...
}
1
2
3
4
5
6
7
8
9
10
11
12

Here, there are three properties of types Expr, Expr, and int. Expr is thread-safe. Although int paraNum may not seem thread-safe, it is only written to during initialization in the constructor, and read everywhere else. Thus, this int property is also thread-safe in this context.

# 14. Common Mistakes

The most common mistake when using the Enjoy Template Engine is confusing "expressions" and "non-expressions." Expressions refer to everything within the parentheses when calling a directive or template function:

#directiveName(everything here is an expression)
#@functionName(everything here is an expression)
1
2

For example, the correct usage is:

#directiveName(user.name)
1

A common mistake would be:

#directiveName( #(user.name) )
1

Simply put, this mistake involves using a directive where an expression should be used. Never use the '#' character within an expression; just use Java expressions directly.

Last Updated: 9/22/2023, 4:50:57 AM