Voting components
Vote in either card to update only that component's local model. Its typed output is mapped into a parent message, so the parent can identify the reporting instance without relying on a runtime component ID. Then update the Scala props: the title changes while its vote count remains intact.
object VoteComponent
extends LiveComponent.WithOutput[
VoteComponent.Props,
VoteComponent.Msg,
VoteComponent.Model,
VoteComponent.Output
]:
final case class Props(
id: String,
title: String,
description: String,
revision: Int,
resetEpoch: Int)
final case class Model(votes: Int, resetEpoch: Int)
enum Msg:
case Vote
case Reset
enum Output:
case VoteChanged(id: String, votes: Int)
def mount(props: Props, ctx: MountContext): Task[Model] =
ZIO.succeed(Model(0, props.resetEpoch))
override def update(props: Props, model: Model, ctx: UpdateContext): Task[Model] =
ZIO.succeed(if props.resetEpoch == model.resetEpoch then model else Model(0, props.resetEpoch))
def handleMessage(props: Props, model: Model, ctx: MessageContext) =
case Msg.Vote =>
val updated = model.copy(votes = model.votes + 1)
ctx.emit(Output.VoteChanged(props.id, updated.votes)).as(updated)
case Msg.Reset =>
ctx.emit(Output.VoteChanged(props.id, 0)).as(model.copy(votes = 0))
override def view(props: Signal[Props], model: Signal[Model], self: ComponentRef[Msg]) =
articleTag(
cls := "docs-vote-card",
dataAttr("vote-component") := props.map(_.id),
headerTag(
div(
p(cls := "docs-vote-kicker", "Component-local state"),
h4(props.map(_.title))
),
div(
cls := "docs-vote-meta",
code(dataAttr("component-id") := props.map(_.id), props.map(_.id)),
span(dataAttr("props-revision") := "", props.map(props => s"props r${props.revision}"))
)
),
p(cls := "docs-vote-description", props.map(_.description)),
div(
cls := "docs-vote-count-row",
div(
cls := "docs-vote-metric",
span(dataAttr("vote-label") := "", "Votes"),
strong(dataAttr("vote-count") := "", model.map(_.votes.toString))
),
div(
cls := "docs-vote-actions",
button(
cls := "docs-vote-primary",
typ := "button",
phx.target(self),
on.click.to(self)(Msg.Vote),
"Vote"
),
button(
cls := "docs-vote-secondary",
typ := "button",
phx.target(self),
on.click.to(self)(Msg.Reset),
"Reset"
)
)
)
)
end VoteComponent
final class VotingComponentsExample
extends LiveView[VotingComponentsExample.Msg, VotingComponentsExample.Model]:
import VotingComponentsExample.*
def mount(ctx: MountContext): Task[Model] = ZIO.succeed(Model.initial)
def handleMessage(model: Model, ctx: MessageContext) =
case Msg.ComponentReported(id, votes) =>
ZIO.succeed(
model.copy(status = s"$id reported $votes vote${if votes == 1 then "" else "s"}.")
)
case Msg.UpdateScalaProps =>
val revision = model.scalaRevision + 1
ctx.components
.sendUpdate(ScalaVote, scalaProps(revision, model.resetEpoch)).as(
model
.copy(scalaRevision = revision, status = s"Parent sent Scala props revision $revision.")
)
case Msg.Reset =>
ZIO.succeed(Model.initial.copy(resetEpoch = model.resetEpoch + 1))
override def view(model: Signal[Model]): HtmlElement[Msg] =
div(
cls := "docs-voting-components",
sectionTag(
cls := "docs-vote-parent",
aria.label := "Parent LiveView state",
div(
p(cls := "docs-vote-kicker", "Parent-owned state"),
p(
dataAttr("vote-status") := "",
role := "status",
aria.live := "polite",
model.map(_.status)
)
),
button(typ := "button", on.click(Msg.UpdateScalaProps), "Parent updates Scala props")
),
div(
cls := "docs-vote-grid",
ScalaVote.render(
model.map(model => scalaProps(model.scalaRevision, model.resetEpoch)),
outputToMessage
),
ZioVote.render(
model.map(model =>
VoteComponent.Props(
"zio-vote",
"ZIO ecosystem",
"A second stable instance proves that models and output attribution stay isolated.",
0,
model.resetEpoch
)
),
outputToMessage
)
)
)
private def outputToMessage(output: VoteComponent.Output): Msg = output match
case VoteComponent.Output.VoteChanged(id, votes) => Msg.ComponentReported(id, votes)
end VotingComponentsExample
object VotingComponentsExample:
final case class Model(scalaRevision: Int, resetEpoch: Int, status: String)
object Model:
val initial = Model(0, 0, "No component has reported a vote.")
enum Msg:
case ComponentReported(id: String, votes: Int)
case UpdateScalaProps
case Reset
private val ScalaVote = component(VoteComponent, "scala-vote")
private val ZioVote = component(VoteComponent, "zio-vote")
private def scalaProps(revision: Int, resetEpoch: Int) =
VoteComponent.Props(
"scala-vote",
"Scala language",
"Parent updates change these props while preserving the component's local vote count.",
revision,
resetEpoch
)View source (documentation/site/src/scalive/docs/examples/VotingComponentsExample.scala:8-171)
Result
ConnectedReconnectingRead-onlyParent-owned state
No component has reported a vote.
Component-local state
Scala language
Parent updates change these props while preserving the component's local vote count.
Component-local state
ZIO ecosystem
A second stable instance proves that models and output attribution stay isolated.
Disconnected. Controls resume after reconnection.
Related guidance: build stateful components and communicate with their owner.