This is an automated email from the ASF dual-hosted git repository.

mridulpathak pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git


The following commit(s) were added to refs/heads/trunk by this push:
     new ab38bcdd65 Improved: MacroFormRenderer builds the form structure and 
hyperlink macro calls with RenderableFtlFormElementsBuilder (OFBIZ-13680)
ab38bcdd65 is described below

commit ab38bcdd65e6f830f89a4c0974a109e98ff817ca
Author: Mridul Pathak <[email protected]>
AuthorDate: Fri Oct 16 13:31:35 2026 +0530

    Improved: MacroFormRenderer builds the form structure and hyperlink macro 
calls with RenderableFtlFormElementsBuilder (OFBIZ-13680)
    
    MacroFormRenderer built the macro calls of the form structure (form, list 
and row wrappers, header, cell and field row macros, boundary comments) and of 
the hyperlinks as text and compiled them. RenderableFtlFormElementsBuilder now 
creates these macro calls, so that FtlWriter passes their values as data, and 
the hand-built copy of the hyperlink code in MacroFormRenderer is replaced by 
the one that the builder already had, which now gives the same output. The 
macro signatures and the o [...]
    
    - the parameters of a layered modal link are passed as the JSON string that 
the macro expects, with the presentation layer, instead of as a map, and the 
text of the link is passed as well.
    - the form of a hidden form link in a multi form is written after the 
closing of the multi form, which never happened for the sub hyperlinks of the 
display entity and text fields.
    - FtlWriter writes a RenderableFtlString as it is instead of compiling it 
as a template, so that an interpolation in the description of a hidden form 
link is not evaluated.
    - the description of a hidden form link with a quote does not show a 
backslash before the quote anymore.
---
 .../ofbiz/widget/renderer/macro/FtlWriter.java     |  11 +-
 .../widget/renderer/macro/MacroFormRenderer.java   | 575 ++-------------------
 .../macro/RenderableFtlFormElementsBuilder.java    | 345 ++++++++++++-
 .../macro/renderable/RenderableFtlString.java      |   4 +
 .../ofbiz/widget/renderer/macro/FtlWriterTest.java |  75 +++
 .../renderer/macro/MacroFormRendererTest.java      | 203 ++++----
 .../RenderableFtlFormElementsBuilderTest.java      | 346 +++++++++++++
 7 files changed, 910 insertions(+), 649 deletions(-)

diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/FtlWriter.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/FtlWriter.java
index c7e9ed3dc9..8261efbd8b 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/FtlWriter.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/FtlWriter.java
@@ -172,8 +172,8 @@ public final class FtlWriter {
     }
 
     /**
-     * Visitor for RenderableFtl objects that will process the contents of the 
objects as FTL templates and write the
-     * results to the configured Appendable.
+     * Visitor for RenderableFtl objects that will call the macros of the 
macro library, or write the finished output
+     * of a RenderableFtlString as it is, to the configured Appendable.
      */
     private final class RenderableFtlFtlWriterVisitor implements 
RenderableFtlVisitor {
 
@@ -199,7 +199,12 @@ public final class FtlWriter {
 
         @Override
         public void visit(RenderableFtlString renderableFtlString, Locale 
locale) {
-            processFtlString(writer, locale, 
renderableFtlString.getFtlString());
+            // the string holds finished output that can contain data, so it 
is written as it is and never compiled
+            try {
+                writer.append(renderableFtlString.getFtlString());
+            } catch (IOException e) {
+                Debug.logError(e, "Error writing the output of a 
RenderableFtlString", MODULE);
+            }
         }
     }
 }
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
index e1cb37ba1f..c3f8144f3e 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
@@ -21,7 +21,6 @@ package org.apache.ofbiz.widget.renderer.macro;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.io.UnsupportedEncodingException;
-import java.net.URI;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
@@ -29,21 +28,17 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
-import java.util.stream.Collectors;
 
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpSession;
 
-import org.apache.commons.text.StringEscapeUtils;
 import org.apache.ofbiz.base.util.Debug;
-import org.apache.ofbiz.base.util.StringUtil;
 import org.apache.ofbiz.base.util.UtilCodec;
 import org.apache.ofbiz.base.util.UtilGenerics;
 import org.apache.ofbiz.base.util.UtilHttp;
 import org.apache.ofbiz.base.util.UtilMisc;
 import org.apache.ofbiz.base.util.UtilProperties;
-import org.apache.ofbiz.base.util.UtilRandom;
 import org.apache.ofbiz.base.util.UtilValidate;
 import org.apache.ofbiz.base.util.string.FlexibleStringExpander;
 import org.apache.ofbiz.entity.Delegator;
@@ -53,7 +48,6 @@ import org.apache.ofbiz.webapp.taglib.ContentUrlTag;
 import org.apache.ofbiz.widget.WidgetWorker;
 import org.apache.ofbiz.widget.content.StaticContentUrlProvider;
 import org.apache.ofbiz.widget.model.CommonWidgetModels;
-import org.apache.ofbiz.widget.model.FieldInfo;
 import org.apache.ofbiz.widget.model.ModelForm;
 import org.apache.ofbiz.widget.model.ModelFormField;
 import org.apache.ofbiz.widget.model.ModelFormField.CheckField;
@@ -80,7 +74,6 @@ import 
org.apache.ofbiz.widget.model.ModelFormField.TextFindField;
 import org.apache.ofbiz.widget.model.ModelFormField.TextareaField;
 import org.apache.ofbiz.widget.model.ModelScreenWidget;
 import org.apache.ofbiz.widget.model.ModelSingleForm;
-import org.apache.ofbiz.widget.model.ModelTheme;
 import org.apache.ofbiz.widget.model.ModelWidget;
 import org.apache.ofbiz.widget.model.ThemeFactory;
 import org.apache.ofbiz.widget.renderer.FormRenderer;
@@ -90,7 +83,6 @@ import org.apache.ofbiz.widget.renderer.UtilHelpText;
 import org.apache.ofbiz.widget.renderer.VisualTheme;
 import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtl;
 import 
org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtlMacroCall;
-import org.jsoup.nodes.Element;
 
 /**
  * Widget Library - Form Renderer implementation based on Freemarker macros
@@ -228,10 +220,11 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
         this.request.setAttribute("width", hyperlinkField.getWidth());
         this.request.setAttribute("height", hyperlinkField.getHeight());
         this.request.setAttribute("text", hyperlinkField.getText(context));
-        makeHyperlinkByType(writer, hyperlinkField.getLinkType(), 
modelFormField.getWidgetStyle(), hyperlinkField.getUrlMode(),
-                hyperlinkField.getTarget(context), 
hyperlinkField.getParameterMap(context, modelFormField.getEntityName(),
-                modelFormField.getServiceName()), 
hyperlinkField.getDescription(context), hyperlinkField.getTargetWindow(context),
-                hyperlinkField.getConfirmation(context), modelFormField, 
this.request, this.response, context);
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.makeHyperlinkByType(hyperlinkField.getLinkType(),
+                modelFormField.getWidgetStyle(), hyperlinkField.getUrlMode(), 
hyperlinkField.getTarget(context),
+                hyperlinkField.getParameterMap(context, 
modelFormField.getEntityName(), modelFormField.getServiceName()),
+                hyperlinkField.getDescription(context), 
hyperlinkField.getTargetWindow(context),
+                hyperlinkField.getConfirmation(context), modelFormField, 
this.request, this.response, context));
         this.appendTooltip(writer, context, modelFormField);
         this.request.removeAttribute("image");
         this.request.removeAttribute("descriptionSize");
@@ -345,7 +338,6 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
             } else {
                 titleText = UtilHttp.encodeAmpersands(titleText);
                 final String encodedTitleText = encodeForMacro(titleText, 
modelFormField, context);
-                titleText = encode(titleText, modelFormField, context);
                 if (UtilValidate.isNotEmpty(modelFormField.getHeaderLink())) {
                     StringBuilder targetBuffer = new StringBuilder();
                     FlexibleStringExpander target = 
FlexibleStringExpander.getInstance(modelFormField.getHeaderLink());
@@ -357,8 +349,8 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
                         targetType = "plain";
                     }
                     StringWriter sr = new StringWriter();
-                    makeHyperlinkString(sr, 
modelFormField.getHeaderLinkStyle(), targetType, targetBuffer.toString(), null, 
titleText, "",
-                            modelFormField, this.request, this.response, 
context, "");
+                    writeFtlElement(sr, 
renderableFtlFormElementsBuilder.hyperlinkMacroCall(modelFormField.getHeaderLinkStyle(),
 targetType,
+                            targetBuffer.toString(), null, encodedTitleText, 
"", modelFormField, this.request, this.response, context, ""));
                     String hyperlinkTitle = sr.toString().replace("\"", "\'");
                     writeFtlElement(writer, 
renderableFtlFormElementsBuilder.hyperlinkTitle(modelFormField.getModelForm().getName(),
                             hyperlinkTitle, false));
@@ -407,81 +399,12 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
         } else {
             renderBeginningBoundaryComment(writer, "Grid Widget - Grid 
Element", modelForm);
         }
-        String targetType = modelForm.getTargetType();
-        String targ = modelForm.getTarget(context, targetType);
-        StringBuilder linkUrl = new StringBuilder();
-        if (UtilValidate.isNotEmpty(targ)) {
-            final URI linkUri = WidgetWorker.buildHyperlinkUri(targ, 
targetType, null, null, false, false, true,
-                    request, response);
-            linkUrl.append(linkUri.toString());
-        }
-        String formType = modelForm.getType();
-        String targetWindow = modelForm.getTargetWindow(context);
-        String containerId = FormRenderer.getCurrentContainerId(modelForm, 
context);
-        String containerStyle = modelForm.getContainerStyle();
-        String autocomplete = "";
-        String name = FormRenderer.getCurrentFormName(modelForm, context);
-        String viewIndexField = modelForm.getMultiPaginateIndexField(context);
-        String viewSizeField = modelForm.getMultiPaginateSizeField(context);
-        int viewIndex = Paginator.getViewIndex(modelForm, context);
-        int viewSize = Paginator.getViewSize(modelForm, context);
-        boolean useRowSubmit = modelForm.getUseRowSubmit();
-        if (!modelForm.getClientAutocompleteFields()) {
-            autocomplete = "off";
-        }
-        String hasRequiredField = "";
-        for (ModelFormField formField : modelForm.getFieldList()) {
-            if (formField.getRequiredField()) {
-                hasRequiredField = "Y";
-                break;
-            }
-        }
-        String focusFieldName = modelForm.getFocusFieldName();
-
-        // Generate CSRF name & value for form
-        String csrfNameValue = CsrfUtil.getTokenNameNonAjax() + " " + 
CsrfUtil.generateTokenForNonAjax(request, targ);
-
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormOpen ");
-        sr.append(" linkUrl=\"");
-        sr.append(linkUrl);
-        sr.append("\" formType=\"");
-        sr.append(formType);
-        sr.append("\" targetWindow=\"");
-        sr.append(targetWindow);
-        sr.append("\" containerId=\"");
-        sr.append(containerId);
-        sr.append("\" containerStyle=\"");
-        sr.append(containerStyle);
-        sr.append("\" autocomplete=\"");
-        sr.append(autocomplete);
-        sr.append("\" name=\"");
-        sr.append(name);
-        sr.append("\" focusFieldName=\"");
-        sr.append(focusFieldName);
-        sr.append("\" hasRequiredField=\"");
-        sr.append(hasRequiredField);
-        sr.append("\" viewIndexField=\"");
-        sr.append(viewIndexField);
-        sr.append("\" viewSizeField=\"");
-        sr.append(viewSizeField);
-        sr.append("\" viewIndex=\"");
-        sr.append(Integer.toString(viewIndex));
-        sr.append("\" viewSize=\"");
-        sr.append(Integer.toString(viewSize));
-        sr.append("\" useRowSubmit=");
-        sr.append(Boolean.toString(useRowSubmit));
-        sr.append(" csrfNameValue=\"");
-        sr.append(csrfNameValue);
-        sr.append("\" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formOpen(context, modelForm));
     }
 
     @Override
     public void renderFormClose(Appendable writer, Map<String, Object> 
context, ModelForm modelForm) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormClose />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, renderableFtlFormElementsBuilder.formClose());
         if (modelForm instanceof ModelSingleForm) {
             renderEndingBoundaryComment(writer, "Form Widget - Form Element", 
modelForm);
         } else {
@@ -509,15 +432,9 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
                 // this.renderFormatItemRowCellClose(writer, context, 
modelForm, submitField);
             }
         }
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderMultiFormClose />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.multiFormClose());
         // see if there is anything that needs to be added outside of the 
multi-form
-        Map<String, Object> wholeFormContext = 
UtilGenerics.cast(context.get("wholeFormContext"));
-        Appendable postMultiFormWriter = wholeFormContext != null ? 
(Appendable) wholeFormContext.get("postMultiFormWriter") : null;
-        if (postMultiFormWriter != null) {
-            writer.append(postMultiFormWriter.toString());
-        }
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.postMultiFormContent(context));
         if (modelForm instanceof ModelSingleForm) {
             renderEndingBoundaryComment(writer, "Form Widget - Form Element", 
modelForm);
         } else {
@@ -546,63 +463,17 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
         if (this.renderPagination) {
             this.renderNextPrev(writer, context, modelForm);
         }
-        List<ModelFormField> childFieldList = modelForm.getFieldList();
-        List<String> columnStyleList = new LinkedList<>();
-        List<String> fieldNameList = new LinkedList<>();
-        for (ModelFormField childField : childFieldList) {
-            int childFieldType = childField.getFieldInfo().getFieldType();
-            if (childFieldType == FieldInfo.HIDDEN || childFieldType == 
FieldInfo.IGNORED) {
-                continue;
-            }
-            String areaStyle = childField.getTitleAreaStyle();
-            if (UtilValidate.isEmpty(areaStyle)) {
-                areaStyle = "";
-            }
-            if (fieldNameList.contains(childField.getName())) {
-                if (UtilValidate.isNotEmpty(areaStyle)) {
-                    
columnStyleList.set(fieldNameList.indexOf(childField.getName()), areaStyle);
-                }
-            } else {
-                columnStyleList.add(areaStyle);
-                fieldNameList.add(childField.getName());
-            }
-        }
-        String columnStyleListString =
-                columnStyleList.stream().map(str -> "'" + str + 
"'").collect(Collectors.joining(", "));
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatListWrapperOpen ");
-        sr.append(" formName=\"");
-        sr.append(modelForm.getName());
-        sr.append("\" style=\"");
-        
sr.append(FlexibleStringExpander.expandString(modelForm.getDefaultTableStyle(), 
context));
-        sr.append("\" columnStyles=[");
-        if (UtilValidate.isNotEmpty(columnStyleListString)) {
-            // this is a fix for forms with no fields
-            sr.append(columnStyleListString);
-        }
-        sr.append("] />");
-        executeMacro(writer, sr.toString());
-
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatListWrapperOpen(context, modelForm));
     }
 
     @Override
     public void renderEmptyFormDataMessage(Appendable writer, Map<String, 
Object> context, ModelForm modelForm) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderEmptyFormDataMessage");
-        sr.append(" message=\"");
-        sr.append(modelForm.getEmptyFormDataMessage(context));
-        sr.append("\" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.emptyFormDataMessage(context, modelForm));
     }
 
     @Override
     public void renderFormatListWrapperClose(Appendable writer, Map<String, 
Object> context, ModelForm modelForm) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatListWrapperClose");
-        sr.append(" formName=\"");
-        sr.append(modelForm.getName());
-        sr.append("\" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatListWrapperClose(modelForm));
         if (this.renderPagination) {
             this.renderNextPrev(writer, context, modelForm);
         }
@@ -615,230 +486,112 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
 
     @Override
     public void renderFormatHeaderOpen(Appendable writer, Map<String, Object> 
context, ModelForm modelForm) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatHeaderOpen ");
-        sr.append(" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatHeaderOpen());
     }
 
     @Override
     public void renderFormatHeaderClose(Appendable writer, Map<String, Object> 
context, ModelForm modelForm) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatHeaderClose");
-        sr.append(" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatHeaderClose());
     }
 
     @Override
     public void renderFormatHeaderRowOpen(Appendable writer, Map<String, 
Object> context, ModelForm modelForm) throws IOException {
-        String headerStyle = 
FlexibleStringExpander.expandString(modelForm.getHeaderRowStyle(), context);
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatHeaderRowOpen ");
-        sr.append(" style=\"");
-        sr.append(headerStyle);
-        sr.append("\" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatHeaderRowOpen(context, modelForm));
     }
 
     @Override
     public void renderFormatHeaderRowClose(Appendable writer, Map<String, 
Object> context, ModelForm modelForm) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatHeaderRowClose />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatHeaderRowClose());
     }
 
     @Override
     public void renderFormatHeaderRowCellOpen(Appendable writer, Map<String, 
Object> context, ModelForm modelForm, ModelFormField modelFormField,
                                               int positionSpan) throws 
IOException {
-        String areaStyle = modelFormField.getTitleAreaStyle();
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatHeaderRowCellOpen ");
-        sr.append(" style=\"");
-        sr.append(areaStyle);
-        sr.append("\" positionSpan=");
-        sr.append(Integer.toString(positionSpan));
-        sr.append(" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatHeaderRowCellOpen(modelFormField, 
positionSpan));
     }
 
     @Override
     public void renderFormatHeaderRowCellClose(Appendable writer, Map<String, 
Object> context, ModelForm modelForm, ModelFormField modelFormField)
             throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatHeaderRowCellClose />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatHeaderRowCellClose());
     }
 
     @Override
     public void renderFormatHeaderRowFormCellOpen(Appendable writer, 
Map<String, Object> context, ModelForm modelForm) throws IOException {
-        String areaStyle = modelForm.getFormTitleAreaStyle();
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatHeaderRowFormCellOpen ");
-        sr.append(" style=\"");
-        sr.append(areaStyle);
-        sr.append("\" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatHeaderRowFormCellOpen(modelForm));
     }
 
     @Override
     public void renderFormatHeaderRowFormCellClose(Appendable writer, 
Map<String, Object> context, ModelForm modelForm) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatHeaderRowFormCellClose />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatHeaderRowFormCellClose());
     }
 
     @Override
     public void renderFormatHeaderRowFormCellTitleSeparator(Appendable writer, 
Map<String, Object> context, ModelForm modelForm,
                                                             ModelFormField 
modelFormField, boolean isLast) throws IOException {
-        String titleStyle = modelFormField.getTitleStyle();
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatHeaderRowFormCellTitleSeparator ");
-        sr.append(" style=\"");
-        sr.append(titleStyle);
-        sr.append("\" isLast=");
-        sr.append(Boolean.toString(isLast));
-        sr.append(" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatHeaderRowFormCellTitleSeparator(modelFormField,
 isLast));
     }
 
     @Override
     public void renderFormatItemRowOpen(Appendable writer, Map<String, Object> 
context, ModelForm modelForm) throws IOException {
-        Integer itemIndex = (Integer) context.get("itemIndex");
-        String altRowStyles = "";
-        String evenRowStyle = "";
-        String oddRowStyle = "";
-        if (itemIndex != null) {
-            altRowStyles = modelForm.getStyleAltRowStyle(context);
-            if (itemIndex % 2 == 0) {
-                evenRowStyle = modelForm.getEvenRowStyle();
-            } else {
-                oddRowStyle = 
FlexibleStringExpander.expandString(modelForm.getOddRowStyle(), context);
-            }
-        }
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatItemRowOpen ");
-        sr.append(" formName=\"");
-        sr.append(modelForm.getName());
-        sr.append("\" itemIndex=");
-        sr.append(String.valueOf(itemIndex));
-        sr.append(" altRowStyles=\"");
-        sr.append(altRowStyles);
-        sr.append("\" evenRowStyle=\"");
-        sr.append(evenRowStyle);
-        sr.append("\" oddRowStyle=\"");
-        sr.append(oddRowStyle);
-        sr.append("\" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatItemRowOpen(context, modelForm));
     }
 
     @Override
     public void renderFormatItemRowClose(Appendable writer, Map<String, 
Object> context, ModelForm modelForm) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatItemRowClose ");
-        sr.append(" formName=\"");
-        sr.append(modelForm.getName());
-        sr.append("\"/>");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatItemRowClose(modelForm));
     }
 
     @Override
     public void renderFormatItemRowCellOpen(Appendable writer, Map<String, 
Object> context, ModelForm modelForm, ModelFormField modelFormField,
                                             int positionSpan) throws 
IOException {
-        String areaStyle = modelFormField.getWidgetAreaStyle();
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatItemRowCellOpen ");
-        sr.append(" fieldName=\"");
-        sr.append(modelFormField.getName());
-        sr.append("\" style=\"");
-        sr.append(areaStyle);
-        sr.append("\" positionSpan=");
-        sr.append(Integer.toString(positionSpan));
-        sr.append(" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatItemRowCellOpen(modelFormField, 
positionSpan));
     }
 
     @Override
     public void renderFormatItemRowCellClose(Appendable writer, Map<String, 
Object> context, ModelForm modelForm, ModelFormField modelFormField)
             throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatItemRowCellClose");
-        sr.append(" fieldName=\"");
-        sr.append(modelFormField.getName());
-        sr.append("\"/>");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatItemRowCellClose(modelFormField));
     }
 
     @Override
     public void renderFormatItemRowFormCellOpen(Appendable writer, Map<String, 
Object> context, ModelForm modelForm) throws IOException {
-        String areaStyle = modelForm.getFormTitleAreaStyle();
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatItemRowFormCellOpen ");
-        sr.append(" style=\"");
-        sr.append(areaStyle);
-        sr.append("\" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatItemRowFormCellOpen(modelForm));
     }
 
     @Override
     public void renderFormatItemRowFormCellClose(Appendable writer, 
Map<String, Object> context, ModelForm modelForm) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatItemRowFormCellClose />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatItemRowFormCellClose());
     }
 
     @Override
     public void renderFormatSingleWrapperOpen(Appendable writer, Map<String, 
Object> context, ModelForm modelForm) throws IOException {
-        String style = 
FlexibleStringExpander.expandString(modelForm.getDefaultTableStyle(), context);
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatSingleWrapperOpen ");
-        sr.append(" formName=\"");
-        sr.append(modelForm.getName());
-        sr.append("\" style=\"");
-        sr.append(style);
-        sr.append("\" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatSingleWrapperOpen(context, modelForm));
     }
 
     @Override
     public void renderFormatSingleWrapperClose(Appendable writer, Map<String, 
Object> context, ModelForm modelForm) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatSingleWrapperClose");
-        sr.append(" formName=\"");
-        sr.append(modelForm.getName());
-        sr.append("\"/>");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatSingleWrapperClose(modelForm));
     }
 
     @Override
     public void renderFormatFieldRowOpen(Appendable writer, Map<String, 
Object> context, ModelForm modelForm) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatFieldRowOpen />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatFieldRowOpen());
     }
 
     @Override
     public void renderFormatFieldRowClose(Appendable writer, Map<String, 
Object> context, ModelForm modelForm) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatFieldRowClose />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatFieldRowClose());
     }
 
     @Override
     public void renderFormatFieldRowTitleCellOpen(Appendable writer, 
Map<String, Object> context, ModelFormField modelFormField) throws IOException {
-        String style = modelFormField.getTitleAreaStyle();
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatFieldRowTitleCellOpen ");
-        sr.append(" style=\"");
-        sr.append(style);
-        sr.append("\" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatFieldRowTitleCellOpen(modelFormField));
     }
 
     @Override
     public void renderFormatFieldRowTitleCellClose(Appendable writer, 
Map<String, Object> context, ModelFormField modelFormField) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatFieldRowTitleCellClose />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatFieldRowTitleCellClose());
     }
 
     @Override
@@ -848,30 +601,18 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
     @Override
     public void renderFormatFieldRowWidgetCellOpen(Appendable writer, 
Map<String, Object> context, ModelFormField modelFormField, int positions,
                                                    int positionSpan, Integer 
nextPositionInRow) throws IOException {
-        String areaStyle = modelFormField.getWidgetAreaStyle();
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatFieldRowWidgetCellOpen ");
-        sr.append(" positionSpan=");
-        sr.append(Integer.toString(positionSpan));
-        sr.append(" style=\"");
-        sr.append(areaStyle);
-        sr.append("\" />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatFieldRowWidgetCellOpen(modelFormField, 
positionSpan));
     }
 
     @Override
     public void renderFormatFieldRowWidgetCellClose(Appendable writer, 
Map<String, Object> context, ModelFormField modelFormField, int positions,
                                                     int positionSpan, Integer 
nextPositionInRow) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatFieldRowWidgetCellClose />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatFieldRowWidgetCellClose());
     }
 
     @Override
     public void renderFormatEmptySpace(Appendable writer, Map<String, Object> 
context, ModelForm modelForm) throws IOException {
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderFormatEmptySpace />");
-        executeMacro(writer, sr.toString());
+        writeFtlElement(writer, 
renderableFtlFormElementsBuilder.formatEmptySpace());
     }
 
     @Override
@@ -1743,16 +1484,8 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
      */
     public void renderBeginningBoundaryComment(Appendable writer, String 
widgetType, ModelWidget modelWidget) {
         if (this.widgetCommentsEnabled) {
-            StringWriter sr = new StringWriter();
-            sr.append("<@formatBoundaryComment ");
-            sr.append(" boundaryType=\"");
-            sr.append("Begin");
-            sr.append("\" widgetType=\"");
-            sr.append(widgetType);
-            sr.append("\" widgetName=\"");
-            sr.append(modelWidget.getBoundaryCommentName());
-            sr.append("\" />");
-            executeMacro(writer, sr.toString());
+            writeFtlElement(writer, 
renderableFtlFormElementsBuilder.boundaryComment("Begin", widgetType,
+                    modelWidget.getBoundaryCommentName()));
         }
     }
 
@@ -1764,16 +1497,8 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
      */
     public void renderEndingBoundaryComment(Appendable writer, String 
widgetType, ModelWidget modelWidget) {
         if (this.widgetCommentsEnabled) {
-            StringWriter sr = new StringWriter();
-            sr.append("<@formatBoundaryComment ");
-            sr.append(" boundaryType=\"");
-            sr.append("End");
-            sr.append("\" widgetType=\"");
-            sr.append(widgetType);
-            sr.append("\" widgetName=\"");
-            sr.append(modelWidget.getBoundaryCommentName());
-            sr.append("\" />");
-            executeMacro(writer, sr.toString());
+            writeFtlElement(writer, 
renderableFtlFormElementsBuilder.boundaryComment("End", widgetType,
+                    modelWidget.getBoundaryCommentName()));
         }
     }
 
@@ -1854,23 +1579,10 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
         writeFtlElement(writer, 
renderableFtlFormElementsBuilder.tooltip(context, modelFormField));
     }
 
-    public void makeHyperlinkString(Appendable writer, 
ModelFormField.SubHyperlink subHyperlink, Map<String, Object> context) throws 
IOException {
-        if (subHyperlink == null) {
-            return;
-        }
-        if (subHyperlink.shouldUse(context)) {
-            if (UtilValidate.isNotEmpty(subHyperlink.getWidth())) {
-                this.request.setAttribute("width", subHyperlink.getWidth());
-            }
-            if (UtilValidate.isNotEmpty(subHyperlink.getHeight())) {
-                this.request.setAttribute("height", subHyperlink.getHeight());
-            }
+    private void makeHyperlinkString(Appendable writer, 
ModelFormField.SubHyperlink subHyperlink, Map<String, Object> context) throws 
IOException {
+        if (subHyperlink != null && subHyperlink.shouldUse(context)) {
             writer.append(' ');
-            makeHyperlinkByType(writer, subHyperlink.getLinkType(), 
subHyperlink.getStyle(context), subHyperlink.getUrlMode(),
-                    subHyperlink.getTarget(context), 
subHyperlink.getParameterMap(context, 
subHyperlink.getModelFormField().getEntityName(),
-                            
subHyperlink.getModelFormField().getServiceName()), 
subHyperlink.getDescription(context),
-                    subHyperlink.getTargetWindow(context), "", 
subHyperlink.getModelFormField(), this.request, this.response,
-                    context);
+            writeFtlElement(writer, 
renderableFtlFormElementsBuilder.makeHyperlinkString(subHyperlink, context));
         }
     }
 
@@ -1886,203 +1598,6 @@ public final class MacroFormRenderer implements 
FormStringRenderer {
         writer.append(location);
     }
 
-    public void makeHyperlinkByType(Appendable writer, String linkType, String 
linkStyle, String targetType, String target, Map<String,
-            String> parameterMap, String description, String targetWindow, 
String confirmation, ModelFormField modelFormField,
-            HttpServletRequest request, HttpServletResponse response, 
Map<String, Object> context) throws IOException {
-        String realLinkType = WidgetWorker.determineAutoLinkType(linkType, 
target, targetType, request);
-        UtilCodec.SimpleEncoder simpleEncoder = null;
-        String encodedDescription = null;
-        if 
(description.equals(StringEscapeUtils.unescapeEcmaScript(StringEscapeUtils.unescapeHtml4(description))))
 {
-            simpleEncoder = internalEncoder;
-        } else {
-            simpleEncoder = UtilCodec.getEncoder("string");
-        }
-        if (simpleEncoder != null) {
-            encodedDescription = simpleEncoder.encode(description);
-        }
-        // get the parameterized pagination index and size fields
-        int paginatorNumber = WidgetWorker.getPaginatorNumber(context);
-        ModelForm modelForm = modelFormField.getModelForm();
-        ModelTheme modelTheme = visualTheme.getModelTheme();
-        String viewIndexField = modelForm.getMultiPaginateIndexField(context);
-        String viewSizeField = modelForm.getMultiPaginateSizeField(context);
-        int viewIndex = Paginator.getViewIndex(modelForm, context);
-        int viewSize = Paginator.getViewSize(modelForm, context);
-        if (("viewIndex" + "_" + paginatorNumber).equals(viewIndexField)) {
-            viewIndexField = "VIEW_INDEX" + "_" + paginatorNumber;
-        }
-        if (("viewSize" + "_" + paginatorNumber).equals(viewSizeField)) {
-            viewSizeField = "VIEW_SIZE" + "_" + paginatorNumber;
-        }
-        if ("hidden-form".equals(realLinkType)) {
-            parameterMap.put(viewIndexField, Integer.toString(viewIndex));
-            parameterMap.put(viewSizeField, Integer.toString(viewSize));
-            if ("multi".equals(modelForm.getType())) {
-                final Element anchorElement = 
WidgetWorker.makeHiddenFormLinkAnchorElement(linkStyle,
-                        encodedDescription, confirmation, modelFormField, 
request, context);
-                writer.append(anchorElement.outerHtml());
-                // this is a bit trickier, since we can't do a nested form 
we'll have to put the link to submit the form in place,
-                // but put the actual form def elsewhere, ie after the big 
form is closed
-                Map<String, Object> wholeFormContext = 
UtilGenerics.cast(context.get("wholeFormContext"));
-                Appendable postMultiFormWriter = wholeFormContext != null ? 
(Appendable) wholeFormContext.get("postMultiFormWriter") : null;
-                if (postMultiFormWriter == null) {
-                    postMultiFormWriter = new StringWriter();
-                    wholeFormContext.put("postMultiFormWriter", 
postMultiFormWriter);
-                }
-                final Element hiddenFormElement = 
WidgetWorker.makeHiddenFormLinkFormElement(target, targetType,
-                        targetWindow, parameterMap, modelFormField, request, 
response, context);
-                postMultiFormWriter.append(hiddenFormElement.outerHtml());
-
-            } else {
-                final Element hiddenFormElement = 
WidgetWorker.makeHiddenFormLinkFormElement(target, targetType,
-                        targetWindow, parameterMap, modelFormField, request, 
response, context);
-                writer.append(hiddenFormElement.outerHtml());
-                final Element anchorElement = 
WidgetWorker.makeHiddenFormLinkAnchorElement(linkStyle,
-                        encodedDescription, confirmation, modelFormField, 
request, context);
-                if (anchorElement != null) {
-                    writer.append(anchorElement.outerHtml());
-                }
-            }
-        } else {
-            if ("layered-modal".equals(realLinkType)) {
-                String uniqueItemName = UtilRandom.getUnique("Modal_", true);
-                String width = (String) this.request.getAttribute("width");
-                if (UtilValidate.isEmpty(width)) {
-                    width = 
String.valueOf(modelTheme.getLinkDefaultLayeredModalWidth());
-                    this.request.setAttribute("width", width);
-                }
-                String height = (String) this.request.getAttribute("height");
-                if (UtilValidate.isEmpty(height)) {
-                    height = 
String.valueOf(modelTheme.getLinkDefaultLayeredModalHeight());
-                    this.request.setAttribute("height", height);
-                }
-                this.request.setAttribute("uniqueItemName", uniqueItemName);
-                makeHyperlinkString(writer, linkStyle, targetType, target, 
parameterMap, encodedDescription, confirmation, modelFormField, request,
-                        response, context, targetWindow);
-                this.request.removeAttribute("uniqueItemName");
-                this.request.removeAttribute("height");
-                this.request.removeAttribute("width");
-            } else {
-                makeHyperlinkString(writer, linkStyle, targetType, target, 
parameterMap, encodedDescription, confirmation, modelFormField, request,
-                        response, context, targetWindow);
-            }
-        }
-    }
-
-    public void makeHyperlinkString(Appendable writer, String linkStyle, 
String targetType, String target, Map<String, String> parameterMap,
-                                    String description, String confirmation, 
ModelFormField modelFormField, HttpServletRequest request,
-                                    HttpServletResponse response, Map<String, 
Object> context, String targetWindow) throws IOException {
-        if (description != null || 
UtilValidate.isNotEmpty(request.getAttribute("image"))) {
-            StringBuilder linkUrl = new StringBuilder();
-            final URI linkUri = WidgetWorker.buildHyperlinkUri(target, 
targetType,
-                    
UtilValidate.isEmpty(request.getAttribute("uniqueItemName")) ? parameterMap : 
null,
-                    null, false, false, true, request, response);
-            linkUrl.append(linkUri.toString());
-            String event = "";
-            String action = "";
-            String imgSrc = "";
-            String imgTitle = "";
-            String alt = "";
-            String id = "";
-            String uniqueItemName = "";
-            String width = "";
-            String height = "";
-            String title = "";
-            String text = "";
-            String hiddenFormName = 
WidgetWorker.makeLinkHiddenFormName(context, modelFormField);
-            if (UtilValidate.isNotEmpty(modelFormField.getEvent()) && 
UtilValidate.isNotEmpty(modelFormField.getAction(context))) {
-                event = modelFormField.getEvent();
-                action = modelFormField.getAction(context);
-            }
-            if (UtilValidate.isNotEmpty(request.getAttribute("image"))) {
-                imgSrc = request.getAttribute("image").toString();
-            }
-            if (UtilValidate.isNotEmpty(request.getAttribute("imageTitle"))) {
-                imgTitle = request.getAttribute("imageTitle").toString();
-            }
-            if (UtilValidate.isNotEmpty(request.getAttribute("alternate"))) {
-                alt = request.getAttribute("alternate").toString();
-            }
-            int size = 0;
-            if 
(UtilValidate.isNotEmpty(request.getAttribute("descriptionSize"))) {
-                size = 
Integer.parseInt(request.getAttribute("descriptionSize").toString());
-            }
-            // if description is truncated, always use description as title
-            if (UtilValidate.isNotEmpty(description) && size > 0 && 
description.length() > size) {
-                title = description;
-                description = 
StringUtil.truncateEncodedStringToLength(description, size);
-            } else if (UtilValidate.isNotEmpty(request.getAttribute("title"))) 
{
-                title = request.getAttribute("title").toString();
-            }
-            if (UtilValidate.isNotEmpty(request.getAttribute("id"))) {
-                id = request.getAttribute("id").toString();
-            }
-            if (UtilValidate.isNotEmpty(request.getAttribute("text"))) {
-                text = request.getAttribute("text").toString();
-            }
-            if 
(UtilValidate.isNotEmpty(request.getAttribute("uniqueItemName"))) {
-                uniqueItemName = 
request.getAttribute("uniqueItemName").toString();
-                width = request.getAttribute("width").toString();
-                height = request.getAttribute("height").toString();
-            }
-            StringBuilder targetParameters = new StringBuilder();
-            if (UtilValidate.isNotEmpty(parameterMap) || 
UtilValidate.isNotEmpty(uniqueItemName)) {
-                try {
-                    Map<String, Object> params = new java.util.TreeMap<>();
-                    if (UtilValidate.isNotEmpty(parameterMap)) {
-                        params.putAll(parameterMap);
-                    }
-                    if (UtilValidate.isNotEmpty(uniqueItemName)) {
-                        params.put("presentation", "layer");
-                    }
-                    
targetParameters.append(org.apache.ofbiz.base.lang.JSON.from(params).toString());
-                } catch (Exception e) {
-                    Debug.logError(e, "Error converting dialog params to 
JSON", MODULE);
-                }
-            }
-            StringWriter sr = new StringWriter();
-            sr.append("<@makeHyperlinkString ");
-            sr.append("linkStyle=\"");
-            sr.append(linkStyle == null ? "" : linkStyle);
-            sr.append("\" hiddenFormName=\"");
-            sr.append(hiddenFormName);
-            sr.append("\" event=\"");
-            sr.append(event);
-            sr.append("\" action=\"");
-            sr.append(action);
-            sr.append("\" imgSrc=\"");
-            sr.append(imgSrc);
-            sr.append("\" imgTitle=\"");
-            sr.append(imgTitle);
-            sr.append("\" title=\"");
-            sr.append(title);
-            sr.append("\" alternate=\"");
-            sr.append(alt);
-            sr.append("\" targetParameters=\"");
-            
sr.append(MacroCommonRenderer.escapeFtlString(targetParameters.toString()));
-            sr.append("\" linkUrl=\"");
-            sr.append(linkUrl.toString());
-            sr.append("\" targetWindow=\"");
-            sr.append(targetWindow);
-            sr.append("\" description=\"");
-            sr.append(description);
-            sr.append("\" confirmation=\"");
-            sr.append(confirmation);
-            sr.append("\" uniqueItemName=\"");
-            sr.append(uniqueItemName);
-            sr.append("\" height=\"");
-            sr.append(height);
-            sr.append("\" width=\"");
-            sr.append(width);
-            sr.append("\" id=\"");
-            sr.append(id);
-            sr.append("\" text=\"");
-            sr.append(text);
-            sr.append("\" />");
-            executeMacro(writer, sr.toString());
-        }
-    }
-
     @Override
     public void renderContainerFindField(Appendable writer, Map<String, 
Object> context, ContainerField containerField) throws IOException {
         final RenderableFtlMacroCall containerMc = 
renderableFtlFormElementsBuilder.containerMacroCall(context, containerField);
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
index d5171cf636..539ad1d401 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
@@ -39,6 +39,7 @@ import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpSession;
 
 import com.ibm.icu.util.Calendar;
+import org.apache.ofbiz.base.lang.JSON;
 import org.apache.ofbiz.base.util.Debug;
 import org.apache.ofbiz.base.util.StringUtil;
 import org.apache.ofbiz.base.util.UtilCodec;
@@ -50,10 +51,12 @@ import org.apache.ofbiz.base.util.UtilProperties;
 import org.apache.ofbiz.base.util.UtilRandom;
 import org.apache.ofbiz.base.util.UtilValidate;
 import org.apache.ofbiz.base.util.string.FlexibleStringExpander;
+import org.apache.ofbiz.security.CsrfUtil;
 import org.apache.ofbiz.webapp.control.RequestHandler;
 import org.apache.ofbiz.widget.WidgetWorker;
 import org.apache.ofbiz.widget.content.StaticContentUrlProvider;
 import org.apache.ofbiz.widget.model.CommonWidgetModels;
+import org.apache.ofbiz.widget.model.FieldInfo;
 import org.apache.ofbiz.widget.model.ModelForm;
 import org.apache.ofbiz.widget.model.ModelFormField;
 import org.apache.ofbiz.widget.model.ModelFormField.ContainerField;
@@ -828,7 +831,9 @@ public final class RenderableFtlFormElementsBuilder {
             if ("multi".equals(modelForm.getType())) {
                 final Element anchorElement = 
WidgetWorker.makeHiddenFormLinkAnchorElement(linkStyle,
                         description, confirmation, modelFormField, request, 
context);
-                htmlStringBuilder.append(anchorElement.outerHtml());
+                if (anchorElement != null) {
+                    htmlStringBuilder.append(anchorElement.outerHtml());
+                }
 
                 // this is a bit trickier, since we can't do a nested form 
we'll have to put the link to submit the
                 // form in place, but put the actual form def elsewhere, ie 
after the big form is closed
@@ -845,7 +850,9 @@ public final class RenderableFtlFormElementsBuilder {
                 htmlStringBuilder.append(hiddenFormElement.outerHtml());
                 final Element anchorElement = 
WidgetWorker.makeHiddenFormLinkAnchorElement(linkStyle,
                         description, confirmation, modelFormField, request, 
context);
-                htmlStringBuilder.append(anchorElement.outerHtml());
+                if (anchorElement != null) {
+                    htmlStringBuilder.append(anchorElement.outerHtml());
+                }
             }
 
             return renderableFtlStringBuilder.build();
@@ -898,6 +905,7 @@ public final class RenderableFtlFormElementsBuilder {
             String width = "";
             String height = "";
             String title = "";
+            String text = "";
             String hiddenFormName = 
WidgetWorker.makeLinkHiddenFormName(context, modelFormField);
             if (UtilValidate.isNotEmpty(modelFormField.getEvent())
                     && 
UtilValidate.isNotEmpty(modelFormField.getAction(context))) {
@@ -926,31 +934,51 @@ public final class RenderableFtlFormElementsBuilder {
             if (UtilValidate.isNotEmpty(request.getAttribute("id"))) {
                 id = request.getAttribute("id").toString();
             }
+            if (UtilValidate.isNotEmpty(request.getAttribute("text"))) {
+                text = request.getAttribute("text").toString();
+            }
             if 
(UtilValidate.isNotEmpty(request.getAttribute("uniqueItemName"))) {
                 uniqueItemName = 
request.getAttribute("uniqueItemName").toString();
                 width = request.getAttribute("width").toString();
                 height = request.getAttribute("height").toString();
             }
+            // the macro expects the parameters of the link as a JSON string
+            String targetParameters = "";
+            if (UtilValidate.isNotEmpty(parameterMap) || 
UtilValidate.isNotEmpty(uniqueItemName)) {
+                try {
+                    final Map<String, Object> params = new 
java.util.TreeMap<>();
+                    if (UtilValidate.isNotEmpty(parameterMap)) {
+                        params.putAll(parameterMap);
+                    }
+                    if (UtilValidate.isNotEmpty(uniqueItemName)) {
+                        params.put("presentation", "layer");
+                    }
+                    targetParameters = JSON.from(params).toString();
+                } catch (Exception e) {
+                    Debug.logError(e, "Error converting dialog params to 
JSON", MODULE);
+                }
+            }
 
             return RenderableFtlMacroCall.builder()
                     .name("makeHyperlinkString")
                     .stringParameter("linkStyle", linkStyle == null ? "" : 
linkStyle)
-                    .stringParameter("hiddenFormName", hiddenFormName)
+                    .stringParameter("hiddenFormName", nonNull(hiddenFormName))
                     .stringParameter("event", event)
                     .stringParameter("action", action)
                     .stringParameter("imgSrc", imgSrc)
                     .stringParameter("imgTitle", imgTitle)
                     .stringParameter("title", title)
                     .stringParameter("alternate", alt)
-                    .mapParameter("targetParameters", parameterMap)
+                    .stringParameter("targetParameters", targetParameters)
                     .stringParameter("linkUrl", linkUrl.toString())
-                    .stringParameter("targetWindow", targetWindow)
-                    .stringParameter("description", description)
-                    .stringParameter("confirmation", confirmation)
+                    .stringParameter("targetWindow", nonNull(targetWindow))
+                    .stringParameter("description", nonNull(description))
+                    .stringParameter("confirmation", nonNull(confirmation))
                     .stringParameter("uniqueItemName", uniqueItemName)
                     .stringParameter("height", height)
                     .stringParameter("width", width)
                     .stringParameter("id", id)
+                    .stringParameter("text", text)
                     .build();
         } else {
             return RenderableFtlNoop.INSTANCE;
@@ -1549,6 +1577,309 @@ public final class RenderableFtlFormElementsBuilder {
         return builder.build();
     }
 
+    /**
+     * Creates the macro call that opens a form.
+     * @param context the context
+     * @param modelForm the form
+     * @return the RenderableFtl
+     */
+    public RenderableFtl formOpen(final Map<String, Object> context, final 
ModelForm modelForm) {
+        final String targetType = modelForm.getTargetType();
+        final String target = modelForm.getTarget(context, targetType);
+        String linkUrl = "";
+        if (UtilValidate.isNotEmpty(target)) {
+            linkUrl = WidgetWorker.buildHyperlinkUri(target, targetType, null, 
null, false, false, true,
+                    request, response).toString();
+        }
+        String hasRequiredField = "";
+        for (final ModelFormField formField : modelForm.getFieldList()) {
+            if (formField.getRequiredField()) {
+                hasRequiredField = "Y";
+                break;
+            }
+        }
+        // Generate CSRF name & value for form
+        final String csrfNameValue = CsrfUtil.getTokenNameNonAjax() + " " + 
CsrfUtil.generateTokenForNonAjax(request, target);
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormOpen")
+                .stringParameter("linkUrl", linkUrl)
+                .stringParameter("formType", nonNull(modelForm.getType()))
+                .stringParameter("targetWindow", 
nonNull(modelForm.getTargetWindow(context)))
+                .stringParameter("containerId", 
nonNull(FormRenderer.getCurrentContainerId(modelForm, context)))
+                .stringParameter("containerStyle", 
nonNull(modelForm.getContainerStyle()))
+                .stringParameter("autocomplete", 
modelForm.getClientAutocompleteFields() ? "" : "off")
+                .stringParameter("name", 
nonNull(FormRenderer.getCurrentFormName(modelForm, context)))
+                .stringParameter("focusFieldName", 
nonNull(modelForm.getFocusFieldName()))
+                .stringParameter("hasRequiredField", hasRequiredField)
+                .stringParameter("viewIndexField", 
nonNull(modelForm.getMultiPaginateIndexField(context)))
+                .stringParameter("viewSizeField", 
nonNull(modelForm.getMultiPaginateSizeField(context)))
+                .stringParameter("viewIndex", 
Integer.toString(Paginator.getViewIndex(modelForm, context)))
+                .stringParameter("viewSize", 
Integer.toString(Paginator.getViewSize(modelForm, context)))
+                .booleanParameter("useRowSubmit", modelForm.getUseRowSubmit())
+                .stringParameter("csrfNameValue", csrfNameValue)
+                .build();
+    }
+
+    /**
+     * Creates the macro call that closes a form.
+     * @return the RenderableFtl
+     */
+    public RenderableFtl formClose() {
+        return macroWithoutParameters("renderFormClose");
+    }
+
+    /**
+     * Creates the macro call that closes a multi form.
+     * @return the RenderableFtl
+     */
+    public RenderableFtl multiFormClose() {
+        return macroWithoutParameters("renderMultiFormClose");
+    }
+
+    /**
+     * Gets what has to be rendered after the closing of a multi form, such as 
the forms of the hidden form links.
+     * @param context the context
+     * @return the RenderableFtl, which renders nothing if there is none
+     */
+    public RenderableFtl postMultiFormContent(final Map<String, Object> 
context) {
+        final Map<String, Object> wholeFormContext = 
UtilGenerics.cast(context.get("wholeFormContext"));
+        final RenderableFtl postMultiFormContent = wholeFormContext == null ? 
null
+                : (RenderableFtl) 
wholeFormContext.get("postMultiFormRenderableFtl");
+        return postMultiFormContent == null ? RenderableFtlNoop.INSTANCE : 
postMultiFormContent;
+    }
+
+    /**
+     * Creates the macro call that opens the wrapper of a list form.
+     * @param context the context
+     * @param modelForm the form
+     * @return the RenderableFtl
+     */
+    public RenderableFtl formatListWrapperOpen(final Map<String, Object> 
context, final ModelForm modelForm) {
+        final List<String> columnStyleList = new ArrayList<>();
+        final List<String> fieldNameList = new ArrayList<>();
+        for (final ModelFormField childField : modelForm.getFieldList()) {
+            final int childFieldType = 
childField.getFieldInfo().getFieldType();
+            if (childFieldType == FieldInfo.HIDDEN || childFieldType == 
FieldInfo.IGNORED) {
+                continue;
+            }
+            final String areaStyle = nonNull(childField.getTitleAreaStyle());
+            if (fieldNameList.contains(childField.getName())) {
+                if (UtilValidate.isNotEmpty(areaStyle)) {
+                    
columnStyleList.set(fieldNameList.indexOf(childField.getName()), areaStyle);
+                }
+            } else {
+                columnStyleList.add(areaStyle);
+                fieldNameList.add(childField.getName());
+            }
+        }
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormatListWrapperOpen")
+                .stringParameter("formName", nonNull(modelForm.getName()))
+                .stringParameter("style", 
FlexibleStringExpander.expandString(modelForm.getDefaultTableStyle(), context))
+                .objectParameter("columnStyles", columnStyleList)
+                .build();
+    }
+
+    /**
+     * Creates the macro call that renders the message of a form without data.
+     * @param context the context
+     * @param modelForm the form
+     * @return the RenderableFtl
+     */
+    public RenderableFtl emptyFormDataMessage(final Map<String, Object> 
context, final ModelForm modelForm) {
+        return RenderableFtlMacroCall.builder()
+                .name("renderEmptyFormDataMessage")
+                .stringParameter("message", 
nonNull(modelForm.getEmptyFormDataMessage(context)))
+                .build();
+    }
+
+    /**
+     * Creates the macro call that closes the wrapper of a list form.
+     * @param modelForm the form
+     * @return the RenderableFtl
+     */
+    public RenderableFtl formatListWrapperClose(final ModelForm modelForm) {
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormatListWrapperClose")
+                .stringParameter("formName", nonNull(modelForm.getName()))
+                .build();
+    }
+
+    public RenderableFtl formatHeaderOpen() {
+        return macroWithoutParameters("renderFormatHeaderOpen");
+    }
+
+    public RenderableFtl formatHeaderClose() {
+        return macroWithoutParameters("renderFormatHeaderClose");
+    }
+
+    public RenderableFtl formatHeaderRowOpen(final Map<String, Object> 
context, final ModelForm modelForm) {
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormatHeaderRowOpen")
+                .stringParameter("style", 
FlexibleStringExpander.expandString(modelForm.getHeaderRowStyle(), context))
+                .build();
+    }
+
+    public RenderableFtl formatHeaderRowClose() {
+        return macroWithoutParameters("renderFormatHeaderRowClose");
+    }
+
+    public RenderableFtl formatHeaderRowCellOpen(final ModelFormField 
modelFormField, final int positionSpan) {
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormatHeaderRowCellOpen")
+                .stringParameter("style", 
nonNull(modelFormField.getTitleAreaStyle()))
+                .intParameter("positionSpan", positionSpan)
+                .build();
+    }
+
+    public RenderableFtl formatHeaderRowCellClose() {
+        return macroWithoutParameters("renderFormatHeaderRowCellClose");
+    }
+
+    public RenderableFtl formatHeaderRowFormCellOpen(final ModelForm 
modelForm) {
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormatHeaderRowFormCellOpen")
+                .stringParameter("style", 
nonNull(modelForm.getFormTitleAreaStyle()))
+                .build();
+    }
+
+    public RenderableFtl formatHeaderRowFormCellClose() {
+        return macroWithoutParameters("renderFormatHeaderRowFormCellClose");
+    }
+
+    public RenderableFtl formatHeaderRowFormCellTitleSeparator(final 
ModelFormField modelFormField, final boolean isLast) {
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormatHeaderRowFormCellTitleSeparator")
+                .stringParameter("style", 
nonNull(modelFormField.getTitleStyle()))
+                .booleanParameter("isLast", isLast)
+                .build();
+    }
+
+    public RenderableFtl formatItemRowOpen(final Map<String, Object> context, 
final ModelForm modelForm) {
+        final Integer itemIndex = (Integer) context.get("itemIndex");
+        String altRowStyles = "";
+        String evenRowStyle = "";
+        String oddRowStyle = "";
+        if (itemIndex != null) {
+            altRowStyles = modelForm.getStyleAltRowStyle(context);
+            if (itemIndex % 2 == 0) {
+                evenRowStyle = modelForm.getEvenRowStyle();
+            } else {
+                oddRowStyle = 
FlexibleStringExpander.expandString(modelForm.getOddRowStyle(), context);
+            }
+        }
+        final RenderableFtlMacroCallBuilder builder = 
RenderableFtlMacroCall.builder()
+                .name("renderFormatItemRowOpen")
+                .stringParameter("formName", nonNull(modelForm.getName()))
+                .stringParameter("altRowStyles", nonNull(altRowStyles))
+                .stringParameter("evenRowStyle", nonNull(evenRowStyle))
+                .stringParameter("oddRowStyle", nonNull(oddRowStyle));
+        if (itemIndex != null) {
+            builder.intParameter("itemIndex", itemIndex);
+        }
+        return builder.build();
+    }
+
+    public RenderableFtl formatItemRowClose(final ModelForm modelForm) {
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormatItemRowClose")
+                .stringParameter("formName", nonNull(modelForm.getName()))
+                .build();
+    }
+
+    public RenderableFtl formatItemRowCellOpen(final ModelFormField 
modelFormField, final int positionSpan) {
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormatItemRowCellOpen")
+                .stringParameter("fieldName", 
nonNull(modelFormField.getName()))
+                .stringParameter("style", 
nonNull(modelFormField.getWidgetAreaStyle()))
+                .intParameter("positionSpan", positionSpan)
+                .build();
+    }
+
+    public RenderableFtl formatItemRowCellClose(final ModelFormField 
modelFormField) {
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormatItemRowCellClose")
+                .stringParameter("fieldName", 
nonNull(modelFormField.getName()))
+                .build();
+    }
+
+    public RenderableFtl formatItemRowFormCellOpen(final ModelForm modelForm) {
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormatItemRowFormCellOpen")
+                .stringParameter("style", 
nonNull(modelForm.getFormTitleAreaStyle()))
+                .build();
+    }
+
+    public RenderableFtl formatItemRowFormCellClose() {
+        return macroWithoutParameters("renderFormatItemRowFormCellClose");
+    }
+
+    public RenderableFtl formatSingleWrapperOpen(final Map<String, Object> 
context, final ModelForm modelForm) {
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormatSingleWrapperOpen")
+                .stringParameter("formName", nonNull(modelForm.getName()))
+                .stringParameter("style", 
FlexibleStringExpander.expandString(modelForm.getDefaultTableStyle(), context))
+                .build();
+    }
+
+    public RenderableFtl formatSingleWrapperClose(final ModelForm modelForm) {
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormatSingleWrapperClose")
+                .stringParameter("formName", nonNull(modelForm.getName()))
+                .build();
+    }
+
+    public RenderableFtl formatFieldRowOpen() {
+        return macroWithoutParameters("renderFormatFieldRowOpen");
+    }
+
+    public RenderableFtl formatFieldRowClose() {
+        return macroWithoutParameters("renderFormatFieldRowClose");
+    }
+
+    public RenderableFtl formatFieldRowTitleCellOpen(final ModelFormField 
modelFormField) {
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormatFieldRowTitleCellOpen")
+                .stringParameter("style", 
nonNull(modelFormField.getTitleAreaStyle()))
+                .build();
+    }
+
+    public RenderableFtl formatFieldRowTitleCellClose() {
+        return macroWithoutParameters("renderFormatFieldRowTitleCellClose");
+    }
+
+    public RenderableFtl formatFieldRowWidgetCellOpen(final ModelFormField 
modelFormField, final int positionSpan) {
+        return RenderableFtlMacroCall.builder()
+                .name("renderFormatFieldRowWidgetCellOpen")
+                .intParameter("positionSpan", positionSpan)
+                .stringParameter("style", 
nonNull(modelFormField.getWidgetAreaStyle()))
+                .build();
+    }
+
+    public RenderableFtl formatFieldRowWidgetCellClose() {
+        return macroWithoutParameters("renderFormatFieldRowWidgetCellClose");
+    }
+
+    /**
+     * Creates the macro call that renders a boundary comment.
+     * @param boundaryType Begin or End
+     * @param widgetType the type of the widget, such as Form Widget - Form 
Element
+     * @param widgetName the name of the widget
+     * @return the RenderableFtl
+     */
+    public RenderableFtl boundaryComment(final String boundaryType, final 
String widgetType, final String widgetName) {
+        return RenderableFtlMacroCall.builder()
+                .name("formatBoundaryComment")
+                .stringParameter("boundaryType", boundaryType)
+                .stringParameter("widgetType", widgetType)
+                .stringParameter("widgetName", nonNull(widgetName))
+                .build();
+    }
+
+    private static RenderableFtl macroWithoutParameters(final String 
macroName) {
+        return RenderableFtlMacroCall.builder().name(macroName).build();
+    }
+
     private static String nonNull(final String value) {
         return value == null ? "" : value;
     }
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/renderable/RenderableFtlString.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/renderable/RenderableFtlString.java
index 4c718fcf27..eb251ed9f5 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/renderable/RenderableFtlString.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/renderable/RenderableFtlString.java
@@ -21,6 +21,10 @@ package org.apache.ofbiz.widget.renderer.macro.renderable;
 import java.util.Locale;
 import java.util.function.Consumer;
 
+/**
+ * Finished output, such as the HTML of a link, that is written as it is. It 
is not a template: nothing in it is
+ * interpreted, so it can hold values of the data.
+ */
 public final class RenderableFtlString implements RenderableFtl {
     private final String ftlString;
 
diff --git 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/FtlWriterTest.java
 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/FtlWriterTest.java
new file mode 100644
index 0000000000..e51d501c76
--- /dev/null
+++ 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/FtlWriterTest.java
@@ -0,0 +1,75 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ 
*******************************************************************************/
+package org.apache.ofbiz.widget.renderer.macro;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import java.io.StringWriter;
+
+import org.apache.ofbiz.base.util.template.FreeMarkerWorker;
+import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtlString;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import freemarker.core.Environment;
+import freemarker.template.Template;
+
+public final class FtlWriterTest {
+
+    private final Template template = mock(Template.class);
+    private final Environment environment = mock(Environment.class);
+
+    private MockedStatic<FreeMarkerWorker> freeMarkerWorkerMock;
+
+    @BeforeEach
+    public void setupMockups() {
+        freeMarkerWorkerMock = mockStatic(FreeMarkerWorker.class, 
CALLS_REAL_METHODS);
+        freeMarkerWorkerMock.when(() -> 
FreeMarkerWorker.getTemplate(anyString())).thenReturn(template);
+        freeMarkerWorkerMock.when(() -> 
FreeMarkerWorker.renderTemplate(any(Template.class), anyMap(), 
any())).thenReturn(environment);
+    }
+
+    @AfterEach
+    public void tearDownMockups() {
+        freeMarkerWorkerMock.close();
+    }
+
+    @Test
+    public void renderableFtlStringIsWrittenAsItIsAndNeverCompiled() throws 
Exception {
+        final FtlWriter ftlWriter = new FtlWriter("LIBRARY");
+        final StringWriter writer = new StringWriter();
+        final String html = "<a href=\"#\">${1+1} <#stop \"stopped\"> 
#{2}</a>";
+
+        ftlWriter.processFtl(writer, null, new RenderableFtlString(html));
+
+        assertThat(writer.toString(), equalTo(html));
+        // a compiled string would have been included in the environment of 
the writer
+        verify(environment, never()).include(any(Template.class));
+    }
+}
diff --git 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
index ab1574b133..fe7e566970 100644
--- 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
+++ 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
@@ -20,6 +20,7 @@ package org.apache.ofbiz.widget.renderer.macro;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.startsWith;
@@ -35,8 +36,10 @@ import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.CALLS_REAL_METHODS;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
 import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -55,7 +58,6 @@ import jakarta.servlet.http.HttpSession;
 import org.apache.ofbiz.base.util.UtilHttp;
 import org.apache.ofbiz.base.util.UtilProperties;
 import org.apache.ofbiz.base.util.template.FreeMarkerWorker;
-import org.apache.ofbiz.webapp.control.ConfigXMLReader;
 import org.apache.ofbiz.webapp.control.RequestHandler;
 import org.apache.ofbiz.widget.model.FieldInfo;
 import org.apache.ofbiz.widget.model.ModelForm;
@@ -71,6 +73,7 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.MockedStatic;
 
 import com.google.common.collect.ImmutableMap;
@@ -356,108 +359,134 @@ public final class MacroFormRendererTest {
     @Test
     public void formOpenedMacroRendered() throws IOException {
         final ModelSingleForm modelSingleForm = mock(ModelSingleForm.class);
-        when(modelSingleForm.getType()).thenReturn("single");
+        when(renderableFtlFormElementsBuilder.formOpen(notNull(), 
eq(modelSingleForm))).thenReturn(genericMacroCall);
 
         macroFormRenderer.renderFormOpen(appendable, ImmutableMap.of(), 
modelSingleForm);
-        assertAndGetMacroString("renderFormOpen", ImmutableMap.of("formType", 
"single"));
+
+        genericSingleMacroRenderedVerification();
     }
 
     @Test
     public void formClosedMacroRendered() throws IOException {
         final ModelSingleForm modelSingleForm = mock(ModelSingleForm.class);
+        
when(renderableFtlFormElementsBuilder.formClose()).thenReturn(genericMacroCall);
+
         macroFormRenderer.renderFormClose(appendable, ImmutableMap.of(), 
modelSingleForm);
-        assertAndGetMacroString("renderFormClose");
+
+        genericSingleMacroRenderedVerification();
     }
 
     @Test
-    public void multiFormClosedMacroRendered() throws IOException {
+    public void multiFormClosedMacroRenderedBeforeThePostFormContent() throws 
IOException {
         final ModelForm modelForm = mock(ModelForm.class);
+        
when(renderableFtlFormElementsBuilder.multiFormClose()).thenReturn(genericMacroCall);
+        
when(renderableFtlFormElementsBuilder.postMultiFormContent(notNull())).thenReturn(genericHyperlinkMacroCall);
+
         macroFormRenderer.renderMultiFormClose(appendable, ImmutableMap.of(), 
modelForm);
-        assertAndGetMacroString("renderMultiFormClose");
+
+        final InOrder inOrder = inOrder(ftlWriter);
+        inOrder.verify(ftlWriter).processFtl(appendable, null, 
genericMacroCall);
+        inOrder.verify(ftlWriter).processFtl(appendable, null, 
genericHyperlinkMacroCall);
     }
 
     @Test
     public void listWrapperOpenMacroRendered() throws IOException {
         final ModelSingleForm modelSingleForm = mock(ModelSingleForm.class);
+        when(renderableFtlFormElementsBuilder.formatListWrapperOpen(notNull(), 
eq(modelSingleForm))).thenReturn(genericMacroCall);
+
         macroFormRenderer.setRenderPagination(false);
         macroFormRenderer.renderFormatListWrapperOpen(appendable, new 
HashMap<>(), modelSingleForm);
-        assertAndGetMacroString("renderFormatListWrapperOpen");
+
+        genericSingleMacroRenderedVerification();
     }
 
     @Test
     public void emptyFormDataMacroRendered() throws IOException {
         final ModelSingleForm modelSingleForm = mock(ModelSingleForm.class);
-        
when(modelSingleForm.getEmptyFormDataMessage(notNull())).thenReturn("EMPTY");
+        when(renderableFtlFormElementsBuilder.emptyFormDataMessage(notNull(), 
eq(modelSingleForm))).thenReturn(genericMacroCall);
 
         macroFormRenderer.renderEmptyFormDataMessage(appendable, new 
HashMap<>(), modelSingleForm);
-        assertAndGetMacroString("renderEmptyFormDataMessage", 
ImmutableMap.of("message", "EMPTY"));
+
+        genericSingleMacroRenderedVerification();
     }
 
     @Test
     public void listWrapperCloseMacroRendered() throws IOException {
         final ModelSingleForm modelSingleForm = mock(ModelSingleForm.class);
+        
when(renderableFtlFormElementsBuilder.formatListWrapperClose(modelSingleForm)).thenReturn(genericMacroCall);
+
         macroFormRenderer.setRenderPagination(false);
         macroFormRenderer.renderFormatListWrapperClose(appendable, new 
HashMap<>(), modelSingleForm);
-        assertAndGetMacroString("renderFormatListWrapperClose");
+
+        genericSingleMacroRenderedVerification();
     }
 
     @Test
     public void itemRowOpenMacroRendered() throws IOException {
         final ModelForm modelForm = mock(ModelForm.class);
-        when(modelForm.getName()).thenReturn("FORMNAME");
-        when(modelForm.getEvenRowStyle()).thenReturn("EVENSTYLE");
+        when(renderableFtlFormElementsBuilder.formatItemRowOpen(notNull(), 
eq(modelForm))).thenReturn(genericMacroCall);
 
         macroFormRenderer.renderFormatItemRowOpen(appendable, 
ImmutableMap.of("itemIndex", 2), modelForm);
-        assertAndGetMacroString("renderFormatItemRowOpen", ImmutableMap.of(
-                "formName", "FORMNAME",
-                "itemIndex", 2,
-                "evenRowStyle", "EVENSTYLE"));
+
+        genericSingleMacroRenderedVerification();
     }
 
     @Test
     public void itemRowCellOpenMacroRendered() throws IOException {
         final ModelForm modelForm = mock(ModelForm.class);
         final ModelFormField localModelFormField = mock(ModelFormField.class);
-        when(localModelFormField.getWidgetAreaStyle()).thenReturn("AREASTYLE");
-        when(localModelFormField.getName()).thenReturn("FIELDNAME");
+        
when(renderableFtlFormElementsBuilder.formatItemRowCellOpen(localModelFormField,
 2)).thenReturn(genericMacroCall);
 
         macroFormRenderer.renderFormatItemRowCellOpen(appendable, 
ImmutableMap.of(), modelForm, localModelFormField, 2);
-        assertAndGetMacroString("renderFormatItemRowCellOpen", ImmutableMap.of(
-                "fieldName", "FIELDNAME",
-                "positionSpan", 2,
-                "style", "AREASTYLE"));
+
+        genericSingleMacroRenderedVerification();
     }
 
     @Test
     public void itemRowFormCellOpenMacroRendered() throws IOException {
         final ModelForm modelForm = mock(ModelForm.class);
-        when(modelForm.getFormTitleAreaStyle()).thenReturn("AREASTYLE");
+        
when(renderableFtlFormElementsBuilder.formatItemRowFormCellOpen(modelForm)).thenReturn(genericMacroCall);
 
         macroFormRenderer.renderFormatItemRowFormCellOpen(appendable, 
ImmutableMap.of(), modelForm);
-        assertAndGetMacroString("renderFormatItemRowFormCellOpen", 
ImmutableMap.of("style", "AREASTYLE"));
+
+        genericSingleMacroRenderedVerification();
     }
 
     @Test
     public void singleWrapperOpenMacroRendered() throws IOException {
         final ModelForm modelForm = mock(ModelForm.class);
-        
when(modelForm.getDefaultTableStyle()).thenReturn("STYLE${styleParam}");
-        when(modelForm.getName()).thenReturn("FORMNAME");
+        
when(renderableFtlFormElementsBuilder.formatSingleWrapperOpen(notNull(), 
eq(modelForm))).thenReturn(genericMacroCall);
+
+        macroFormRenderer.renderFormatSingleWrapperOpen(appendable, 
ImmutableMap.of(), modelForm);
 
-        macroFormRenderer.renderFormatSingleWrapperOpen(appendable, 
ImmutableMap.of("styleParam", "ABCD"), modelForm);
-        assertAndGetMacroString("renderFormatSingleWrapperOpen", 
ImmutableMap.of(
-                "formName", "FORMNAME",
-                "style", "STYLEABCD"));
+        genericSingleMacroRenderedVerification();
     }
 
     @Test
     public void fieldRowWidgetCellOpenMacroRendered() throws IOException {
         final ModelFormField localModelFormField = mock(ModelFormField.class);
-        when(localModelFormField.getWidgetAreaStyle()).thenReturn("AREASTYLE");
+        
when(renderableFtlFormElementsBuilder.formatFieldRowWidgetCellOpen(localModelFormField,
 1)).thenReturn(genericMacroCall);
 
         macroFormRenderer.renderFormatFieldRowWidgetCellOpen(appendable, 
ImmutableMap.of(), localModelFormField, 1, 1, null);
-        assertAndGetMacroString("renderFormatFieldRowWidgetCellOpen", 
ImmutableMap.of(
-                "positionSpan", 1,
-                "style", "AREASTYLE"));
+
+        genericSingleMacroRenderedVerification();
+    }
+
+    @Test
+    public void boundaryCommentsRenderedOnlyWhenEnabled() throws IOException {
+        final ModelSingleForm modelSingleForm = mock(ModelSingleForm.class);
+        when(modelSingleForm.getBoundaryCommentName()).thenReturn("FORMNAME");
+        when(renderableFtlFormElementsBuilder.formOpen(notNull(), 
eq(modelSingleForm))).thenReturn(genericMacroCall);
+        when(renderableFtlFormElementsBuilder.boundaryComment("Begin", "Form 
Widget - Form Element", "FORMNAME"))
+                .thenReturn(genericTooltipMacroCall);
+
+        utilPropertiesMock.when(() -> 
UtilProperties.getPropertyValue("widget", 
"widget.verbose")).thenReturn("false");
+        macroFormRenderer.renderFormOpen(appendable, ImmutableMap.of(), 
modelSingleForm);
+        verify(ftlWriter, never()).processFtl(appendable, null, 
genericTooltipMacroCall);
+
+        utilPropertiesMock.when(() -> 
UtilProperties.getPropertyValue("widget", "widget.verbose")).thenReturn("true");
+        macroFormRenderer.renderFormOpen(appendable, ImmutableMap.of(), 
modelSingleForm);
+        verify(ftlWriter).processFtl(appendable, null, 
genericTooltipMacroCall);
     }
 
     @Test
@@ -692,94 +721,50 @@ public final class MacroFormRendererTest {
     }
 
     @Test
-    public void hyperlinkFieldMacroRenderedTitleNotTruncated() throws 
IOException {
+    public void hyperlinkFieldRendersHyperlinkWithTooltip() throws IOException 
{
         final ModelFormField.HyperlinkField hyperlinkField = 
mock(ModelFormField.HyperlinkField.class);
-        when(hyperlinkField.getModelFormField()).thenReturn(modelFormField);
-        final String description = "DESCRIPTION";
-        final String title = "TITLE";
-
-        when(hyperlinkField.getDescription(notNull())).thenReturn(description);
-        when(hyperlinkField.getTarget(notNull())).thenReturn("#");
-        when(request.getAttribute("title")).thenReturn(title);
+        
when(renderableFtlFormElementsBuilder.makeHyperlinkByType(nullable(String.class),
 nullable(String.class),
+                nullable(String.class), nullable(String.class), anyMap(), 
nullable(String.class), nullable(String.class),
+                nullable(String.class), eq(modelFormField), eq(request), 
eq(response), notNull()))
+                .thenReturn(genericHyperlinkMacroCall);
+        genericTooltipRenderedExpectation(hyperlinkField);
 
         macroFormRenderer.renderHyperlinkField(appendable, new HashMap<>(), 
hyperlinkField);
-        assertAndGetMacroString("makeHyperlinkString", 
ImmutableMap.of("description", description, "title", title));
-    }
 
-    @Test
-    public void hyperlinkFieldMacroRenderedTruncatedNoTitle() throws 
IOException {
-        final ModelFormField.HyperlinkField hyperlinkField = 
mock(ModelFormField.HyperlinkField.class);
-        when(hyperlinkField.getModelFormField()).thenReturn(modelFormField);
-        final String description = "DESCRIPTION";
-
-        when(hyperlinkField.getDescription(notNull())).thenReturn(description);
-        when(hyperlinkField.getTarget(notNull())).thenReturn("#");
-        when(request.getAttribute("descriptionSize")).thenReturn(5);
-
-        macroFormRenderer.renderHyperlinkField(appendable, new HashMap<>(), 
hyperlinkField);
-        assertAndGetMacroString("makeHyperlinkString", 
ImmutableMap.of("description", "DESCR…", "title", description));
+        genericSubHyperlinkRenderedVerification();
+        genericTooltipRenderedVerification();
     }
 
     @Test
-    public void hyperlinkFieldMacroRenderedTruncatedWithTitle() throws 
IOException {
-        final ModelFormField.HyperlinkField hyperlinkField = 
mock(ModelFormField.HyperlinkField.class);
-        when(hyperlinkField.getModelFormField()).thenReturn(modelFormField);
-        final String description = "DESCRIPTION";
-        final String title = "TITLE";
+    public void fieldTitleWithHeaderLinkRendersHyperlinkTitle() throws 
IOException {
+        when(modelFormField.getTitle(notNull())).thenReturn("FIELDTITLE");
+        when(modelFormField.getHeaderLink()).thenReturn("/target");
+        when(modelFormField.getModelForm().getName()).thenReturn("FORMNAME");
+        when(renderableFtlFormElementsBuilder.hyperlinkTitle(eq("FORMNAME"), 
anyString(), eq(false))).thenReturn(genericMacroCall);
 
-        when(hyperlinkField.getDescription(notNull())).thenReturn(description);
-        when(hyperlinkField.getTarget(notNull())).thenReturn("#");
-        when(hyperlinkField.getTitle()).thenReturn(title);
-        when(request.getAttribute("descriptionSize")).thenReturn(5);
+        macroFormRenderer.renderFieldTitle(appendable, ImmutableMap.of(), 
modelFormField);
 
-        macroFormRenderer.renderHyperlinkField(appendable, new HashMap<>(), 
hyperlinkField);
-        assertAndGetMacroString("makeHyperlinkString", 
ImmutableMap.of("description", "DESCR…", "title", description));
+        
verify(renderableFtlFormElementsBuilder).hyperlinkMacroCall(nullable(String.class),
 anyString(), eq("/target"), isNull(),
+                eq("FIELDTITLE"), eq(""), eq(modelFormField), eq(request), 
eq(response), notNull(), eq(""));
+        genericSingleMacroRenderedVerification();
     }
 
     @Test
-    public void hyperlinkFieldMacroRenderedModalParameters() throws 
IOException {
-        final ModelFormField.HyperlinkField hyperlinkField = 
mock(ModelFormField.HyperlinkField.class);
-        when(hyperlinkField.getModelFormField()).thenReturn(modelFormField);
-        final String title = "TitleValue";
-        final String text = "TextValue";
-        final String description = "DescriptionValue";
-        final String target = "Encoded Target";
-        final String id = "IdValue";
-        final String uniqueItemName = "UniqueItemName";
-        final String width = "650";
-        final String height = "150";
-        final String confirmation = "Are you sure ?";
-        final String targetWindow = "_blank";
-        final Map<String, ConfigXMLReader.RequestMap> requestMapMap = new 
HashMap<>();
-        final Map<String, String> parameterMap = new HashMap<>();
-        parameterMap.put("k1", "v1");
-        parameterMap.put("k2", "v2");
-
-        when(hyperlinkField.getDescription(notNull())).thenReturn(description);
-        when(hyperlinkField.getTarget(notNull())).thenReturn(target);
-        when(hyperlinkField.getParameterMap(notNull(), isNull(), 
isNull())).thenReturn(parameterMap);
-        
when(hyperlinkField.getConfirmation(notNull())).thenReturn(confirmation);
-        
when(hyperlinkField.getTargetWindow(notNull())).thenReturn(targetWindow);
-        when(request.getAttribute("title")).thenReturn(title);
-        when(request.getAttribute("text")).thenReturn(text);
-        when(request.getAttribute("requestMapMap")).thenReturn(requestMapMap);
-        when(request.getAttribute("id")).thenReturn(id);
-        
when(request.getAttribute("uniqueItemName")).thenReturn(uniqueItemName);
-        when(request.getAttribute("width")).thenReturn(width);
-        when(request.getAttribute("height")).thenReturn(height);
+    public void dropDownSubHyperlinkIsPrecededByASpace() throws IOException {
+        final ModelFormField.DropDownField dropDownField = 
mock(ModelFormField.DropDownField.class);
+        final ModelFormField.SubHyperlink subHyperlink = 
mock(ModelFormField.SubHyperlink.class);
+        when(dropDownField.getSubHyperlink()).thenReturn(subHyperlink);
+        when(subHyperlink.shouldUse(notNull())).thenReturn(true);
+        when(renderableFtlFormElementsBuilder.dropDownField(notNull(), 
eq(dropDownField), anyBoolean())).thenReturn(genericMacroCall);
+        
when(renderableFtlFormElementsBuilder.makeHyperlinkString(eq(subHyperlink), 
notNull())).thenReturn(genericHyperlinkMacroCall);
+        genericTooltipRenderedExpectation(dropDownField);
 
-        macroFormRenderer.renderHyperlinkField(appendable, new HashMap<>(), 
hyperlinkField);
-        ImmutableMap<String, Object> result = ImmutableMap.<String, 
Object>builder()
-                .put("title", title)
-                .put("description", description)
-                .put("linkUrl", "Encoded%20Target")
-                .put("id", id)
-                .put("targetParameters", 
"\\{\\\"k1\\\":\\\"v1\\\",\\\"k2\\\":\\\"v2\\\",\\\"presentation\\\":\\\"layer\\\"}")
-                .put("width", width)
-                .put("confirmation", confirmation)
-                .put("targetWindow", targetWindow)
-                .build();
-        assertAndGetMacroString("makeHyperlinkString", result);
+        macroFormRenderer.renderDropDownField(appendable, ImmutableMap.of(), 
dropDownField);
+
+        assertThat(appendable.toString(), equalTo(" "));
+        genericSingleMacroRenderedVerification();
+        genericSubHyperlinkRenderedVerification();
+        genericTooltipRenderedVerification();
     }
 
     private String assertAndGetMacroString(final String expectedName) {
diff --git 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderTest.java
 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderTest.java
index 0bbb861ba3..8389332225 100644
--- 
a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderTest.java
+++ 
b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderTest.java
@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableMap;
 import org.apache.ofbiz.webapp.control.ConfigXMLReader;
 import org.apache.ofbiz.webapp.control.RequestHandler;
 import org.apache.ofbiz.widget.content.StaticContentUrlProvider;
+import org.apache.ofbiz.widget.model.FieldInfo;
 import org.apache.ofbiz.widget.model.ModelForm;
 import org.apache.ofbiz.widget.model.ModelFormField;
 import org.apache.ofbiz.widget.model.ModelScreenWidget;
@@ -30,6 +31,7 @@ import org.apache.ofbiz.widget.renderer.VisualTheme;
 import 
org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtlMacroCall;
 import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtl;
 import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtlNoop;
+import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtlString;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -718,4 +720,348 @@ public final class RenderableFtlFormElementsBuilderTest {
         assertThat(withTooltip, 
MacroCallMatcher.hasNameAndParameters("renderSortField",
                 MacroCallParameterMatcher.hasNameAndStringValue("tooltip", 
"TIP")));
     }
+
+    private static RenderableFtlMacroCall macroCall(final RenderableFtl 
renderableFtl) {
+        return (RenderableFtlMacroCall) renderableFtl;
+    }
+
+    private ModelFormField columnField(final String name, final int fieldType, 
final String titleAreaStyle) {
+        final ModelFormField columnField = mock(ModelFormField.class, 
RETURNS_DEEP_STUBS);
+        when(columnField.getName()).thenReturn(name);
+        when(columnField.getFieldInfo().getFieldType()).thenReturn(fieldType);
+        when(columnField.getTitleAreaStyle()).thenReturn(titleAreaStyle);
+        return columnField;
+    }
+
+    @Test
+    public void formOpenSetsTheFormTypeAndTheViewParameters() {
+        final ModelForm modelForm = mock(ModelForm.class);
+        when(modelForm.getType()).thenReturn("single");
+        when(modelForm.getUseRowSubmit()).thenReturn(true);
+
+        final RenderableFtl renderableFtl = 
renderableFtlFormElementsBuilder.formOpen(new HashMap<>(), modelForm);
+        assertThat(renderableFtl, 
MacroCallMatcher.hasNameAndParameters("renderFormOpen",
+                MacroCallParameterMatcher.hasNameAndStringValue("formType", 
"single"),
+                
MacroCallParameterMatcher.hasNameAndStringValue("autocomplete", "off"),
+                
MacroCallParameterMatcher.hasNameAndStringValue("hasRequiredField", ""),
+                
MacroCallParameterMatcher.hasNameAndBooleanValue("useRowSubmit", true),
+                MacroCallParameterMatcher.hasName("viewIndex"),
+                MacroCallParameterMatcher.hasName("viewSize"),
+                MacroCallParameterMatcher.hasName("csrfNameValue")));
+    }
+
+    @Test
+    public void formOpenFlagsTheFormWithARequiredField() {
+        final ModelForm modelForm = mock(ModelForm.class);
+        when(modelForm.getClientAutocompleteFields()).thenReturn(true);
+        final ModelFormField requiredField = mock(ModelFormField.class);
+        when(requiredField.getRequiredField()).thenReturn(true);
+        when(modelForm.getFieldList()).thenReturn(List.of(requiredField));
+
+        final RenderableFtl renderableFtl = 
renderableFtlFormElementsBuilder.formOpen(new HashMap<>(), modelForm);
+        assertThat(renderableFtl, 
MacroCallMatcher.hasNameAndParameters("renderFormOpen",
+                
MacroCallParameterMatcher.hasNameAndStringValue("autocomplete", ""),
+                
MacroCallParameterMatcher.hasNameAndStringValue("hasRequiredField", "Y")));
+    }
+
+    @Test
+    public void closingMacrosHaveNoParameters() {
+        
assertThat(macroCall(renderableFtlFormElementsBuilder.formClose()).getName(), 
equalTo("renderFormClose"));
+        
assertThat(macroCall(renderableFtlFormElementsBuilder.multiFormClose()).getName(),
 equalTo("renderMultiFormClose"));
+        
assertThat(macroCall(renderableFtlFormElementsBuilder.formatHeaderOpen()).getName(),
 equalTo("renderFormatHeaderOpen"));
+        
assertThat(macroCall(renderableFtlFormElementsBuilder.formatHeaderClose()).getName(),
 equalTo("renderFormatHeaderClose"));
+        
assertThat(macroCall(renderableFtlFormElementsBuilder.formatHeaderRowClose()).getName(),
 equalTo("renderFormatHeaderRowClose"));
+        
assertThat(macroCall(renderableFtlFormElementsBuilder.formatHeaderRowCellClose()).getName(),
+                equalTo("renderFormatHeaderRowCellClose"));
+        
assertThat(macroCall(renderableFtlFormElementsBuilder.formatHeaderRowFormCellClose()).getName(),
+                equalTo("renderFormatHeaderRowFormCellClose"));
+        
assertThat(macroCall(renderableFtlFormElementsBuilder.formatItemRowFormCellClose()).getName(),
+                equalTo("renderFormatItemRowFormCellClose"));
+        
assertThat(macroCall(renderableFtlFormElementsBuilder.formatFieldRowOpen()).getName(),
 equalTo("renderFormatFieldRowOpen"));
+        
assertThat(macroCall(renderableFtlFormElementsBuilder.formatFieldRowClose()).getName(),
 equalTo("renderFormatFieldRowClose"));
+        
assertThat(macroCall(renderableFtlFormElementsBuilder.formatFieldRowTitleCellClose()).getName(),
+                equalTo("renderFormatFieldRowTitleCellClose"));
+        
assertThat(macroCall(renderableFtlFormElementsBuilder.formatFieldRowWidgetCellClose()).getName(),
+                equalTo("renderFormatFieldRowWidgetCellClose"));
+        
assertThat(macroCall(renderableFtlFormElementsBuilder.formatHeaderRowClose()).getParameters().isEmpty(),
 equalTo(true));
+    }
+
+    @Test
+    public void postMultiFormContentIsNothingWhenThereIsNone() {
+        assertThat(renderableFtlFormElementsBuilder.postMultiFormContent(new 
HashMap<>()), equalTo(RenderableFtlNoop.INSTANCE));
+
+        final Map<String, Object> context = new HashMap<>();
+        context.put("wholeFormContext", new HashMap<String, Object>());
+        
assertThat(renderableFtlFormElementsBuilder.postMultiFormContent(context), 
equalTo(RenderableFtlNoop.INSTANCE));
+    }
+
+    @Test
+    public void postMultiFormContentIsWhatTheHiddenFormLinksStored() {
+        final RenderableFtlMacroCall stored = 
RenderableFtlMacroCall.builder().name("stored").build();
+        final Map<String, Object> wholeFormContext = new HashMap<>();
+        wholeFormContext.put("postMultiFormRenderableFtl", stored);
+        final Map<String, Object> context = new HashMap<>();
+        context.put("wholeFormContext", wholeFormContext);
+
+        
assertThat(renderableFtlFormElementsBuilder.postMultiFormContent(context), 
equalTo(stored));
+    }
+
+    @Test
+    public void formatListWrapperOpenCollectsOneStylePerVisibleColumn() {
+        final ModelForm modelForm = mock(ModelForm.class);
+        when(modelForm.getName()).thenReturn("FORMNAME");
+        
when(modelForm.getDefaultTableStyle()).thenReturn("STYLE${styleParam}");
+        final List<ModelFormField> fields = List.of(
+                columnField("first", FieldInfo.DISPLAY, "FIRSTSTYLE"),
+                columnField("hidden", FieldInfo.HIDDEN, "HIDDENSTYLE"),
+                columnField("second", FieldInfo.TEXT, "it's a style"),
+                // a second field with the name of the first one only replaces 
the style
+                columnField("first", FieldInfo.TEXT, "REPLACEDSTYLE"),
+                columnField("third", FieldInfo.TEXT, null));
+        when(modelForm.getFieldList()).thenReturn(fields);
+
+        final RenderableFtlMacroCall macroCall = macroCall(
+                
renderableFtlFormElementsBuilder.formatListWrapperOpen(ImmutableMap.of("styleParam",
 "ABCD"), modelForm));
+        assertThat(macroCall, 
MacroCallMatcher.hasNameAndParameters("renderFormatListWrapperOpen",
+                MacroCallParameterMatcher.hasNameAndStringValue("formName", 
"FORMNAME"),
+                MacroCallParameterMatcher.hasNameAndStringValue("style", 
"STYLEABCD")));
+        assertThat(macroCall.getParameters().get("columnStyles"), 
equalTo(List.of("REPLACEDSTYLE", "it's a style", "")));
+    }
+
+    @Test
+    public void formatListWrapperOpenWithoutFieldsHasNoColumnStyles() {
+        final ModelForm modelForm = mock(ModelForm.class);
+
+        final RenderableFtlMacroCall macroCall = 
macroCall(renderableFtlFormElementsBuilder.formatListWrapperOpen(new 
HashMap<>(), modelForm));
+        assertThat(macroCall.getParameters().get("columnStyles"), 
equalTo(List.of()));
+    }
+
+    @Test
+    public void emptyFormDataMessageSetsTheMessage() {
+        final ModelForm modelForm = mock(ModelForm.class);
+        when(modelForm.getEmptyFormDataMessage(notNull())).thenReturn("EMPTY 
${notEvaluated}");
+
+        assertThat(renderableFtlFormElementsBuilder.emptyFormDataMessage(new 
HashMap<>(), modelForm),
+                
MacroCallMatcher.hasNameAndParameters("renderEmptyFormDataMessage",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("message", "EMPTY 
${notEvaluated}")));
+    }
+
+    @Test
+    public void formatListWrapperCloseSetsTheFormName() {
+        final ModelForm modelForm = mock(ModelForm.class);
+        when(modelForm.getName()).thenReturn("FORMNAME");
+
+        
assertThat(renderableFtlFormElementsBuilder.formatListWrapperClose(modelForm),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatListWrapperClose",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("formName", "FORMNAME")));
+    }
+
+    @Test
+    public void formatHeaderRowOpenExpandsTheStyle() {
+        final ModelForm modelForm = mock(ModelForm.class);
+        when(modelForm.getHeaderRowStyle()).thenReturn("HEADER${styleParam}");
+
+        
assertThat(renderableFtlFormElementsBuilder.formatHeaderRowOpen(ImmutableMap.of("styleParam",
 "ABCD"), modelForm),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatHeaderRowOpen",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("style", "HEADERABCD")));
+    }
+
+    @Test
+    public void formatHeaderCellsSetStyleAndPositionSpan() {
+        final ModelFormField localModelFormField = mock(ModelFormField.class);
+        
when(localModelFormField.getTitleAreaStyle()).thenReturn("TITLEAREASTYLE");
+        when(localModelFormField.getTitleStyle()).thenReturn("TITLESTYLE");
+        final ModelForm modelForm = mock(ModelForm.class);
+        
when(modelForm.getFormTitleAreaStyle()).thenReturn("FORMTITLEAREASTYLE");
+
+        
assertThat(renderableFtlFormElementsBuilder.formatHeaderRowCellOpen(localModelFormField,
 3),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatHeaderRowCellOpen",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("style", "TITLEAREASTYLE"),
+                        
MacroCallParameterMatcher.hasNameAndIntegerValue("positionSpan", 3)));
+        
assertThat(renderableFtlFormElementsBuilder.formatHeaderRowFormCellOpen(modelForm),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatHeaderRowFormCellOpen",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("style", 
"FORMTITLEAREASTYLE")));
+        
assertThat(renderableFtlFormElementsBuilder.formatHeaderRowFormCellTitleSeparator(localModelFormField,
 true),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatHeaderRowFormCellTitleSeparator",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("style", "TITLESTYLE"),
+                        
MacroCallParameterMatcher.hasNameAndBooleanValue("isLast", true)));
+    }
+
+    @Test
+    public void formatItemRowOpenUsesTheEvenRowStyleForAnEvenIndex() {
+        final ModelForm modelForm = mock(ModelForm.class);
+        when(modelForm.getName()).thenReturn("FORMNAME");
+        when(modelForm.getEvenRowStyle()).thenReturn("EVENSTYLE");
+        when(modelForm.getOddRowStyle()).thenReturn("ODDSTYLE");
+
+        
assertThat(renderableFtlFormElementsBuilder.formatItemRowOpen(ImmutableMap.of("itemIndex",
 2), modelForm),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatItemRowOpen",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("formName", "FORMNAME"),
+                        
MacroCallParameterMatcher.hasNameAndIntegerValue("itemIndex", 2),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("evenRowStyle", "EVENSTYLE"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("oddRowStyle", "")));
+    }
+
+    @Test
+    public void formatItemRowOpenUsesTheExpandedOddRowStyleForAnOddIndex() {
+        final ModelForm modelForm = mock(ModelForm.class);
+        when(modelForm.getEvenRowStyle()).thenReturn("EVENSTYLE");
+        when(modelForm.getOddRowStyle()).thenReturn("ODD${styleParam}");
+
+        
assertThat(renderableFtlFormElementsBuilder.formatItemRowOpen(ImmutableMap.of("itemIndex",
 3, "styleParam", "ABCD"), modelForm),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatItemRowOpen",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("evenRowStyle", ""),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("oddRowStyle", "ODDABCD")));
+    }
+
+    @Test
+    public void formatItemRowOpenWithoutAnIndexLeavesTheIndexOut() {
+        final ModelForm modelForm = mock(ModelForm.class);
+
+        final RenderableFtlMacroCall macroCall = 
macroCall(renderableFtlFormElementsBuilder.formatItemRowOpen(new HashMap<>(), 
modelForm));
+        assertThat(macroCall.getParameters(), not(hasKey("itemIndex")));
+    }
+
+    @Test
+    public void formatItemRowCellsSetFieldNameStyleAndPositionSpan() {
+        final ModelFormField localModelFormField = mock(ModelFormField.class);
+        when(localModelFormField.getName()).thenReturn("FIELDNAME");
+        when(localModelFormField.getWidgetAreaStyle()).thenReturn("AREASTYLE");
+        final ModelForm modelForm = mock(ModelForm.class);
+        when(modelForm.getName()).thenReturn("FORMNAME");
+        
when(modelForm.getFormTitleAreaStyle()).thenReturn("FORMTITLEAREASTYLE");
+
+        
assertThat(renderableFtlFormElementsBuilder.formatItemRowCellOpen(localModelFormField,
 2),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatItemRowCellOpen",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("fieldName", "FIELDNAME"),
+                        
MacroCallParameterMatcher.hasNameAndIntegerValue("positionSpan", 2),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("style", "AREASTYLE")));
+        
assertThat(renderableFtlFormElementsBuilder.formatItemRowCellClose(localModelFormField),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatItemRowCellClose",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("fieldName", "FIELDNAME")));
+        
assertThat(renderableFtlFormElementsBuilder.formatItemRowFormCellOpen(modelForm),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatItemRowFormCellOpen",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("style", 
"FORMTITLEAREASTYLE")));
+        
assertThat(renderableFtlFormElementsBuilder.formatItemRowClose(modelForm),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatItemRowClose",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("formName", "FORMNAME")));
+    }
+
+    @Test
+    public void formatSingleWrapperSetsTheFormNameAndTheExpandedStyle() {
+        final ModelForm modelForm = mock(ModelForm.class);
+        when(modelForm.getName()).thenReturn("FORMNAME");
+        
when(modelForm.getDefaultTableStyle()).thenReturn("STYLE${styleParam}");
+
+        
assertThat(renderableFtlFormElementsBuilder.formatSingleWrapperOpen(ImmutableMap.of("styleParam",
 "ABCD"), modelForm),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatSingleWrapperOpen",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("formName", "FORMNAME"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("style", "STYLEABCD")));
+        
assertThat(renderableFtlFormElementsBuilder.formatSingleWrapperClose(modelForm),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatSingleWrapperClose",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("formName", "FORMNAME")));
+    }
+
+    @Test
+    public void formatFieldRowCellsSetStyleAndPositionSpan() {
+        final ModelFormField localModelFormField = mock(ModelFormField.class);
+        
when(localModelFormField.getTitleAreaStyle()).thenReturn("TITLEAREASTYLE");
+        when(localModelFormField.getWidgetAreaStyle()).thenReturn("AREASTYLE");
+
+        
assertThat(renderableFtlFormElementsBuilder.formatFieldRowTitleCellOpen(localModelFormField),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatFieldRowTitleCellOpen",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("style", "TITLEAREASTYLE")));
+        
assertThat(renderableFtlFormElementsBuilder.formatFieldRowWidgetCellOpen(localModelFormField,
 1),
+                
MacroCallMatcher.hasNameAndParameters("renderFormatFieldRowWidgetCellOpen",
+                        
MacroCallParameterMatcher.hasNameAndIntegerValue("positionSpan", 1),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("style", "AREASTYLE")));
+    }
+
+    @Test
+    public void boundaryCommentSetsTheTypeAndTheName() {
+        assertThat(renderableFtlFormElementsBuilder.boundaryComment("Begin", 
"Form Widget - Form Element", "FORMNAME"),
+                MacroCallMatcher.hasNameAndParameters("formatBoundaryComment",
+                        
MacroCallParameterMatcher.hasNameAndStringValue("boundaryType", "Begin"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("widgetType", "Form Widget - 
Form Element"),
+                        
MacroCallParameterMatcher.hasNameAndStringValue("widgetName", "FORMNAME")));
+    }
+
+    private RenderableFtlMacroCall hyperlink(final String description, final 
Map<String, String> parameterMap) {
+        return 
macroCall(renderableFtlFormElementsBuilder.makeHyperlinkByType(null, null, 
null, "#", parameterMap, description, "", "",
+                modelFormField, request, response, new HashMap<>()));
+    }
+
+    @Test
+    public void 
hyperlinkUsesTheTitleOfTheRequestWhenTheDescriptionIsNotTruncated() {
+        when(request.getAttribute("title")).thenReturn("TITLE");
+
+        assertThat(hyperlink("DESCRIPTION", new HashMap<>()), 
MacroCallMatcher.hasNameAndParameters("makeHyperlinkString",
+                MacroCallParameterMatcher.hasNameAndStringValue("description", 
"DESCRIPTION"),
+                MacroCallParameterMatcher.hasNameAndStringValue("title", 
"TITLE")));
+    }
+
+    @Test
+    public void hyperlinkUsesTheDescriptionAsTitleWhenItIsTruncated() {
+        when(request.getAttribute("descriptionSize")).thenReturn(5);
+        when(request.getAttribute("title")).thenReturn("TITLE");
+
+        assertThat(hyperlink("DESCRIPTION", new HashMap<>()), 
MacroCallMatcher.hasNameAndParameters("makeHyperlinkString",
+                MacroCallParameterMatcher.hasNameAndStringValue("description", 
"DESCR…"),
+                MacroCallParameterMatcher.hasNameAndStringValue("title", 
"DESCRIPTION")));
+    }
+
+    @Test
+    public void hyperlinkPassesTheDescriptionAsItIs() {
+        final String description = "say \"hi\" \\ ${1+1} #{2} <#stop>";
+
+        assertThat(hyperlink(description, new HashMap<>()), 
MacroCallMatcher.hasNameAndParameters("makeHyperlinkString",
+                MacroCallParameterMatcher.hasNameAndStringValue("description", 
description)));
+    }
+
+    @Test
+    public void layeredModalHyperlinkPassesTheParametersAsJsonText() {
+        final Map<String, String> parameterMap = new HashMap<>();
+        parameterMap.put("k1", "v1");
+        parameterMap.put("k2", "v2");
+        when(request.getAttribute("title")).thenReturn("TitleValue");
+        when(request.getAttribute("text")).thenReturn("TextValue");
+        when(request.getAttribute("id")).thenReturn("IdValue");
+        
when(request.getAttribute("uniqueItemName")).thenReturn("UniqueItemName");
+        when(request.getAttribute("width")).thenReturn("650");
+        when(request.getAttribute("height")).thenReturn("150");
+
+        assertThat(hyperlink("DescriptionValue", parameterMap), 
MacroCallMatcher.hasNameAndParameters("makeHyperlinkString",
+                MacroCallParameterMatcher.hasNameAndStringValue("description", 
"DescriptionValue"),
+                MacroCallParameterMatcher.hasNameAndStringValue("title", 
"TitleValue"),
+                MacroCallParameterMatcher.hasNameAndStringValue("text", 
"TextValue"),
+                MacroCallParameterMatcher.hasNameAndStringValue("id", 
"IdValue"),
+                
MacroCallParameterMatcher.hasNameAndStringValue("uniqueItemName", 
"UniqueItemName"),
+                MacroCallParameterMatcher.hasNameAndStringValue("width", 
"650"),
+                MacroCallParameterMatcher.hasNameAndStringValue("height", 
"150"),
+                
MacroCallParameterMatcher.hasNameAndStringValue("targetParameters",
+                        
"{\"k1\":\"v1\",\"k2\":\"v2\",\"presentation\":\"layer\"}")));
+    }
+
+    @Test
+    public void hyperlinkWithoutParametersHasNoTargetParameters() {
+        assertThat(hyperlink("DESCRIPTION", new HashMap<>()), 
MacroCallMatcher.hasNameAndParameters("makeHyperlinkString",
+                
MacroCallParameterMatcher.hasNameAndStringValue("targetParameters", "")));
+    }
+
+    @Test
+    public void 
hiddenFormHyperlinkInAMultiFormKeepsTheFormForAfterTheClosingOfTheForm() {
+        final ModelForm modelForm = mock(ModelForm.class);
+        when(modelForm.getType()).thenReturn("multi");
+        when(modelFormField.getModelForm()).thenReturn(modelForm);
+        when(request.getAttribute("title")).thenReturn("");
+        final Map<String, Object> context = new HashMap<>();
+        context.put("wholeFormContext", new HashMap<String, Object>());
+
+        final RenderableFtl link = 
renderableFtlFormElementsBuilder.makeHyperlinkByType("hidden-form", "STYLE", 
null, "#",
+                new HashMap<>(), "A ${description}", "", "", modelFormField, 
request, response, context);
+
+        assertThat(link instanceof RenderableFtlString, equalTo(true));
+        assertThat(((RenderableFtlString) link).getFtlString().contains("A 
${description}"), equalTo(true));
+        
assertThat(renderableFtlFormElementsBuilder.postMultiFormContent(context) 
instanceof RenderableFtlString, equalTo(true));
+    }
 }

Reply via email to