Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Main method launcher #198 #200

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/.tyrian-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.6.2
0.6.3-SNAPSHOT
9 changes: 9 additions & 0 deletions examples/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -190,6 +198,7 @@ lazy val exampleProjects: List[String] =
"http",
"http4sdom",
"indigo",
"mainlauncher",
"mario",
"nonpm",
"subcomponents",
Expand Down
19 changes: 19 additions & 0 deletions examples/main-launcher/README.md
Original file line number Diff line number Diff line change
@@ -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/)
16 changes: 16 additions & 0 deletions examples/main-launcher/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<title>Main Launcher Example</title>
<script type="module" src="./target/scala-3.2.2/main-launcher-fastopt.js"></script>
</head>

<body>
<h1>Main launcher example</h1>
<div data-tyrian-app="CounterApp" data-tyrian-flag-initial-counter="42"></div>
<div data-tyrian-app="ChatApp" data-tyrian-flag-initial-message="Hello"></div>
</body>

</html>
10 changes: 10 additions & 0 deletions examples/main-launcher/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
43 changes: 43 additions & 0 deletions examples/main-launcher/src/main/scala/example/ChatApp.scala
Original file line number Diff line number Diff line change
@@ -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]:
davesmith00000 marked this conversation as resolved.
Show resolved Hide resolved

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

46 changes: 46 additions & 0 deletions examples/main-launcher/src/main/scala/example/CounterApp.scala
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions examples/main-launcher/src/main/scala/example/Main.scala
Original file line number Diff line number Diff line change
@@ -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
)
}
davesmith00000 marked this conversation as resolved.
Show resolved Hide resolved
8 changes: 8 additions & 0 deletions tyrian-io/src/main/scala/tyrian/TyrianApp.scala
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
8 changes: 8 additions & 0 deletions tyrian-zio/src/main/scala/tyrian/TyrianApp.scala
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
49 changes: 49 additions & 0 deletions tyrian/js/src/main/scala/tyrian/TyrianAppF.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
})
davesmith00000 marked this conversation as resolved.
Show resolved Hide resolved

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