Browser integration
Run a composed client-only command, then ask a focused JavaScript hook to copy a sample string. Clipboard access depends on browser permissions and a secure context, so denial is handled as an ordinary typed result rather than a crash.
final class BrowserInteropExample(instanceId: String)
extends LiveView[BrowserInteropExample.Msg, BrowserInteropExample.Model]:
import BrowserInteropExample.*
private val hookRef = DomRef(s"$instanceId-hook")
private val panelRef = DomRef(s"$instanceId-panel")
private val placeholderRef = DomRef(s"$instanceId-placeholder")
private val detailRef = DomRef(s"$instanceId-detail")
private val clientOnlyCommand =
JS.show(to = panelRef.selector)
.hide(to = placeholderRef.selector)
.toggle(to = detailRef.selector)
private val resetCommand =
JS.push(Msg.Reset)
.hide(to = panelRef.selector)
.hide(to = detailRef.selector)
.show(to = placeholderRef.selector)
override def hooks: LiveHooks[Msg, Model] =
LiveHooks.empty[Msg, Model].onBrowserEvent(CopyResultEvent) { (model, result, _) =>
ZIO.succeed(applyCopyResult(model, result))
}
def mount(ctx: MountContext): Task[Model] = ZIO.succeed(Model())
def handleMessage(model: Model, ctx: MessageContext) =
case Msg.CopySample =>
val requestNumber = model.requestNumber + 1
val requestId = s"copy-$requestNumber"
ctx.client
.push(CopyRequestEvent, CopyRequest(requestId, SampleText))
.as(model.copy(requestNumber = requestNumber, operation = CopyOperation.Pending(requestId)))
case Msg.Reset => ZIO.succeed(Model())
def view(model: Signal[Model]): HtmlElement[Msg] =
div(
dom.hook(HookName, hookRef),
cls := "docs-browser-integration",
sectionTag(
dataAttr("example-controls") := "",
h3("Compose client-only commands"),
p(
"This click shows a panel, hides the placeholder, and toggles a detail without sending a server message."
),
button(typ := "button", on.click(clientOnlyCommand), "Run composed command"),
p(placeholderRef.attr, "The command will hide this placeholder."),
div(
panelRef.attr,
styleAttr := "display: none;",
strong("The command showed this panel."),
p(
detailRef.attr,
styleAttr := "display: none;",
"Run it again to toggle this detail."
)
)
),
sectionTag(
dataAttr("example-controls") := "",
h3("Exchange typed browser events"),
p("Scala asks the hook to copy this sample and accepts only the correlated result."),
codeTag(SampleText),
div(
cls := "docs-browser-actions",
button(
typ := "button",
on.click(Msg.CopySample),
model.map(model =>
if model.operation.isPending then "Retry copy" else "Copy sample text"
)
),
button(typ := "button", on.click(resetCommand), "Reset browser integration")
),
p(
dataAttr("browser-copy-status") := "",
role := "status",
model.map(_.operation.label)
)
)
)
end BrowserInteropExample
object BrowserInteropExample:
final case class CopyRequest(requestId: String, text: String) derives JsonEncoder
final case class CopyResult(requestId: String, ok: Boolean) derives JsonDecoder
final case class Model(
requestNumber: Long = 0,
operation: CopyOperation = CopyOperation.Idle)
enum Msg:
case CopySample
case Reset
enum CopyOperation(val label: String, val traceLabel: String):
case Idle extends CopyOperation("No browser operation requested yet.", "idle")
case Pending(requestId: String)
extends CopyOperation("Waiting for the browser result. Retry if needed.", "pending")
case Succeeded extends CopyOperation("Browser operation completed.", "succeeded")
case Failed extends CopyOperation("Browser operation could not be completed.", "failed")
def isPending: Boolean = this match
case Pending(_) => true
case _ => false
val SampleText = "Scalive keeps server-to-browser event payloads typed."
private val HookName = "BrowserInterop"
private val CopyResultEvent = BrowserToServerEvent[CopyResult]("browser-copy-result")
private val CopyRequestEvent = ServerToBrowserEvent[CopyRequest]("browser-copy-request")
private val codeTag = HtmlTag("code")
def applyCopyResult(model: Model, result: CopyResult): Model =
val nextOperation = model.operation match
case CopyOperation.Pending(requestId) if requestId == result.requestId =>
if result.ok then CopyOperation.Succeeded else CopyOperation.Failed
case current => current
model.copy(operation = nextOperation)
end BrowserInteropExampleView source (documentation/site/src/scalive/docs/examples/BrowserInteropExample.scala:9-129)
const maxRequestIdLength = 64
const maxTextLength = 4096
export function readCopyRequest(payload) {
const requestId = typeof payload?.requestId === "string" ? payload.requestId : ""
const text = typeof payload?.text === "string" ? payload.text : undefined
if (
requestId.length === 0 ||
requestId.length > maxRequestIdLength ||
text === undefined ||
text.length > maxTextLength
) return undefined
return { requestId, text }
}
export function createBrowserInteropHook(clipboard = globalThis.navigator?.clipboard) {
return {
mounted() {
this.isDestroyed = false
this.handleEvent("browser-copy-request", async (payload) => {
if (this.isDestroyed) return
const request = readCopyRequest(payload)
let ok = false
if (request && clipboard?.writeText) {
try {
await clipboard.writeText(request.text)
ok = true
} catch {
ok = false
}
}
if (this.isDestroyed) return
try {
await this.pushEvent("browser-copy-result", {
requestId: request?.requestId ?? "",
ok,
})
} catch {
// The LiveSocket may disconnect while browser work is completing.
}
})
},
destroyed() {
this.isDestroyed = true
},
}
}View source (documentation/site/assets/js/browser-interop.js:2-51)
Live result
ConnectedReconnectingRead-onlyCompose client-only commands
This click shows a panel, hides the placeholder, and toggles a detail without sending a server message.
The command will hide this placeholder.
Exchange typed browser events
Scala asks the hook to copy this sample and accepts only the correlated result.
Scalive keeps server-to-browser event payloads typed.No browser operation requested yet.
Disconnected. Controls resume after reconnection.
Interaction inspector
See how actions in the live result above travel through typed messages, server state, protocol frames, and DOM updates.
The live result and its inspector resume when the connection returns.
Related guidance: integrate browser behavior with commands, directional events, stable hook IDs, correlation, and cleanup.