Advanced Topics
Lifecycle Hooks
WebView provides rich lifecycle hooks for side effects and actor integration:
class ChatWebView extends WebView[ChatState, ChatEvent] {
// Called after initial mount with WebViewContext
override def afterMount(state: ChatState, context: WebViewContext): Unit = {
// Subscribe to pub/sub for chat messages
// Note: Pub/sub would need to be implemented separately via actors
// Load initial data asynchronously
Future {
val messages = loadMessagesFromDB()
context.sendSelf(LoadedMessages(messages))
}
}
// Called before processing an event
override def beforeUpdate(event: ChatEvent, state: ChatState, context: WebViewContext): Unit = {
// Log events for debugging
println(s"Processing event: $event")
// Check authorization
if (!isAuthorized(event, state.user)) {
throw new UnauthorizedException()
}
}
// Called after processing an event
override def afterUpdate(
event: ChatEvent,
oldState: ChatState,
newState: ChatState,
context: WebViewContext
): Unit = {
event match {
case SendMessage(msg) =>
// Broadcast to other users
context.tellPath("/user/chat-broadcaster", BroadcastMessage(msg))
case _ => ()
}
}
// Called before rendering (transform state for view)
override def beforeRender(state: ChatState): ChatState = {
// Add computed fields
state.copy(
unreadCount = state.messages.count(!_.read),
sortedMessages = state.messages.sortBy(_.timestamp)
)
}
// Clean up on termination
override def terminate(reason: Option[Throwable], state: ChatState): Unit = {
// Close resources and clean up
state.connection.foreach(_.close())
}
}
WebViewContext
The WebViewContext provides access to the actor system for actor communication:
case class WebViewContext(
system: ActorSystem,
sendSelf: Any => Unit // Send message to this WebView's actor
)
// Use in lifecycle hooks
override def afterMount(state: State, context: WebViewContext): Unit = {
// Send message to self (received via handleInfo)
context.sendSelf(InitComplete)
// Send message to another actor by path
context.tellPath("/user/my-actor", SomeMessage)
// Create a new actor
val worker = context.system.actorOf[WorkerActor]("worker")
// Send message to the worker
context.system.tell(worker, DoWork)
}
Error Boundaries
WebView includes built-in error boundaries for graceful error recovery:
class ResilientWebView extends WebView[MyState, MyEvent] {
// Attempt to recover from errors
override def onError(
error: Throwable,
state: MyState,
phase: ErrorPhase
): Option[MyState] = {
phase match {
case ErrorPhase.Mount =>
// Recover to default state
Some(MyState.default)
case ErrorPhase.Event =>
// Clear problematic data
Some(state.copy(errorMessage = Some(error.getMessage)))
case ErrorPhase.Render =>
// Reset to last good state
Some(state.copy(debugMode = true))
case _ =>
None // No recovery, show error UI
}
}
// Custom error UI
override def renderError(error: Throwable, phase: ErrorPhase): String = {
s"""
<div class="error-container">
<h2>Oops! Something went wrong in ${phase.name}</h2>
<p>${error.getMessage}</p>
<button wv-click="Retry">Try Again</button>
<button onclick="location.reload()">Reload Page</button>
</div>
"""
}
// Control retry logic
override def shouldRetry(
error: Throwable,
phase: ErrorPhase,
attemptCount: Int
): Boolean = {
// Retry up to 3 times for transient errors
error match {
case _: NetworkException if attemptCount < 3 => true
case _ => false
}
}
}
Error Phases
ErrorPhase.Mount- Error during initial mountErrorPhase.Event- Error during event handlingErrorPhase.Info- Error during info message handlingErrorPhase.Render- Error during renderingErrorPhase.Lifecycle- Error in lifecycle hooks
Actor Communication
WebViews can communicate with other actors in the system:
override def afterMount(state: State, context: WebViewContext): Unit = {
// Create a worker actor
val workerRef = context.system.actorOf[DataLoaderActor]("data-loader")
// Store reference and send initial message
context.sendSelf(WorkerCreated(workerRef))
context.system.tell(workerRef, StartLoading)
}
override def afterUpdate(
event: Event,
oldState: State,
newState: State,
context: WebViewContext
): Unit = {
event match {
case RequestData =>
// Send message to worker actor
newState.workerRef.foreach { ref =>
context.system.tell(ref, LoadDataRequest)
}
case _ => ()
}
}
override def handleInfo(msg: Any, state: State): State = {
msg match {
case DataLoaded(data) =>
state.copy(data = Some(data), loading = false)
case LoadError(error) =>
state.copy(error = Some(error), loading = false)
case WorkerCreated(ref) =>
state.copy(workerRef = Some(ref))
case _ =>
state
}
}
WebView Server Configuration
With Parameters and Session
val server = WebViewServer()
.withWebViewRoute(
path = "/app",
webView = new MyWebView(),
params = Map("theme" -> "dark"),
session = Session(Map("userId" -> "123"))
)
.start(port = 8080)
// Access in mount():
override def mount(params: Map[String, String], session: Session): MyState = {
val theme = params.get("theme") // Some("dark")
val userId = session.get[String]("userId") // Some("123")
MyState(theme = theme, userId = userId)
}
With Route Factory (Per-Connection Instances)
val server = WebViewServer()
.withWebViewRouteFactory("/chat", () => new ChatWebView())
.start(port = 8080)
// Creates a new ChatWebView instance for each WebSocket connection
Mixing WebViews with Custom Routes
import dev.alteration.branch.spider.server.*
import java.nio.file.Path
val server = WebViewServer()
.withWebViewRoute("/app", new MyWebView())
.withHttpRoute("/static", new FileHandler(Path.of("public")))
.withWebSocketRoute("/ws/echo", new EchoWebSocketHandler())
.start(port = 8080)
DevTools
WebView includes built-in DevTools for debugging and monitoring (enabled with .withDevMode(true)):
val server = WebViewServer()
.withDevMode(true) // Adds /__devtools route
.withWebViewRoute("/app", new MyWebView())
.start(port = 8080)
// Visit http://localhost:8080/__devtools for real-time debugging
Features
- Component Inspector: View active WebView instances and their state
- Timeline: See all events, state changes, and info messages
- Performance Metrics: Track render times, event processing, and memory usage
- Connection Status: Monitor WebSocket connections and disconnections
- State Diff: Compare state before and after events
DevTools Integration
DevTools automatically tracks all WebView activity:
// All of this is automatically captured:
override def mount(...) = MyState(...) // Recorded: Mount event
override def handleEvent(Increment, state) = ... // Recorded: Event + state diff
override def handleInfo(msg, state) = ... // Recorded: Info message
// Render time is automatically measured
Complete Example
Here's a complete example showing many WebView features:
import dev.alteration.branch.spider.webview.*
import dev.alteration.branch.spider.webview.html.*
import dev.alteration.branch.spider.webview.html.Tags.*
import dev.alteration.branch.spider.webview.html.Attributes.*
import dev.alteration.branch.spider.webview.html.Components.*
import dev.alteration.branch.spider.webview.styling.*
// State
case class TodoState(
todos: List[Todo],
inputValue: String,
filter: Filter,
error: Option[String] = None
)
case class Todo(id: String, text: String, completed: Boolean, createdAt: Instant)
enum Filter {
case All, Active, Completed
}
// Events
sealed trait TodoEvent derives EventCodec
case class AddTodo(text: String) extends TodoEvent
case class ToggleTodo(id: String) extends TodoEvent
case class DeleteTodo(id: String) extends TodoEvent
case class UpdateInput(value: String) extends TodoEvent
case class SetFilter(filter: Filter) extends TodoEvent
case object ClearCompleted extends TodoEvent
// Styles
object TodoStyles extends StyleSheet {
val container = style(
"max-width" -> "600px",
"margin" -> "0 auto",
"padding" -> "20px"
)
val todoItem = style(
"display" -> "flex",
"gap" -> "10px",
"padding" -> "10px",
"border-bottom" -> "1px solid #eee"
)
val completed = style(
"text-decoration" -> "line-through",
"opacity" -> "0.6"
)
}
// WebView
class TodoWebView extends WebView[TodoState, TodoEvent] {
override def mount(params: Map[String, String], session: Session): TodoState = {
TodoState(todos = List.empty, inputValue = "", filter = Filter.All)
}
override def handleEvent(event: TodoEvent, state: TodoState): TodoState = {
event match {
case AddTodo(text) if text.trim.nonEmpty =>
val todo = Todo(
id = UUID.randomUUID().toString,
text = text.trim,
completed = false,
createdAt = Instant.now()
)
state.copy(todos = state.todos :+ todo, inputValue = "")
case ToggleTodo(id) =>
state.copy(todos = state.todos.map { t =>
if (t.id == id) t.copy(completed = !t.completed) else t
})
case DeleteTodo(id) =>
state.copy(todos = state.todos.filterNot(_.id == id))
case UpdateInput(value) =>
state.copy(inputValue = value)
case SetFilter(filter) =>
state.copy(filter = filter)
case ClearCompleted =>
state.copy(todos = state.todos.filterNot(_.completed))
case _ =>
state
}
}
override def beforeRender(state: TodoState): TodoState = {
// Add computed fields
val filteredTodos = state.filter match {
case Filter.All => state.todos
case Filter.Active => state.todos.filterNot(_.completed)
case Filter.Completed => state.todos.filter(_.completed)
}
state.copy(todos = filteredTodos)
}
override def render(state: TodoState): String = {
val visibleTodos = state.filter match {
case Filter.All => state.todos
case Filter.Active => state.todos.filterNot(_.completed)
case Filter.Completed => state.todos.filter(_.completed)
}
div(cls := TodoStyles.container)(
h1()("Todo List"),
// Input form
div()(
textInput("todo-input", state.inputValue, "UpdateInput",
placeholder = Some("What needs to be done?")),
button(wvClick := AddTodo(state.inputValue))("Add")
),
// Filter buttons
div()(
clickButton("All", "SetFilter",
extraAttrs = Seq(classWhen("active" -> (state.filter == Filter.All)))),
clickButton("Active", "SetFilter",
extraAttrs = Seq(classWhen("active" -> (state.filter == Filter.Active)))),
clickButton("Completed", "SetFilter",
extraAttrs = Seq(classWhen("active" -> (state.filter == Filter.Completed))))
),
// Todo list
ul()(
visibleTodos.map { todo =>
li(cls := TodoStyles.todoItem + (if (todo.completed) " " + TodoStyles.completed else ""))(
checkbox(todo.id, todo.completed, s"ToggleTodo:${todo.id}"),
span()(text(todo.text)),
targetButton("Delete", "DeleteTodo", todo.id)
)
} *
),
// Stats
div()(
text(s"${state.todos.count(!_.completed)} items left"),
when(state.todos.exists(_.completed))(
button(wvClick := ClearCompleted)("Clear completed")
)
),
// Include styles
raw(TodoStyles.toStyleTag)
).render
}
override def onError(
error: Throwable,
state: TodoState,
phase: ErrorPhase
): Option[TodoState] = {
Some(state.copy(error = Some(error.getMessage)))
}
}
// Server
@main def runTodoApp(): Unit = {
val server = WebViewServer()
.withRoute("/todos", new TodoWebView())
.withHtmlPages()
.withDevMode(true)
.start(port = 8080)
println("Todo app running at http://localhost:8080/todos")
println("DevTools at http://localhost:8080/__devtools")
scala.io.StdIn.readLine()
server.stop()
}
Next Steps
- Learn about the HTML DSL for building UIs
- Explore Styling for CSS-in-Scala
- Return to WebView Overview