11. Details of snippet class

11.1. Rendering APIs

Asta4D provides various rendering APIs to help developers render value to the page. By those APIs, developer can render text under a DOM element, set the attribute value for a DOM element, also can convert a list data to a list of DOM element, and also other various manipulation on DOM elements.

11.1.1. Create and add Renderer instance

There are almost same two sets of overloaded methods: create and add. The create methods are static and can be used to create a Renderer instance. The add methods are instance methods and can be used to add a implicitly created Renderer instance to an existing Renderer instance. Both of the create and add methods return the created Renderer instance therefore chain invoking can be performed as well.

Example 11.1. 



Renderer renderer = Renderer.create("#someId", "xyz").add("sometag", "hello");
renderer.add(".someclass", "abc").add(".someclass2", "abc2");

Renderer renderer2 = Renderer.create("#someId2", "xyz2");
Renderer renderer3 = Renderer.create("#someId3", "xyz3");

//add renderer2 and renderer3 to renderer
renderer.add(renderer2);
renderer.add(renderer3);

        

Note that the order of a Renderer instance being added is significant but the target instance which a Renderer is added to has no effect on the rendering order. In the following example, "add renderer2 to renderer then add renderer3 to renderer2" is completely equal to "add renderer2 and renderer3 to renderer" at above example.

Example 11.2. 



//add renderer2 to renderer then add renderer3 to renderer2
renderer.add(renderer2);
renderer2.add(renderer3);


        

The following is equal too:

Example 11.3. 



//add renderer3 to renderer2 then add renderer2 to renderer
renderer2.add(renderer3);
renderer.add(renderer2);


        

A instance of Renderer should not be considered as a single rendering declaration only, A instance of Renderer is exactly a rendering chain holder, you can call add method on any instance of the chain but the added Renderer instance will be always added to the tail of the chain. If the added Renderer instance is holding over than one Renderer instance in its own chain, the held chain will be added to the tail of the chain of the target Renderer.

There is also a non-parameter create method which can by used to create a "do nothing" Renderer for source convenience. In our practice, we write the following line at the beginning of most of our snippet methods.

Example 11.4. 


Renderer renderer = Renderer.create();

        

In following sections, we will introduce add method only, but you should remember that there should be a equal create method for most cases. You can also read the Java doc of Renderer for more details.

11.1.2. CSS Selector

Asta4D is using a modified version of jsoup library to afford CSS selector function. Currently, we support the following selectors:

Table 11.1. Supported selectors

PatternMatchesExample
*any element*
tagelements with the given tag namediv
ns|Eelements of type E in the namespace nsfb|name finds <fb:name> elements
#idelements with attribute ID of "id"div#wrap, #logo
.classelements with a class name of "class"div.left, .result
[attr]elements with an attribute named "attr" (with any value)a[href], [title]
[^attrPrefix]elements with an attribute name starting with "attrPrefix". Use to find elements with HTML5 datasets[^data-], div[^data-]
[attr=val]elements with an attribute named "attr", and value equal to "val"img[width=500], a[rel=nofollow]
[attr^=valPrefix]elements with an attribute named "attr", and value starting with "valPrefix"a[href^=http:]
[attr$=valSuffix]elements with an attribute named "attr", and value ending with "valSuffix"img[src$=.png]
[attr*=valContaining]elements with an attribute named "attr", and value containing "valContaining"a[href*=/search/]
[attr~=regex]elements with an attribute named "attr", and value matching the regular expressionimg[src~=(?i)\\.(png|jpe?g)]
 The above may be combined in any orderdiv.header[title]
   
Combinators
E Fan F element descended from an E elementdiv a, .logo h1
E > Fan F direct child of Eol > li
E + Fan F element immediately preceded by sibling Eli + li, div.head + div
E ~ Fan F element preceded by sibling Eh1 ~ p
E, F, Gall matching elements E, F, or Ga[href], div, h3
   
Pseudo selectors
:lt(n)elements whose sibling index is less than ntd:lt(3) finds the first 2 cells of each row
:gt(n)elements whose sibling index is greater than ntd:gt(1) finds cells after skipping the first two
:eq(n)elements whose sibling index is equal to ntd:eq(0) finds the first cell of each row
:has(selector)elements that contains at least one element matching the selectordiv:has(p) finds divs that contain p elements
:not(selector)elements that do not match the selector. See also Elements.not(String)

div:not(.logo) finds all divs that do not have the "logo" class.

div:not(:has(div)) finds divs that do not contain divs.

:contains(text)elements that contains the specified text. The search is case insensitive. The text may appear in the found element, or any of its descendants.p:contains(jsoup) finds p elements containing the text "jsoup".
:matches(regex)elements whose text matches the specified regular expression. The text may appear in the found element, or any of its descendants.td:matches(\\d+) finds table cells containing digits. div:matches((?i)login) finds divs containing the text, case insensitively.
:containsOwn(text)elements that directly contains the specified text. The search is case insensitive. The text must appear in the found element, not any of its descendants.p:containsOwn(jsoup) finds p elements with own text "jsoup".
:matchesOwn(regex)elements whose own text matches the specified regular expression. The text must appear in the found element, not any of its descendants.td:matchesOwn(\\d+) finds table cells directly containing digits. div:matchesOwn((?i)login) finds divs containing the text, case insensitively.
 The above may be combined in any order and with other selectors.light:contains(name):eq(0)
   
Structural pseudo selectors
:rootThe element that is the root of the document. In HTML, this is the html element. In a snippet method, this is the element where the current snippet method is declared. In a recursive Renderer, this is the target element which the current renderer is applied to.:root
:nth-child(an+b)elements that have an+b-1 siblings before it in the document tree, for any positive integer or zero value of n, and has a parent element. For values of a and b greater than zero, this effectively divides the element's children into groups of a elements (the last group taking the remainder), and selecting the bth element of each group. For example, this allows the selectors to address every other row in a table, and could be used to alternate the color of paragraph text in a cycle of four. The a and b values must be integers (positive, negative, or zero). The index of the first child of an element is 1. In addition to this, :nth-child() can take odd and even as arguments instead. odd has the same signification as 2n+1, and even has the same signification as 2n.tr:nth-child(2n+1) finds every odd row of a table. :nth-child(10n-1) the 9th, 19th, 29th, etc, element. li:nth-child(5) the 5h li
:nth-last-child(an+b)elements that have an+b-1 siblings after it in the document tree. Otherwise like :nth-child()tr:nth-last-child(-n+2) the last two rows of a table
:nth-of-type(an+b)pseudo-class notation represents an element that has an+b-1 siblings with the same expanded element name before it in the document tree, for any zero or positive integer value of n, and has a parent elementimg:nth-of-type(2n+1)
:nth-last-of-type(an+b)pseudo-class notation represents an element that has an+b-1 siblings with the same expanded element name after it in the document tree, for any zero or positive integer value of n, and has a parent elementimg:nth-last-of-type(2n+1)
:first-childelements that are the first child of some other element.div > p:first-child
:last-childelements that are the last child of some other element.ol > li:last-child
:first-of-typeelements that are the first sibling of its type in the list of children of its parent elementdl dt:first-of-type
:last-of-typeelements that are the last sibling of its type in the list of children of its parent elementtr > td:last-of-type
:only-childelements that have a parent element and whose parent element hasve no other element children 
:only-of-typean element that has a parent element and whose parent element has no other element children with the same expanded element name 
:emptyelements that have no children at all 
   

Note: Because of the internal implementation of how to perform Renderer on target element, there may be unexpected temporary elements to be added into the Current DOM tree, which will be removed safely at the final stage of page production but may cause the structural pseudo selectors work incorrectly (However, the :root selector can still work well).

11.1.3. Render text

add(String selector, String value) can be used to render a text under the element specified by given selector. All child nodes of the target element specified by selector will be emptied and the given String value will be rendered as a single text node of the target element.

Example 11.5. 


renderer.add("#someId", "xyz");

        

Long/long, Integer/int, Boolean/boolean will be treated as text rendering too.

Example 11.6. 


renderer.add("#someIdForLong", 123L);
renderer.add("#someIdForInt", 123);
renderer.add("#someIdForBool", true);

        

11.1.4. Render DOM attribution

add(String selector, String attr, String value) can be used to render attribute value of a DOM element. There are some rules will be applied for the pattern of specified "attr" and "value":

  • add("+class", value)

    call addClass(value) on target Element, null value will be treated as "null".

  • add("-class", value)

    call removeClass(value) on target Element, null value will be treated as "null".

  • add("class", value)

    call attr("class", value) on target Element if value is not null, for a null value, removeAttr("class") will be called.

  • add("anyattr", value)

    call attr("anyattr", value) on target Element if value is not null, for a null value, removeAttr("anyattr") will be called.

  • add("anyattr", SpecialRenderer.Clear)

    call removeAttr("anyattr") on target Element.

  • add("+anyattr", value)

    call attr("anyattr", value) on target Element if value is not null, for a null value, removeAttr("anyattr") will be called.

  • add("+anyattr", SpecialRenderer.Clear)

    call removeAttr("anyattr") on target Element.

  • add("-anyattr", value)

    call removeAttr("anyattr") on target Element.

There is also an add method for attribution rendering that accepts arbitrary data as Object type: add(String selector, String attr, Object value). When the "value" is specified as a non-string value and the "attr" is specified as "+class" or "-class", A IllegalArgumentException will be thrown. When an arbitrary Object value is rendered to attribute by such method, an internal object reference id will be rendered to the target attribute instead of the original object since it cannot be treated as attribute string value directly. The object reference id can be used by variable injection for the nested snippet rendering. See the following example, we pass a Date instance to the nested snippet method:

Example 11.7. 


<div afd:render="MySnippet:outer">
  <div id="inner" afd:render="MySnippet:inner">
    <span id="current-date"></span>
  </div>
</div>

        

public class MySnippet{

    public Renderer outer(){
        return Renderer.create("#innder", "now", new Date());
    }
    
    public Renderer inner(Date now){
        return Renderer.create("#current-date", now);
    }
}

        

This mechanism can be used for parametrized embedding too. Parameters can be specified by attribution rendering in the snippet method of parent template file and can be retrieved by the snippet method of child template file as same as the above example.

Example 11.8. 


<!-- parent template -->
<div afd:render="MySnippet:outer">
  <afd:embed id="inner" target="child.html"></afd:embed>
</div>

        

<!-- child template -->
  <div afd:render="MySnippet:embed">
    <span id="current-date"></span>
  </div>

        

public class MySnippet{

    public Renderer outer(){
        return Renderer.create("#innder", "now", new Date());
    }
    
    public Renderer embed(Date now){
        return Renderer.create("#current-date", now);
    }
}

        

11.1.5. Clear an Element

On all the rendering methods, if the specified value is null, the target element will be removed. And there is also an enumeration value of SpecialRenderer.Clear which can be used to declare a remove operation for an element too.

Example 11.9. 


//if the String value is null, the target element will be removed
String txt = null;
Renderer.create("#someId", txt);

        

import static com.astamuse.asta4d.render.SpecialRenderer.Clear

Renderer.create("#someId", Clear);

        

Note that on the attribute rendering methods, if null value or SpecialRenderer.Clear are specified, as we mentioned in the previous section, the target attribute will be removed and the target element will remain.

11.1.6. Render raw Element

An existing DOM element can be replaced by a new element by specifying the new element as the rendering value. The new element can be generated by DOM APIs or simply parsed from raw html source.

Example 11.10. 



Renderer renderer = Renderer.create();

Element ul = new Element(Tag.valueOf("ul"), "");
List<Node> lis = new ArrayList<>();
ul.appendChild(new Element(Tag.valueOf("li"), "").appendText("This text is created by snippet.(1)"));
ul.appendChild(new Element(Tag.valueOf("li"), "").appendText("This text is created by snippet.(2)"));
ul.appendChild(new Element(Tag.valueOf("li"), "").appendText("This text is created by snippet.(3)"));

renderer.add("#someId", ul);

String html = "<a href=\"https://github.com/astamuse/asta4d\">asta4d hp</a>";
renderer.add("#hp-link", ElementUtil.parseAsSingle(html));


        

There are several utility methods in ElementUtil that can help you operate DOM easier.

ElementUtil#parseAsSingle would cause potential cross-site issues, so take care of it and make sure that you have escaped all the raw html source correctly. Since Asta4D has provided plenty of rendering methods, you should try your best to avoid create element from raw html source. If you have to do something on raw element level, your first option should be DOM APIs and parsing raw html source should be your last alternative.

11.1.7. Arbitrary rendering for an Element

Sometimes you will want to access the rendering target on raw element level and Asta4D allow you access the rendering target element by the interface of ElementSetter. ElementSetter asks the implementation of a callback method "set(Element elem)". As a matter of fact, the text rendering and attribute rendering are achieved by built-in implementation of ElementSetter(TextSetter and AttributeSetter).There is also another built-in ElementSetter implementation called ChildReplacer which can replace the children of rendering target by given element.

Example 11.11. source of ChildReplacer



public class ChildReplacer implements ElementSetter {

    private Element newChild;

    /**
     * Constructor
     * 
     * @param newChild
     *            the new child node
     */
    public ChildReplacer(Element newChild) {
        this.newChild = newChild;
    }

    @Override
    public void set(Element elem) {
        elem.empty();
        elem.appendChild(newChild);
    }
}

        

You can regard the built-in TextSetter, AttributeSetter and ChildReplacer as reference implementation in case of you need implement your own ElementSetter. Following example shows how to use ChildReplacer or declare an anonymous class for ElementSetter on rendering.

Example 11.12. 



Renderer renderer = Renderer.create();

renderer.add("#someId", new ChildReplacer(ElementUtil.parseAsSingle("<div>aaa</div>")));

renderer.add("#someNode", new ElementSetter(){
    public void set(Element elem) {
        if(elem.tagName().equals("div")){
            elem.addClass("someClass);
        }
    }
});


        

On element level, you can also parse element by Element#html(String html) method which will also cause potential cross-site issues. As same as the raw element rendering, parsing raw html source should be your last alternative.

11.1.8. Recursive rendering

For a snippet method, the returned Renderer will be applied to the target element which declares the current snippet method. In the returned Renderer chain, you can also specify recursive Renderer which is only applied to the element specified by given CSS selector.

Example 11.13. 


Renderer.create("#someId", Renderer.create("a", "href", "https://github.com/astamuse/asta4d"));

        

11.1.9. Debug renderer

Because the Renderer is applied after your snippet method finished, it is difficult to debug if there is something wrong. Asta4D affords rendering debug function too. There are two overloaded debug method, one accepts a log message only and another accepts a log message and a selector. The debug renderer will output the current status of rendering target element to log file, if the selector is not specified, the whole rendering target of current snippet method will be output, if the selector is specified, only the matched child elements will be output. Log level can be configured by "com.astamuse.asta4d.render.DebugRenderer".

Example 11.14. 



//the whole target rendering target will be output before and after

renderer.addDebugger("before render value");

renderer.add("#someId", value);

renderer.addDebugger("after render value");


//only the a tag element will be output before and after

renderer.addDebugger("before render value for link", "a");

renderer.add("a", "href", url);

renderer.addDebugger("after render value for link", "a");


        
If you specified a selector on debugger but do not get any output, it usually means you specified wrong selector or the target element has been removed by previous rendering or outer snippet rendering.

11.1.10. Missing selector warning

If your specified selector on rendering method cannot match any element, Asta4D will output a warning message to log as "There is no element found for selector [#someId] at [ com.astamuse.asta4d.test.render.RenderingTest$TestRender.classAttrSetting(RenderingTest.java:73) ], if it is deserved, try Renderer#disableMissingSelectorWarning() to disable this message and Renderer#enableMissingSelectorWarning could enable this warning again in your renderer chain".

Note that the place where the missing selector declared is output to log too, which is disabled by default since it is expensive on production environment. You can enable the source row number output by Configuration#setSaveCallstackInfoOnRendererCreation.

Sometimes, the missing selector may be designed by your logic, you can disable the warning message on your rendering chain temporarily as following:

Example 11.15. 



renderer.disableMissingSelectorWarning();

renderer.add("#notExistsId", "abc");

renderer.enableMissingSelectorWarning();

        

11.1.11. List rendering

Asta4D does not only allow you to rendering single value to an element, but also allow you to duplicate elements by list data, which is usually used to perform list rendering. java.lang.Iterable of String/Integer/Long/Boolean can be rendered directly as text rendering:

Example 11.16. 



Renderer renderer = Renderer.create();

renderer.add("#strList", Arrays.asList("a", "b", "c"));

renderer.add("#intList", Arrays.asList(1, 2, 3));

renderer.add("#longList", Arrays.asList(1L, 2L, 3L));

renderer.add("#boolList", Arrays.asList(true, false, true));


        

On list rendering, the matched element will be duplicated times as the size of the list and the value will be rendered to each duplicated element. You can also perform complex rendering for arbitrary list data by RowConvertor:

Example 11.17. 



render.add("#someIdForRenderer", Arrays.asList(123, 456, 789), new RowConvertor<Integer, Renderer>() {
    @Override
    public Renderer convert(int rowIndex, Integer obj) {
        return Renderer.create("#id", "id-" + obj).add("#otherId", "otherId-" + obj);
    }
});


        

Since we will return Renderer in most cases, the RowConvertor can be replaced by RowRenderer which can omit the type declaration of Renderer:

Example 11.18. 



render.add("#someIdForRenderer", Arrays.asList(123, 456, 789), new RowRenderer<Integer>() {
    @Override
    public Renderer convert(int rowIndex, Integer obj) {
        return Renderer.create("#id", "id-" + obj).add("#otherId", "otherId-" + obj);
    }
});


        

The returned Renderer by RowConvertor/RowRenderer will be applied to each duplicated element by the given list data.

On list rendering, the rendering method accepts java.lang.Iterable rather than java.util.List, which afford more flexibilities to developers. Note that if the given iterable is empty, the target element will be duplicated 0 times, which means the target element will be removed.

Asta4D also afford parallel list rendering for you, simply use ParallelRowConvertor/ParallelRowRenderer instead of RowConvertor/RowRenderer:

Example 11.19. 



render.add("#someIdForRenderer", Arrays.asList(123, 456, 789), new ParallelRowRenderer<Integer>() {
    @Override
    public Renderer convert(int rowIndex, Integer obj) {
        return Renderer.create("#id", "id-" + obj).add("#otherId", "otherId-" + obj);
    }
});


        

The parallel list rendering ability is provided by an utility class called ListConvertUtil which basically provides various transform methods for list data conversion like the map function in Scala/Java8. For Java8, lambda and stream api support will be added in future.

The ListConvertUtil uses a thread pool to perform parallel transforming, the size of pool can be configured by Configuration#listExecutorFactory#setPoolSize. There is also an import configuration of ParallelRecursivePolicy. When perform the parallel list transforming recursively, thread dead lock would potentially occur, so you have to choose a policy to handle this case:

  • EXCEPTION

    When recursive parallel list transforming is identified, throw an RuntimeException.

  • CURRENT_THREAD

    When recursive parallel list transforming is identified, the child transforming will be performed in the same thread of parent transforming without picking up an usable thread from pool.

  • NEW_THREAD

    When recursive parallel list transforming is identified, the child transforming will be performed in a newly generated thread out of the configured thread pool, the generated thread will be finished immediately after the child transforming finished.

We recommend EXCEPTION or CURRENT_THREAD and the default is EXCEPTION.

11.2. Other things about snippet and rendering

11.2.1. Nested snippet

Snippet can be declared recursively.There is a convention that the outer snippet will always be executed on a prior order.

Example 11.20. 


<div afd:render="OuterSnippet">
    <div id="inner" afd:render="InnerSnippet">  
        <p id="name">name:<span>dummy name</span></p>  
        <p id="age">age:<span>0</span></p>  
    </div>
</div>

      

In the above example, the "InnerSnippet" will be executed after the "OuterSnippet" has been executed, which also means you can configure the inner snippet dynamically.

Example 11.21. 


public class OuterSnippet{

    public Renderer render(){
        return Renderer.create("#inner", "hasprofile", "true");
    }

}

public class InnerSnippet{

    public Renderer render(bool hasprofile){
        if(hasprofile){
            return Renderer.create("#name span", "Bob").add("#age span", 27);
        }else{
            return Renderer.create("*", Clear);
        }
    }

}

      

11.2.2. InitializableSnippet

The snippet class instance is singleton in request scope and will be created at the first time a snippet is required. After the snippet class instance has been created, field value injection will be applied to the created instance once(About detail of value injection, see the later chapter). After all the field has been injected, the framework will check whether the current class implements the "InitializableSnippet" interface, if true, the init method of InitializableSnippet will be invoked once.

Example 11.22. 


public static class InitSnippet implements InitializableSnippet {

    @ContextData
    private String value;

    private String resolvedValue;

    @Override
    public void init() throws SnippetInvokeException {
        resolvedValue = value + "-resolved";
    }
}

      

In the above example, the field "value" will be injected after the instance is created, then the init method will be applied to finish the complete initialization logic of the snippet class.

11.2.3. Component rendering

For the view first policy, we will treat the template files as first class things. The embed file mechanism allow you separate some view blocks as independent components at template file layer. There is also a snippet class level mechanism that afford you the same ability as embed file, which is called "Component".

Why we need "Component"? The embed file can only be include statically, but the "Component" is an extendible Java class which can be configured by arbitrary Java code and polymorphism can be easily performed too. Basically, The "Component" mechanism can help developer to build independent view component easier than static embed file.

The constructor of Component accepts a string value as an embed file path or an instance of Element, and also an optional AttributesRequire can be specified to provide some initialization parameters as same as the parametrized embedding introduced in the previous section.

Example 11.23. constructors of Component


    public Component(Element elem, AttributesRequire attrs) throws Exception {
        ...
    }

    public Component(Element elem) throws Exception {
        ...
    }

    public Component(String path, AttributesRequire attrs) throws Exception {
        ...
    }

    public Component(String path) throws Exception {
        ...
    }

      

Example 11.24. render a component


public Renderer render(final String ctype) throws Exception {
    return Renderer.create("span", new Component("/ComponentRenderingTest_component.html", new AttributesRequire() {
        @Override
        protected void prepareAttributes() {
            this.add("value", ctype);
        }

    }));
}

      

In this example, the target element specified by the selector "span" will be completely replaced by the result of Component#toElement().


Commonly, we do not render a component as above example, we usually extend from Component and expose a constructor with business initialization parameters or supply a group of business initialization methods, by which we can build an independent view component simply.

Further, Component also has a "toHtml" method which will return the html source of the component rendering result. This method can be used by ajax request to acquire a dynamically rendered component.

11.2.4. SnippetInterceptor

11.2.5. SnippetExtractor

11.2.6. SnippetResolver

11.2.7. SnippetInvoker