Server-Side Launchpad Elements
Launchpad Elements are HTML elements that have a variety of features that allow the element to interact with the server, but also allow for client-side logic. It uses a Groovy class format with a multitude of different features to leverage both the server-side and client-side capabilities. This structure offers a seamless way to build complex, full-stack components.
# Provided by Launchpad
Launchpad Elements are a feature provided by Launchpad, used inside Launchpad Templates (.ghtml files). They are
defined by a Groovy class that extends spaceport.launchpad.elements.Element, placed inside the /launchpad/elements/
folder. This class is then used similarly as a web component in your HTML. For a primer on Launchpad and its broader
set of features in relationship to a Spaceport application, see the Launchpad Documentation.
# Element Structure and Core Concepts
Launchpad Elements are a powerful way to create reusable components that can encapsulate HTML, CSS, JavaScript, and Groovy code into a single component that can be used throughout your Launchpad templates. They can be used to create interactive UI components, form elements, and more. They offer a variety of features that allow the element to interact with the server, but also allow for client-side logic.
The very most basic element will take advantage of the prerender method, which is the only required property or
method of an Element. This method is called on the server-side when the element is rendered for the first time
to send to the client as the HTTP response. It returns a string that is the inner HTML of the element.
## The prerender Method
Once your element implements the Element interface, you must include a prerender method that Launchpad uses to
render the element on the server-side, as the initial page is being built and sent as the HTTP response. This method
takes two parameters, body and attributes that both feed from the HTML structure of the element in the template.
A very basic example of an element that implements the prerender method might look like this:
import spaceport.launchpad.element.*
class Highlight implements Element {
String prerender(String body, Map attributes) {
return """
<mark ${ "style='background-color: ${ attributes.color }'".if { attributes.color } }>
${ body }
</mark>"""
}
}
Just a note! The .if{...} syntax you see is the above example is provided by Spaceport's Class Enhancements module, and are perfect for injecting attributes into HTML string output.
The element can then be used in a .ghtml file with a g: prefix to the element name. It's recommended to use
Camel Casing for the groovy class name, as it will be converted internally to a standard html kebap-case name. Inside
the HTML, they should always be lowercase. Using the above example would result in the following HTML usage:
<g:highlight color="yellow">This text is highlighted in yellow.</g:highlight>
The body parameter will contain the inner HTML of the element, and the attributes parameter will contain a map
of the attributes that were placed on the element in the HTML, with String keys and String values. In the above
example, the attributes map would contain a key of color with a value of yellow. The body parameter would
contain the string This text is highlighted in yellow. The prerender method must return a string that is
the inner HTML of the element. This string can contain any HTML, and can be built dynamically based on
the body and attributes.
## Property Annotations
A powerful mechanism of Launchpad Elements is the ability to annotate properties of the class with a multitude of property annotations. These properties are then parsed with a built-in transformation that will inject the CSS and JavaScript into the element on the client-side with a variety of different strategies.
Available property annotations include:
@CSS- Indicates that the property contains CSS that should be injected into the element on the client-side. No need for a<style>tag, just the CSS itself.@ScopedCSS- Acts just like@CSS, but the CSS is scoped to the specific instances of the element itself.@Javascript- Indicates that the property contains JavaScript, and can be used to run some client-side logic, or to define functions that can be called on the client-side.@Prepend- Allows for the injection of unsafe HTML at the start of the page, or directly after the<head>, guaranteed to be evaluated before theprerenderoutput.@ScopedPrepend- Unsafe HTML that is guaranteed to be evaluated before theprerenderoutput, but injected right before the specific element itself on the client-side.@Append- Allows for the injection of unsafe HTML at the end of the page, guaranteed to be evaluated after theprerenderoutput.@ScopedAppend- Unsafe HTML that is guaranteed to be evaluated after theprerenderoutput, but injected right after the specific element itself on the client-side.@Bind- Binds a server-side Groovy method to a client-side function for server interaction through the client.
See the section on Property Annotations In Depth for more information on each of these annotations.
# Building Launchpad Elements
Launchpad Elements offer a flexible way to manage component logic and state by allowing you to employ server-focused and client-focused strategies, which can be used in tandem. This dual capability is what makes them so powerful: server-side Groovy code and Launchpad features handle initial rendering, state management, and interaction with server resources, while client-side code (primarily JavaScript and CSS) handles snappy UI interactions, styling, and communication back to the server.
## A Client-Focused Strategy
A good UI strategy might be to make as much of the interaction client-side as possible. Keeping the interactions on
the client-side can have the benefit of extremely snappy interactions, but the server will have to be interacted with
manually to update its state on the server. If the element is just used within a form, this is a good strategy,
provided the element implements a getValue() or .value field.
While a star rating could technically benefit from having its value managed on the server (e.g., to immediately
persist the rating), for this example, we'll showcase a client-focused strategy to demonstrate the powerful
use of the @Javascript annotation for client-side behavior.
import spaceport.launchpad.element.*
class StarRating implements Element {
// Include some CSS to style the element. Use '&' to reference the TAGNAME of the element.
@CSS String css = """
& {
display: inline-block;
user-select: none;
cursor: pointer;
span {
font-size: 2em;
color: goldenrod;
}
}
"""
// Pre-render the element on the server-side. This is important for SEO and
// for instant delivery to the client. The body and attributes are passed in
// from the usage of the element in the template and can be used to set the
// initial state of the element.
String prerender(String body, Map attributes) {
// Pre-render the element for instant delivery to the client
def stars = 0
if (attributes.containsKey('value')) {
stars = attributes.get('value').toInteger()
}
// Use .combine to build the inner HTML
return (1..5).combine {
i -> (i <= stars) ? "<span i=${ i }>★</span>" : "<span i=${ i }>☆</span>"
}
}
// Utilize the 'constructed' property to set up the element on the client-side
@Javascript String constructed = / language=js / """
function(element) {
// This function is called when the element is constructed, referencing
// the newly formed element itself
element.listen('click', function(e) {
// Which star was clicked?
if (e.target.tagName === 'SPAN') {
let value = e.target.getAttribute('i');
// Handle an edge case for setting 0 stars
if (element.getValue() === 1 && value === '1') {
element.setValue(0);
} else {
element.setValue(e.target.getAttribute('i'))
}
}
})
}
"""
// Additional @JavaScript annotated properties become functions owned by the element
// on the client-side. They also get access to 'this', which is the element itself.
// Notice that these functions don't get the element automatically passed in like
// constructed and deconstructed, allowing for other function parameters to be used.
@Javascript def getValue = / language=javascript / """
function() {
// Count the number of filled stars and return that value
let stars = 0;
for (let char of this.innerText) {
if (char === '★') stars++;
}
return stars;
}
"""
@Javascript def setValue = / language=javascript / """
function(value) {
this.innerHTML = '';
for (let i = 1; i <= 5; i++) {
if (i <= value) {
this.innerHTML += '<span i=' + i + '>★</span>';
} else {
this.innerHTML += '<span i=' + i + '>☆</span>';
}
}
// Implicitly set .value property
this.value = Number.parseInt(value);
// Update the attribute, too, for reference
this.setAttribute('value', this.value);
// Do the things that you might do on a standard web component, like fire an event
this.dispatchEvent(new Event('change'));
}
"""
}
### Client-Side Setup and Lifecycle
The constructed property, annotated with @Javascript, is a specialized property that is automatically executed
on the client-side after the element's HTML output from the prerender method has been inserted into the
Document Object Model (DOM). The function it defines receives the DOM element itself as a parameter, allowing you
to attach event listeners and perform initial client-side setup.
In the StarRating example, the element uses the custom .listen(event, handler) method to attach a click
event handler. This helper method functions similarly to the standard element.addEventListener(), but provides an
important benefit: the listener will be automatically cleaned up if the element is removed from the DOM. This
lifecycle management prevents memory leaks without requiring you to manually manage event detachment. This can only
be used on the root element itself, not on child elements, however you can always use standard JavaScript to attach
listeners to child elements. Be sure to clean them up!
In addition to constructed, the client lifecycle also includes a property named deconstructed. Like
constructed, this property, when annotated with @Javascript, receives the DOM element as a parameter, and is
called when the element is removed from the DOM. This can be used for any necessary manual cleanup of resources
external to Launchpad's management.
### Javascript-Annotated Functions and Inline Execution
The other @Javascript annotated properties, such as getValue and setValue, become functions owned by the element
on the client-side. The name of the Groovy property is used as the client-side function name
(e.g., element.getValue()). When these functions are invoked on the client, they gain access to this, which is a
reference to the element's DOM node, allowing them to manipulate the element's content, attributes, and properties.
Furthermore, properties annotated with @Javascript can also be used to run inline JavaScript without it needing to
be invoked as a function. This is achieved by simply omitting the function(...) syntax in the string content.
This script will execute on the client before the constructed function is called, allowing for very early setup
or script loading.
## A Server-Focused Strategy
A server-focused strategy relies on the server for all critical state management and updates. While this may result in a slight delay per interaction due to the required server round trip, it simplifies development by keeping the core logic in familiar Groovy code and leveraging Launchpad's reactive engine for UI updates.
Sure, the counter example could benefit from client-side logic, for this example, we'll showcase how to manage all state on the server.
import spaceport.launchpad.element.*
import spaceport.computer.memory.virtual.Cargo
class Counter implements Element {
// Use a Cargo object to hold the server-side state of the counter
def counter = new Cargo(0)
def max = 100
String prerender(String body, Map attributes) {
// Use the value attribute to set the initial count
if (attributes.containsKey('value')) {
counter.set(attributes.getInteger('value'))
// Remove attribute from renderingto the client
attributes.remove('value')
}
// Set a max value if provided
if (attributes.containsKey('max')) {
max = attributes.getInteger('max')
}
return """
<button on-click=${_ { if (counter.get() > 0) counter.dec() }}>-</button>
<span id="count">${{ counter.get() }}</span>
<button on-click=${_ { if (counter.get() < max) counter.inc() }}>+</button>
"""
}
// Provide a getValue and setValue to allow manipulating the value from the client with Javascript
@Bind def getValue() {
return counter.get()
}
@Bind def setValue(def value) {
def val = value.toInteger()
// Enforce min/max
if (val < 0) val = 0
if (val > max) val = max
counter.set(val)
}
}
The Counter element is a simple numerical counter that prevents the count from going below zero or above a defined
maximum value.
Template Usage
<g:counter value="10" max="50"></g:counter>
### Implementation Details
The class can have standard properties, just like a standard OOP Groovy class. The prerender method uses these
properties to set the initial state: it checks the incoming attributes map for a value and max attribute to
initialize the counter and max properties.
Crucially, the server-side state of the counter is managed using a Cargo object (new Cargo(0)). Cargo is a
thread-safe, shared memory object provided by Spaceport. It ensures consistency across server requests and, most
importantly, automatically integrates with Launchpad's reactive system, triggering updates when its value changes. More
detailed information about the persistence and functionality of Cargo can be found in the
Cargo documentation. Cargo provides a lot of different features with regard to client and server data sharing,
but it's important to know that any object can be used inside a server element, not just Cargo.
The @Bind annotation on getValue() and setValue() creates a bridge, binding these server-side Groovy methods to
client-side functions that are generated on the element instance. This allows client-side JavaScript to request or
modify the server state directly (e.g., document.querySelector('counter').setValue(25)).
The interactive logic on the buttons uses Launchpad's server-action syntax (on-click=${_ { ... }}). When a button
is clicked, a light-weight AJAX call is sent to the server to execute the enclosed Groovy closure, which modifies
the counter Cargo object. This feature is provided by Launchpad and can also be used in standard HTML elements
in Launchpad templates.
Finally, the element also utilizes the server reactive syntax (${{ counter.get() }}) inside the tag. When
the server-action modifies the counter Cargo object, Launchpad automatically detects the change, re-evaluates only
this expression, and pushes the updated value to the client, refreshing the UI in real time.
## A Mixed Strategy
A mixed strategy is more likely what you would use in a real-world scenario. It combines client-side responsiveness
with server-side persistence, often providing the optimal balance for complex UI components. Client-side logic handles
instant feedback and simple interactions, while @Bind methods and server-actions handle persistence and complex
server-side updates. Check out this example.
import spaceport.launchpad.element.*
import spaceport.computer.memory.virtual.Cargo
class PageNotes implements Element {
@CSS String style = """
& {
.active {
color: red;
}
}
"""
Cargo pageNotes
Cargo pageNoteFavorites
// We'll use this for scope, using an attribute to identify the page
def pageID = "default"
def saveNotes = { String notes ->
pageNotes.set(notes.clean()) // Clean to avoid any malicious code
}
String prerender(String body, Map attributes) {
// Use the route attribute to set the page ID for storing notes
if (attributes.containsKey('route')) {
pageID = attributes.get('route')
}
pageNotes = dock.'page-notes'.get(pageID)
pageNoteFavorites = dock.'page-notes'.get('_favorites') // Use as List-type Cargo
return """
<div contenteditable="true" id="notes" on-blur=${_ { t -> saveNotes(t.innerText) }}>
${ pageNotes ?: "Click here to add notes..." }
</div>
<div class='heart ${ 'active'.if { pageNoteFavorites.contains(pageID) }}'>♥</div>
"""
}
@Javascript constructed = / language=js / """
function(element) {
// Create a function that can toggle the favorite status of the page
element.listen('click', function(e) {
e.stopPropagation();
if (!e.target.classList.contains('heart')) return; // Only handle clicks on the heart
if (e.target.classList.contains('active')) {
// Optimistically update the UI
e.target.classList.remove('active');
element.setFavorite(false); // Catch up the server with the new state
} else {
e.target.classList.add('active');
element.setFavorite(true);
}
})
}
"""
// Just handle server-state, no client-side consideration here
@Bind def setFavorite(def b) {
if (b) pageNoteFavorites.addToSet(pageID)
else pageNoteFavorites.takeFromSet(pageID)
}
}
This element essentially makes a server-side entry keyed to the page route, allowing the user to provide some
internal notes and a 'favorites' toggle. The dock object is a built-in feature of both Launchpad templates and
elements that provides a shared, persistent key-value store using Cargo objects. It's a session-persistent store
that can be used to hold user-specific data across requests.
Template Usage
<g:page-notes route="${ r.context.target }"> </g:page-notes>
The route attribute is used to uniquely identify the page, allowing the notes and favorite status to be scoped
to that specific page. The r.context.target variable is a built-in variable in Launchpad templates that provides the
current route path. While elements don't get access to the r variable directly, attributes can be passed in from the
template to the element as Strings, or directly as variables using the ${ _{...}} syntax, covered more deeply in later
in this documentation.
### The Mixed Strategy Breakdown
This example uses a mixed strategy, with server-side state management for the notes and favorites, but also client-side interactivity for the heart toggle and contenteditable div.
The actual notes and the list of favorited pages are stored in Cargo objects, keyed to the specific pageID. The
note content is updated by a server-action (on-blur) which calls the saveNotes Groovy closure, ensuring notes
are persisted and cleaned of malicious code. This is a great example of server-side state management.
Using client-side interactivity and additional scripting, the heart toggle demonstrates optimistic UI updates. When the user clicks the heart:
- The client-side
constructedJavaScript immediately toggles theactiveCSS class (e.target.classList.remove/add('active')). This updates the UI instantly, giving the user immediate visual feedback. - Immediately after the UI is updated, the JavaScript calls the
@Bindmethod,element.setFavorite(true/false), which sends an asynchronous call to the server to update the persistent state inpageNoteFavorites.
This combination ensures the user experience is highly responsive while guaranteeing that the critical state (the favorite status) is correctly persisted on the server using Groovy logic.
### Other Opportunities for Mixed Strategies
The mixed strategy opens up many possibilities for complex, fast UIs:
- Client-Side Validation with Server Feedback: Use client-side JavaScript for immediate validation (e.g., required fields, format), but use a
@Bindmethod to check for server-side conflicts (e.g., username availability) before final submission. - Drag-and-Drop Operations: Handle the full drag-and-drop animation and UI state change on the client, and then use a single, final
@Bindcall at the end of thedropevent to notify the server of the new arrangement. - Real-time Filters: Client-side input listeners can immediately filter a local data set, and then use a debounced
@Bindcall to update a server-side preference or analytics log without blocking the main interaction.
# Property Annotations In Depth
Now that you've seen some examples of Launchpad Elements in action, let's take a deeper look at some of the core concepts and features that make them so powerful, pointing out some additional features, best practices, as well as potential gotchas to avoid.
SPACEPORT DOCS