boxlang-component-development

Installation
SKILL.md

BoxLang Component Development

Overview

BoxLang components (also called "custom tags") are reusable markup-oriented units invoked with tag-like syntax in .bxm templates. Unlike classes, components are designed for output generation and template composition. They live in the components/ directory of a module and are registered globally when the module loads.

BoxLang Component Syntax — NOT CFML

All built-in and custom components in BoxLang use the bx: prefix. Never use cf-prefixed functions or tags — those are CFML, not BoxLang.

// CORRECT BoxLang component syntax
bx:header name="Content-Type" value="application/json";
bx:location url="/dashboard" addToken=false;
bx:abort;
bx:include template="partials/nav.bxm";

// Paired component with body
bx:transaction {
    // body
}

// WRONG \u2014 these are CFML, not BoxLang
cfheader( name="Content-Type", value="application/json" )   // \u274c CFML function
<cfabort>                                                    // \u274c CFML tag
<cflocation url="/dashboard">                               // \u274c CFML tag

Calling Components in Script

Self-closing components end with a semicolon to terminate the invocation:

// Self-closing \u2014 semicolon required in script
bx:header name="X-Custom" value="hello";
bx:setting showdebugoutput=false;

// Paired \u2014 body in braces (no trailing semicolon on the closing brace)
bx:savecontent variable="local.output" {
    writeOutput( "captured content" )
}

Property Declarations (class body)

In a class or component body, bx:property declarations also end with a semicolon:

class {
    bx:property name="title"  type="string"  default="";
    bx:property name="count"  type="numeric" default=0;
    bx:property name="active" type="boolean" default=true;
}

Component vs Class

Component (.bx in components/) Class (.bx anywhere)
Invocation <bx:myTag attr="val"> new MyClass()
Purpose Output/template composition Logic, services, models
Body Can have child content N/A
Use in .bxm templates Anywhere

Basic Component Structure

// components/Alert.bx
// Invoked as: <bx:alert type="warning" message="Something happened!" />

class {

    // Declare accepted attributes
    property name="type"    type="string"  default="info"
    property name="message" type="string"  required="true"
    property name="closable" type="boolean" default="true"

    /**
     * Called when the opening tag is encountered.
     * Return false to suppress body execution.
     */
    boolean function onStartTag( struct attributes, struct caller ) {
        // Normalize attributes
        if ( !listFindNoCase("info,success,warning,danger", attributes.type) ) {
            attributes.type = "info"
        }
        return true  // true = execute body (if any)
    }

    /**
     * Called after the body content (for paired tags) or after the tag (for self-closing).
     * Use this to render output.
     */
    void function onEndTag( struct attributes, struct caller, string generatedContent ) {

        // Render the component HTML
        writeOutput( '<div class="alert alert-#attributes.type#">' )

        if ( attributes.closable ) {
            writeOutput( '<button type="button" class="btn-close" data-bs-dismiss="alert"></button>' )
        }

        writeOutput( attributes.message )

        // Include any child body content
        if ( len( trim(generatedContent) ) ) {
            writeOutput( generatedContent )
        }

        writeOutput( '</div>' )
    }

}

Usage in a Template

// Simple self-closing
<bx:alert type="success" message="Record saved successfully!" />

// With body content
<bx:alert type="warning">
    <strong>Please note:</strong> Your session will expire in 5 minutes.
</bx:alert>

Component with Full Output Control

// components/DataTable.bx
class {

    property name="query"      required="true"
    property name="columns"    type="array"     default="#[]#"
    property name="cssClass"   type="string"    default="table"
    property name="caption"    type="string"    default=""

    boolean function onStartTag( struct attributes, struct caller ) {
        return true
    }

    void function onEndTag( struct attributes, struct caller, string generatedContent ) {
        var qry     = attributes.query
        var cols    = attributes.columns.len() ? attributes.columns : listToArray( qry.columnList )
        var cssClass = attributes.cssClass

        savecontent variable="local.tableHtml" {
            writeOutput( '<table class="#cssClass#">' )

            if ( len(attributes.caption) ) {
                writeOutput( '<caption>#encodeForHTML(attributes.caption)#</caption>' )
            }

            // Header row
            writeOutput( '<thead><tr>' )
            cols.each( (col) -> writeOutput('<th>#encodeForHTML(col)#</th>') )
            writeOutput( '</tr></thead>' )

            // Data rows
            writeOutput( '<tbody>' )
            for ( var i = 1; i <= qry.recordCount; i++ ) {
                writeOutput( '<tr>' )
                cols.each( (col) -> {
                    var cellVal = qry[col][i] ?: ""
                    writeOutput( '<td>#encodeForHTML(cellVal.toString())#</td>' )
                })
                writeOutput( '</tr>' )
            }
            writeOutput( '</tbody></table>' )
        }

        writeOutput( local.tableHtml )
    }

}
// Usage
<bx:dataTable
    query="#userQuery#"
    columns="#['name','email','status']#"
    cssClass="table table-striped"
    caption="Active Users"
/>

Component with Body Processing

// components/Cache.bx
// Similar to <bx:cache> — caches child content

class {

    property name="key"       required="true"
    property name="timespan"  required="true"
    property name="cacheName" default="default"

    variables.cachedContent = ""
    variables.useCache      = false

    boolean function onStartTag( struct attributes, struct caller ) {
        // Check if we have a cached version
        var cached = cacheGet( attributes.key, false, attributes.cacheName )
        if ( !isNull(cached) ) {
            writeOutput( cached )
            variables.useCache = true
            return false  // false = skip executing the body
        }
        return true  // true = execute body and capture it in generatedContent
    }

    void function onEndTag( struct attributes, struct caller, string generatedContent ) {
        if ( !variables.useCache ) {
            // Cache the generated body content
            cachePut(
                attributes.key,
                generatedContent,
                attributes.timespan,
                "",
                attributes.cacheName
            )
            writeOutput( generatedContent )
        }
    }

}

Accessing Caller Scope

The caller argument gives access to the calling template's scope:

void function onEndTag( struct attributes, struct caller, string generatedContent ) {
    // Read a variable from the calling template
    var userId = caller.userId ?: ""

    // Set a variable in the calling template
    caller.componentResult = processData( attributes.data )
}

Attribute Validation

boolean function onStartTag( struct attributes, struct caller ) {
    // Required attribute check
    if ( !structKeyExists(attributes, "query") || isNull(attributes.query) ) {
        throw(
            message = "The 'query' attribute is required for <bx:dataTable>",
            type    = "MyModule.MissingAttributeError"
        )
    }

    // Type coercion
    if ( structKeyExists(attributes, "maxRows") ) {
        attributes.maxRows = val( attributes.maxRows )
        if ( attributes.maxRows < 1 ) attributes.maxRows = 100
    }

    return true
}

Registering Component Paths in a Module

In ModuleConfig.bx:

class {

    // Register the module's components/ directory
    this.componentPaths = [
        "#moduleRecord.physicalPath#/components"
    ]

    // Or register a specific namespace
    this.componentNamespace = "mymodule"
    // Usage: <bx:mymodule:alert type="info" message="Hello!" />

}

After registration, components are available globally in all templates:

// Available after module loads (no imports needed):
<bx:alert type="info" message="Hello from my module!" />
<bx:dataTable query="#qry#" />
<bx:cache key="homePage" timespan="#createTimeSpan(0,1,0,0)#">
    <!-- expensive content here -->
</bx:cache>

Self-Closing vs Paired Tags

BoxLang handles both automatically:

// Self-closing — onStartTag + onEndTag called with empty generatedContent
<bx:myTag attr="val" />

// Paired — body is executed and passed as generatedContent to onEndTag
<bx:myTag attr="val">
    body content here
</bx:myTag>

Component Output Buffering

// Capture component output into a variable instead of writing to response
savecontent variable="myOutput" {
    // <bx:myTag ...> goes here in markup files
    // In script: invoke the component
    include template="components/Alert.bx"
        attributes={ type: "info", message: "Test" }
}
writeOutput( myOutput )

Testing Custom Components

// tests/specs/AlertComponentTest.bx
class extends="testbox.system.BaseSpec" {

    function run() {
        describe( "Alert Component", function() {

            it( "should render an info alert", function() {
                savecontent variable="local.output" {
                    include template="#expandPath('./../../components/Alert.bx')#"
                        attributes={ type: "info", message: "Test message" }
                }
                expect( local.output ).toInclude( 'class="alert alert-info"' )
                expect( local.output ).toInclude( "Test message" )
            })

            it( "should default to info type when invalid type given", function() {
                savecontent variable="local.output" {
                    include template="#expandPath('./../../components/Alert.bx')#"
                        attributes={ type: "invalid", message: "Hello" }
                }
                expect( local.output ).toInclude( 'alert-info' )
            })

        })
    }

}

CFML Compatibility Note

BoxLang components map to CFML custom tags (<cf_myTag>) when the bx-compat-cfml module is enabled. Existing CFML custom tags work without modification.

References

Related skills

More from ortus-boxlang/skills

Installs
3
First Seen
Apr 15, 2026