-
-
Notifications
You must be signed in to change notification settings - Fork 14
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
Feature request: simplify building of bidirectional routing to avoid Url -> Page -> Url cycle (especially for fragment-based) #23
Comments
Hi, you mentioned "A plain implementation like the following introduces a infinite cycle" – but it doesn't seem that you've included the code for it in the issue? If I understand correctly, the code you shared is that of your workaround, but I would like to see the code that's actually failing with the infinite loop. OTOH even without the workaround, I don't yet see why a combination of In general, yes, calling pushState when observing currentPageSignal would cause an infinite loop if there are no filters in the loop – but, that isn't any different than e.g. updating a Var from which a signal is derived, in the observer of that signal – that would also cause an infinite loop, because that's what the code is instructed to do. Is there a router-specific use case that needs to pushState in the observer of currentPageSignal? |
To clarify what I'm asking, I see your workaround code, but I don't readily understand how to make that code fail with an infinite loop by removing the workaround. So I'd like to see the plain code that tries to do the same thing as your code that uses the workaround, but fails, due to the infinite loop issue that the workaround fixes. |
@raquo sure thing! Here's an example type Page = Option[String]
import com.raquo.laminar.api.L.*
import com.raquo.laminar.api.L.given
import com.raquo.waypoint.*
import io.circe.syntax.given
val route = Route.onlyQuery[Page, Option[String]](
encode = identity,
decode = identity,
pattern = (root / endOfSegments) ? param[String]("q").?,
basePath = Route.fragmentBasePath
)
val router = new Router[Page](
routes = List(route),
getPageTitle = _ => "My Page",
serializePage = _.asJson.noSpaces,
deserializePage = pageStr =>
io.circe.parser
.parse(pageStr)
.flatMap(_.as[Page])
.toOption
.flatten
)(popStateEvents = windowEvents(_.onPopState), owner = unsafeWindowOwner)
val search = Var(initial = "")
val items = Vector("Foo", "Bar", "Baz", "Apple")
val content = div(
input(value <-- search.signal, onInput.mapToValue --> search.writer),
ol(
items.map(txt =>
li(display <-- search.signal.map(s => if txt.toLowerCase.contains(s.toLowerCase) then "block" else "none"), txt)
)*
),
router.currentPageSignal.map(_.getOrElse("")) --> search.writer,
//
// uncomment any one of the below to get a cycle:
//
// search.signal.map(Some(_).filterNot(_.isEmpty)) --> router.pushState,
//
// search.signal.changes.map(Some(_).filterNot(_.isEmpty)) --> router.pushState,
//
// search.signal.changes
// .debounce(1000)
// .map(s =>
// println(s"pushing $s") // no console error now, but there's still an infinite loop
// Some(s).filterNot(_.isEmpty)
// ) --> router.pushState,
emptyNode
)
def main(): Unit =
renderOnDomContentLoaded(org.scalajs.dom.document.querySelector("#appContainer"), content)
|
Maybe just |
Thanks, that helps me understand the use case. In your code, you have an infinite loop that can be concisely expressed as So, such loop breaking should probably be explicit, opt-in. Ideally we would use Airstream's built-in operators like Could we add a Looking at your code, the following stands out to me as a potentially good place to fix the issue conceptually: router.currentPageSignal.map(_.getOrElse("")) --> search.writer It should be instead: router.currentPageSignal.map(_.getOrElse("")) --> search.writerDistinct But, Hrm. Not super happy with any of that. Generalizing a bit in the other direction, I guess we could add some kind of Thoughts? Also – I do wonder if there is a valid use case for calling |
The most confusing part for me was that I like the idea of I know there's extension [A](v: Var[A]) // just to test it compiles, can be added as native methods
// the name inspired by `Map[A, B].updatedWith`
def updateWith[B](f: (A, B) => Option[A]): Observer[B] = Observer[B](onNext = b => f(v.now(), b).foreach(v.set))
def distinctWriter: Any = updateWith[A]((a, b) => Some(a).filterNot(b.==)) At the end, just curious, what could be any practical use of variable signals / writers to emit/process same values as previous? |
Yes, the name
Most use cases don't care one way or the other, but prior to 15.0.0 we've had problems with doing automatic See https://laminar.dev/blog/2023/03/22/laminar-v15.0.0#no-more-automatic--checks-in-signals
Yes, that's more or less how it should be implemented. Need to think about the name for More on the immediate subject matter, I just realized that |
II agree that it might have sense not to have What if |
You know, that's a very good idea. I think perhaps we could have syntax like: val distinctVar1 = Var(42).distinct
val distinctVar2 = Var(foo).distinctBy(_.id) And so on, matching Airstream's This syntax would provide a more flexible API to users, e.g. you could create an always-distinct Var like I did above, or have just one of the paths into the Var distinct, as below: val myVar = Var(42)
onClick.mapTo(1) --> myVar.distinct // these updates are distinct
onClick.mapTo(foo) --> myVar.updater(_ + _.size) // but these are not And in other cases we could still do Do you think this would sufficiently help out with your use case to not need your workaround, until we do a proper |
I've implemented .distinct operators for Vars in Airstream 17.2.0-M1. It's compatible with Airstream 17.1.0 and Laminar 17.0.0, so you can try it out already. New docs here |
@raquo thank you for the update, |
Rationale:
Router can be used in order for SPA user to share / bookmark URLs that correspond to specific page (app state), i.e. users should be able to just copy the browser url and get a reproducible result if it's opened somewhere else (new tab / browser / device). Also, users may manually edit the URL and we want the app to adjust the route.
Issues
A plain implementation like the following introduces a infinite cycle (Transaction max depth error in console) on any url change.
That means, even if pushed state is the same, it's not ignored by the underlying observers, continuing the loop.
router.currentPageSignal
to react either on initial url, or manual fragment part changes (browser doesn't reload the page in that case).router.pushState
(orreplaceState
, if we ignore back/forward history navigation)Workarounds
I end up with a wrapper adding external / internal change flag to the Page, only producing "external" when decoded or deserialized.
so then I can use it like
It would be nice if something serving the same purpose will be available out of the box.
I would be happy to help with PR in case some initial guidance were provided.
If I just missing something, I can help with adding a note to the documentation.
The text was updated successfully, but these errors were encountered: