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 all 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
29 changes: 22 additions & 7 deletions indigo-sandbox/src/main/scala/example/IndigoSandbox.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,18 @@ object IndigoSandbox extends TyrianApp[Msg, Model]:
val gameId1: IndigoGameId = IndigoGameId("reverse")
val gameId2: IndigoGameId = IndigoGameId("combine")

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

def init(flags: Map[String, String]): (Model, Cmd[Task, Msg]) =
(Model.init, Cmd.Emit(Msg.StartIndigo))

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

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

case Msg.NewRandomInt(i) =>
(model.copy(randomNumber = i), Cmd.None)

Expand Down Expand Up @@ -90,6 +98,11 @@ object IndigoSandbox extends TyrianApp[Msg, Model]:

div(
div(hidden(false))("Random number: " + model.randomNumber.toString),
div(
a(href := "/another-page")("Internal link (will be ignored)"),
br,
a(href := "http://tyrian.indigoengine.io/")("Tyrian website")
),
div(id := gameDivId1)(),
div(id := gameDivId2)(),
div(
Expand Down Expand Up @@ -122,13 +135,15 @@ object IndigoSandbox extends TyrianApp[Msg, Model]:
)

enum Msg:
case NewContent(content: String) extends Msg
case Insert extends Msg
case Remove extends Msg
case Modify(i: Int, msg: Counter.Msg) extends Msg
case StartIndigo extends Msg
case IndigoReceive(msg: String) extends Msg
case NewRandomInt(i: Int) extends Msg
case NewContent(content: String)
case Insert
case Remove
case Modify(i: Int, msg: Counter.Msg)
case StartIndigo
case IndigoReceive(msg: String)
case NewRandomInt(i: Int)
case FollowLink(href: String)
case NoOp

object Counter:

Expand Down
79 changes: 49 additions & 30 deletions sandbox/src/main/scala/example/Sandbox.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,32 @@ import tyrian.websocket.*

import scala.concurrent.duration.*
import scala.scalajs.js.annotation.*
import scala.util.Random

import scalajs.js

@JSExportTopLevel("TyrianApp")
object Sandbox extends TyrianApp[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: Location => Msg =
case loc: Location.Internal =>
loc.pathName match
case "/" => Msg.NavigateTo(Page.Page1)
case "/page1" => Msg.NavigateTo(Page.Page1)
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 _ =>
println("Unknown route: " + loc.url)
Msg.NoOp

case loc: Location.External =>
Msg.NavigateToUrl(loc.href)

val hotReloadKey: String = "hotreload"

def init(flags: Map[String, String]): (Model, Cmd[IO, Msg]) =
Expand All @@ -29,10 +49,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 All @@ -45,6 +61,9 @@ object Sandbox extends TyrianApp[Msg, Model]:
(Model.init, cmds)

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

case Msg.AddFruit =>
(model.copy(fruit = Fruit(model.fruitInput, false) :: model.fruit), Cmd.None)

Expand Down Expand Up @@ -187,11 +206,14 @@ object Sandbox extends TyrianApp[Msg, Model]:
(model.copy(tmpSaveData = content), Cmd.None)

case Msg.JumpToHomePage =>
(model, Navigation.setLocationHash(Page.Page1.toHash))
(model.copy(page = Page.Page1), Nav.pushUrl(Page.Page1.toUrlPath))

case Msg.NavigateTo(page) =>
(model.copy(page = page), Cmd.None)

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

case Msg.TakeSnapshot =>
(model, HotReload.snapshot(hotReloadKey, model, Model.encode))

Expand Down Expand Up @@ -294,8 +316,19 @@ object Sandbox extends TyrianApp[Msg, Model]:
val navItems =
Page.values.toList.map { pg =>
if pg == model.page then li(style := CSS.`font-family`("sans-serif"))(pg.toNavLabel)
else li(style := CSS.`font-family`("sans-serif"))(a(href := pg.toHash)(pg.toNavLabel))
}
else
li(style := CSS.`font-family`("sans-serif")) {
a(href := pg.toUrlPath)(pg.toNavLabel)
}
} ++
List(
li(style := CSS.`font-family`("sans-serif")) {
a(href := "#foo" + Random.nextInt())("Random link")
},
li(style := CSS.`font-family`("sans-serif")) {
a(href := "https://tyrian.indigoengine.io/")("Tyrian's Website")
}
)

val counters = model.components.zipWithIndex.map { case (c, i) =>
Counter.view(c).map(msg => Msg.Modify(i, msg))
Expand Down Expand Up @@ -569,7 +602,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 All @@ -594,6 +626,7 @@ enum Msg:
case ClearStorage(key: String)
case DataLoaded(data: String)
case NavigateTo(page: Page)
case NavigateToUrl(href: String)
case JumpToHomePage
case OverwriteModel(model: Model)
case TakeSnapshot
Expand All @@ -617,6 +650,7 @@ enum Msg:
case AddFruit
case UpdateFruitInput(input: String)
case ToggleFruitAvailability(name: String)
case NoOp

enum Status:
case Connecting
Expand Down Expand Up @@ -685,29 +719,14 @@ enum Page:
case Page5 => "Http"
case Page6 => "Form"

def toHash: String =
def toUrlPath: String =
this match
case Page1 => "#page1"
case Page2 => "#page2"
case Page3 => "#page3"
case Page4 => "#page4"
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
case Page1 => "/page1"
case Page2 => "/page2"
case Page3 => "/page3"
case Page4 => "/page4"
case Page5 => "/page5"
case Page6 => "/page6"

object Model:
// val echoServer = "ws://ws.ifelse.io" // public echo server
Expand Down
3 changes: 0 additions & 3 deletions tyrian-io/src/main/scala/tyrian/TyrianApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ package tyrian
import cats.effect.IO
import cats.effect.kernel.Resource
import cats.effect.unsafe.implicits.global
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
* as providing a number of standard app launching methods.
*/
Expand Down
4 changes: 0 additions & 4 deletions tyrian-zio/src/main/scala/tyrian/TyrianApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@ package tyrian

import cats.effect.Async
import cats.effect.kernel.Resource
import cats.effect.unsafe.implicits.global
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 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.
*/
Expand Down
46 changes: 46 additions & 0 deletions tyrian/js/src/main/scala/tyrian/Location.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package tyrian

import cats.effect.kernel.Async
import cats.effect.kernel.Resource
import org.scalajs.dom.Element
import org.scalajs.dom.PopStateEvent
import org.scalajs.dom.document
import org.scalajs.dom.window

import scala.scalajs.js
import scala.scalajs.js.annotation._

sealed trait Location:
def locationDetails: LocationDetails
def isInternal: Boolean
def isExternal: Boolean
def href: String
def url: String

object Location:

final case class Internal(locationDetails: LocationDetails) extends Location:
export locationDetails.*
val isInternal: Boolean = true
val isExternal: Boolean = false

final case class External(locationDetails: LocationDetails) extends Location:
export locationDetails.*
val isInternal: Boolean = false
val isExternal: Boolean = true

/** Construct a Location from a given url, decides internal / external based on comparison with `currentLocation`
*/
def fromUrl(url: String, currentLocation: Location.Internal): Location =
val ld = LocationDetails.fromUrl(url)

if ld.protocol.isEmpty then Location.Internal(ld)
else if ld.origin == currentLocation.origin then Location.Internal(ld)
else Location.External(ld)

/** Location instances created from JS Location's are assumed to be internal links.
*/
def fromJsLocation(location: org.scalajs.dom.Location): Location.Internal =
Location.Internal(
LocationDetails.fromUrl(location.href)
)
Loading