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