Launchpad: Server-Side Templating & Reactivity
Launchpad is Spaceport's integrated templating engine and reactive UI system. It enables you to build dynamic, interactive web applications by combining server-side HTML generation with real-time reactivity, powerful UI components, and seamless server-client communication—all without writing extensive JavaScript.
Launchpad represents a paradigm shift in web development: instead of managing complex client-side state and API calls, you write intuitive Groovy code that runs on the server and automatically keeps your UI in sync. The result is a development experience that feels like working with a modern JavaScript framework, but with the security, simplicity, and power of server-side logic.
# Core Philosophy
Launchpad is built on several key principles:
- Server-First: Keep critical logic, validation, and state on the server where it's secure and easy to manage
- Progressive Enhancement: Start with simple server-rendered HTML, then add interactivity as needed
- Minimal JavaScript: Eliminate boilerplate client-side code for common UI patterns
- Reactive by Default: UI updates happen automatically when data changes, without manual DOM manipulation
- Component-Oriented: Build reusable, encapsulated UI components that work seamlessly together
# Feature Overview
Launchpad provides four interconnected systems that work together to create dynamic, interactive user interfaces:
Templates provide server-side HTML generation with embedded Groovy code, giving you the full power of a programming language in your markup. Unlike traditional templating engines limited to simple variable interpolation, Launchpad templates can execute complex logic, interact with databases, and make decisions about what to render—all before the HTML reaches the browser.
Server Actions bridge the gap between user interactions and server-side logic by allowing you to bind Groovy closures directly to DOM events. When a user clicks a button, submits a form, or changes an input, your server-side code executes with full access to your application's business logic, authentication context, and database—no REST endpoints or API routes required.
Reactive Variables eliminate the tedious work of manually updating the DOM when data changes. By wrapping expressions in special syntax, Launchpad automatically tracks dependencies and sends targeted updates to the browser whenever the underlying data changes, creating fluid, responsive interfaces without writing DOM manipulation code.
Server Elements take component-based development to the next level by encapsulating HTML structure, CSS styling, JavaScript behavior, and server-side logic into reusable units. They feel like native HTML elements but are backed by powerful Groovy classes that can maintain state, interact with databases, and respond to events—all while being as simple to use as .
Together, these systems form a cohesive development experience that dramatically reduces the complexity of building modern web applications. Where traditional frameworks require you to maintain separate frontend and backend codebases with complex communication protocols between them, Launchpad unifies the stack under a single, intuitive paradigm.
# Getting Started
## Directory Structure
Create a launchpad/ directory in your Spaceport project root with two subdirectories:
/[SPACEPORT_ROOT]
launchpad/
parts/ # GHTML templates (pages and partials)
elements/ # Server Element definitions (optional)
## Basic Setup
In your source module, initialize a Launchpad instance and define routes that use your templates:
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.HttpResult
import spaceport.launchpad.Launchpad
class Router {
static Launchpad launchpad = new Launchpad()
@Alert('on / hit')
static void _index(HttpResult r) {
launchpad.assemble(['header.ghtml', 'index.ghtml', 'footer.ghtml'])
.launch(r, 'wrapper.ghtml')
}
}
What's happening here:
- We create a
Launchpadinstance using the defaultlaunchpad/directory location assemble()takes a list of template filenames (relative toparts/) and combines them in orderlaunch()wraps the assembled content in a base template (wrapper.ghtml), processes it, and sends it to the client
The wrapper (or vessel) is optional but useful for consistent layout, shared assets, and common scripts/styles
across pages. Use the tag in your wrapper to indicate where the assembled content should be inserted.
For more advanced routing patterns including regex-based dynamic routes and directory organization, see Dynamic Template Selection in the Templates in Depth section.
## Including HUD-Core
For Server Actions, Reactive Variables, and real-time updates to work, include HUD-Core in your HTML:
<script defer src='https://cdn.jsdelivr.net/gh/spaceport-dev/hud-core.js@latest/hud-core.min.js'></script>
HUD-Core is Launchpad's lightweight (~23KB minified) client-side library that handles websockets, event bindings, DOM updates, and reactivity. It's not required for basic server-rendered pages, but essential for interactivity.
# Core Features
## 1. Templates
Templates are Groovy-powered HTML files (.ghtml) that live in your launchpad/parts/ directory. They use Groovy's
native templating syntax to generate dynamic HTML on the server.
Basic Example:
<!-- launchpad/parts/welcome.ghtml -->
<html>
<head>
<title>Welcome</title>
</head>
<body>
<h1>Hello, User ${ client.userID }!</h1>
<p>Current time: ${ new Date() }</p>
</body>
</html>
Built-in Variables:
Every template has access to these variables:
data: Request parameters from GET query strings or POST/PUT bodies, plus any additional data set by middleware or route handlers (e.g.,r.context.data.post = document)client: TheClientobject (.authenticatedif the user is logged in)dock: A session-scopedCargoobject for storing user-specific datacookies: Request cookiescontext: Additional request context (headers, method, target)r: The currentHttpResultobject
See Template Structure for complete details.
## 2. Server Actions
Server Actions allow you to bind server-side Groovy closures directly to client-side events like click, submit, change, and more. When the event fires, your closure executes on the server with full access to your application logic, database, and session data.
Basic Example:
<button on-click=${ _{ println "Button clicked!" } }>
Click Me
</button>
When the button is clicked, "Button clicked!" appears in your server logs—no JavaScript required.
With Incoming Transmission:
Server Actions use Spaceport's transmission system—both for receiving client state (the incoming transmission parameter, commonly t) and for sending back instructions to update the UI (the outgoing transmission).
<input type="text" on-input=${ _{ t ->
println "User typed: ${t.value}"
}} />
The incoming transmission (t) provides access to the source element's properties, form data, event information (key presses, mouse position), and other browser state. This rich client context lets you make informed server-side decisions without additional requests.
Updating the UI with Outgoing Transmissions:
<button target="self" on-click=${ _{ "Button was clicked!" }}>
Click Me
</button>
By returning a value (the outgoing transmission) and using target="self", the button's text updates to "Button was clicked!" when clicked. Outgoing transmissions can be simple strings, maps with multiple DOM updates, or complex instruction sets—all sent to HUD-Core for efficient client-side application.
See Server Actions in Depth for comprehensive coverage of both incoming and outgoing transmissions.
## 3. Reactive Variables
Reactive Variables automatically update the UI when server-side data changes. Launchpad provides two approaches for reactive variables: Reactive Literals and Document Data.
Reactive Literals use ${{ }} syntax, where Launchpad tracks dependencies, detects changes, and updates only the affected DOM regions:
<p>Counter: ${{ dock.counter.getDefaulted(0) }}</p>
<button on-click=${ _{ dock.counter.inc() }}>Increment</button>
When the button is clicked, the counter increments and the tag updates automatically. No manual DOM updates, no state synchronization code—it just works.
Document Data uses the .cdata() method combined with the bind attribute for client-side data binding. This approach is particularly useful for hydrating forms, passing configuration objects, or synchronizing multiple elements with a single data source:
<%
out << ['username': 'alice', 'status': 'active'].cdata()
%>
<input bind="username" />
<div bind="status"></div>
Both reactive approaches can work together—you can even combine them by using .cdata() inside a ${{ }} block for powerful data synchronization patterns.
See Reactive Variables in Depth for comprehensive coverage of Reactive Literals, and explore the Advanced Topics section for Document Data patterns.
## 4. Server Elements
Server Elements are reusable, full-stack components defined as Groovy classes. They encapsulate HTML structure, CSS styling, JavaScript behavior, and server-side logic into single, composable units that feel like native HTML elements.
Basic Usage:
<g:star-rating value="4" on-change=${ _{ t ->
dock.ratings.put('product-123', t.value)
}}></g:star-rating>
But Server Elements are far more powerful than simple custom tags. They can:
Maintain Internal State: Each element instance can track its own state separately from the global dock, making them truly encapsulated and reusable across the page without conflicts.
Process Server-Side Logic: Before rendering, Server Elements can query databases, call services, perform calculations, or make decisionsâ€"the HTML they generate is the result of full server-side computation.
Self-Contain Styling and Scripts: Include scoped CSS and JavaScript directly in the element definition, ensuring styles don't leak and behavior is bundled with structure.
Expose Client and Server Functions: Server Elements can define both client-side JavaScript functions and server-side Groovy functions that are callable from your front-end code. This bridges the client-server divide elegantly—you can invoke server logic directly from JavaScript without writing API endpoints, while also providing rich client-side interactivity. Both function types are available in your normal JavaScript, making Server Elements feel like native, full-featured components with seamless server communication.
Server Elements deserve their own deep dive. See the Server Elements documentation for complete details on creating custom elements, handling lifecycle events, and advanced composition patterns.
# Templates in Depth
Launchpad templates are HTML-first—you write standard HTML, CSS, and JavaScript, then enhance it with Groovy where needed. This section covers the built-in variables available in every template, how to compose templates together, working with vanilla web technologies, and the Groovy syntax you'll use to create dynamic content.
## Escaping Dollar Signs
Since Groovy uses $ for expressions, every literal dollar sign must be escaped with a backslash (\$) in your templates—regardless of whether it appears in HTML, CSS, or JavaScript embedded in the template.
This only applies to code embedded directly in .ghtml files. External JavaScript and CSS files linked via or don't require any escaping.
<!-- HTML content with prices -->
<span class="price">\$19.99</span>
<p>Total: \$${total}</p>
<script>
// JavaScript string concatenation
const message = 'Total: \$' + total;
// JavaScript template literals require escaping EVERY dollar sign
const formatted = Total: \$\$\${total}; // Escapes literal $ and the ${} placeholder
</script>
<style>
/ CSS also requires escaping dollar signs /
.price::before {
content: '\$'; / Escape the dollar sign /
}
</style>
<!-- External files work normally - no escaping needed -->
<link rel="stylesheet" href="/assets/css/styles.css" />
<script src="/assets/js/app.js"></script>
Note: JavaScript template literals become very ugly when you need to escape dollar signs (e.g.,\$\$\${variable}for what would normally be$${variable}). For displaying currency or other dollar-prefixed values, string concatenation ('Total: \$' + value) produces much cleaner, more readable code.
## Working with HTML, CSS, and JavaScript
Launchpad templates are standard HTML files with .ghtml extension. Write your markup, styles, and scripts as you normally would—aside from escaping dollar signs, everything works exactly as in standard web development.
## Built-in Variables
Every template has access to these variables:
r (HttpResult)
The HttpResult object for the current request, providing access to modify the response:
r.setStatus(code) // Set HTTP status code
r.setRedirectUrl(url) // Redirect to another URL
r.addResponseCookie(name, value) // Add a cookie to the response
r.setContentType(type) // Set Content-Type header
The r object is your interface to both the incoming request and outgoing response, with methods for setting headers, managing cookies, controlling redirects, and more.
For complete details on the HttpResult API and all available methods, see the Routing documentation.
client (Client)
The Client object representing the current user (always present, even for guests):
client.authenticated // Boolean: is user logged in?
client.document // The user's Document (if authenticated)
client.userID // User's ID (if authenticated)
Every request has a client object, whether the user is logged in or browsing as a guest. Check client.authenticated to determine if the user has logged in.
dock (Cargo)
A session-scoped Cargo object for storing user data persistently across requests:
dock.cartItems // Access nested Cargo: dock.cartItems.get()
dock.gamerTag.set('aufdemrand') // Set values
dock.preferences.put('theme', 'dark') // Use as map
The dock is tied to the spaceport-uuid cookie and persists across requests for the duration of the session. For authenticated users, the dock is stored in the database and survives server restarts. For anonymous users, it's stored in memory only.
The dock is perfect for shopping carts, user preferences, form data, and any other session-specific state.
For a complete understanding of how Spaceport manages sessions, authentication, and the dock's persistence model, see the Sessions & Client Management documentation.
data (Map)
Request parameters from GET query strings or POST/PUT bodies, plus any additional data set by middleware or route handlers:
data.username // Access form fields or query params
data.page // Pagination parameter
data.post // Custom data from route handler (e.g., r.context.data.post = document)
cookies (Map)
Access to cookies sent with the request:
cookies.'theme-preference' // Access cookie values
cookies.'language' // Another example
Cookies are powerful state mechanisms that work exactly as they do in standard web development—Spaceport doesn't change how cookies function. They're perfect for user preferences, tracking, and client-side state that persists across sessions.
For a comprehensive guide to cookies, see MDN's Using HTTP Cookies documentation.
context (HttpContext)
The HTTP context providing access to request details and low-level servlet objects:
context.method // HTTP method (GET, POST, etc.)
context.target // Request path
context.headers // Request headers map
context.request // Raw HttpServletRequest (for advanced usage)
context.response // Raw HttpServletResponse (for advanced usage)
Spaceport builds on standard Java servlet APIs without disabling them. The raw HttpServletRequest and HttpServletResponse objects are available for advanced or low-level operations when needed.
## Template Composition
Templates can be composed using the assemble() method. At its simplest, you can render a single template without a wrapper:
// Single template, no wrapper
launchpad.assemble(['index.ghtml']).launch(r)
For more complete pages, combine multiple templates with a wrapper that provides common layout:
// Multiple parts with wrapper
launchpad.assemble(['nav.ghtml', 'hero.ghtml', 'footer.ghtml'])
.launch(r, 'base.ghtml')
Wrappers are optional but useful for shared layout, assets, and scripts. When using a wrapper, include a tag where assembled content should be inserted:
<!-- base.ghtml (wrapper) -->
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
<script defer src='https://cdn.jsdelivr.net/gh/spaceport-dev/hud-core.js@latest/hud-core.min.js'></script>
</head>
<body>
<payload/> <!-- Assembled templates go here -->
</body>
</html>
You can also build the parts list dynamically:
def parts = ['header.ghtml']
if (client.authenticated && client.document.hasPermission('admin')) {
parts << 'admin-panel.ghtml'
}
parts << 'content.ghtml'
parts << 'footer.ghtml'
launchpad.assemble(parts).launch(r, 'wrapper.ghtml')
## Part Stacking and Variable Scope
When you assemble multiple templates, they stack in a specific order with the wrapper at the bottom. Each part can access and modify variables defined in parts below it in the stack.
Stack Order Example:
launchpad.assemble(['header.ghtml', 'content.ghtml', 'footer.ghtml'])
.launch(r, 'wrapper.ghtml')
Stack order (bottom to top): 1. wrapper.ghtml (bottom-most) 2. header.ghtml 3. content.ghtml 4. footer.ghtml (top-most)
Variable Sharing:
wrapper.ghtml:
<%
def theme = 'dark'
def company = 'Spaceport Inc.'
%>
<!DOCTYPE html>
<html data-theme="${theme}">
<head><title>${company}</title></head>
<body>
<payload/>
</body>
</html>
content.ghtml:
<%
// Can access and modify 'theme' from wrapper
theme = 'light'
%>
<div class="content">
<h1>Welcome to ${company}</h1>
<p>Current theme: ${theme}</p>
</div>
Using @Provided for Clarity:
While accessing variables from lower parts works automatically, use the @Provided annotation to document variable origins:
<%
@Provided def theme // Documents that theme comes from a lower part
theme = 'light' // Now we modify it
@Provided String company // Can optionally include type for IDE support
%>
<div class="content">
<h1>Welcome to ${company}</h1>
<p>Current theme: ${theme}</p>
</div>
The @Provided annotation:
- Must be inline (on the same line or directly before the variable)
- Documents variable origins for other developers
- Enables better IDE autocompletion and type hints
- Is optional but recommended for clarity
- Can include an optional type declaration
## Groovy Expressions
Use standard Groovy template syntax for dynamic server-rendered content and logic:
<!-- Expressions with ${ } -->
<h1>Welcome, ${ client.document?.name ?: 'Guest' }!</h1>
<!-- Scriptlets with <% %> -->
<%
def items = dock.cartItems.get() as List
def total = items.sum { it.price * it.quantity }
%>
<!-- Control structures -->
<% if (items) { %>
<p>Cart total: $${ total }</p>
<% } else { %>
<p>Your cart is empty</p>
<% } %>
<!-- Loops -->
<ul>
<% items.each { item -> %>
<li>${ item.name } - $${ item.price }</li>
<% } %>
</ul>
## Safe Output Helpers
Spaceport's Class Enhancements provide helpful methods for safe HTML generation:
<!-- .if{} - Conditional inclusion -->
<div ${ "class='admin'".if { client.authenticated && client.document?.hasPermission('admin') } }></div>
<!-- .clean() - XSS protection -->
<p>${ userInput.clean() }</p>
<!-- .unless{} - Inverse conditional -->
<button ${ "disabled".unless { formValid } }>Submit</button>
## Imports
Templates follow standard Java/Groovy import rules. You have two options for importing classes:
Standard Import Statements:
Place imports at the top of your template inside a <% %> scriptlet, just like in regular Groovy:
<%
import com.myapp.services.UserService
import com.myapp.models.Product
import spaceport.computer.cargo.Cargo
%>
<!DOCTYPE html>
<html>
<body>
<%
def products = Product.getAllProducts()
def userInfo = UserService.getCurrentUser()
%>
<!-- template content -->
</body>
</html>
Shorthand Page Directive Syntax:
Use the <%@ page import="..." %> directive to import multiple classes in a single line with semicolon separation:
<%@ page import="com.myapp.services.UserService; com.myapp.models.Product; spaceport.computer.cargo.Cargo" %>
<!DOCTYPE html>
<html>
<body>
<%
def products = Product.getAllProducts()
def userInfo = UserService.getCurrentUser()
%>
<!-- template content -->
</body>
</html>
Both approaches work equally well—choose based on your preference and code style. Standard imports are more familiar to Groovy developers, while the page directive syntax is more compact for multiple imports.
# Server Actions in Depth
Server Actions bind server-side Groovy closures to DOM events, executing your code on the server when users interact with your UI. They bridge the gap between client interactions and server logic without requiring REST endpoints or API routes.
## Basic Syntax
The simplest Server Action is an inline closure that executes when an event fires:
<button on-click=${ _{ println "Button clicked at ${new Date()}" }}>
Click Me
</button>
Server Actions work with standard DOM events:
<!-- Click events -->
<button on-click=${ _{ println "Clicked!" }}>Button</button>
<!-- Input events -->
<input type="text" on-input=${ _{ println "User is typing..." }} />
<!-- Form submission -->
<form on-submit=${ _{ println "Form submitted!" }}>
<button type="submit">Submit</button>
</form>
<!-- Focus events -->
<input on-focus=${ _{ println "Field focused" }}
on-blur=${ _{ println "Field blurred" }} />
Note on Standard Client-Side Events:
Standard inline event handlers (without the on- prefix) work normally and are guaranteed to run before Server Actions due to latency. Server Actions execute asynchronously—their return values depend on the round trip to the server:
<!-- Standard onclick runs immediately, Server Action runs after round trip -->
<button onclick="console.log('Client-side: instant')"
on-click=${ _{ "Server-side: after round trip" }}>
Click Me
</button>
This makes standard events useful for immediate UI feedback (like disabling a button) while the Server Action handles the server-side logic.
## Decoupled Server Actions
For complex logic or reusability, define your closure in a scriptlet and reference it:
<%
def handleLogin = _{
println "Processing login..."
// Complex login logic here
def success = authenticateUser()
if (success) {
println "Login successful!"
} else {
println "Login failed!"
}
}
%>
<button on-click=${ handleLogin() }>Login</button>
<a href="#" on-click=${ handleLogin() }>Or click here to login</a>
This approach keeps your HTML clean and allows you to reuse the same logic across multiple elements.
## Scope and Timing
Server Action closures have access to all variables in the template's scope and can interact with source modules, but they execute later—when the user triggers the event, not during template rendering.
Accessing Template Variables:
<%
def username = data.username ?: 'Guest'
def loginAttempts = dock.loginAttempts.getDefaulted(0)
def handleClick = _{
// Has access to username and loginAttempts
println "User ${username} clicked (${loginAttempts} previous attempts)"
dock.loginAttempts.inc()
}
%>
<h1>Welcome, ${username}!</h1>
<button on-click=${ handleClick() }>Click Me</button>
Interacting with Source Modules:
<%@ page import="com.myapp.services.UserService; com.myapp.utils.ValidationUtils" %>
<%
def processRegistration = _{
// Call static methods from source modules
def isValid = ValidationUtils.validateEmail(data.email)
if (isValid) {
UserService.createUser(data.email, data.password)
dock.message.set("Registration successful!")
} else {
dock.message.set("Invalid email address")
}
}
%>
<form on-submit=${ processRegistration() }>
<!-- form fields -->
</form>
Tip: The<%@ page import="..." %>syntax is a JSP/GSP feature supported by many IDEs for better autocompletion and type hints. Standard Groovy import statements (import com.myapp.Class) work too—use whichever you prefer.
Understanding Timing:
- Template rendering: The closure is defined but not executed
- User interaction: The closure executes on the server with fresh access to all variables
- Multiple calls: Each time the user clicks, the closure runs again with current state
This means your Server Actions can access the latest data from the dock, documents, or any other source each time they execute.
## Sending Data Back with Transmissions
Server Actions can return data to update the UI without a full page reload. Use the target attribute to specify what element should receive the update:
<!-- Update the button itself -->
<button target="self" on-click=${ _{
def count = dock.clickCount.getDefaulted(0)
dock.clickCount.set(count + 1)
"Clicked ${count + 1} times"
}}>
Click Me
</button>
<!-- Update a different element by ID -->
<div id="status">Ready</div>
<button target="#status" on-click=${ _{
"Processing at ${new Date()}"
}}>
Start Process
</button>
The returned string becomes the innerText of the target element. This is the foundation of Launchpad's transmission system for updating the DOM.
## Accessing Event and Element Data
Server Actions can receive information about the triggering event and element by adding a parameter (commonly t for transmission):
<%
def scrambleText = { t ->
// t.value contains the input's current value
def chars = t.value.toList()
def scrambled = chars.shuffled().join('')
scrambled ?: "Type something first!"
}
%>
<input target="#scrambled" type="text" placeholder="Type something..."
on-input=${ _{ t -> scrambleText(t) }} />
<div id="scrambled"></div>
The incoming transmission (t) provides access to the source element, form elements, event properties (like key presses and mouse position), and more—giving you rich client context without additional requests.
## Understanding Transmissions
Transmissions are Launchpad's way of updating the UI from server-side code. When a Server Action returns a value, that value becomes a transmission—instructions sent to HUD-Core for updating the DOM.
Transmissions can be: - Simple strings: Update text content - Maps: Perform multiple DOM updates - Arrays: Execute sequential operations
The goal of transmissions is to reduce common JavaScript patterns for DOM manipulation, form handling, and UI updates—not to eliminate JavaScript entirely. For complex client-side interactions, animations, or third-party library integrations, you'll still use JavaScript as needed. But for common patterns like showing/hiding elements, updating text, adding CSS classes, or basic form validation, transmissions handle it server-side with less code.
Transmissions support a wide range of operations: setting attributes, toggling classes, manipulating HTML, redirecting, showing alerts, and more. For the complete transmission syntax and all available operations, see the Transmissions documentation.
## Advanced Example: SPA-Style Form
Here's a practical example showing Server Actions handling form submission with validation and feedback:
<%
import com.myapp.services.ContactService
import com.myapp.utils.ValidationUtils
def submitContact = _{ form ->
def errors = []
// Extract form data
def name = form.elements['name']?.value
def email = form.elements['email']?.value
def message = form.elements['message']?.value
// Validate on server
if (!name || name.length() < 2) {
errors << "Name must be at least 2 characters"
}
if (!ValidationUtils.isValidEmail(email)) {
errors << "Please enter a valid email address"
}
if (!message || message.length() < 10) {
errors << "Message must be at least 10 characters"
}
// Return errors or success
if (errors) {
return [
'> .error-list': errors.collect { "<li>${it}</li>" }.join(''),
'> .errors': ['@show': null], // Show error container
'+has-errors': 'self'
]
} else {
// Save to database
ContactService.saveMessage(name, email, message)
// Clear form and show success
return [
'self': ['@reset': null], // Reset the form
'> .success': "Thank you ${name}! We'll be in touch soon.",
'> .errors': ['@hide': null], // Hide errors
'-has-errors': 'self'
]
}
}
%>
<form on-submit=${ submitContact }>
<div class="form-group">
<label>Name</label>
<input type="text" name="name" required />
</div>
<div class="form-group">
<label>Email</label>
<input type="email" name="email" required />
</div>
<div class="form-group">
<label>Message</label>
<textarea name="message" required></textarea>
</div>
<div class="errors" style="display: none;">
<ul class="error-list"></ul>
</div>
<div class="success"></div>
<button type="submit">Send Message</button>
</form>
This example demonstrates: - Server-side validation: All validation logic stays on the server where it's secure - Multiple DOM updates: One Server Action updates errors, success messages, and form state - No page reload: The form submission happens seamlessly without navigation - Progressive enhancement: The form would still work with JavaScript disabled (traditional POST)
Server Actions transform traditional form handling into a modern, SPA-like experience while keeping your business logic secure and centralized on the server.
# Reactive Variables in Depth
Reactive Variables automatically track dependencies and update the UI when data changes. They're defined with double-brace syntax: ${{ }}.
## Basic Syntax
<p>Counter: ${{ dock.counter.getDefaulted(0) }}</p>
<button on-click=${ _{ dock.counter.inc() }}>+1</button>
When the button is clicked:
1. The counter increments on the server
2. Launchpad detects the change to dock.counter
3. The reactive expression re-evaluates
4. Only the changed content updates in the DOM
## How Reactivity Works
When you define a Reactive Variable, Launchpad:
1. Wraps the expression in special HTML comments to mark its location
2. Tracks dependencies by monitoring which Cargo objects are accessed
3. Registers listeners on those Cargo objects
4. Re-evaluates the expression when any dependency changes
5. Updates the DOM with only the changed content
This happens automatically—no manual subscription management, no event handlers, no state synchronization code.
## Reactive Expressions
Reactive Variables can contain any Groovy expression:
<!-- Simple value -->
<p>Name: ${{ dock.user.name.get() }}</p>
<!-- Computed value -->
<p>Total: $${{ dock.items.get().sum { it.price * it.qty } }}</p>
<!-- Conditional -->
<p>${{ dock.cart.isEmpty() ? 'Empty cart' : "${dock.cart.size()} items" }}</p>
<!-- Complex expression -->
<div class="${{
dock.status.get() == 'error' ? 'alert-danger' :
dock.status.get() == 'success' ? 'alert-success' :
'alert-info'
}}">
Status: ${{ dock.status.get() }}
</div>
## Multiple Dependencies
A single Reactive Variable can depend on multiple Cargo objects:
<p>
${{
def quantity = dock.quantity.getDefaulted(1)
def price = dock.price.getDefaulted(0)
def tax = dock.taxRate.getDefaulted(0.1)
def subtotal = quantity * price
def total = subtotal * (1 + tax)
"Quantity: ${quantity} × \$${price} = \$${subtotal.round(2)} (+ \$${(subtotal * tax).round(2)} tax) = \$${total.round(2)}"
}}
</p>
When any of quantity, price, or taxRate changes, the entire expression re-evaluates and updates.
## Reactive Loops
You can use reactive expressions inside loops:
<ul>
<% dock.todoItems.get().each { item -> %>
<li>
${{ item.title }}
<span class="status">${{ item.completed ? '✓' : '○' }}</span>
</li>
<% } %>
</ul>
However, this approach has a limitation: changes to the list structure (adding/removing items) require a full re-render of the loop. For dynamic lists, consider returning HTML from a Server Action:
<div id="todo-list">
<!-- Initial render -->
<% dock.todoItems.get().each { item -> %>
<div>${item.title}</div>
<% } %>
</div>
<button target="#todo-list" on-click=${ _{
def items = dock.todoItems.get()
items << [title: "New item", completed: false]
dock.todoItems.set(items)
// Return updated HTML
items.collect { "<div>${it.title}</div>" }.join('')
}}>Add Item</button>
## Nested Cargo Reactivity
Reactive Variables work seamlessly with nested Cargo structures:
<div>
<h3>${{ dock.user.profile.name.get() }}</h3>
<p>${{ dock.user.profile.bio.get() }}</p>
<small>Member since ${{ dock.user.createdAt.get() }}</small>
</div>
<!-- Any change to the nested structure triggers updates -->
<button on-click=${ _{
dock.user.profile.bio.set("Updated bio text")
}}>Update Bio</button>
## Reactivity with Documents
Reactive Variables work with Cargo objects created from Document instances:
<%
def userDoc = Document.get(dock.userId.get(), 'users')
def user = Cargo.fromDocument(userDoc)
%>
<div class="profile">
<h2>${{ user.name.get() }}</h2>
<p>${{ user.email.get() }}</p>
</div>
<form on-submit=${ _{ form ->
user.name.set(form.elements['name'].value)
user.email.set(form.elements['email'].value)
userDoc.save() // Persist to database
"Profile updated!"
}}>
<input name="name" value="${{ user.name.get() }}" />
<input name="email" value="${{ user.email.get() }}" />
<button type="submit">Save</button>
</form>
This creates a powerful pattern: your UI stays in sync with your database automatically.
## Performance Considerations
Reactive Variables are efficient, but keep these points in mind:
1. Granular updates: Only the specific DOM nodes containing changed reactive expressions are updated 2. Batching: Multiple changes are batched into a single update cycle 3. Websockets: Reactivity requires a websocket connection (HUD-Core handles this automatically) 4. Computation: Complex expressions re-evaluate on every dependency change—keep them reasonably simple
For heavy computations, consider computing values in a Server Action and storing the result:
<!-- Instead of this (re-computes on every change): -->
<p>${{ heavyComputation(dock.data.get()) }}</p>
<!-- Do this (computes once, stores result): -->
<p>${{ dock.computed.get() }}</p>
<button on-click=${ _{
dock.computed.set(heavyComputation(dock.data.get()))
}}>Recalculate</button>
# Combining Features
The real power of Launchpad comes from combining Server Actions, Reactive Variables, and Transmissions. Here's a comprehensive shopping cart example:
<%
def cart = dock.shoppingCart
def cartItems = cart.items.getDefaulted([])
%>
<div class="cart">
<h2>Shopping Cart (${{ cart.items.size() }} items)</h2>
<% cartItems.eachWithIndex { item, index -> %>
<div class="cart-item">
<span>${{ item.name }}</span>
<span>$${{ item.price }}</span>
<input
type="number"
value="${{ item.quantity }}"
on-change=${ _{ input ->
def items = cart.items.get()
items[index].quantity = input.value.toInteger()
cart.items.set(items)
cart.total.set(items.sum { it.price * it.quantity })
}}
/>
<button on-click=${ _{
def items = cart.items.get()
items.removeAt(index)
cart.items.set(items)
cart.total.set(items.sum { it.price * it.quantity })
['@remove': 'parent']
}}>Remove</button>
</div>
<% } %>
<div class="cart-total">
Total: $${{ cart.total.getDefaulted(0) }}
</div>
<button
target="self"
on-click=${ _{
if (cartItems.isEmpty()) {
return ['innerText': 'Cart is empty!', '+error': true]
}
def orderId = OrderService.createOrder(dock.userId.get(), cartItems)
cart.items.set([])
cart.total.set(0)
['@redirect': "/orders/${orderId}"]
}}
>
Checkout
</button>
</div>
This example demonstrates: - Reactive display of cart count and total - Server Actions for quantity changes and item removal - Transmissions for partial updates and navigation - Cargo for persistent cart state
# Integration with Spaceport Features
## With Documents
Launchpad works seamlessly with Spaceport's Document system:
// Source module
@Alert('on /article/:id GET')
static void _viewArticle(HttpResult r) {
def articleId = r.context.params.id
def article = ArticleDocument.get(articleId, 'articles')
// Pass article via context
r.context.article = article
new Launchpad()
.assemble(['article-view.ghtml'])
.launch(r, 'wrapper.ghtml')
}
<!-- article-view.ghtml -->
<%
def article = context.article
def articleCargo = Cargo.fromDocument(article)
%>
<article>
<h1>${{ articleCargo.title.get() }}</h1>
<div class="content">${{ articleCargo.body.get() }}</div>
<div class="meta">
Views: ${{ articleCargo.views.get() }}
</div>
</article>
<button on-click=${ _{
articleCargo.views.inc()
article.save()
"View recorded!"
}}>Like This Article</button>
## With Alerts
Use Alerts to respond to Launchpad events:
import spaceport.computer.alerts.Alert
class ArticleHandler {
@Alert('on article.viewed')
static void _recordView(def articleId, def userId) {
def analytics = Document.get("analytics-${articleId}", 'analytics')
analytics.cargo.totalViews.inc()
analytics.cargo.viewers.addToSet(userId)
analytics.save()
}
}
<!-- Trigger alert from template -->
<button on-click=${ _{
Alert.call('on article.viewed', [context.params.id, dock.userId.get()])
"Thanks for reading!"
}}>I Read This</button>
## With Static Assets
Reference static assets in your templates:
<link rel="stylesheet" href="/assets/css/style.css" />
<script src="/assets/js/utilities.js"></script>
<img src="/assets/images/logo.png" alt="Logo" />
Configure static asset paths in your config.spaceport:
static assets:
paths:
/assets/* : assets/
See the Static Assets documentation for more details.
## With Authentication
Launchpad integrates seamlessly with Spaceport's authentication system through the client object.
Basic Authentication Check:
<% if (client?.authenticated) { %>
<h1>Welcome back, ${{ client.document.name }}!</h1>
<a href="/logout">Logout</a>
<% } else { %>
<h1>Welcome, Guest!</h1>
<a href="/login">Login</a>
<% } %>
Permission-Based Content:
<%
def isAdmin = client?.authenticated && client.document.hasPermission('admin')
%>
<nav>
<a href="/">Home</a>
<a href="/profile">Profile</a>
<% if (isAdmin) { %>
<a href="/admin">Admin Panel</a>
<% } %>
</nav>
Protected Server Actions:
<button on-click=${ _{
if (!client?.authenticated) {
return ['@redirect': '/login']
}
if (!client.document.hasPermission('delete-posts')) {
return ['innerText': 'Unauthorized', '+error': true]
}
PostService.deletePost(postId)
['@redirect': '/posts']
}}>Delete Post</button>
Login/Logout Actions:
<!-- Login Form -->
<form on-submit=${ _{ form ->
def username = form.elements['username'].value
def password = form.elements['password'].value
def authenticatedClient = Client.getAuthenticatedClient(username, password)
if (authenticatedClient) {
authenticatedClient.attachCookie(cookies.'spaceport-uuid')
['@redirect': '/dashboard']
} else {
['> .error': 'Invalid credentials']
}
}}>
<input type="text" name="username" required />
<input type="password" name="password" required />
<div class="error"></div>
<button type="submit">Login</button>
</form>
<!-- Logout Button -->
<button on-click=${ _{
if (client) {
client.removeCookie(cookies.'spaceport-uuid')
}
['@redirect': '/']
}}>Logout</button>
For complete details on authentication, see the Sessions & Client Management documentation.
# Best Practices
## Security
Always sanitize user input:
<!-- Bad: XSS vulnerable -->
<p>${ userInput }</p>
<!-- Good: Sanitized -->
<p>${ userInput.clean() }</p>
Keep sensitive logic on the server:
<!-- Bad: Exposes admin check to client -->
<% def isAdmin = dock.role.get() == 'admin' %>
<button
${ "disabled".unless { isAdmin } }
on-click=${ _{ performAdminAction() }}
>Admin Action</button>
<!-- Good: Check authorization in Server Action -->
<button on-click=${ _{
if (dock.role.get() != 'admin') {
return ['innerText': 'Unauthorized', '+error': true]
}
performAdminAction()
"Action completed!"
}}>Admin Action</button>
## Performance
Minimize reactive expression complexity:
<!-- Bad: Heavy computation in reactive expression -->
<p>${{ computeExpensiveValue(dock.data.get()) }}</p>
<!-- Good: Pre-compute and store -->
<% dock.cachedValue.set(computeExpensiveValue(dock.initialData.get())) %>
<p>${{ dock.cachedValue.get() }}</p>
Use specific target selectors:
<!-- Bad: Global selector -->
<button target="body .notification" on-click=${ _{ "Updated!" }}>Click</button>
<!-- Good: Specific ID -->
<button target="#notification" on-click=${ _{ "Updated!" }}>Click</button>
<div id="notification"></div>
## Code Organization
Extract complex closures to source modules:
// In source module
class FormHandlers {
static def processLogin(def dock, def email, def password) {
def user = UserService.authenticate(email, password)
if (user) {
dock.userId.set(user.id)
return ['@redirect': '/dashboard']
} else {
return ['> .error': 'Invalid credentials']
}
}
}
<!-- In template -->
<form on-submit=${ _{ form ->
FormHandlers.processLogin(
dock,
form.elements['email'].value,
form.elements['password'].value
)
}}>
<!-- form fields -->
</form>
Use template composition:
// Break large templates into smaller, focused parts
launchpad.assemble([
'components/header.ghtml',
'components/nav.ghtml',
'pages/dashboard.ghtml',
'components/footer.ghtml'
]).launch(r, 'layouts/base.ghtml')
## Debugging
Enable debug mode in Spaceport:
In debug mode, Launchpad will: - Log template compilation - Show reactive variable evaluations - Track Server Action invocations - Reload templates on file changes
Use the browser console:
HUD-Core logs websocket activity and transmission applications:
// In browser console
HUD.debug = true // Enable verbose logging
# Common Patterns
## Form Validation
<form on-submit=${ _{ form ->
def errors = []
def email = form.elements['email']?.value
def password = form.elements['password']?.value
if (!email?.contains('@')) errors << 'Invalid email'
if (password?.length() < 8) errors << 'Password too short'
if (errors) {
return ['> .errors': errors.collect { "<li>${it}</li>" }.join('')]
}
// Process valid form
UserService.register(email, password)
['@redirect': '/welcome']
}}>
<input type="email" name="email" required />
<input type="password" name="password" required />
<ul class="errors"></ul>
<button type="submit">Register</button>
</form>
## Pagination
<%
def page = dock.currentPage.getDefaulted(1)
def pageSize = 10
def items = ItemService.getItems(page, pageSize)
def totalPages = ItemService.getTotalPages(pageSize)
%>
<div class="items">
<% items.each { item -> %>
<div class="item">${ item.name }</div>
<% } %>
</div>
<div class="pagination">
<button
${ "disabled".if { page <= 1 } }
on-click=${ _{
dock.currentPage.set(page - 1)
['@reload': null]
}}
>Previous</button>
<span>Page ${{ dock.currentPage.get() }} of ${totalPages}</span>
<button
${ "disabled".if { page >= totalPages } }
on-click=${ _{
dock.currentPage.set(page + 1)
['@reload': null]
}}
>Next</button>
</div>
## Real-Time Notifications
<div id="notifications">
${{
def notifications = dock.notifications.getDefaulted([])
if (notifications.isEmpty()) {
'No new notifications'
} else {
notifications.collect { n ->
"<div class='notification ${n.type}'>${n.message}</div>"
}.join('')
}
}}
</div>
<!-- Trigger notifications from anywhere -->
<button on-click=${ _{
def notifications = dock.notifications.getDefaulted([])
notifications << [
type: 'success',
message: 'Action completed!',
timestamp: System.currentTimeMillis()
]
dock.notifications.set(notifications)
"Done!"
}}>Trigger Action</button>
## Optimistic UI Updates
<div class="post">
<p>${{ post.content.get() }}</p>
<div class="likes">
<span>${{ post.likes.get() }}</span>
<button on-click=${ _{
// Optimistically update UI
def currentLikes = post.likes.get() as Integer
post.likes.set(currentLikes + 1)
// Send to server
def result = PostService.likePost(post._id, dock.userId.get())
// Rollback if failed
if (!result.success) {
post.likes.set(currentLikes)
return ['+error': true, 'innerText': 'Failed']
}
['+liked': true, 'innerText': 'Liked!']
}}>Like</button>
</div>
</div>
# Advanced Topics
## Custom Websocket Handling
While Launchpad manages websockets automatically, you can access the connection directly:
@Alert('on websocket.connected')
static void _socketConnected(SocketContext socket) {
Command.debug("Client connected: ${socket.id}")
// Send custom message
socket.send([
type: 'welcome',
message: 'Connected to server!'
])
}
## Dynamic Template Selection
For more complex applications with directory-based organization, you can use regex patterns to match routes dynamically and conditionally select templates.
Simple Conditional Selection:
@Alert('on /dashboard GET')
static void _dashboard(HttpResult r) {
def client = r.context.client
def template = if (client?.authenticated) {
def doc = client.document
doc.hasPermission('admin') ? 'admin-dashboard.ghtml' : 'user-dashboard.ghtml'
} else {
'guest-dashboard.ghtml'
}
new Launchpad()
.assemble([template])
.launch(r, 'wrapper.ghtml')
}
Advanced: Regex Routes with Directory Organization
Organize templates by feature or section in subdirectories, then use regex to capture URL parameters:
Directory Structure:
/[SPACEPORT_ROOT]
launchpad/
parts/
home.ghtml
about.ghtml
blog/
list.ghtml
post.ghtml
products/
catalog.ghtml
detail.ghtml
Router with Dynamic Matching:
class Router {
static Launchpad launchpad = new Launchpad()
// Static route for home
@Alert('on / hit')
static void _index(HttpResult r) {
launchpad.assemble(['home.ghtml'])
.launch(r, 'wrapper.ghtml')
}
// Dynamic route for blog posts
@Alert('~on /blog/(.*) hit')
static void _blogPost(HttpResult r) {
def slug = r.matches[0] // Captured from regex
def post = Document.get("blog-${slug}", 'posts')
r.context.data.post = post
launchpad.assemble(['blog/post.ghtml'])
.launch(r, 'wrapper.ghtml')
}
// Dynamic route for product details
@Alert('~on /products/(.*) hit')
static void _productDetail(HttpResult r) {
def productId = r.matches[0] // Captured from regex
if (productId == '' || productId == 'catalog') {
// Show catalog
def products = ProductService.getAllProducts()
r.context.data.products = products
launchpad.assemble(['products/catalog.ghtml'])
.launch(r, 'wrapper.ghtml')
} else {
// Show product detail
def product = Document.get(productId, 'products')
r.context.data.product = product
launchpad.assemble(['products/detail.ghtml'])
.launch(r, 'wrapper.ghtml')
}
}
}
Example blog/post.ghtml:
<%
def post = data.post
data.pageTitle = post.fields.title
%>
<article>
<h1>${ post.fields.title }</h1>
<p class="meta">Published: ${ post.fields.publishedDate }</p>
<div class="content">
${ post.fields.body }
</div>
</article>
<button on-click=${ _{
post.cargo.views.inc()
post.save()
"Thanks for reading!"
}}>Mark as Read</button>
This approach allows you to:
- Organize templates by feature in subdirectories
- Match dynamic URLs with regex patterns
- Extract URL parameters using r.matches
- Conditionally render different templates based on captured values
- Share common layouts across all pages with wrappers
## Mixing Launchpad with Traditional AJAX
Launchpad doesn't prevent you from using traditional JavaScript when needed:
<script>
async function fetchData() {
const response = await fetch('/api/data');
const data = await response.json();
// Update Cargo through a Server Action
await fetch('/api/update-cargo', {
method: 'POST',
body: JSON.stringify({ data })
});
}
</script>
<button onclick="fetchData()">Fetch and Update</button>
<!-- Reactive display updates automatically -->
<div>${{ dock.data.get() }}</div>
# Troubleshooting
## Reactive Variables Not Updating
Problem: Changes to Cargo don't trigger UI updates
Solutions:
- Ensure HUD-Core is included:
- Check that you're modifying the Cargo object (.set(), .inc(), etc.), not replacing it
- Verify the websocket connection is active (check browser console)
<!-- Wrong: Doesn't trigger reactivity -->
<button on-click=${ _{ dock.counter = 5 }}>Set</button>
<!-- Right: Triggers reactivity -->
<button on-click=${ _{ dock.counter.set(5) }}>Set</button>
## Server Actions Not Firing
Problem: Clicking elements doesn't trigger Server Actions
Solutions:
- Check browser console for JavaScript errors
- Verify HUD-Core is loaded
- Ensure the on-* attribute is properly formatted: on-click=${ _{ ... }}
- Check that the Server Action closure is syntactically correct
## Template Compilation Errors
Problem: Templates fail to compile
Solutions:
- Check for unbalanced <% %> or ${ } tags
- Verify Groovy syntax is correct inside scriptlets
- Look at the server logs for detailed error messages
- Enable debug mode for more verbose output
## Session Loss
Problem: dock data disappears between requests
Solutions: - Verify session management is configured correctly - Check that cookies are enabled in the browser - Ensure the dock is being used properly in Server Actions - Consider using Cargo mirrored to Documents for persistent storage beyond sessions
# Next Steps
Now that you understand Launchpad's core concepts, explore these advanced topics:
- Sessions & Client Management: Master authentication, the dock's persistence model, and multi-session support
- Transmissions: Master all transmission formats and advanced DOM manipulation patterns
- Server Elements: Build reusable, full-stack components with encapsulated logic
- Cargo: Deep dive into Spaceport's reactive data layer
- Documents: Connect your UI to persistent database storage
- Alerts: Leverage Spaceport's event system for complex workflows
For a hands-on introduction, try the Tic-Tac-Toe Tutorial, which demonstrates Launchpad's capabilities in a simple, complete application.
# Summary
Launchpad is Spaceport's answer to modern UI development challenges. It provides:
- Templates for dynamic HTML generation with Groovy
- Server Actions for binding server-side logic to UI events
- Reactive Variables for automatic UI updates when data changes
- Server Elements for reusable, full-stack components
- Seamless integration with Cargo, Documents, and the rest of Spaceport
By keeping logic on the server and minimizing JavaScript, Launchpad enables you to build sophisticated, interactive web applications with less code, better security, and faster development cycles. The result is a development experience that combines the best aspects of server-side and client-side frameworks without the complexity of either.
Welcome to the future of web development. Welcome to Launchpad.
SPACEPORT DOCS