HTL Tutorial #11: data-sly-element - Dynamic HTML Tags

HTL Tutorial #11: data-sly-element - Dynamic HTML Tags

What is data-sly-element?

data-sly-element replaces the HTML tag of an element with another dynamically specified tag.

Syntax

<div data-sly-element="${tagName}">
  Content
</div>

Basic Example

<!-- The div becomes h2 -->
<div data-sly-element="h2">
  Dynamic Title
</div>

Output:

<h2>
  Dynamic Title
</h2>

Why Use It?

1. Dynamic Headings

Heading level based on page depth:

<div data-sly-use.page="com.example.PageModel"
     data-sly-element="${page.headingLevel}">
  ${page.title}
</div>

Java Model:

@Model(adaptables = Resource.class)
public class PageModel {

    @Inject
    private Page currentPage;

    public String getHeadingLevel() {
        int depth = currentPage.getDepth();

        // Homepage: h1, Level 1: h2, Level 2: h3, etc.
        int level = Math.min(depth, 6);
        return "h" + level;
    }

    public String getTitle() {
        return currentPage.getTitle();
    }
}

Output (depth=2):

<h2>
  Example Page
</h2>

2. Ordered vs Unordered Lists

<div data-sly-element="${properties.ordered ? 'ol' : 'ul'}">
  <li data-sly-list.item="${properties.items}">
    ${item}
  </li>
</div>

If properties.ordered = true:

<ol>
  <li>First</li>
  <li>Second</li>
  <li>Third</li>
</ol>

If properties.ordered = false:

<ul>
  <li>First</li>
  <li>Second</li>
  <li>Third</li>
</ul>

3. Semantic HTML Elements

<div data-sly-element="${properties.semantic || 'div'}">
  ${properties.content}
</div>

The author can choose: article, section, aside, nav, main, etc.

Safe Tag Whitelist

HTL has a whitelist of allowed HTML tags for security reasons. If you specify a tag not in the list, HTL uses <div> as a fallback.

Allowed Tags

a, abbr, address, article, aside, b, bdi, bdo, blockquote, br, caption,
cite, code, col, colgroup, data, dd, del, dfn, div, dl, dt, em, figcaption,
figure, footer, h1, h2, h3, h4, h5, h6, header, hr, i, ins, kbd, li, main,
mark, nav, ol, p, pre, q, rp, rt, ruby, s, samp, section, small, span,
strong, sub, sup, table, tbody, td, tfoot, th, thead, time, tr, u, ul, var,
wbr

NOT Allowed Tags (Example)

<!-- BLOCKED - script is not in the whitelist -->
<div data-sly-element="script">
  alert('XSS')
</div>

<!-- Output (fallback to div): -->
<div>
  alert('XSS')
</div>

This prevents XSS injection!

Common Patterns

1. Hierarchical Heading

<sly data-sly-template.heading="${@ level, text}">
  <div data-sly-element="${'h' + level}">
    ${text}
  </div>
</sly>

<!-- Usage -->
<sly data-sly-call="${heading @ level=1, text='Main Title'}"></sly>
<sly data-sly-call="${heading @ level=2, text='Subtitle'}"></sly>
<sly data-sly-call="${heading @ level=3, text='Section'}"></sly>

Output:

<h1>Main Title</h1>
<h2>Subtitle</h2>
<h3>Section</h3>

2. Link vs Span

<div data-sly-element="${link.url ? 'a' : 'span'}"
     data-sly-attribute.href="${link.url}">
  ${link.text}
</div>

With URL:

<a href="/page">Click here</a>

Without URL:

<span>Plain text</span>

3. Semantic Container

<div data-sly-use.comp="com.example.ContainerModel"
     data-sly-element="${comp.semanticTag}"
     data-sly-attribute.class="${comp.cssClass}">

  <div data-sly-list.child="${comp.children}">
    ${child}
  </div>
</div>

Model:

public class ContainerModel {

    @ValueMapValue
    private String containerType; // "article", "section", "aside", "div"

    public String getSemanticTag() {
        if (containerType != null) {
            return containerType;
        }
        return "div"; // default
    }

    public String getCssClass() {
        return "container-" + getSemanticTag();
    }
}

Combining with Other Statements

With data-sly-test

<!-- Heading only if there's a title -->
<div data-sly-test="${properties.title}"
     data-sly-element="${properties.headingLevel || 'h2'}"
     data-sly-text="${properties.title}">
  Placeholder
</div>

With data-sly-list

<div data-sly-element="${properties.listType || 'ul'}">
  <li data-sly-list.item="${properties.items}">
    ${item}
  </li>
</div>

With data-sly-attribute

<div data-sly-element="${properties.tag || 'div'}"
     data-sly-attribute.id="${properties.id}"
     data-sly-attribute.class="${properties.cssClass}">
  ${properties.content}
</div>

Complete Example: Title Component

<div data-sly-use.title="com.example.TitleComponent"
     data-sly-test="${title.text}"
     data-sly-element="${title.type}"
     data-sly-attribute.class="${'title ' + title.styleClass}"
     data-sly-attribute.id="${title.anchorId}">
  ${title.text}
</div>

Java Model:

@Model(adaptables = Resource.class)
public class TitleComponent {

    @ValueMapValue
    private String text;

    @ValueMapValue
    private String type; // h1, h2, h3, h4, h5, h6

    @ValueMapValue
    private String size; // small, medium, large

    @ValueMapValue
    private String anchorId;

    public String getText() {
        return text;
    }

    public String getType() {
        return type != null ? type : "h2";
    }

    public String getStyleClass() {
        String sizeClass = "";
        if ("small".equals(size)) {
            sizeClass = "title-sm";
        } else if ("large".equals(size)) {
            sizeClass = "title-lg";
        }
        return sizeClass;
    }

    public String getAnchorId() {
        return anchorId;
    }
}

Dialog XML (_cq_dialog.xml snippet):

<type
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/select"
    fieldLabel="Heading Type"
    name="./type">
    <items jcr:primaryType="nt:unstructured">
        <h1 jcr:primaryType="nt:unstructured" text="H1" value="h1"/>
        <h2 jcr:primaryType="nt:unstructured" text="H2" value="h2"/>
        <h3 jcr:primaryType="nt:unstructured" text="H3" value="h3"/>
        <h4 jcr:primaryType="nt:unstructured" text="H4" value="h4"/>
        <h5 jcr:primaryType="nt:unstructured" text="H5" value="h5"/>
        <h6 jcr:primaryType="nt:unstructured" text="H6" value="h6"/>
    </items>
</type>

Output (type=h3, size=large, anchorId=intro):

<h3 class="title title-lg" id="intro">
  Introduction to the Project
</h3>

Complete Example: Button Component

<div data-sly-use.button="com.example.ButtonComponent"
     data-sly-element="${button.tag}"
     data-sly-attribute.href="${button.href}"
     data-sly-attribute.type="${button.type}"
     data-sly-attribute.class="${button.classes}"
     data-sly-attribute.target="${button.target}"
     data-sly-attribute.rel="${button.rel}">
  ${button.text}
</div>

Model:

public class ButtonComponent {

    @ValueMapValue
    private String text;

    @ValueMapValue
    private String link;

    @ValueMapValue
    private boolean openInNewTab;

    @ValueMapValue
    private String style; // primary, secondary, link

    public String getTag() {
        return link != null && !link.isEmpty() ? "a" : "button";
    }

    public String getHref() {
        return "a".equals(getTag()) ? link : null;
    }

    public String getType() {
        return "button".equals(getTag()) ? "button" : null;
    }

    public String getClasses() {
        String baseClass = "btn";
        String styleClass = "btn-" + (style != null ? style : "primary");
        return baseClass + " " + styleClass;
    }

    public String getTarget() {
        return openInNewTab && "a".equals(getTag()) ? "_blank" : null;
    }

    public String getRel() {
        return openInNewTab && "a".equals(getTag()) ? "noopener noreferrer" : null;
    }

    public String getText() {
        return text;
    }
}

Output (link with new tab):

<a href="/page" class="btn btn-primary" target="_blank" rel="noopener noreferrer">
  Learn More
</a>

Output (button without link):

<button type="button" class="btn btn-primary">
  Submit
</button>

Best Practices

  1. Always provide a fallback: ${tag || 'div'}
  2. Validate author input: Use whitelist in model
  3. Semantic HTML: Prefer semantic tags (article, section, nav)
  4. Heading levels: Maintain correct hierarchy (h1 → h2 → h3)
  5. Accessibility: Use appropriate tags for screen readers
  6. Don't overuse: Use only when necessary
  7. Testing: Test with various values, including null/empty

Tag Validation in Model

private static final Set<String> ALLOWED_TAGS = Set.of(
    "div", "section", "article", "aside", "nav", "main"
);

public String getSafeTag() {
    if (tag != null && ALLOWED_TAGS.contains(tag.toLowerCase())) {
        return tag.toLowerCase();
    }
    return "div"; // safe fallback
}

Common Mistakes

❌ Dynamic tag without fallback

<!-- RISKY - if properties.tag is null -->
<div data-sly-element="${properties.tag}">

<!-- CORRECT - with fallback -->
<div data-sly-element="${properties.tag || 'div'}">

❌ Unsafe tag

<!-- BLOCKED - script not allowed -->
<div data-sly-element="script">

<!-- Use only whitelisted tags -->
<div data-sly-element="section">

❌ Confusing with data-sly-attribute

<!-- WRONG - this changes attribute, not tag -->
<div data-sly-attribute.element="h2">

<!-- CORRECT -->
<div data-sly-element="h2">

Practical Exercises

  1. Heading Component: Heading with selectable h1-h6 levels
  2. List Component: ol/ul list with customizable type
  3. Container Component: Choose between div/section/article/aside
  4. Button/Link: Switch between and

Next Lesson

In the next lesson we'll see data-sly-unwrap to remove wrapper elements while keeping only the content.


Lesson #11 of the HTL Tutorial series. ← Previous lesson | Next lesson →