Search code examples
javaspringmodelthymeleafextending

Thymeleaf Element Model processing


How do I properly process HTML tag and its body in my own Element Model Processor? For example, I have the following HTML structure:

<my:form id="some_id">
    <my:input type="text" name="input-1"/>
    <my:input type="number" name="input-2"/>
</my:form>

I would like to alter both the wrapping my:form tag by adding some generated attribute so that output would look something like <form id="some_id" other-attr="_generated_value_">...</form> and process each of the inner my:input by adding an attribute with form id to each of those inputs (and generally process those tags as they should be), e.g. <input type="text" name="input-1" id="generated_1" form-id="some_id">. Normally my:input is processed by my AbstractAttributeTagProcessor extension.

As of now I have extended the AbstractElementModelProcessor creating the processor like this:

public class MyFormProcessor extends AbstractElementModelProcessor {

    private static final String ELEMENT_NAME = "form";
    private static final int PRECEDENCE = 1000;

    public MyFormProcessor( final String dialectPrefix ) {
        super(
                TemplateMode.HTML,
                dialectPrefix, // dialect prefix - 'my' in this case
                ELEMENT_NAME,
                true, // filter by element name prefix
                null,
                false,
                PRECEDENCE
        );
    }


    @Override
    protected void doProcess( ITemplateContext context,
                              IModel model,
                              IElementModelStructureHandler structureHandler ) {
        // what goes here?
    }
}

The processor is registered correctly and does turm the form and its body into a series of events, but I'm yet to figure out how to modify the model properly as I cannot find any modification methods on its elements.

Thanks in advance for any guidance!


Solution

  • To make your example work, I needed to make a change to your custom Thymeleaf input structure:

    <my:form id="some_id">
        <my:input type="text" name="input-1">
        <my:input type="number" name="input-2">
    </my:form>
    

    The difference is that I do not self-close the <my:input> tags (they end with > not with />. This is so that they reflect the structure of the target "standalone" <input> elements, which are also not self-closing.

    My approach processes the entire fragment of input HTML - so, if you also already have a separate processor for <my:input> tags, then I assume this would need to be a higher priority than that.

    Here is the doProcess() method, with comments in the code:

    @Override
    protected void doProcess(ITemplateContext context,
            IModel model,
            IElementModelStructureHandler structureHandler) {
        final IModelFactory modelFactory = context.getModelFactory();
    
        // first handle the form element:
        String formID = null;
        final ITemplateEvent formEvent = model.get(0);
        if (formEvent instanceof IOpenElementTag) {
            // retrieve the form's ID value:
            IOpenElementTag ele = (IOpenElementTag) formEvent;
            IAttribute attr = ele.getAttribute("id");
            formID = attr.getValue();
        }
    
        // build the attributes we want the form element to use:
        Map<String, String> formAttrs = new HashMap<>();
        formAttrs.put("id", formID);
        formAttrs.put("other-attr", "_generated_value_");
        // create the form element: 
        IOpenElementTag formOpen = modelFactory
                .createOpenElementTag("form", formAttrs,
                        AttributeValueQuotes.DOUBLE,
                        false);
    
        model.replace(0, formOpen);
    
        int idInt = 1; // used to increment ID values
        // the loop processes all of the elements inside the form's opening
        // and closing tags:
        for (int i = 1; i < model.size() - 1; i++) {
            final ITemplateEvent inputEvent = model.get(i);
    
            if (inputEvent instanceof IOpenElementTag) {
                IOpenElementTag ele = (IOpenElementTag) inputEvent;
                // we will add some more attributes here:
                Map<String, String> attrs = ele.getAttributeMap();
                attrs.put("form-id", formID);
                attrs.put("id", "generated_" + idInt++); // the "idInt++" is just a demo - use whatever sequence you want here
    
                IOpenElementTag openEle = modelFactory
                        .createOpenElementTag("input", attrs,
                                AttributeValueQuotes.DOUBLE,
                                false);
                model.replace(i, openEle);
            }
        }
    
    }
    

    The end-result HTML is as follows:

    <form other-attr="_generated_value_" id="some_id">
        <input type="text" name="input-1" form-id="some_id" id="generated_1">
        <input type="number" name="input-2" form-id="some_id" id="generated_2">
    </form>
    

    Notes

    There may be more you could do to re-factor the code.

    There are probably alternative (and maybe better) approaches to the one shown here. With Thymeleaf, there is often more than one way to do something.