Slots
cf_Island captures any HTML between its opening and closing tags and passes it to your component as the default slot. Use the <cf_Slot name="..."> child tag to project additional named slots. Both forms accept dynamic CFML markup (queries, conditionals, includes) and stream it straight into a Vue/React component.
Default slot
Section titled “Default slot”CFML side
Section titled “CFML side”<cfquery name="userPosts" datasource="myDB"> SELECT title, body, createdAt FROM posts WHERE userID = <cfqueryparam value="#session.userID#"></cfquery>
<cf_Island framework="#Vue#" path="./src/PostList.vue" strategy="visible"> <cfoutput query="userPosts"> <article class="post"> <h2>#title#</h2> <p>#body#</p> <time>#createdAt#</time> </article> </cfoutput></cf_Island>Vue component
Section titled “Vue component”<template> <div class="post-list"> <slot /> </div></template>React component
Section titled “React component”export default function PostList({ children }) { return <div className="post-list">{children}</div>;}Named slots
Section titled “Named slots”Wrap any sub-section of the body in <cf_Slot name="..."> to expose it as a named slot. The wrapped HTML is removed from the default slot and routed to the framework as a named slot (Vue) or a same-named prop (React).
CFML side
Section titled “CFML side”<cf_Island framework="#Vue#" path="./src/Card.vue" strategy="visible"> <p>This goes into the default slot.</p>
<cf_Slot name="header"> <h1>#post.title#</h1> <time>#dateFormat(post.createdAt, "yyyy-mm-dd")#</time> </cf_Slot>
<cf_Slot name="footer"> <cfoutput query="comments"> <div class="comment">#author#: #body#</div> </cfoutput> </cf_Slot></cf_Island>Vue component
Section titled “Vue component”Named slots are exposed via <slot name="...">, exactly as Vue normally handles them:
<template> <article class="card"> <header><slot name="header" /></header> <main><slot /></main> <footer><slot name="footer" /></footer> </article></template>React component
Section titled “React component”React doesn’t have a slot concept, so each named slot is delivered as a prop of the same name, pre-wrapped in a <span> with the slot’s HTML. The default slot is still children.
export default function Card({ header, footer, children }) { return ( <article className="card"> <header>{header}</header> <main>{children}</main> <footer>{footer}</footer> </article> );}Rules for cf_Slot
Section titled “Rules for cf_Slot”- Must be nested directly inside a
cf_Island. ThrowsColdspa.OrphanSlototherwise. nameis required and must be a valid identifier matching^[A-Za-z_][A-Za-z0-9_]*$. ThrowsColdspa.InvalidSlotNameotherwise.name="default"is reserved. Place default-slot content directly insidecf_Island.- Last write wins. Two
<cf_Slot name="header">tags in the same island? The second overwrites the first. - The slot’s own markup is stripped from the parent’s
generatedContent, so it never leaks into the default slot.
How it works
Section titled “How it works”cf_Islandruns on both passes:- Start pass: initializes
namedSlots = {}on the tag scope so child<cf_Slot>tags have somewhere to attach. - End pass: reads the rendered body from
thisTag.generatedContent(default slot) plus the populatednamedSlotsstruct.
- Start pass: initializes
- Each
<cf_Slot>runs on its end pass, walks up to the parent viagetBaseTagData("CF_ISLAND"), and writesparent.namedSlots[name] = body. - Each slot’s HTML (default + named) is stashed in a sibling
<template id="slot-<uid>[-<name>]">element.<template>content is inert in the DOM, so scripts inside don’t execute. - The mount options passed to the client include
{ slotId, hasSlot, namedSlotIds: { name: id, ... } }. - On the SSR sidecar, the renderer assembles a slots object —
defaultplus one entry per named slot — using Vue’screateStaticVNode. The same payload ({ slotHtml, namedSlots }) is sent to/render/<framework>so SSR matches client byte-for-byte. - On the client:
- Vue reads each
<template>by id and builds the same slots object. - React reads each
<template>and spreads named slots as props ({...props, header: <span/>, footer: <span/>}), with the default slot aschildren.
- Vue reads each
Consequences and gotchas
Section titled “Consequences and gotchas”These follow from running the tag on the end pass.
Errors in the body abort the page
Section titled “Errors in the body abort the page”If a <cfquery> or expression inside the slot throws, the tag never gets to emit anything — the error propagates as it would in plain CFML. The island can’t try/catch its own body.
cfabort / cfexit in the body suppresses the island entirely
Section titled “cfabort / cfexit in the body suppresses the island entirely”If the body short-circuits, no mount div, no <template>, no <script> — nothing gets emitted.
The body always runs, even with no slot
Section titled “The body always runs, even with no slot”Self-closing form (<cf_Island ... />) doesn’t execute body code. But <cf_Island ...></cf_Island> does — even when empty. Don’t put expensive queries in slots you don’t actually project somewhere.
No nested islands
Section titled “No nested islands”Putting <cf_Island> inside another <cf_Island>’s body is currently not supported:
- The inner island’s
<script type="module">ends up serialized inside the outer’s<template>element. <template>content is inert in the DOM, so the inner script never executes.- The inner mount div renders, but never hydrates.
Keep islands sibling-level rather than nested.
Slot HTML inherits CFML output rules
Section titled “Slot HTML inherits CFML output rules”If you reference variables (#name#) inside the slot, you still need a surrounding <cfoutput> — same as any CFML body. JSX-style implicit interpolation does not happen.
<cfsetting enableCFOutputOnly="true">
Section titled “<cfsetting enableCFOutputOnly="true">”If a parent template has this set globally, body content that isn’t inside an explicit <cfoutput> won’t appear in the slot HTML. Wrap slot bodies in <cfoutput> when in doubt.
Whitespace is preserved (mostly fine)
Section titled “Whitespace is preserved (mostly fine)”Outer whitespace is trimmed; interior whitespace from <cfoutput query> between tags is preserved. Vue’s createStaticVNode skips diffing static blocks, so this won’t cause hydration mismatches today — but if we ever switch slot rendering to a real template compile, this would need revisiting.
No early-exit from the island itself
Section titled “No early-exit from the island itself”You can’t decide inside the tag to skip rendering after seeing the body. Wrap the whole <cf_Island> in a <cfif> if you need conditional rendering.