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:
- Everything is DRY — no chance to accidentally call the wrong function
 
- The server can provide custom data to initialise our app — just need to provide a codec, ser/deser and JS plumbing handled automatically
 
- We avoid a typical AJAX call to initialise our app — faster and better experience for users
 
- HTML is generated for us — things like escaping handled automatically
 
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:
- how to retrieve a username depends on your app
 
- how to serve HTML depends on your choice of web server
 
- how to serve the frontend JS depends on your app and your choice of web server!
 
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")};""")
    }
  }
}