SPACEPORT DOCS

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:

# 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?

[ '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?

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

## Keyboard Events

## Input & Form Events

## Drag & Drop Events

## Touch Events

## Lifecycle Events

# 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.

# 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:

### 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:

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.

// 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>