Launchpad Transmissions
Transmissions are a powerful feature provided by Launchpad that radically accelerates UI/UX development
in your Spaceport application. It allows your server-side Groovy code, using Launchpad Server Actions,
to directly manipulate the DOM enabling you to build rich, interactive experiences with little to no additional JavaScript.
This server-driven approach streamlines the implementation of typical UI interactions by centralizing presentation
logic with the backend, right inside your templates.
It's important to get out the way that every Transmission requires a server round-trip. For situations demanding
instantaneous user feedback, Spaceport offers other features that can be used with transmissions, allowing a hybrid
approach for enabling strategies like optimistic updating. By leveraging features like
Document Data and Server Elements, you can achieve a snappy user experience, with
the bonus of keeping your application logic securely on the server.
# Installation & Usage
Transmissions require HUD-Core.js to be included in your project. HUD-Core is a lightweight JavaScript library
that powers the client-side functionality of Launchpad, including handling events, page mutations, syncing data with
the server, applying transmissions, and more. You can include HUD-Core in your project by adding the following script
tag to your HTML:
<script defer src='https://cdn.jsdelivr.net/gh/spaceport-dev/hud-core.js@latest/hud-core.min.js'></script>
You can view the latest release at JSDelivr, or view the source code and official repository on Github.
# Transmission Formats
There are three primary formats for a transmission, each suited for different use cases:
- Map Transmission: The most powerful and flexible format, for performing multiple, complex operations.
- Array Transmission: A concise format for chaining a sequence of simple actions or class changes.
- Single Value Transmission: The simplest format, for directly updating an element's content with a hypermedia update.
# Rationales and a Core Examples
Why use Transmissions? Here are two key rationales, each illustrated with a core example.
## Rationale: Less Javascript
An overarching goal of Launchpad is to eliminate the need for custom JavaScript for common UI interactions. Instead of writing frontend code to handle what happens after a button click, you can define that behavior directly on the server, right next to your page composition. This keeps your templates cleaner and your development faster.
Core Example: Here is a simple button that updates itself after being clicked.
<button target="self" on-click=${ _{ ['innerText': 'Confirmed!', '+confirmed': 'it'] }}>
Confirm
</button>
What's Happening?
on-click: The user clicks the button, triggering a server action.- Server Logic: The Groovy code
_{ ... }runs on the server. It doesn't need to perform any complex logic; it just returns a Map Transmission. - Transmission: The map
['innerText': 'Confirmed!', '+confirmed': 'it']is sent back to the browser. - UI Update: Launchpad receives the map and follows its instructions:
[ 'innerText': 'Confirmed!', // tells it to change the button's text.
'+confirmed': 'it' ] // tells it to add the CSS class confirmed to the button itself (it).
## Rationale: Server State
While Launchpad provides tools for managing state on the client (like Document Data for optimistic
updates), one of the core strengths of the Transmission pattern lies in its ability to rely on server state.
Why is this important?
- Reliability: The server becomes the single source of truth. The state of your application isn't just a temporary condition in the user's browser; it's a persistent fact stored on your server (e.g., in a database or session).
- Consistency: The user gets the same experience whether they refresh the page, close their browser and come back, or log in from a different device.
- Security: Sensitive calculations and business logic remain on the server, preventing client-side manipulation.
Example: A Persistent Counter
This simple counter's value is stored in the user's page session on the server. Every click updates the counter variable,
and the server simply tells the client what the new value is, passing along the server state.
<% // persistent-counter-example.ghtml
// Define a local variable, bound to the session by Launchpad when used in
// conjunction with HUD-Core
def counter = 0;
// Closure to increment the counter
def increment = {
counter++
return counter + ' hot cross buns' // Return the new value directly
}
// Closure to decrement the counter
def decrement = {
counter--
return counter + ' hot cross buns' // Ultimately returned by the transmission
}
%>
<div class="counter-widget">
<button on-click=${ _{ decrement() }} target="#count-display">-</button>
<span id="count-display">${ counter } hot cross buns</span>
<button on-click=${ _{ increment() }} target="#count-display">+</button>
</div>
In this example, clicking + or - runs the corresponding Groovy closure on the server using a server action. The closure modifies
counter, a local variable to the Launchpad session, and then returns the new integer value. This Single Value Transmission
is received by the client, and Launchpad updates the innerHTML of the <span id="count-display"> to show the new,
authoritative count from the server.
# Available Server Events
Launchpad listens for a wide range of standard browser DOM events. You can attach a server action to any of these
events by creating an attribute with an on- prefix (e.g., on-click, on-submit). When the event occurs on that element,
it will trigger a call to the server and process the returned transmission.
Notice that the syntax for a Launchpad Server Event differs from a standard client-side inline event with a dash (-) in its attribute name. This allows for leverage of both server-side and client-side inline events when necessary.
## Mouse Events
on-click,on-dblclick,on-mousedown,on-mouseup,on-mouseover,on-mouseout,on-mouseenter,on-mouseleave,on-mousemove,on-wheel,on-contextmenu
## Keyboard Events
on-keydown,on-keyup,on-keypress
## Input & Form Events
on-submit,on-change,on-input,on-select,on-focus,on-blur,on-focusin,on-focusout,on-formblur(custom)
## Drag & Drop Events
on-dragenter,on-dragleave,on-drop
## Touch Events
on-touchstart,on-touchmove,on-touchend,on-touchcancel
## Lifecycle Events
on-load,on-beforeunload,on-nudge(custom)
# Data Sent to the Server
When a Launchpad event is triggered, a rich payload of contextual data is automatically collected from the client and
sent to your server-side Groovy closure. This data is available in the t object within the server action closure
(e.g., _{ t -> ... }). This allows your server code to make decisions with additional context about the event,
including the state of the element that triggered the event, any form data, and more.
While it's referred to as t in this documentation, you can name this parameter anything you like. In some nested code situations
it will be necessary to use an alternate name as t may already be in use.
| Category | Property | Description |
|---|---|---|
| Element Value | value |
The primary value of the element. This is intelligently determined: it can be an <input>'s text, a checkbox's state, a file's content as Base64, or the trimmed innerHTML of a standard element. |
| Element Info | elementId, tagName, classList, innerText, textContent |
Core properties of the activeTarget element (see source attribute below). |
| Event Info | key, keyCode, shiftKey, ctrlKey, altKey, metaKey, repeat |
Details for keyboard events. Note: shiftKey, ctrlKey, etc. appear only if true. |
clientX, clientY, pageX, pageY, button, buttons, offsetX, offsetY, movementX, movementY |
Details for mouse events. | |
| Form Data | [input-name] |
If the element is inside a <form>, all named inputs from that form are automatically included by their name attribute. Launchpad correctly handles text fields, textareas, checkboxes, radio buttons, select lists (single and multiple), and file inputs. |
| Custom Data | [data-attribute] |
All data-* attributes on the element are sent as top-level properties in the t object (e.g., data-user-id="123" becomes t.userId). |
| URL Data | [query-param] |
All query parameters from the current page's URL are included as top-level properties. |
| Included Data | [storage-key] |
You can use the include attribute on an element to explicitly send specific localStorage (key) or sessionStorage (~key) values. You can also include standard element attributes by name (e.g., include="id, theme"). |
## Working with the t Object on the Server
The t object gives your server-side Groovy code direct access to all the data sent from the client. However, since
this data comes from HTML attributes and form fields, it often arrives as strings. To make working with this data easier
and safer, the t object is equipped with several helper methods to reliably convert these values into the
data types you need.
| Method | Description | Example Usage |
|---|---|---|
t.getString('key') |
Safely converts the value of the given key to a String. |
def name = t.getString('username') |
t.getBool('key') |
Coerces the value into a boolean. Handles "true", "on", "yes", and checkbox states. Returns false for other values. |
def isAdmin = t.getBool('isAdmin') |
t.getNumber('key') |
Intelligently converts the value to a Long or Double, depending on whether it contains a decimal point. Returns 0L on failure. |
def price = t.getNumber('itemPrice') |
t.getInteger('key') |
Coerces the value into an Integer. Returns 0 on failure. |
def quantity = t.getInteger('quantity') |
t.getList('key') |
Converts a value into a List. It can parse comma-separated strings, JSON arrays, or wrap a single item in a list. |
def tags = t.getList('tags') |
Example: Using Helpers in a Server Action
<%
def processOrder = { t ->
// Direct access might give you a string "5"
def quantityStr = t.quantity
// Using a helper ensures you get an Integer for calculations
def quantity = t.getInteger('quantity') // Safely returns 5 (Integer)
// A checkbox might send "on" or just exist if checked
def isPriority = t.getBool('priorityShipping') // Safely returns true or false
// A data attribute might be a string "19.99"
def price = t.getNumber('price') // Safely returns 19.99 (Double)
if (isPriority && quantity > 0) {
// ... process order with correct data types
}
// Return a transmission to give updates and feedback using a direct hypertext replacement
// along with a targeted transmission
return [ '#order-status' : 'Order processed!', 'disabled' : 'true' ]
}
%>
<form on-submit=${ _{ t -> processOrder(t) }} target='button'>
<input name="quantity" value="5" data-price="19.99">
<input type="checkbox" name="priorityShipping" checked>
<button type="submit">Submit</button>
<div id="order-status"></div>
</form>
# The source Attribute: Pinpointing Event Origins
The source attribute gives you precise control over which element's data is sent to the server, which is
especially useful for event delegation.
Imagine you have a list where each item should be clickable. Instead of putting an on-click on every
single <li>, you can put one on the parent <ul>. But how do you know which <li> was clicked? The
source attribute solves this.
source Value |
Behavior | Use Case |
|---|---|---|
| (not set) | By default, the data comes from the event.target (the actual element clicked). |
Simple cases where the clickable element is the one with the on-* listener. |
| CSS Selector | The on-* listener is on a container, but the data payload is gathered from the element that matches the selector from the source element. |
A ul with on-click and source="li". When you click an li, the server receives the value, data-* attributes, etc., of that specific li. |
strict |
The event will only fire if the element clicked (event.target) is the exact same element that has the on-* listener (event.currentTarget). Clicks on child elements are ignored. |
Preventing actions from firing when a user clicks on an icon or <strong> tag inside a button. |
auto |
Explicitly sets the default behavior where the event.target is the source of the data. |
Can be used to clarify intent, but is rarely needed as it's the default behavior. |
The target Attribute
The target attribute is fundamental to Launchpad, as it dictates which element in the DOM receives the update
from a server transmission. It provides a powerful and declarative way to manipulate elements without writing custom
Javascript to find them.
When an event fires, Launchpad looks for the target attribute by first checking the element itself, and then walking
up the DOM tree to see if an ancestor has one.
| Target Value | Description | Additional Attributes | Example (HTML) |
|---|---|---|---|
self |
The update is applied to the element that the on-* event is on. |
(none) | <button target="self" on-click=${...}>Update Me</button> |
none |
Explicitly specifies that there is no target for the update. | (none) | <button target="none" on-click=${...}>Fire and Forget</button> |
parent |
Targets the immediate parent element. | (none) | <div> <button target="parent" on-click=${...}>Update Div</button> </div> |
grandparent |
Targets the parent of the parent element. | (none) | <body> <div> <button target="grandparent" on-click=${...}>Update Body</button> </div> </body> |
next / previous |
Targets the next or previous sibling element at the same level. | (none) | <div id="a"></div> <button target="previous" on-click=${...}></button> |
nextnext / previousprevious |
Targets the next or previous sibling element's next or previous sibling element at the same level. | (none) | <div id="a"></div> <img> <button target="previousprevious" on-click=${...}></button> |
first / last |
Targets the first or last child element inside the current element. | (none) | <div on-click=${...} target="first"> <p>Target Me</p> <p>Not Me</p> </div> |
append / prepend |
Inserts a new element as the last/first child of the current element and targets it. | wrapper (optional, defaults to div) |
<ul on-click=${...} target="append" wrapper="li">Add Item</ul> |
after / before |
Inserts a new element after/before the current element and targets it. | wrapper (optional, defaults to div) |
<div on-click=${...} target="after">Insert New Div After</div> |
nth-child |
Targets a child of the current element by its index (0-based). | index="n" |
<div on-click=${...} target="nth-child" index="1"> <p>0</p> <p>Target Me</p> </div> |
nth-sibling |
Targets a sibling of the current element by its index (0-based). | index="n" |
<p>0</p> <button on-click=${...} target="nth-sibling" index="0"></button> |
> selector |
Uses a CSS selector to find a descendant within the current element. | (none) | <div on-click=${...} target="> .item-details"> <p class="item-details"></p> </div> |
< selector |
Uses a CSS selector to find an ancestor of the current element. | (none) | <section> ... <div on-click=${...} target="< section"> <p class="item-details"></p> </div> ... </section> |
selector |
Any other string is treated as a global CSS selector for the entire document. Ideal for IDs, or advanced selectors. | (none) | <button on-click=${...} target="#main-content">Update Main</button> |
### A Special Case: target="outer"
The outer value is a special modifier. It targets the element itself (just like self), but it changes how the
update is applied for single value transmissions.
- Standard Target (
self,#id, etc.): The transmission updates theinnerHTMLof the target. target="outer": The transmission replaces the entire target element with the new content (by setting itsouterHTML).
# Transmission Formats
There are three primary formats for a transmission, each suited for different use cases, controlling unnecessary complexity when possible.
## The Map Transmission
A Map transmission is a Groovy map ([key: value]) where each key-value pair represents a specific instruction for the
client. This is the most versatile format, but might not be necessary for simple updates.
### Content & Attribute Manipulation
| Prefix / Key | Description | Example (Groovy) |
|---|---|---|
| (none) | Sets a standard HTML attribute on the target element. | ['disabled': true, 'title': 'Processing...'] |
* |
Sets a data- attribute. The in the key is replaced with data-. |
['user-id': 123, 'role': 'admin'] |
value |
Sets the .value property of the target (e.g., for <input>). |
['value': 'Initial text'] |
innerHTML |
Replaces the entire inner HTML content of the target. | ['innerHTML': '<strong>Update Complete!</strong>'] |
outerHTML |
Replaces the entire target element with the provided HTML string. | ['outerHTML': '<div class="alert">Done.</div>'] |
innerText |
Sets the rendered text content of the target. | ['innerText': 'Are you sure?'] |
append |
Inserts HTML at the very end of the target element's children. | ['append': '<li>New Item</li>'] |
prepend |
Inserts HTML at the very beginning of the target element's children. | ['prepend': '<li>First Item</li>'] |
insertAfter |
Inserts HTML immediately after the target element. | ['insertAfter': '<hr>'] |
insertBefore |
Inserts HTML immediately before the target element. | ['insertBefore': '<h2>Section Start</h2>'] |
### Styling & Classes
| Prefix | Description | Example (Groovy) |
|---|---|---|
& |
Sets an inline CSS style property on the target element. | ['&backgroundColor': 'yellow', '&fontWeight': 'bold'] |
+ |
Adds a CSS class to the target element. | ['+is-valid': 'it', '+highlight': 'this'] |
- |
Removes a CSS class from the target element. | ['-is-loading': 'it'] |
### Element & Form Actions
| Action Key | Description | Value Type(s) | Example (Groovy) |
|---|---|---|---|
@alert |
Shows a browser alert() dialog. |
String |
['@alert': 'Record saved successfully!'] |
@log, @table |
Logs data to the browser's developer console. | any |
['@log': 'Debug info here...', '@table': someDataObject] |
@click |
Programmatically triggers a click event. | null, 'this', 'it', 'source' |
['@click': 'it'] |
@focus, @blur |
Sets or removes focus from an element. | null, 'this', 'it', 'source' |
['@focus': 'source'] |
@select, @end |
Selects text or moves the cursor to the end of an input. | null, 'this', 'it', 'source' |
['@select': 'this'] |
@submit, @reset |
Submits or resets a form. | null, 'this', 'it', 'source' |
['@submit': '#main-form'] |
@show, @hide |
Shows or hides an element (by toggling display: none). |
null, 'this', 'it', 'source' |
['@hide': 'it'] |
@open, @close |
Opens/closes a <details> or <dialog>, or a window. |
String (URL), null, 'this', 'it', 'source' |
['@open': '#my-modal'] |
@remove |
Removes an element from the DOM. | null, 'this', 'it', 'source' |
['@remove': '.item-to-delete'] |
@clear |
Clears an element's value or innerHTML. |
null, 'this', 'it', 'source' |
['@clear': '#search-input'] |
@download |
Triggers a file download. | String (URL) |
['@download': '/path/to/report.pdf'] |
@nudge |
Triggers a nudge event. | null, 'this', 'it', 'source' |
['@nudge': 'it'] |
### Action Targets: this, it, and source
When you specify an action in a Map Transmission, you can control which context element the action applies to,
effectively overriding the payloadTarget determined by the target attribute. This is done by setting the action's
value to one of the following special keywords:
- Default (no value or
null): The action applies to thepayloadTarget, which is the element determined by thetargetattribute on the element that has theon-*event listener. 'this': The action applies to theevent.target, which is the specific element the user actually clicked or interacted with. This could be a child of the element that has theon-*event listener.'it': The action applies to theevent.currentTarget, which is the element that has theon-*event listener attached to it.'source': The action applies to theactiveTarget, which is the element that provided the data request payload (as determined by thesourceattribute).
### Browser & Storage Control
| Prefix / Key | Description | Value Type(s) | Example (Groovy) |
|---|---|---|---|
? |
Sets a URL query parameter without reloading the page. | String |
['?page': 2, '?sort': 'asc'] |
~ |
Sets a key-value pair in the browser's localStorage. |
String |
['~theme': 'dark'] |
~~ |
Sets a key-value pair in the browser's sessionStorage. |
String |
['~~sessionToken': 'xyz123'] |
@redirect |
Navigates the browser to a new URL. | String (URL) |
['@redirect': '/dashboard'] |
@reload |
Reloads the current page. | null |
['@reload': null] |
@back, @forward |
Navigates back or forward in the browser's history. | null |
['@back': null] |
@print |
Opens the browser's print dialog. | null |
['@print': null] |
### Selector-Based Map Entries
In addition to using a target attribute, Map Transmissions support selector-style keys for direct DOM updates. These selector entries always perform an innerHTML replacement on the matched element(s).
| Key Format | Behavior | Example |
|---|---|---|
#id |
Updates the element with a specific ID. | ['#status': 'Saved!'] |
> selector |
Finds a descendant of the source element. | ['> .details': '<p>Updated details</p>'] |
< selector |
Finds the closest ancestor of the source element. | ['< section': '<h2>Section Removed</h2>'] |
Any other selector |
Treated as a global querySelector against the document. | ['.notification': '<div>New Notice</div>'] |
These entries do not require a target attribute. The key itself determines where the content is applied. If you do specify a target on the element, both the target and the selector entries can be combined in the same transmission.
Working Together: Targets + Selectors
One of the most powerful patterns is combining targeted updates with selector-based replacements. For example, you can:
- Use a target to update the element the action is bound to (change attributes, toggle classes, disable a button, etc.).
- At the same time, use a
#idor> selectorentry to update a different region of the DOM with fresh HTML.
Example:
// Returning from a on-click bound to a button.
return [
'disabled': true, // disables the button (targeted element)
'+loading': null, // adds a 'loading' class to the button
'#order-status': 'Saving…' // updates the status display by selector
]
Here, the target ensures the button reflects its new state, while the selector-based entry updates a completely separate element. This dual mechanism allows a single server action to coordinate stateful changes (attributes, classes) and content updates (innerHTML replacement) across the DOM.
## The Array Transmission
An Array transmission is a shorthand for applying a sequence of simple, parameter-less instructions to the target element. It's perfect for managing classes and chaining basic actions.
### Class Manipulation
| Prefix | Behavior | Example (Groovy) |
|---|---|---|
| (none) | Toggles a CSS class. If it exists, it's removed; if not, it's added. | ['selected', 'active'] |
+ |
Adds a CSS class. | ['+active', '+processing'] |
- |
Removes a CSS class. | ['-active', '-processing'] |
### Chaining Actions
You can trigger a sequence of actions on the target element.
Supported Actions: @click, @focus, @blur, @select, @submit, @reset, @remove, @show, @hide,
@scroll-to, @clear, @reload, @back, @forward, @print, and @nudge.
Example:
// On form submission success:
// 1. Remove the 'processing' class from the form.
// 2. Add the 'completed' class to the form.
// 3. Clear the text inside the '#response-message' element.
// 4. Toggle the 'visible' class on it.
return ['-processing', '+completed', '@clear', 'visible']
## The Single Value Transmission
This is the simplest transmission format. When your server action returns a string it's used to directly update the content of the target element.
- Default Behavior: Launchpad intelligently places the content in the
.valueproperty (for inputs) or theinnerHTML(for other elements). target="outer"Override: If the triggering element hastarget="outer", the entire target element is replaced by the returned string.
// Groovy action to get a status message
return "Last saved: ${ new Date().format('h:mm:ss a') }"
# Examples in a Launchpad Template
Here’s how you can put these concepts together in a real Launchpad template. The server logic is defined directly
within the on-* attributes using a Groovy closure syntax: ${ _{ t -> ... } }. The t parameter holds all the
data sent from the client.
## Example 1: Simple Action
This example uses an Array Transmission to perform a single, parameter-less action. No data is needed from the client,
and the action (@print) affects the whole browser window.
<button on-click="${ _{ [ '@print' ] }}">
Print Poster
</button>
> [!NOTE] > Notice that the transmission is alone inside the server action. By default, Groovy returns the last value, so a return keyword is optional when sending the transmission back to the client.
## Example 2: Form Submission and Data Handling
This example shows a form that, upon submission, sends all its input values to the server. The Groovy closure accesses
this data via the t object, performs a database operation, and then returns a transmission to reload the page.
<%
// Assume 'gb' is a guestbook document object
@Given Guestbook gb
// Define the server-side logic in a closure
def editGuestbook = { t ->
// Access form inputs from the 't' object
gb.info.name = t.name.clean()
gb.info.open = t.getBool('open')
gb.save() // Save changes to the database
// Return a transmission to reload the page
[ '@reload' ]
}
%>
<form on-submit=${ _{ t -> editGuestbook(t) }}>
<input name='name' value="${ gb.info.name }">
<input type='checkbox' name='open' ${ gb.info.open ? 'checked' : '' }>
<button type='submit'>Update</button>
</form>
> [!NOTE]
> The t parameter is optional if you don't require any client-side context aside from the firing of the event itself. Including the parameter, however, unlocks all of the contextual data that HUD-Core will send for your server-side logic to assess.
## Example 3: Inline Action with Contextual Data
Here, assume we're iterating through a list of participants. The on-click action needs to know which participant to remove.
We pass the unique participant.cookie from the current loop iteration directly into the server-side
removeParticipant method. The transmission then targets the parent <div> and removes it from the page,
providing instant feedback.
<%
// Assume 'gb' is a guestbook document object
@Given Guestbook gb
for (participant in gb.participants) {
%>
<div class='participant-entry'>
<strong>${ participant.name }</strong>
<span target='parent' style='cursor: pointer;'
on-click=${ _{ gb.removeParticipant(participant.cookie); [ '@remove' ] }}>
🗑️
</span>
</div>
<% } %>
# UI/UX Pattern Examples
Here are some common UI/UX patterns implemented using Launchpad's transmission system. These examples demonstrate how to create dynamic, interactive components with minimal code. Each example includes both the HTML structure and the necessary Groovy server-side logic to place within a Launchpad template.
## Edit-in-Place
This pattern allows users to click an "Edit" button to turn a piece of text into an input field, and then save their
changes. It makes great use of target="outer" to swap between a "view" state and an "edit" state.
<%
// Assume 'user' is a document object with a 'name' property
def userName = user.name
// Closure to show the editing UI
def showEditUI = {
// Use a Groovy multi-line string to define the HTML for the edit state
return """
<div id="user-profile" target="outer">
<input type="text" name="newName" value="${userName.escape()}">
<button on-click=${ _{ t -> saveUserName(t.newName) }}>Save</button>
<button on-click=${ _{ showViewUI() }}>Cancel</button>
</div>
"""
}
// Closure to save the new name and show the view UI
def saveUserName = { newName ->
user.name = newName
user.save()
// After saving, return the view state UI
return showViewUI()
}
// Closure to show the viewing UI
def showViewUI = {
return """
<div id="user-profile" target="outer">
<span>${user.name.escape()}</span>
<button on-click=${ _{ showEditUI() }}>Edit</button>
</div>
"""
}
%>
<div id="user-profile" target="outer">
<span>${userName.escape()}</span>
<button on-click=${ _{ showEditUI() }}>Edit</button>
</div>
## "Load More" Button
This pattern is used for paginating through a long list of items without full page reloads. It uses the append
transmission to add new items to the list and can hide itself when there's no more data.
<%
// Server-side function to fetch a "page" of items
def getItems = { page = 0, perPage = 5 ->
// In a real app, this could be a View, or some other document query
def allItems = (1..23).collect { "Item #$it" }
// Determine the slice of items for the current page
def start = page * perPage
def end = Math.min(start + perPage, allItems.size())
// If the start index is beyond the list size, return an empty result
if (start >= allItems.size()) return [:]
// Otherwise, return the items and whether there's more to load
return [
items: allItems[start..< end], // sublist for the current page
hasMore: end < allItems.size() // are there more?
]
}
// Closure for the button's on-click event
def loadMoreItems = { t ->
// Get the next page number from the button's data attribute
def nextPage = t.page.toInteger()
def results = getItems(nextPage)
// Build the HTML for the new items
def newItemsHtml = results.items.collect { "<li>${it}</li>" }.join('')
// Build the transmission
def transmission = [
// Use 'append' on the <ul> to add the new items
append: newItemsHtml,
// Update the button's data-page attribute for the next click
'#page': nextPage + 1
]
// If there are no more items, add an instruction to hide the button
if (!results.hasMore) {
transmission['@hide'] = 'it' // 'it' refers to the button itself
}
return transmission
}
%>
<ul id="item-list">
<% getItems().items.each { item -> %>
<li>${item}</li>
<% } %>
</ul>
<button target="#item-list"
data-page="1"
on-click=${ _{ t -> loadMoreItems(t) }}>
Load More
</button>
SPACEPORT DOCS