Dynamic Target Audience Selector
This example shows how to configure a dynamic target audience selector in a Sitevision WebApp configuration view.
The page is divided into two parts:
- A Velocity template that renders the settings UI.
- A server route that resolves target audience groups and values used by the template.
At runtime, these two parts form one loop:
- The route collects available target audience groups.
- The selected group (if any) is read from app data.
- The route resolves target audience values for that selected group.
- The template renders one default selector and one selector per resolved target audience value.
- Submitted values are stored under their input names and become available on the next request.
Velocity Template
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">
<%= i18n('defaultSettings') %>
</h3>
</div>
<div class="panel-body">
<div class="form-group">
<label>
<%= i18n('archiveOrFolderSelector') %>
</label>
<input type="hidden" class="form-control" data-component="page-list" name="archive-list-default"
data-types="sv:archive,sv:folder" required />
<p class="help-block">
<%= i18n('archiveOrFolderSelectorHelptext') %>
</p>
</div>
<div class="form-group">
<label>
<%= i18n('targetAudienceGroupRepositorySelector') %>
</label>
<select id="targetAudienceGroupRepositorySelector" name="targetAudienceGroupRepository" class="form-control"
required>
<% _.each(targetAudienceGroups, function(targetAudienceGroup) { %>
<option value="<%= targetAudienceGroup.id %>">
<%= targetAudienceGroup.name %>
</option>
<% }) %>
</select>
<p class="help-block">
<%= i18n('targetAudienceGroupRepositorySelectorHelptext') %>
</p>
</div>
</div>
</div>
<% _.each(targetAudienceValues, function(targetAudienceValue) { %>
<div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
<%= i18n('settings') %>
<%= i18n('for') %>
<%= targetAudienceValue.name %>
</h3>
</div>
<div class="panel-body">
<div class="form-group">
<label>
<%= i18n('archiveOrFolderSelector') %>
</label>
<input type="hidden" class="form-control" data-component="page-list"
name="archive-list-<%= targetAudienceValue.id %>" data-types="sv:archive,sv:folder" />
<p class="help-block">
<%= i18n('archiveOrFolderSelectorHelptext') %>
</p>
</div>
</div>
</div>
</div>
<% }) %>
Server Route
/* eslint-disable @typescript-eslint/no-var-requires */
(() => {
const router = require("router");
const resourceLocatorUtil = require("ResourceLocatorUtil");
const nodeResolverUtil = require("NodeResolverUtil");
const properties = require('Properties');
const globalAppData = require("globalAppData");
router.get("/", (req, res) => {
const targetAudienceValues = [];
const selectedTargetGroup = globalAppData.get("targetAudienceGroupRepository");
if (selectedTargetGroup) {
const targetGroupRepository = resourceLocatorUtil.getNodeByIdentifier(selectedTargetGroup);
const targetGroupResolver = nodeResolverUtil.getTargetAudiencesResolver();
const targetRepositoryResolvedList = targetGroupResolver.resolve(targetGroupRepository);
if (targetRepositoryResolvedList) {
for (let n = 0; n < targetRepositoryResolvedList.size(); n++) {
const targetGroup = targetRepositoryResolvedList.get(n);
const name = properties.get(targetGroup, "displayName");
const id = properties.get(targetGroup, "jcr:uuid");
targetAudienceValues.push({ name, id });
}
}
}
const targetAudienceGroups = [];
const targetAudienceGroupRepository = resourceLocatorUtil.getTargetAudienceGroupRepository();
const targetAudienceGroupNodes = targetAudienceGroupRepository.getNodes();
while (targetAudienceGroupNodes.hasNext()) {
const targetAudienceGroupNode = targetAudienceGroupNodes.nextNode();
targetAudienceGroups.push({
name: properties.get(targetAudienceGroupNode, "displayName"),
id: properties.get(targetAudienceGroupNode, "jcr:uuid")
});
}
res.render({
targetAudienceGroups,
targetAudienceValues
});
});
})();
How It Works Together
The route and template share a simple data contract. The route provides two arrays to res.render(...):
targetAudienceGroups: all selectable target audience group repositories.targetAudienceValues: resolved values inside the currently selected repository.
The template consumes those arrays in two different places:
- It loops over
targetAudienceGroupsto create<option>rows in the repository<select>. - It loops over
targetAudienceValuesto create dynamicarchive-list-<id>fields.
This means the number of generated archive/folder selectors is dynamic and depends on the selected repository.
Detailed Request Lifecycle
- The configuration page is opened and triggers
router.get("/"). - The route initializes
targetAudienceValuesas an empty array. - The route reads
targetAudienceGroupRepositoryfromglobalAppData. - If a repository id exists:
- The node is resolved via
resourceLocatorUtil.getNodeByIdentifier(...). - A target audience resolver is fetched via
nodeResolverUtil.getTargetAudiencesResolver(). - Resolved audience nodes are iterated.
- Each node is transformed into
{ name, id }usingdisplayNameandjcr:uuid.
- The node is resolved via
- Independently of selection, all target audience group repositories are collected from
getTargetAudienceGroupRepository().getNodes(). - The route renders the template with both arrays.
- The template always shows:
- A default archive/folder selector (
archive-list-default). - A repository selector (
targetAudienceGroupRepository).
- A default archive/folder selector (
- The template conditionally shows one additional archive/folder selector per resolved audience value:
archive-list-<targetAudienceValue.id>.
Data Model and Naming Strategy
The naming strategy is what makes this setup maintainable:
- Static keys:
targetAudienceGroupRepositoryarchive-list-default
- Dynamic keys:
archive-list-<targetAudienceId>
Because dynamic field names include the target audience id, each saved selection can be tied to exactly one audience value without extra mapping tables.
Why There Is a Default Selector
archive-list-default acts as a fallback when no audience-specific value exists or when no target audience group has been selected yet. This prevents empty behavior and gives editors a baseline source.
Practical Example
If the selected target audience group resolves to three values:
- Students (
uuid-a) - Staff (
uuid-b) - Alumni (
uuid-c)
then the form will render these keys:
archive-list-defaultarchive-list-uuid-aarchive-list-uuid-barchive-list-uuid-c
On later reads, your runtime logic can choose the most specific key first and fall back to archive-list-default when needed.
Notes
targetAudienceGroupRepositorystores the currently selected target audience group in app data.- Each rendered
archive-list-<targetAudienceId>field allows individual archive or folder configuration per target audience value. - Keep i18n keys synchronized with the template labels and help texts.
- The route is intentionally defensive: if no group is selected,
targetAudienceValuesremains empty and only default settings are shown.