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 3 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-app-flag-initial-counter="42"></div>
<div data-tyrian-app="ChatApp" data-tyrian-app-flag-initial-message="Hello"></div>
davesmith00000 marked this conversation as resolved.
Show resolved Hide resolved
</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 NavigateTo() => (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 =
_ => NavigateTo()

def subscriptions(model: ChatAppModel): Sub[IO, ChatAppMsg] =
Sub.None

case class ChatAppModel(chatInput: String, messages: Seq[String])

sealed abstract class ChatAppMsg
case class NavigateTo() 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.NavigateTo => (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.NavigateTo
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I did this correctly... just made the update do nothing

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfectly valid, although I'd rename the event to Msg.Ignore or Msg.NoOp or something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is another example taken from the IndigoSandbox object, just if you're interested. This one ignores internal links but follows external ones.

  def router: Location => Msg = Routing.externalOnly(Msg.NoOp, Msg.FollowLink(_))

  def update(model: Model): Msg => (Model, Cmd[Task, Msg]) =
    case Msg.NoOp =>
      (model, Cmd.None)

    case Msg.FollowLink(href) =>
      (model, Nav.loadUrl(href))

https://github.com/PurpleKingdomGames/tyrian/blob/main/indigo-sandbox/src/main/scala/example/IndigoSandbox.scala#L22


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, NavigateTo
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[F[_] : Async](appDirectory: (String, TyrianAppF[F, _, _])*): Unit =
TyrianAppF.onLoad(appDirectory: _*)

def launch[F[_] : Async](appDirectory: (String, TyrianAppF[F, _, _])*): Unit =
TyrianAppF.launch(appDirectory: _*)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In these versions, you can fix the type to IO and ZIO respectively, rather than F.

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[F[_] : Async](appDirectory: (String, TyrianAppF[F, _, _])*): Unit =
TyrianAppF.onLoad(appDirectory: _*)

def launch[F[_] : Async](appDirectory: (String, TyrianAppF[F, _, _])*): Unit =
TyrianAppF.launch(appDirectory: _*)
47 changes: 47 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,47 @@ 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: (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

/** Find data-tyrian-app HTMLElements and launch corresponding TyrianAppF instances
*/
def launch[F[_] : Async](appDirectory: (String, TyrianAppF[F, _, _])*): Unit =
val appMap = appDirectory.toMap
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 <- appMap.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("tyrianAppFlag")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this prefix a bit large / wordy? In html form it takes up quite a lot of space?
data-tyrian-app-flag-...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could change it to tyrian-flag or flag ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tyrian-flag seems nice?

flagName = dataAttr.replaceFirst("^tyrianAppFlag", "")
davesmith00000 marked this conversation as resolved.
Show resolved Hide resolved
} yield (flagName, attrValue)
appFlags.toMap