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

Frontend Routing #197

Merged
merged 24 commits into from
May 16, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a4e26ca
Move ready() implementation up to IO/ZIO level
davesmith00000 May 2, 2023
f70031f
Extending MultiPage give hash based routing
davesmith00000 May 2, 2023
5aa91aa
Make TyrianRoutedAppF methods private
davesmith00000 May 3, 2023
aec93e7
Popstate based routing
davesmith00000 May 5, 2023
88e1de8
Remove Navigation class
davesmith00000 May 6, 2023
6f5e8cd
Introduce a location class
davesmith00000 May 6, 2023
7933970
Add deprecation warning for TyrianApp
davesmith00000 May 6, 2023
b11420e
Add a random hash link to the sandbox
davesmith00000 May 6, 2023
7877cfc
Paranoid save - works, contains TODOs
davesmith00000 May 8, 2023
9b11491
Added an external link to the sandbox, which errors.
davesmith00000 May 8, 2023
847678d
Added broken Location tests, ready to fix!
davesmith00000 May 9, 2023
e327c46
WIP: LocationDetails
davesmith00000 May 12, 2023
eff8baf
LocationDetails path matching
davesmith00000 May 12, 2023
7e87ca0
LocationDetails works
davesmith00000 May 13, 2023
ce8c3bb
Location type working
davesmith00000 May 13, 2023
760e505
Clean up
davesmith00000 May 13, 2023
c180ac3
Improve sandbox example
davesmith00000 May 13, 2023
0e90fac
Only pushstate on internal links
davesmith00000 May 13, 2023
790fe84
Formatting
davesmith00000 May 13, 2023
8e8953f
Restore TyrianApp now includes routing requirement
davesmith00000 May 13, 2023
fc86ac4
Updating Routing
davesmith00000 May 13, 2023
ca077c7
Fixed scaladocs
davesmith00000 May 15, 2023
ce046a5
Added an `all` Router + more scaladocs
davesmith00000 May 16, 2023
fa65c96
Nav forward and back + scaladocs
davesmith00000 May 16, 2023
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
3 changes: 2 additions & 1 deletion indigo-sandbox/src/main/scala/example/IndigoSandbox.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package example
import example.game.MyAwesomeGame
import org.scalajs.dom.document
import tyrian.Html.*
import tyrian.SinglePage
import tyrian.*
import tyrian.cmds.Logger
import tyrian.cmds.Random
Expand All @@ -12,7 +13,7 @@ import zio.interop.catz.*
import scala.scalajs.js.annotation.*

@JSExportTopLevel("TyrianApp")
object IndigoSandbox extends TyrianApp[Msg, Model]:
object IndigoSandbox extends SinglePage[Msg, Model]:
davesmith00000 marked this conversation as resolved.
Show resolved Hide resolved

val gameDivId1: String = "my-game-1"
val gameDivId2: String = "my-game-2"
Expand Down
34 changes: 12 additions & 22 deletions sandbox/src/main/scala/example/Sandbox.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,17 @@ import scala.scalajs.js.annotation.*
import scalajs.js

@JSExportTopLevel("TyrianApp")
object Sandbox extends TyrianApp[Msg, Model]:
object Sandbox extends MultiPage[Msg, Model]:

// Here we just do a simple string match, but this could be a route matching
// lib like: https://github.com/sherpal/url-dsl
def router: String => Msg =
case "/#page2" => Msg.NavigateTo(Page.Page2)
case "/#page3" => Msg.NavigateTo(Page.Page3)
case "/#page4" => Msg.NavigateTo(Page.Page4)
case "/#page5" => Msg.NavigateTo(Page.Page5)
case "/#page6" => Msg.NavigateTo(Page.Page6)
case _ => Msg.NavigateTo(Page.Page1)

val hotReloadKey: String = "hotreload"

Expand All @@ -29,10 +39,6 @@ object Sandbox extends TyrianApp[Msg, Model]:
case Right(model) => Msg.OverwriteModel(model)
},
Logger.info(flags.toString),
Navigation.getLocationHash {
case Navigation.Result.CurrentHash(hash) => Msg.NavigateTo(Page.fromString(hash))
case _ => Msg.NavigateTo(Page.Page1)
},
LocalStorage.key(0) {
case LocalStorage.Result.Key(key) => Msg.Log("Found local storage key: " + key)
case _ => Msg.Log("No local storage enties found.")
Expand Down Expand Up @@ -187,7 +193,7 @@ object Sandbox extends TyrianApp[Msg, Model]:
(model.copy(tmpSaveData = content), Cmd.None)

case Msg.JumpToHomePage =>
(model, Navigation.setLocationHash(Page.Page1.toHash))
(model, setLocationHash(Page.Page1.toHash))

case Msg.NavigateTo(page) =>
(model.copy(page = page), Cmd.None)
Expand Down Expand Up @@ -569,7 +575,6 @@ object Sandbox extends TyrianApp[Msg, Model]:

Sub.Batch(
webSocketSubs,
Navigation.onLocationHashChange(hashChange => Msg.NavigateTo(Page.fromString(hashChange.newFragment))),
simpleSubs,
clockSub,
Sub.animationFrameTick("frametick") { t =>
Expand Down Expand Up @@ -694,21 +699,6 @@ enum Page:
case Page5 => "#page5"
case Page6 => "#page6"

object Page:
def fromString(pageString: String): Page =
pageString match
case "#page2" => Page2
case "page2" => Page2
case "#page3" => Page3
case "page3" => Page3
case "#page4" => Page4
case "page4" => Page4
case "#page5" => Page5
case "page5" => Page5
case "#page6" => Page6
case "page6" => Page6
case _ => Page1

object Model:
// val echoServer = "ws://ws.ifelse.io" // public echo server
val echoServer = "ws://localhost:8080/wsecho"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ package tyrian
import cats.effect.IO
import cats.effect.kernel.Resource
import cats.effect.unsafe.implicits.global
import org.scalajs.dom.Element
import org.scalajs.dom.document
import tyrian.TyrianAppF
import tyrian.runtime.TyrianRuntime

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
/** The MultiPage trait can be extended to conveniently prompt you for all the methods needed for a Tyrian app, as well
* as providing a number of standard app launching methods.
*/
trait TyrianApp[Msg, Model] extends TyrianAppF[IO, Msg, Model]:
trait MultiPage[Msg, Model] extends TyrianRoutedAppF[IO, Msg, Model]:

val run: Resource[IO, TyrianRuntime[IO, Model, Msg]] => Unit =
_.map(_.start()).useForever.unsafeRunAndForget()
19 changes: 19 additions & 0 deletions tyrian-io/src/main/scala/tyrian/SinglePage.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tyrian

import cats.effect.IO
import cats.effect.kernel.Resource
import cats.effect.unsafe.implicits.global
import org.scalajs.dom.Element
import org.scalajs.dom.document
import tyrian.TyrianAppF
import tyrian.runtime.TyrianRuntime

import scala.scalajs.js.annotation._

/** The SinglePage trait can be extended to conveniently prompt you for all the methods needed for a Tyrian app, as well
* as providing a number of standard app launching methods.
*/
trait SinglePage[Msg, Model] extends TyrianAppF[IO, Msg, Model]:

val run: Resource[IO, TyrianRuntime[IO, Model, Msg]] => Unit =
_.map(_.start()).useForever.unsafeRunAndForget()
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tyrian
import cats.effect.Async
import cats.effect.kernel.Resource
import cats.effect.unsafe.implicits.global
import org.scalajs.dom.Element
import org.scalajs.dom.document
import tyrian.TyrianAppF
import tyrian.runtime.TyrianRuntime
Expand All @@ -12,10 +13,10 @@ import zio.Unsafe

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
/** The MultiPage trait can be extended to conveniently prompt you for all the methods needed for a Tyrian app, as well
* as providing a number of standard app launching methods.
*/
trait TyrianApp[Msg, Model](using Async[Task]) extends TyrianAppF[Task, Msg, Model]:
trait MultiPage[Msg, Model](using Async[Task]) extends TyrianRoutedAppF[Task, Msg, Model]:

val run: Resource[Task, TyrianRuntime[Task, Model, Msg]] => Unit = res =>
val runtime = Runtime.default
Expand Down
27 changes: 27 additions & 0 deletions tyrian-zio/src/main/scala/tyrian/SinglePage.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package tyrian

import cats.effect.Async
import cats.effect.kernel.Resource
import cats.effect.unsafe.implicits.global
import org.scalajs.dom.Element
import org.scalajs.dom.document
import tyrian.TyrianAppF
import tyrian.runtime.TyrianRuntime
import zio.Runtime
import zio.Task
import zio.Unsafe

import scala.scalajs.js.annotation._

/** The SinglePage trait can be extended to conveniently prompt you for all the methods needed for a Tyrian app, as well
* as providing a number of standard app launching methods.
*/
trait SinglePage[Msg, Model](using Async[Task]) extends TyrianAppF[Task, Msg, Model]:

val run: Resource[Task, TyrianRuntime[Task, Model, Msg]] => Unit = res =>
val runtime = Runtime.default
val runnable = res.map(_.start()).useForever

Unsafe.unsafe { implicit unsafe =>
runtime.unsafe.run(runnable).getOrThrowFiberFailure()
}
75 changes: 75 additions & 0 deletions tyrian/js/src/main/scala/tyrian/TyrianRoutedAppF.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package tyrian

import cats.effect.kernel.Async
import cats.effect.kernel.Resource
import org.scalajs.dom.Element
import org.scalajs.dom.Location
import org.scalajs.dom.document
import org.scalajs.dom.window
import tyrian.runtime.TyrianRuntime

import scala.scalajs.js
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
* as providing a number of standard app launching methods.
*/
trait TyrianRoutedAppF[F[_]: Async, Msg, Model] extends TyrianAppF[F, Msg, Model]:

def router: String => Msg

def setLocationHash(newHash: String): Cmd[F, Nothing] =
Routing.setLocation(newHash)

private def _init(flags: Map[String, String]): (Model, Cmd[F, Msg]) =
val (m, cmd) = init(flags)
(m, Routing.getLocation[F, Msg](router) |+| cmd)

private def _update(model: Model): Msg => (Model, Cmd[F, Msg]) =
msg => update(model)(msg)

private def _view(model: Model): Html[Msg] =
view(model)

private def _subscriptions(model: Model): Sub[F, Msg] =
Routing.onLocationChange[F, Msg](router) |+| subscriptions(model)

override def ready(node: Element, flags: Map[String, String]): Unit =
run(
Tyrian.start(
node,
_init(flags),
_update,
_view,
_subscriptions,
MaxConcurrentTasks
)
)

object Routing:

private def locationToRoute(loc: Location): String =
val origin = loc.origin.getOrElse("")
loc.toString.replaceFirst(origin, "")
davesmith00000 marked this conversation as resolved.
Show resolved Hide resolved

def onLocationChange[F[_]: Async, Msg](router: String => Msg): Sub[F, Msg] =
Sub.Batch(
Sub.fromEvent("DOMContentLoaded", window) { _ =>
Option((locationToRoute andThen router)(window.location))
},
Sub.fromEvent("popstate", window) { _ =>
Option((locationToRoute andThen router)(window.location))
}
)

def getLocation[F[_]: Async, Msg](router: String => Msg): Cmd[F, Msg] =
val task =
Async[F].delay {
locationToRoute(window.location)
}
Cmd.Run(task, router)

def setLocation[F[_]: Async](newHash: String): Cmd[F, Nothing] =
Cmd.SideEffect {
window.location.hash = if newHash.startsWith("#") then newHash else "#" + newHash
davesmith00000 marked this conversation as resolved.
Show resolved Hide resolved
}