Last updated on
counter
Example
This example shows a simple counter webapp built using the webapp-lib
.
An example webapp is implemented in 4 parts:
Counter Model (types)
Those include the definition of state, events and view.
package apps.counter
case class View(value: Int)
case class State(value: Int)
enum Event:
case Increment
apps/shared/src/main/scala/apps/counter/types.scala
Counter wire
The serialization and deserialization of the events and view.
package apps.counter
import cs214.webapp.*
import cs214.webapp.DecodingException
import scala.util.{Failure, Success, Try}
object Wire extends AppWire[Event, View]:
import Event.*
import View.*
import ujson.*
override object eventFormat extends WireFormat[Event]:
override def encode(event: Event): Value =
event match
case Increment =>
Obj("type" -> "Increment")
override def decode(js: Value): Try[Event] =
Try:
js("type").str match
case "Increment" =>
Increment
case _ =>
throw DecodingException(f"Invalid counter event $js")
override object viewFormat extends WireFormat[View]:
override def encode(v: View): Value =
Obj("value" -> v.value)
override def decode(js: Value): Try[View] =
Try:
View(js("value").num.toInt)
apps/shared/src/main/scala/apps/counter/Wire.scala
Counter logic
The logic of the counter – a transition system that updates the state based on the events and can project a state into a view.
package apps
package counter
import cs214.webapp.*
import cs214.webapp.server.{StateMachine}
import scala.util.{Failure, Success, Try}
class Logic extends StateMachine[Event, State, View]:
import Action.*
override val appInfo: AppInfo = AppInfo(
id = "counter",
name = "Counter",
description = "A simple counter app that increments a counter by one.",
year = 2024
)
override val wire = counter.Wire
override def init(clients: Seq[UserId]): State = State(0)
override def transition(state: State)(userId: UserId, event: Event): Try[Seq[Action[State]]] =
Try:
event match
case Event.Increment =>
Seq(Render(State(state.value + 1)))
override def project(state: State)(userId: UserId): View =
View(state.value)
apps/jvm/src/main/scala/apps/counter/Logic.scala
Counter UI
The UI representation of the counter view.
package apps
package counter
import apps.counter.*
import cs214.webapp.*
import cs214.webapp.client.*
import cs214.webapp.client.graphics.{HTMLAttribute, TextClientAppInstance, TextSegment, MouseEvent}
import org.scalajs.dom
import scalatags.JsDom.all.*
import scala.scalajs.js.annotation.JSExportTopLevel
@JSExportTopLevel("counter")
object TextUI extends WSClientApp:
def appId: String = "counter"
def uiId: UIId = "text"
def init(userId: UserId, sendMessage: ujson.Value => Unit, target: dom.Element): ClientAppInstance =
TextUIInstance(userId, sendMessage, target)
class TextUIInstance(userId: UserId, sendMessage: ujson.Value => Unit, target: dom.Element)
extends TextClientAppInstance[Event, View](userId, sendMessage, target):
override val wire = counter.Wire
override def handleTextInput(view: View, text: String): Option[Event] = text match
case "increment" | "i" => Some(Event.Increment)
case _ => None
override def renderView(userId: UserId, view: View): Vector[TextSegment] =
Vector(
TextSegment(text = "Counter\n", modifiers = cls := "bold"),
TextSegment("Click the number to increment it!\n"),
TextSegment(
view.value.toString,
onMouseEvent = Some {
case MouseEvent.Click(_) => sendEvent(Event.Increment)
case _ => ()
},
modifiers = cls := "bold clickable huge"
)
)
override def css: String =
"""|.bold {
| font-weight: bold;
|}
|.clickable {
| cursor: pointer;
|}
|.huge {
| font-size: 2em;
|}
|""".stripMargin
apps/js/src/main/scala/apps/counter/TextUI.scala