This guide helps developers familiar with Adobe Experience Manager's HTL (HTML Template Language, formerly Sightly) transition to using Faintly for client-side templating in AEM Edge Delivery Services.
- Quick Reference
- Overview
- Key Differences
- Expression Syntax
- Backend Integration
- Context Options
- Common Patterns
| Feature | HTL | Faintly |
|---|---|---|
| Text expression | ${variable} |
${variable} |
| Directive expression | ${variable} |
variable or ${variable} |
| Comparison | ${a > b} (Java) |
utils:eval(a > b) (JavaScript) |
| Ternary | ${condition ? 'yes' : 'no'} |
${utils:eval(condition ? 'yes' : 'no')} |
| Logical NOT | ${!condition} |
data-fly-not="condition" or utils:eval(!condition) |
| HTL/Sightly | Faintly |
|---|---|
data-sly-test |
data-fly-test |
data-sly-test (negated) |
data-fly-not |
data-sly-list / data-sly-repeat |
data-fly-repeat |
data-sly-use |
Functions in context object |
data-sly-text |
data-fly-content |
data-sly-attribute |
data-fly-attributes |
data-sly-unwrap |
data-fly-unwrap |
data-sly-template |
<template data-fly-name> |
data-sly-call |
data-fly-include |
data-sly-include |
data-fly-include |
<sly> tag |
Any element with data-fly-unwrap |
data-sly-element |
Not needed |
data-sly-resource |
Not applicable |
- ✅ Directive prefix changes:
data-sly-*→data-fly-* - ✅
${}optional in directives: Bothdata-fly-test="expr"anddata-fly-test="${expr}"work - ✅ Inverse conditions made easy: Use
data-fly-not="condition"instead of negating with! - ✅ Use
utils:eval()for complex expressions: Comparisons, ternary, logical operators - ✅ Template logic via context: Replace
data-sly-usewith JavaScript functions in context object - ✅ Functions auto-called: Faintly automatically calls functions in expressions for lazy/conditional loading
- ✅ No context options needed: DOM manipulation handles escaping automatically
- ✅ Client-side execution: Templates render in browser, not server
Both HTL and Faintly share core philosophies:
- ✅ HTML5 data attributes for directives
- ✅ Security-first approach with XSS protection
- ✅ Separation of concerns (logic vs markup)
- ✅ Expression-based templating
- ✅ Conditional rendering and iteration
- ✅ Template reuse and composition
| Aspect | HTL/Sightly | Faintly |
|---|---|---|
| Execution | Server-side (Java/Sling) | Client-side (JavaScript) |
| Expression Context | Server-side Java objects | Client-side JavaScript objects |
| Dynamic Loading | Server renders complete HTML | Client renders from templates |
HTL always requires ${} wrapper:
<!-- HTL -->
<div data-sly-test="${user.isAdmin}">Admin Panel</div>
<p>${user.name}</p>
<a href="${user.profileUrl}">Profile</a>Faintly supports two syntaxes depending on context:
Both syntaxes work in data-fly-* directives:
<!-- Both work in Faintly directives -->
<div data-fly-test="user.isAdmin">Admin Panel</div>
<div data-fly-test="${user.isAdmin}">Admin Panel</div>
<ul data-fly-repeat="items">
<ul data-fly-repeat="${items}"><!-- Faintly requires ${} in regular attributes and text -->
<p>Hello ${user.name}</p>
<a href="${user.profileUrl}">Profile</a>
<div class="card-${index}">Content</div>Faintly supports complex JavaScript expressions using utils:eval():
<!-- HTL uses Java expressions -->
<div data-sly-test="${user.age >= 18}">Adult Content</div>
<!-- Faintly uses JavaScript expressions -->
<div data-fly-test="utils:eval(user.age >= 18)">Adult Content</div>
<p>Status: ${utils:eval(count === 1 ? 'item' : 'items')}</p>
<span class="${utils:eval(user.role === 'admin' ? 'badge-red' : 'badge-blue')}"></span>Supported operations:
- Comparisons:
>,<,>=,<=,===,!== - Logical:
&&,||,! - Ternary:
condition ? true : false - Arithmetic:
+,-,*,/,% - String methods:
.toUpperCase(),.toLowerCase(),.substring(),.trim() - Array methods:
.join(),.map(),.filter(),.length, index access - Object access: nested properties, bracket notation
utils:eval() uses JavaScript eval(). Only use with trusted data. Requires CSP to allow 'unsafe-eval'. For untrusted data, use context functions instead.
In HTL, backend integration uses data-sly-use to load Java classes or Sling Models:
<!-- HTL backend integration -->
<div data-sly-use.model="com.example.models.ArticleModel">
<h1>${model.title}</h1>
<p>${model.description}</p>
</div>
<!-- HTL with JavaScript use-API -->
<div data-sly-use.articles="articles.js">
<ul data-sly-list="${articles.list}">
<li>${item.title}</li>
</ul>
</div>Faintly uses a JavaScript context object passed to renderBlock():
// Faintly backend integration
import { renderBlock } from './faintly.js';
// Context object with data and functions
const context = {
title: 'Article Title',
description: 'Article description',
articles: fetchArticles(), // Can be sync data
loadMore: () => fetchMoreArticles(), // Or async functions
};
const block = document.querySelector('.article-block');
renderBlock(block, context);<!-- Faintly template using context -->
<div class="article-block">
<h1>${title}</h1>
<p>${description}</p>
<ul data-fly-repeat="articles">
<li>${item.title}</li>
</ul>
</div>Faintly automatically calls functions in expressions, enabling lazy/conditional loading:
<!-- Function called only if condition is true -->
<div data-fly-test="user.isAdmin">
<ul data-fly-repeat="fetchAdminData">
<li>${item.name}</li>
</ul>
</div>const context = {
user: { isAdmin: true },
// This function only executes if user.isAdmin is true
fetchAdminData: () => fetch('/api/admin-data').then(r => r.json()),
};HTL pattern:
<div data-sly-use.userService="com.example.UserService">
<p>${userService.userName}</p>
</div>Faintly equivalent:
// In your block's JavaScript
import { renderBlock } from './faintly.js';
const context = {
userName: getUserName(), // Direct function call
// or
user: { name: getUserName() },
};
renderBlock(block, context);<!-- In your block's HTML -->
<div class="user-block">
<p>${userName}</p>
</div>HTL uses context options (@ context) for context-aware escaping:
<!-- HTL context-aware escaping -->
<a href="${link.url @ context='uri'}">Link</a>
<div class="${cssClass @ context='styleToken'}">Content</div>
<script>var data = ${jsonData @ context='unsafe'};</script>Available contexts in HTL:
text(default) - Plain texthtml- HTML contentattribute- HTML attributeuri- URL/URInumber- Numeric valueelementName- HTML element nameattributeName- HTML attribute namescriptToken- JavaScript tokenstyleToken- CSS tokenunsafe- No escaping (dangerous)
Faintly doesn't require context options because:
-
DOM Manipulation vs String Generation
- HTL generates HTML strings server-side → needs context-aware string escaping
- Faintly manipulates DOM directly → browser handles escaping automatically
-
Built-in Browser Security
- Setting
.textContentautomatically escapes HTML - Setting
.setAttribute()handles attribute escaping - DOM APIs provide built-in XSS protection
- Setting
-
Security Module
- Faintly's security module validates attributes and URLs
- Same-origin enforcement for URLs
- Dangerous attribute blocking
- No string-based escaping needed
HTL:
<a href="${article.url @ context='uri'}"
title="${article.title @ context='attribute'}">
${article.name @ context='text'}
</a>Faintly (no context needed):
<a href="${article.url}" title="${article.title}">
${article.name}
</a>The browser automatically handles proper escaping for each context (URL, attribute, text).
HTL:
<div data-sly-test="${user.isLoggedIn}">
<p>Welcome, ${user.name}</p>
</div>
<div data-sly-test="${!user.isLoggedIn}">
<p>Please log in</p>
</div>Faintly:
<div data-fly-test="user.isLoggedIn">
<p>Welcome, ${user.name}</p>
</div>
<!-- Option 1: Using data-fly-not (cleaner) -->
<div data-fly-not="user.isLoggedIn">
<p>Please log in</p>
</div>
<!-- Option 2: Using utils:eval for negation -->
<div data-fly-test="utils:eval(!user.isLoggedIn)">
<p>Please log in</p>
</div>HTL:
<ul data-sly-list.article="${articles}">
<li>
<h3>${article.title}</h3>
<p>${article.description}</p>
</li>
</ul>Faintly:
<!-- Using default 'item' name -->
<ul data-fly-repeat="articles">
<li>
<h3>${item.title}</h3>
<p>${item.description}</p>
</li>
</ul>
<!-- Or using named repeat (closer to HTL) -->
<ul data-fly-repeat.article="articles">
<li>
<h3>${article.title}</h3>
<p>${article.description}</p>
</li>
</ul>HTL:
<div class="card ${item.featured ? 'card--featured' : ''}">
Content
</div>Faintly:
<div class="card ${utils:eval(item.featured ? 'card--featured' : '')}">
Content
</div>
<!-- Or using data-fly-attributes -->
<div class="card"
data-fly-attributes="${utils:eval(item.featured ? {class: 'card card--featured'} : {})}">
Content
</div>HTL:
<a data-sly-attribute.href="${link.url}"
data-sly-attribute.target="${link.external ? '_blank' : '_self'}">
${link.text}
</a>Faintly:
<a href="${link.url}"
target="${utils:eval(link.external ? '_blank' : '_self')}">
${link.text}
</a>
<!-- Or using data-fly-attributes for multiple attributes -->
<a data-fly-attributes="{href: link.url, target: link.target}">
${link.text}
</a>HTL:
<!-- Define template -->
<template data-sly-template.card="${@ title, description}">
<div class="card">
<h3>${title}</h3>
<p>${description}</p>
</div>
</template>
<!-- Call template -->
<div data-sly-call="${card @ title=article.title, description=article.desc}"></div>Faintly:
<!-- Define template -->
<template data-fly-name="card">
<div class="card">
<h3>${title}</h3>
<p>${description}</p>
</div>
</template>
<!-- Include template -->
<div data-fly-include="card" data-fly-attributes="{title: article.title, description: article.desc}"></div>HTL:
<!-- Using sly tag (auto-unwraps) -->
<sly data-sly-test="${user.isAdmin}">
<p>Admin content 1</p>
<p>Admin content 2</p>
</sly>
<!-- Using unwrap directive -->
<div data-sly-unwrap="${true}">
<p>This paragraph will remain</p>
</div>Faintly:
<!-- Any element with data-fly-unwrap -->
<div data-fly-test="user.isAdmin" data-fly-unwrap="true">
<p>Admin content 1</p>
<p>Admin content 2</p>
</div>
<!-- Or explicit unwrap -->
<div data-fly-unwrap="true">
<p>This paragraph will remain</p>
</div>