Closed bmc08gt closed 3 months ago
Was able to kang the browser history from js integration into voyagers and get it partially working.
However GET requests for a screen/path don't resolve yet (push's update the history).
in VoyagerRoutingBuilder.kt
:
@KtorDsl
public fun Route.screen(body: suspend PipelineContext<Unit, ApplicationCall>.() -> Screen) {
handle {
screen {
body(this)
}
+ call.handleScreenTransition()
}
}
+ expect suspend fun ApplicationCall.handleScreenTransition()
in my jsMain
actual suspend fun ApplicationCall.handleScreenTransition() {
when (routeMethod) {
RouteMethod.Push -> {
window.history.pushState(
title = "routing",
url = uri,
data = serialize()
)
}
RouteMethod.Replace -> {
window.history.replaceState(
title = "routing",
url = uri,
data = serialize(),
)
}
RouteMethod.ReplaceAll -> {
while (true) {
window.history.replaceState(
title = "",
url = null,
data = null,
)
window.history.go(-1)
val forceBreak =
runCatching {
withTimeout(1_000) {
suspendCoroutine { continuation ->
window.onpopstate = { event ->
val state = event.state.deserialize()
continuation.resume(state == null)
}
}
}
}.getOrDefault(true)
if (forceBreak) {
break
}
}
window.history.replaceState(
title = "routing",
url = uri,
data = serialize(),
)
}
}
}
@Serializable
internal data class JavascriptRoutingState(
val routeMethod: String,
val name: String,
val uri: String,
val parameters: Map<String, List<String>>,
)
internal fun ApplicationCall.serialize(): String {
val state =
JavascriptRoutingState(
routeMethod = routeMethod.value,
name = name,
uri = uri,
parameters = parameters.toMap(),
)
return Json.encodeToString(state)
}
internal fun Any?.deserialize(): JavascriptRoutingState? =
when (this) {
is String -> toState()
else -> null
}
private fun String.toState(): JavascriptRoutingState? =
runCatching {
Json.decodeFromString<JavascriptRoutingState>(this)
}.getOrNull()
and in my mobileMain (custom group for ios/android)
actual suspend fun ApplicationCall.handleScreenTransition() {}
The Voyager
integration module has no support to web history and the version in javascript
module is experimental.
My knowledge in browser history is too limited. I made based on MDN documentation.
About your sample code it is correct. We need do on each voyager navigation a browser history update.
To solve starting from a GET call, it is not too simple because you need to tell the Voyager about the path. Also check for history to avoid clean it. Something like https://github.com/programadorthi/kotlin-routing/blob/c4c327159703a525478dd835591c5190e288cb6c/integration/javascript/js/src/dev/programadorthi/routing/javascript/JavascriptRoutingStateManager.kt#L63
Maybe doing:
fun main() {
val someLocker = ...
val router = routing (...) { ... }
VoyagerRouting(
routing = router,
content = {
CurrentScreen()
SideEffect {
someLocker.unLock()
}
}
)
// Is onpageshow called on browser or tab opened? I don't know.
window.onpageshow = {
someLocker.wait()
val location = window.location
// https://developer.mozilla.org/en-US/docs/Web/API/Location#examples
val path = location.pathname + location.search + location.hash
router.push|replace(path = path)
}
}
The
Voyager
integration module has no support to web history and the version injavascript
module is experimental. My knowledge in browser history is too limited. I made based on MDN documentation.About your sample code it is correct. We need do on each voyager navigation a browser history update.
To solve starting from a GET call, it is not too simple because you need to tell the Voyager about the path. Also check for history to avoid clean it. Something like
Maybe doing:
fun main() { val someLocker = ... val router = routing (...) { ... } VoyagerRouting( routing = router, content = { CurrentScreen() SideEffect { someLocker.unLock() } } ) // Is onpageshow called on browser or tab opened? I don't know. window.onpageshow = { someLocker.wait() val location = window.location // https://developer.mozilla.org/en-US/docs/Web/API/Location#examples val path = location.pathname + location.search + location.hash router.push|replace(path = path) } }
yep I actually updated the state manager for JS to match what the JS integration has more closely. I turned it into an expect/actual to make it work cross-platform.
internal expect object VoyagerRoutingStateManager {
fun init(routing: Routing, initialScreen: Screen)
suspend fun replaceAll(
call: ApplicationCall,
routing: Routing,
)
}
@Composable
public fun VoyagerRouting(
routing: Routing,
initialScreen: Screen,
disposeBehavior: NavigatorDisposeBehavior = NavigatorDisposeBehavior(),
onBackPressed: OnBackPressed = { true },
key: String = compositionUniqueId(),
content: NavigatorContent = { CurrentScreen() },
) {
CompositionLocalProvider(LocalVoyagerRouting provides routing) {
DisposableEffect(routing) {
VoyagerRoutingStateManager.init(routing, initialScreen)
onDispose {
routing.dispose()
}
}
Navigator(
screen = initialScreen,
disposeBehavior = disposeBehavior,
onBackPressed = onBackPressed,
key = key,
) { navigator ->
SideEffect {
routing.application.voyagerNavigator = navigator
}
content(navigator)
}
}
}
with JS being:
internal actual object VoyagerRoutingStateManager {
actual fun init(routing: Routing, initialScreen: Screen) {
// First time or page refresh we try continue from last state
val notified = routing.tryNotifyTheRoute(state = window.history.state)
resetOnPopStateEvent(routing)
if (!notified) {
val location = window.location
val path = location.pathname + location.search + location.hash
println(path)
routing.push(path = path)
}
}
actual suspend fun replaceAll(
call: ApplicationCall,
routing: Routing,
) {
while (true) {
window.history.replaceState(
title = "",
url = null,
data = null,
)
window.history.go(-1)
val forceBreak =
runCatching {
withTimeout(1_000) {
suspendCoroutine { continuation ->
window.onpopstate = { event ->
val state = event.state.deserialize()
continuation.resume(state == null)
}
}
}
}.getOrDefault(true)
if (forceBreak) {
break
}
}
window.history.replaceState(
title = "routing",
url = call.uri,
data = call.serialize(),
)
resetOnPopStateEvent(routing)
}
private fun resetOnPopStateEvent(routing: Routing) {
window.onpopstate = { event ->
routing.tryNotifyTheRoute(state = event.state)
}
}
private fun Routing.tryNotifyTheRoute(state: Any?): Boolean {
val javascriptState = state.deserialize() ?: return false
val call =
ApplicationCall(
application = application,
name = javascriptState.name,
uri = javascriptState.uri,
routeMethod = RouteMethod.parse(javascriptState.routeMethod),
parameters = parametersOf(javascriptState.parameters),
)
call.neglect = true
execute(call)
window.history.replaceState(
title = "routing",
url = call.uri,
data = call.serialize(),
)
push(javascriptState.uri)
return true
}
}
Nice. I updated the javascript
integration to support history modes. Hash /#/
, Html5 History and Memory.
Most of these behaviors I will export to voyager
integration too.
Checkout release 0.0.15.
Latest releases starting from 0.0.17 should have support. There is no support for WASM but it will be tracked in another issue.
Im setting up a compose multiplatform project using Voyager targeting web to start and im not seeing routes be appended to the URL when push()'ing them nor is the screen's defined by path/name navigatable via URL. Is there something i am missing in the config/setup or is my understanding of this incorrect?
pushing is done via
LocalVoyagerRouting.current.pushNamed(TeaserScreen.key)
orLocalVoyagerRouting.current.push(path ="/home")
I would expect
localhost:8080/home
to resolve while the server is running. (UI is updating appropriately just not browser paths).