diff --git a/examples/.tyrian-version b/examples/.tyrian-version
index b6160487..7d06ba07 100644
--- a/examples/.tyrian-version
+++ b/examples/.tyrian-version
@@ -1 +1 @@
-0.6.2
+0.6.3-SNAPSHOT
diff --git a/examples/build.sbt b/examples/build.sbt
index df2bfd7f..ff0ae43c 100644
--- a/examples/build.sbt
+++ b/examples/build.sbt
@@ -123,6 +123,14 @@ lazy val indigo =
)
)
+lazy val mainlauncher =
+ (project in file("main-launcher"))
+ .enablePlugins(ScalaJSPlugin)
+ .settings(commonSettings: _*)
+ .settings(name := "main-launcher",
+ Compile / mainClass := Some("example.Main"),
+ scalaJSUseMainModuleInitializer := true)
+
lazy val mario =
(project in file("mario"))
.enablePlugins(ScalaJSPlugin)
@@ -190,6 +198,7 @@ lazy val exampleProjects: List[String] =
"http",
"http4sdom",
"indigo",
+ "mainlauncher",
"mario",
"nonpm",
"subcomponents",
diff --git a/examples/main-launcher/README.md b/examples/main-launcher/README.md
new file mode 100644
index 00000000..3520e722
--- /dev/null
+++ b/examples/main-launcher/README.md
@@ -0,0 +1,19 @@
+# Tyrian main launcher example
+
+This is a minimal working project setup to run the main launcher example.
+
+To run the program in a browser you will need to have yarn (or npm) installed.
+
+On first run:
+
+```sh
+yarn install
+```
+
+and from then on
+
+```sh
+yarn start
+```
+
+Then navigate to [http://localhost:1234/](http://localhost:1234/)
diff --git a/examples/main-launcher/index.html b/examples/main-launcher/index.html
new file mode 100644
index 00000000..f499f62c
--- /dev/null
+++ b/examples/main-launcher/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Main Launcher Example
+
+
+
+
+ Main launcher example
+
+
+
+
+
diff --git a/examples/main-launcher/package.json b/examples/main-launcher/package.json
new file mode 100644
index 00000000..e0bd4ba5
--- /dev/null
+++ b/examples/main-launcher/package.json
@@ -0,0 +1,10 @@
+{
+ "scripts": {
+ "start": "parcel index.html --no-cache --dist-dir dist --log-level info",
+ "build": "parcel build index.html --dist-dir dist --log-level info"
+ },
+ "devDependencies": {
+ "parcel": "^2.1.0",
+ "process": "^0.11.10"
+ }
+}
diff --git a/examples/main-launcher/src/main/scala/example/ChatApp.scala b/examples/main-launcher/src/main/scala/example/ChatApp.scala
new file mode 100644
index 00000000..f209b2b6
--- /dev/null
+++ b/examples/main-launcher/src/main/scala/example/ChatApp.scala
@@ -0,0 +1,43 @@
+package example
+
+import cats.effect.IO
+import tyrian.Html.*
+import tyrian.*
+
+import scala.scalajs.js
+import scala.scalajs.js.annotation.*
+
+object ChatApp extends TyrianApp[ChatAppMsg, ChatAppModel]:
+
+ def init(flags: Map[String, String]): (ChatAppModel, Cmd[IO, ChatAppMsg]) =
+ val initialChat = flags.get("InitialMessage").getOrElse("")
+ (ChatAppModel(chatInput = initialChat, messages = Seq()), Cmd.None)
+
+ def update(model: ChatAppModel): ChatAppMsg => (ChatAppModel, Cmd[IO, ChatAppMsg]) =
+ case ChatInput(input) => (model.copy(chatInput = input), Cmd.None)
+ case SendChat() => (model.copy(chatInput = "", messages = model.messages :+ model.chatInput), Cmd.None)
+ case NoOp() => (model, Cmd.None)
+
+ def view(model: ChatAppModel): Html[ChatAppMsg] =
+ div(
+ ul()(
+ for { message <- model.messages.toList }
+ yield li()(message)
+ ),
+ input(onInput(ChatInput.apply), value := model.chatInput),
+ button(onClick(SendChat()))("Send Chat")
+ )
+
+ def router: Location => ChatAppMsg =
+ _ => NoOp()
+
+ def subscriptions(model: ChatAppModel): Sub[IO, ChatAppMsg] =
+ Sub.None
+
+case class ChatAppModel(chatInput: String, messages: Seq[String])
+
+sealed abstract class ChatAppMsg
+case class NoOp() extends ChatAppMsg
+case class ChatInput(input: String) extends ChatAppMsg
+case class SendChat() extends ChatAppMsg
+
diff --git a/examples/main-launcher/src/main/scala/example/CounterApp.scala b/examples/main-launcher/src/main/scala/example/CounterApp.scala
new file mode 100644
index 00000000..603933bc
--- /dev/null
+++ b/examples/main-launcher/src/main/scala/example/CounterApp.scala
@@ -0,0 +1,46 @@
+package example
+
+import cats.effect.IO
+import tyrian.Html.*
+import tyrian.*
+
+import scala.scalajs.js.annotation.*
+import scala.util.Try
+
+object CounterApp extends TyrianApp[Msg, Model]:
+
+ def init(flags: Map[String, String]): (Model, Cmd[IO, Msg]) =
+ val initialValue: Option[Int] = for {
+ initialCounter <- flags.get("InitialCounter")
+ initialCounterInt <- Try(initialCounter.toInt).toOption
+ } yield initialCounterInt
+ (Model(initialValue.getOrElse(0)), Cmd.None)
+
+ def update(model: Model): Msg => (Model, Cmd[IO, Msg]) =
+ case Msg.Increment => (model + 1, Cmd.None)
+ case Msg.Decrement => (model - 1, Cmd.None)
+ case Msg.NoOp => (model, Cmd.None)
+
+ def view(model: Model): Html[Msg] =
+ div(
+ button(onClick(Msg.Decrement))("-"),
+ div(model.toString),
+ button(onClick(Msg.Increment))("+")
+ )
+
+ def router: Location => Msg =
+ _ => Msg.NoOp
+
+ def subscriptions(model: Model): Sub[IO, Msg] =
+ Sub.None
+
+opaque type Model = Int
+object Model:
+ def apply(value: Int): Model = value
+
+ extension (i: Model)
+ def +(other: Int): Model = i + other
+ def -(other: Int): Model = i - other
+
+enum Msg:
+ case Increment, Decrement, NoOp
diff --git a/examples/main-launcher/src/main/scala/example/Main.scala b/examples/main-launcher/src/main/scala/example/Main.scala
new file mode 100644
index 00000000..a726eb14
--- /dev/null
+++ b/examples/main-launcher/src/main/scala/example/Main.scala
@@ -0,0 +1,13 @@
+package example
+
+import cats.effect.IO
+import tyrian.Html.*
+import tyrian.*
+
+object Main {
+ def main(args: Array[String]): Unit =
+ TyrianApp.onLoad(
+ "CounterApp" -> CounterApp,
+ "ChatApp" -> ChatApp
+ )
+}
\ No newline at end of file
diff --git a/tyrian-io/src/main/scala/tyrian/TyrianApp.scala b/tyrian-io/src/main/scala/tyrian/TyrianApp.scala
index 89d7501f..4b69ff27 100644
--- a/tyrian-io/src/main/scala/tyrian/TyrianApp.scala
+++ b/tyrian-io/src/main/scala/tyrian/TyrianApp.scala
@@ -1,6 +1,7 @@
package tyrian
import cats.effect.IO
+import cats.effect.kernel.Async
import cats.effect.kernel.Resource
import cats.effect.unsafe.implicits.global
import tyrian.TyrianAppF
@@ -13,3 +14,10 @@ trait TyrianApp[Msg, Model] extends TyrianAppF[IO, Msg, Model]:
val run: Resource[IO, TyrianRuntime[IO, Model, Msg]] => Unit =
_.map(_.start()).useForever.unsafeRunAndForget()
+
+object TyrianApp:
+ def onLoad(appDirectory: (String, TyrianAppF[IO, _, _])*): Unit =
+ TyrianAppF.onLoad(appDirectory: _*)
+
+ def launch(appDirectory: Map[String, TyrianAppF[IO, _, _]]): Unit =
+ TyrianAppF.launch(appDirectory)
diff --git a/tyrian-zio/src/main/scala/tyrian/TyrianApp.scala b/tyrian-zio/src/main/scala/tyrian/TyrianApp.scala
index 591bda77..c2fcfe57 100644
--- a/tyrian-zio/src/main/scala/tyrian/TyrianApp.scala
+++ b/tyrian-zio/src/main/scala/tyrian/TyrianApp.scala
@@ -1,6 +1,7 @@
package tyrian
import cats.effect.Async
+import cats.effect.kernel.Async
import cats.effect.kernel.Resource
import tyrian.TyrianAppF
import tyrian.runtime.TyrianRuntime
@@ -20,3 +21,10 @@ trait TyrianApp[Msg, Model](using Async[Task]) extends TyrianAppF[Task, Msg, Mod
Unsafe.unsafe { implicit unsafe =>
runtime.unsafe.run(runnable).getOrThrowFiberFailure()
}
+
+object TyrianApp:
+ def onLoad(appDirectory: (String, TyrianAppF[Task, _, _])*)(using Async[Task]): Unit =
+ TyrianAppF.onLoad(appDirectory: _*)
+
+ def launch(appDirectory: Map[String, TyrianAppF[Task, _, _]])(using Async[Task]): Unit =
+ TyrianAppF.launch(appDirectory)
diff --git a/tyrian/js/src/main/scala/tyrian/TyrianAppF.scala b/tyrian/js/src/main/scala/tyrian/TyrianAppF.scala
index 497d816d..e32f1aee 100644
--- a/tyrian/js/src/main/scala/tyrian/TyrianAppF.scala
+++ b/tyrian/js/src/main/scala/tyrian/TyrianAppF.scala
@@ -2,11 +2,14 @@ package tyrian
import cats.effect.kernel.Async
import cats.effect.kernel.Resource
+import org.scalajs.dom.DocumentReadyState
import org.scalajs.dom.Element
+import org.scalajs.dom.HTMLElement
import org.scalajs.dom.document
import org.scalajs.dom.window
import tyrian.runtime.TyrianRuntime
+import scala.scalajs.js.Promise
import scala.scalajs.js.annotation.*
/** The TyrianApp trait can be extended to conveniently prompt you for all the methods needed for a Tyrian app, as well
@@ -127,3 +130,49 @@ trait TyrianAppF[F[_]: Async, Msg, Model]:
case None =>
throw new Exception(s"Missing Element! Could not find an element with id '$containerId' on the page.")
+
+object TyrianAppF:
+ /** Launch app instances after DOMContentLoaded.
+ */
+ def onLoad[F[_] : Async](appDirectory: Map[String, TyrianAppF[F, _, _]]): Unit =
+ val documentReady = new Promise((resolve, _reject) => {
+ document.addEventListener("DOMContentLoaded", _ => {
+ resolve(())
+ })
+ if (document.readyState != DocumentReadyState.loading) {
+ resolve(())
+ }
+ })
+ documentReady.`then`(_ => {
+ launch[F](appDirectory)
+ })
+
+ def onLoad[F[_] : Async](appDirectory: (String, TyrianAppF[F, _, _])*): Unit =
+ onLoad(appDirectory.toMap)
+
+ /** Find data-tyrian-app HTMLElements and launch corresponding TyrianAppF instances
+ */
+ def launch[F[_] : Async](appDirectory: Map[String, TyrianAppF[F, _, _]]): Unit =
+ for {
+ element <- document.querySelectorAll("[data-tyrian-app]")
+ } yield {
+ val tyrianAppElement = element.asInstanceOf[HTMLElement]
+ val tyrianAppName = tyrianAppElement.dataset.get("tyrianApp")
+ val appSupplierOption = for {
+ appName <- tyrianAppName
+ appSupplier <- appDirectory.get(appName)
+ } yield appSupplier
+ appSupplierOption match
+ case Some(appSupplier) =>
+ appSupplier.launch(tyrianAppElement, appElementFlags(tyrianAppElement))
+ case _ =>
+ println(s"Could not find an app entry for ${tyrianAppName.getOrElse("")}")
+ }
+
+ private def appElementFlags(tyrianAppElement: HTMLElement): Map[String,String] =
+ val appFlags = for {
+ (dataAttr, attrValue) <- tyrianAppElement.dataset
+ if dataAttr.startsWith("tyrianFlag")
+ flagName = dataAttr.replaceFirst("^tyrianFlag", "")
+ } yield (flagName, attrValue)
+ appFlags.toMap
\ No newline at end of file