Entrypoint Example

An Entrypoint is a method on the client-side, that you must call in order to start your webapp.

A typical webapp will be served like this:

  <script type="text/javascript" src="/my-app.js"></script>
  <script type="text/javascript">
    MyExampleApp.start();
  </script>

Why?

Generating this stuff manually isn't a huge deal, but in this example we'll use the Entrypoint API for a few advantages:

Shared Definition

We'll start with our entrypoint definition, which is cross-compiled for JVM and JS.

package japgolly.webapputil.examples.entrypoint

import boopickle.DefaultBasic._
import japgolly.webapputil.boopickle._
import japgolly.webapputil.entrypoint._

object EntrypointExample {

  // The name of our app, as it will appear in the JS global namespace when loaded.
  // This is final because its referenced via @JSExportTopLevel on Frontend
  final val Name = "MyExampleApp"

  // In this example, our app will request the user's username as soon as it starts up.
  // The server will provide this to the client.
  final case class InitialData(username: String)

  // This is our codec typeclass from InitialData to binary and back
  implicit val picklerInitialData: Pickler[InitialData] =
    implicitly[Pickler[String]].xmap(InitialData.apply)(_.username)

  // Finally, our entrypoint definition.
  //
  // The Pickler[InitialData] we created above is pulled in implicitly.
  // (Note: Binary is just one supported format, and not at all a necessity.)
  //
  val defn = EntrypointDef[InitialData](Name)
}

Frontend

Next we'll create our Scala.js frontend.

package japgolly.webapputil.examples.entrypoint

import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
import japgolly.webapputil.entrypoint._
import japgolly.webapputil.examples.entrypoint.EntrypointExample.InitialData
import scala.scalajs.js.annotation.JSExportTopLevel

@JSExportTopLevel(EntrypointExample.Name) // Instruct Scala.js to make this available in
                                          // the global JS namespace.
object Frontend extends Entrypoint(EntrypointExample.defn) {

  // Because the line above extends Entrypoint, all we need to do now is implement a
  // run method that takes the decoded InitialData value.
  override def run(i: InitialData): Unit = {
    // Render a simple React component
    val reactApp = Component(i)
    reactApp.renderIntoDOM(`#root`) // `#root` is a helper to find DOM with id=root
  }

  val Component = ScalaComponent.builder[InitialData]
    .render_P { i => <.div(s"Hello @${i.username} and nice to meet you!") }
    .build
}

Backend

Because we initialise our webapp with a username, the backend server needs to generate different HTML depending on who the request is for.

A few important things are out of scope for this demo:

In our example below we simply focus on InitialData => Html.

package japgolly.webapputil.examples.entrypoint

import japgolly.webapputil.entrypoint._

// Here we demonstrate how a backend web server can generate HTML to have the client
// invoke the entrypoint and start the app.
object Backend {

  private val invoker = EntrypointInvoker(EntrypointExample.defn)

  // It's as simple as this: provide an input value, get HTML.
  //
  // You can expect to see something like this:
  //
  // <script type="text/javascript" src="data:application/javascript;base64,…"></script>
  //
  // which is morally equivalent to:
  //
  // <script>
  //   MyExampleApp(encoded(initialData))
  // </script>
  //
  // We'll look at this in more detail in the next section.
  //
  def generateHtml(i: EntrypointExample.InitialData): Html =
    invoker(i).toHtmlScriptTag

  // The same as above, except instead of having the invocation occur immediately,
  // it schedules it to run on window.onload.
  //
  // This is morally equivalent to:
  //
  // <script>
  //   window.onload = function() {
  //     MyExampleApp(encoded(initialData))
  //   }
  // </script>
  //
  // We'll look at this in more detail in the next section, too.
  //
  def generateHtmlToRunOnWindowLoad(i: EntrypointExample.InitialData): Html =
    invoker(Js.Wrapper.windowOnLoad, i).toHtmlScriptTag
}

Backend Test

The only real value in this test is that our Pickler[InitialData] serialises and deserialises correctly. Everything else is just to demonstrate how exactly how the generated HTML works.

package japgolly.webapputil.examples.entrypoint

import boopickle._
import japgolly.microlibs.testutil.TestUtil._
import japgolly.univeq.UnivEq
import japgolly.webapputil.binary.BinaryData
import utest._

object BackendTest extends TestSuite {
  import EntrypointExample.InitialData

  // Declare that == is fine for testing equality of InitialData instances
  private implicit def univEqInitialData: UnivEq[InitialData] = UnivEq.derive

  // Sample data
  private val initData = InitialData(username = "someone123")

  // The base64 encoding of `initData` after binary serialisation
  private val initData64 = "CnNvbWVvbmUxMjM="

  // Helper to parse BinaryData into InitialData
  private def deserialiseInitialData(b: BinaryData): InitialData =
    UnpickleImpl(EntrypointExample.picklerInitialData).fromBytes(b.unsafeByteBuffer)

  override def tests = Tests {

    // =================================================================================
    // Verify the initData64 deserialises to initData
    "pickler" - assertEq(
      deserialiseInitialData(BinaryData.fromBase64(initData64)),
      initData)

    // =================================================================================
    // Let's start with our Backend.generateHtml() method
    "generateHtml" - {

      // Verify the total HTML output
      val js64 = "TXlFeGFtcGxlQXBwLm0oIkNuTnZiV1Z2Ym1VeE1qTT0iKQ=="
      assertEq(
        Backend.generateHtml(initData).asString,
        s"""<script type="text/javascript" src="data:application/javascript;base64,$js64"></script>""")

      // Verify the JS after base64 decoding
      assertEq(
        BinaryData.fromBase64(js64).toStringAsUtf8,
        s"""MyExampleApp.m("$initData64")""")
    }

    // =================================================================================
    // As above, but the RunOnWindowLoad variant
    "generateHtmlToRunOnWindowLoad" - {

      // Verify the total HTML output
      val js64 = "d2luZG93Lm9ubG9hZD1mdW5jdGlvbigpe015RXhhbXBsZUFwcC5tKCJDbk52YldWdmJtVXhNak09Iil9Ow=="
      assertEq(
        Backend.generateHtmlToRunOnWindowLoad(initData).asString,
        s"""<script type="text/javascript" src="data:application/javascript;base64,$js64"></script>""")

      // Verify the JS after base64 decoding
      assertEq(
        BinaryData.fromBase64(js64).toStringAsUtf8,
        s"""window.onload=function(){MyExampleApp.m("$initData64")};""")
    }
  }
}