lhns / http4s-spa   0.4.1

Apache License 2.0 GitHub

Helpers for building a http4s Single Page Application with Scala.js using ES-Modules and Import Maps

Scala versions: 3.x 2.13 2.12
Scala.js versions: 1.x

http4s-spa

build Release Notes Maven Central Apache License 2.0

Helpers for building a http4s Single Page Application with Scala.js using ES-Modules and Import Maps.

Features

  • Simple setup
  • Very customizable
  • Hot-reload using debug-mode in IntelliJ

build.sbt

libraryDependencies += "de.lhns" %% "http4s-spa" % "0.6.2"

Example Setup with scalajs-react

plugins.sbt

addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.1")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0")
addSbtPlugin("de.lolhens" % "sbt-scalajs-webjar" % "0.4.0")

build.sbt

val V = new {
  val http4s = "0.23.16"  
  val http4sSpa = "0.6.0"
  val scalajsDom = "2.0.0"
  val scalajsReact = "2.0.0"
}

lazy val frontend = project
  .enablePlugins(ScalaJSWebjarPlugin)
  .settings(
    libraryDependencies ++= Seq(
      "com.github.japgolly.scalajs-react" %%% "core-bundle-cats_effect" % V.scalajsReact,
      "com.github.japgolly.scalajs-react" %%% "extra" % V.scalajsReact,
      "org.scala-js" %%% "scalajs-dom" % V.scalajsDom,
    ),
    scalaJSLinkerConfig ~= {
      _.withModuleKind(ModuleKind.ESModule)
    },
    scalaJSUseMainModuleInitializer := true,
  )

lazy val frontendWebjar = frontend.webjar
  .settings(
    webjarAssetReferenceType := Some("http4s"),
    libraryDependencies += "org.http4s" %% "http4s-server" % V.http4s,
  )

lazy val server = project
  .enablePlugins(BuildInfoPlugin)
  .dependsOn(frontendWebjar)
  .settings(commonSettings)
  .settings(
    libraryDependencies ++= Seq(
      "de.lhns" %% "http4s-spa" % V.http4sSpa,
      "org.http4s" %% "http4s-circe" % V.http4s,
      "org.http4s" %% "http4s-dsl" % V.http4s,
      "org.http4s" %% "http4s-ember-server" % V.http4s,
    ),
  )

Server.scala

import cats.data.Kleisli
import cats.effect._
import cats.syntax.option._
import com.comcast.ip4s._
import de.lolhens.http4s.spa._
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.implicits._
import org.http4s.server.Server
import org.http4s.server.middleware.ErrorAction
import org.http4s.server.staticcontent.ResourceServiceBuilder
import org.http4s.{HttpApp, Uri}
import org.log4s.getLogger

import scala.concurrent.duration._

object Main extends IOApp {
  private val logger = getLogger

  private val app = SinglePageApp(
    title = "SPA Example",
    webjar = webjars.frontend.webjarAsset,
    dependencies = Seq(
      SpaDependencies.react17,
      SpaDependencies.bootstrap5,
      SpaDependencies.bootstrapIcons1,
      SpaDependencies.mainCss
    )
  )

  private val appController = SinglePageAppController[IO](
    mountPoint = Uri.Root,
    controller = Kleisli.pure(app),
    resourceServiceBuilder = ResourceServiceBuilder[IO]("/assets").some
  )

  override def run(args: List[String]): IO[ExitCode] =
    serverResource(
      SocketAddress(host"0.0.0.0", port"8080"),
      appController.toRoutes.orNotFound
    ).use(_ => IO.never)

  def serverResource[F[_]: Async](socketAddress: SocketAddress[Host], http: HttpApp[F]): Resource[F, Server] =
    EmberServerBuilder.default[F]
      .withHost(socketAddress.host)
      .withPort(socketAddress.port)
      .withHttpApp(ErrorAction.log(
        http = http,
        messageFailureLogAction = (t, msg) => Async[F].delay(logger.debug(t)(msg)),
        serviceErrorLogAction = (t, msg) => Async[F].delay(logger.error(t)(msg))
      ))
      .withShutdownTimeout(1.second)
      .build
}

Frontend.scala

import japgolly.scalajs.react.ScalaComponent
import japgolly.scalajs.react.ScalaComponent.BackendScope
import japgolly.scalajs.react.vdom.VdomElement
import japgolly.scalajs.react.vdom.html_<^._
import org.scalajs.dom

object Main {
  def main(args: Array[String]): Unit = {
    MainComponent.Component()
      .renderIntoDOM(dom.document.getElementById("root"))
  }
}

object MainComponent {
  class Backend($: BackendScope[Unit, Unit]) {
    def render: VdomElement = {
      val state = $.state.unsafeRunSync()

      <.div(
        ^.cls := "container my-4 d-flex flex-column",
        <.h1("Hello World"),
      )
    }
  }

  val Component =
    ScalaComponent.builder[Unit]
      .backend(new Backend(_))
      .render(_.backend.render)
      .build
}

License

This project uses the Apache 2.0 License. See the file called LICENSE.