-
-
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
A LazyImportSplitRender to import views dynamic. #24
Comments
That's a good problem to solve, thanks for the head start! I see how you did it, and that makes sense. Basically it's functional sugar for returning Although the I feel like the solution to this could be a bit higher up, in Airstream. For example, perhaps we should implement import com.raquo.waypoint.*
import com.raquo.laminar.api.L.*
val view = SplitRender[Page, Signal[Option[View]]](Routes.currentPageSignal.debugLog())
.collect[Login](_ => Val(Some(LoginView)))
.collectStatic(Register)(Val(Some(RegisterView())))
.collectStatic(Documentation)(Signal.dynamicImport(DocumentationView()))
.collectSignal[Dashboard](s => Signal.dynamicImport(DemoApp.myApp))
.flattenSwitch {
case None => Val(div("Loading..."))
case Some(viewSignal) => viewSignal
}
div(
child <-- view
) In this example, login and registration pages would be included in the same JS file, whereas documentation and dashboard pages would be loaded dynamically. I haven't compiled this, so there may be typos, but I'm pretty sure it should work. I think I prefer this approach over extending SplitRender for a few reasons:
But yes, my proposal is a bit more verbose. But more explicit and more universal. Tradeoffs... In the future, if / when I implement raquo/Laminar#157, we may be able to simply pass I need some time to think this over, and work on other things, but this problem is definitely something that should be solved in Waypoint. Let's keep this issue open to track it. Adding For 17.2 there's a slight complication that EventStream.fromJsPromise / Signal.fromJsPromise accept the promise by-value, instead of delaying its evaluation until the stream / signal is started, but I can still implement a lazy |
This means that in the future, Airstream's functionality will be used directly to replace SplitRender?
This is the reason why I used ImportWrapper to wrap js.dynamicImport. EventStream/Signal.dynamicImport is a good idea; how are you planning to define their return types, as Signal[A] or Signal[Option[A]]? Both have their own use cases: div(child <- EventStream.dynamicImport(view()))
// and
div(child <- EventStream.dynamicImportOpt(view()).map(_.getOrElse(div(a skeleton element)))
LazyImportSplitRender is implemented as you said, because what I had in mind was to simply replace it without adjusting the code that uses it. import elgca.core.utils.router.LazyImportSplitRender as SplitRender |
Yes. But, the new functionality is macro-based, so it's only for Scala 3. We won't be implementing it for Scala 2. So, I'm not going to throw out SplitRender yet, it will stay for now. If there's enough interest, I may even move SplitRender into Airstream, and rename / rearrange the methods so that the API looks pretty much like the new macros do, (except it will remain without exhaustivity checks – those are not possible without macros). But, only if people ask for it – as it stands, I have higher priority tasks in the queue.
Same as EventStream.fromJsPromise and Signal.fromJsPromise already do – EventStream.fromDynamic would return EventStream[A], and Signal.fromDynamic would return Signal[Option[A]] by default, or, if you provide the initial value as the second argument, then you'll get Signal[A]. Whenever you have an |
@elgca I tried adapting your code to make Signal.dynamicImport, but I can't get it to work. Let's ignore the URL routing part for now – if I just take this part of your code: sealed trait SignalWrapper[View] {
def signal: Signal[View]
}
object SignalWrapper {
@nowarn
inline def apply[View, LazyImport <: Boolean](view: View): SignalWrapper[Option[View]] = {
if constValue[LazyImport] then new LazyImportWrapper(() => js.dynamicImport(view))
else new StaticImportWrapper(Val(Some(view)))
}
}
class LazyImportWrapper[View](dynamicImport: () => js.Promise[View]) extends SignalWrapper[Option[View]]:
private var innerSignal: js.UndefOr[Signal[Option[View]]] = js.undefined
def signal: Signal[Option[View]] = {
if (innerSignal.isEmpty) {
innerSignal = Signal.fromJsPromise(dynamicImport())
}
innerSignal.get
}
end LazyImportWrapper
class StaticImportWrapper[View](viewVal: Val[Option[View]]) extends SignalWrapper[Option[View]]:
def signal: Signal[Option[View]] = viewVal
end StaticImportWrapper And let's say I want to load this module dynamically: import com.raquo.laminar.api.L.*
object FOO {
def apply() = div(
"HELLO FOO",
"FOO",
"FOO",
"FOOOOOOOO"
)
} And I try to use val bus = new EventBus[Unit]
div(
div(
"Click here",
onClick.mapToUnit --> bus,
// this works:
// child.maybe <-- bus.events.flatMapSwitch(_ => LazyImportWrapper(() => js.dynamicImport(FOO())).signal),
// this does not work:
child.maybe <-- bus.events.flatMapSwitch(_ => SignalWrapper[HtmlElement, true](FOO()).signal),
)
) That does not work. Gives me a "stub" exception that originates over here On the other hand, if I replace No matter what I try, I seem to be unable to hide the js.dynamicImport call inside Minimizing the code, with your pattern, I would expect this to work: @nowarn inline def dynamicImportSignal[View](view: View): Signal[Option[View]] = {
Signal.fromValue(()).flatMapSwitch(_ => Signal.fromJsPromise(js.dynamicImport(view)))
} But, it does not – gives the same "stub" exception. I tried that on Scala 3.3.4, 3.6.1, Scala.js 1.16.0 and 1.17.0, in Chrome and Firefox. |
I add some magic to LazyImportSplitRender, and it can now control the switching between dynamic and non-dynamic without adding any operation functions. now, it's good enough for me. import elgca.core.utils.router.LazyImportSplitRender as SplitRender
//import com.raquo.waypoint.SplitRender
val view = SplitRender(Routes.currentPageSignal.debugLog())
.staticImport
.collect[Login](_ => LoginView)
.collectStatic(Register)(RegisterView())
.dynamicImport
.collectStatic(Documentation)(DocumentationView())
.collectSignal[Dashboard](s => DemoApp.myApp)
.staticImport
.collectStatic(NotFound)({
if checkAuth.isEmpty then Routes.pushState(Login())
div(
a(Routes.navigateTo(Login()), "Go to Login"),
div(
a(checkAuth.toString(), Routes.navigateTo(Dashboard("NotFount"))),
),
button(cls := "btn btn-primary", "Go to Dashboard", onClick --> (e => Routes.pushState(Dashboard("NotFount")))),
)
})
.signalWithLoading(loadingPage) |
@raquo |
@raquo I tried it in the Edge browser. versions is : object Versions {
val Scala_3 = "3.3.1"
// -- Shared --
val JsoniterScala = "2.24.0"
val Borer = "1.14.0"
// -- Frontend --
val Laminar = "17.1.0"
val LaminarShoelace = "0.1.0"
val Waypoint = "8.0.0"
val UI5 = "1.24.0"
// -- Backend --
val CatsEffect = "3.5.2"
val Http4s = "0.23.23"
val Log4Cats = "2.7.0"
val Logback = "1.4.12"
val Phobos = "0.21.0"
val ScalaJsStubs = "1.1.0"
// -- Testing --
// val JsDom = "20.0.3"
} Can you create a branch in the laminar-full-stack-demo? I can try it later. |
@elgca Thanks for checking. It appears we may have found a Scala bug. |
Usually, our SPA have many pages and rely on different JavaScript libraries. Using SplitRender would cause all dependent projects to be loaded at startup, even if the current page being accessed is a static page. If the JavaScript library is large, it will slow down the startup speed, and some packages have side effects upon import.
In response to this situation, I have designed this SplitRender, which can automatically split page imports, using the inline keyword and js.dynamicImport .As seen in the figure below, the UI5 JavaScript is no longer being loaded. you just need to provide a loadingPageView.
Dependencies are only loaded when the corresponding View is opened.
As you can see, the JavaScript has been split into multiple modules, and the effect of the import has been deferred until the first invocation.
https://github.com/elgca/Waypoint/blob/master/js/src/main/scala-3/com/raquo/waypoint/LazyImportSplitRender.scala
The text was updated successfully, but these errors were encountered: