Closed daniel-v closed 2 years ago
Hi Daniel,
I'm very glad that you took your time to give a comprehensive feedback. Before I began making any comments, please note that everything is definitely not the best. I'm not familiar with innards of other FWs so I tried different approaches, and picked the ones that sounds about right to me. I'm totally open to improvements and corrections.
I think both are related. Earlier, every widget was responsible for rendering itself and calling build on its childs. /core was intended to provide set of global functions(including methods that build, update widgets). This was clearly an design issue but I carried on with it because that was easier to do at that time.
Framework
and Router
can lives in an instance if widgets aren't responsible for rendering their child or calling rebuild for themselves. Gradually I was able to take away these responsibilities from all widgets except Navigator, ListView, and StatefulWidget. Children of widgets like ListView
are still rendered/updated only when their state allows them to. Working this out requires some sort of scheduler that these widgets can call, or some sort of DI from framework side, or moving their state inside Framework
. I'll think more about it, but if you've any ideas/thoughts, feel free to pitch in.
window.addEventListener("popstate" could be written as window.onPopState.listen
This is definitely more clean. I just have to check whether event stream is broadcast-able.
Navigator doesn't seem to react to manual command in console history.pushState({}, '', 'another')
Right now, Navigator is listening for only pop state events, which aren't triggered through console(as per search results).
From mozilla docs: Note that just calling history.pushState() or history.replaceState() won't trigger a popstate event. The popstate event will be triggered by doing a browser action such as a click on the back or forward button (or calling history.back() or history.forward() in JavaScript).
In local development, using path based routing is cumbersome. In debug mode, it should be permissible to use hash routes. eg. localhost:8080/#/home
That's true, I'm using proxy for developing my projects but I get it proxy isn't ideal.
sitemap is still a thing, there should be a mechanism in place to extract route information
I've never really worked with site-maps so I've no idea about this part. I'll take a look and see what can be done.
Routes should be allowed to be loaded async with deffered as import statements
FW builds only one route at a time(the one that is opened/or where user is landed). So as I understand it, problem is with widget constructors:
routes: [
Route(name: 'home', page: HeavyConstructor1()),
Route(name: 'home', page: HeavyConstructor2()),
]
And, here's how I deffered heavy constructions in the past:
routes: [
Route.lazy(name: 'home', page: () => HeavyConstructor1()),
Route.lazy(name: 'home', page: () => HeavyConstructor2()),
]
If I'm not wrong that'll do it? or is there some other reason for having an AsyncRoute
?
There is a seemingly arbitrary restriction what a path might look like for a Route: RegExp(r'^[a-zA-Z0-9_]+$'). Is there a reason for this?
I think I was worried about slashes at the time of writing this. This restriction is definitely more strict than required so it should be fixed to allow more characters. We can get rid of this completely by encoding path but there's more to it. For example,
Route(path: "/home/profile"),
Here people might assume that they've registered a nested route but that's not true. Without above restriction, they'll see %2Fhome%2Fprofile
popping up in their address bar, and I don't think this will be a good way to introduce someone to a new FW.
With an error, they'll think about "how to do nested routing" in this FW and eventually they'll find the correct way to do it:
Route(
path: "home",
page: Navigator(
routes: [
Route(path: "profile", page: ...),
]
),
);
Is there a way to capture paths as params to a Route? eg. /blog/:slug:
Unfortunately, Router matches only exact segments. I'm not sure why you want to do that?
For example, you can have a Route(name: 'blog')
and if user is on /blog/<some-value>/
:
Navigator.of(context).getValue("blog"); // -> '<some-value>'
I think on click events are used far more often than any other events. The reason GestureDetector
exists has to do with nested widgets that are listening for the same event. GD provides hit test behavior that can be used to control event bubbling and propagation in such cases.
I think the last example you gave is far more easy to use than any other widget ever will be. This looks very clean but we've to use a bit more descriptive name than handle
. Although events should be handled by widgets where they are supposed to be(HTML widgets) but adding more work inside HTML widgets will incur performance degradation. So having a different widget sounds good to me.
Regards,
/erlage
ah forgot to mention, we could add more event types to GestureDetector
as well.
Children of widgets like ListView are still rendered/updated only when their state allows them to.
Could you expand on this a little more? What does it mean "when their state allows them to" and how is it different from a regular Widget?
I just have to check whether event stream is broadcast-able
Indeed, DOM events are broadcast streams. Also, a package I very much enjoy using is rxdart
which has some really nice constructs for managing streams. (though your initial take to have as few external dependencies as possible is probably a good enough reason not to use it)
Note that just calling history.pushState() or history.replaceState() won't trigger a popstate event
Indeed! Never wrote a navigator before. A little bit of research and I'm sure we could tackle this. The URL of a page is a "shared resource". If multiple app instances were to be supported, it should be supported that Navigators can react to external programmatic changes too.
That's true, I'm using proxy for developing my projects but I get it proxy isn't ideal.
For a decent sized project, esp. with a BE, this will become a significant enough hindrance to avoid it. Initially, that is exactly how my projects were set up (though back then we used our own implementation). This is good enough for now though! Definitely not the first thing I would want to work on.
I've never really worked with site-maps so I've no idea about this part. I'll take a look and see what can be done.
The sitemaps themselves are not too exciting. Just a way to tell search engines where to find and what. Also not something that is a must have, most SPAs FWs have no first-class support for it. Not a deal breaker if cannot reasonably be done. Why it is still important to is because it can influence how Navigator and Routes are designed.
If I'm not wrong that'll do it? or is there some other reason for having an
AsyncRoute
?
That will do it - mostly.
If this piece of code works, we have full support:
import 'package:something/aPage.dart' deferred as aPage;
routes: [
Route.lazy(name: 'home', page: () async {
await aPage.loadLibrary();
return APageWidget();
}),
]
Note: there is an extra async step. Loading the library itself that contains definition for APageWidget()
.
This looks very clean but we've to use a bit more descriptive name than handle
Agreed :)
adding more work inside HTML widgets will incur performance degradation
Not at all worried. Performance is of little concern to me at this stage. Premature optimization and all that jazz. :)
What does it mean "when their state allows them to" and how is it different from a regular Widget?
Two important things that every widget do:
FW creates the requested HTML element and give widget a chance manipulate the element by calling render
on widget's render object. Then FW starts building child widgets if widget has provided a non-empty list of childs.
Regular widgets(e.g a Span widget) don't want to know/control how/when their child widgets are built so they simply provide their child widgets in list. But widgets with state wants to build/update their child widgets only when a particular event occurs. These builds are almost spontaneous and exposing Framework.*
was the easiest thing. Since everything in Framework
is static, widgets can start build/rebuild process anywhere in the tree. I guess this is the only thing we've to fix.
If multiple app instances were to be supported, it should be supported that Navigators can react to external programmatic changes too
I think the reason why history.pushState
doesn't fire a pop state event is obivious. Telling whether a pop state event is occured from history.pushState
or a browser action is hard if not immpossible. This needs some work if we want to allow multiple instances of Routers as well(which we should). In particular, each router instance has to ignore events that aren't relevant or captured by the other router else opening route in one router will force the others to switch to their default route.
there is an extra async step
I gotcha. AsyncRoutes are a different thing. Pardon my ignorance:)
That's true, I'm using proxy for developing my projects but I get it proxy isn't ideal.
I've spent good amount of time to get the web debuggers work in proxy mode. I don't know why I was being stupid all the time but thanks for bringing this up. Just realised that all I needed was to prefix path with a #
, and we already have the ability to do that:
RadApp(
routingPath: '#',
)
We can make this implicit in dev mode if you agree on that.
First of all, thank you for working on multiple instances.
Here's update on remaining things:
routes: [
AsyncRoute(
name: 'some_route',
page: () async {} // -> Future<Widget>
preloader: Text('loading'), // -> Widget?
onError: 'whoops', // -> String?
),
Route(name: 'whoops', page: Text('Something went wrong')),
]
routingPath
with a #
doesn't solve anything, so I'll work on it.I think it'd be great if we ignore following things for now:
thanks again.
I dag into what you did here and overall, I really like some of the things I see. With my appreciation to the effort you put into making this happen, there are some areas that I think could benefit from a bit more attentive coding love :)
GJ, so far, I hope you keep on going!
Statics - testability - multiple apps
_registeredWidgetObjects
is static inframework.dart
- multiple instances an app is a no-go?_isInit
valuerunApp(RadApp, someOptions)
to init logging, debug tooling, zones, etc.Navigator and routes
This is tough one. Angular didn't get it right the first time, nor did Flutter. No surprises here :)
window.addEventListener("popstate"
could be written aswindow.onPopState.listen
Navigator
doesn't seem to react to manual command in consolehistory.pushState({}, '', 'another')
localhost:8080/#/home
sitemap
is still a thing, there should be a mechanism in place to extract route informationconst
expressions for routesdeffered as
import statementsAsyncRoute
subclass maybe?page
, there could be aAsyncWidgetBuilder
callback for theRoute
withFutureOr
return value?RegExp(r'^[a-zA-Z0-9_]+$')
. Is there a reason for this?/blog/some-blog-entry-title-sanitized'
/blog/:slug:
Events and event handling
I noticed that there isn't really a consistent handling of events across the widgets. Most, if not all, have an
onClickEventListener
and there isGestureDetector
widget. There are important use-cases that I can't cover with these 2.There is one workaround I found: using a stateful widget where I get to access the DOM element from
initState
and go underneath the FW.While this works, it feels out of place. A way to automagically handle stream subscriptions and their disposals would be beneficial.
Maybe something along these lines?