SPACEPORT DOCS

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:

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:

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:

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

k