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
Add hook to router #1095
Comments
@tivac This doesn't sound like a bad idea, although IMHO it belongs on the router, not the component. |
Yes, I think it could belong on the router as well--I have no strong preference on the API design, just trying to figure out how to do this synchronously. |
I think this is doable with the core router API, but yeah, would be nice to have the state machinery happen under the hood and have a friendlier hook. |
Please let me know if I can help with an API proposal and/or pull request. |
@lhorie What do you mean by that? Had same issue. Was fixing it by conditionally load the normal route config or a login route config routing everything to our login component. Do you mean something like that? Curious to hear from you. |
@s3ththompson proposals are welcome @andi1984 this refers to the rewrite branch, which has a "core" router module ( |
Hi guys! what do you think about a different approach using middlewares. var auth = function(vnode, next){
user.auth(function(err, currentUser){
if (err) return m.route.setPath("/login");
vnode.attrs.user = currentUser;
next();
});
}
var customMiddleware = function(vnode, next){
next();
}
m.route.setMiddlewares([auth]); // global middlewares
m.route(document.body, "/", {
"/": [customMiddleware, home],
"/login": login,
"/dashboard": [customMiddleware, dashboard],
}); |
@lhorie Don't want to put pressure on the project or anything else. I love Mithril for its simplicity, but would like to use this rewrite in production. Any plans when 1.0 is more or less ready to use in production? |
I suggest keeping the router very minimal and not expanding to the full-blown concept of middleware. What if m.route(document.querySelector('#app'), '/', {
'/': function (resolve) {
resolve((Auth.loggedIn()) ? Dashboard : Home);
},
'/about': About,
}); An explicit resolve function also allows us to redirect to another route using m.route(document.querySelector('#app'), '/', {
'/': function (resolve) {
resolve((Auth.loggedIn()) ? Dashboard : Home);
},
'/about': About,
'/account': function(resolve) {
if (!Auth.loggedIn()) {
m.setPath('/login');
} else {
resolve(Account);
}
}
}); |
Any other thoughts on the design of this? |
I like the second proposal, but don't like the function-passing aspect. Something more like this would be my preference. m.route(document.body, "/", {
// normal component as it works now in 1.x and 0.2.x
"/" : Home,
// function arg that is expected to synchronously return the component to use
"/secret" : function() {
return authed ? Secret : Login;
},
// Don't always have to return if you're dynamically changing route
"/redirect" : function() {
if(authed) {
return Secret;
}
m.route.setPath("/");
}
}); @s3ththompson I think that solves your problem (& honestly one of mine in anthracite as well), would that work for you as an API? |
Update: I was experimenting with various ideas and use cases. What I currently have is this: m.route(document.body, "/", {
"/foo/:bar" : {resolve: function(render) {
if (!auth) m.route.setPath("/login")
else render(function view(args) {
return m(Layout, {body: m(Test, args)})
})
}}
})
The use cases that this pattern covers are:
So feature-wise, I think this covers everything I'd like it to cover except caching of asynchronously-resolved components. The downside is that it's not super simple to use. I'm looking for suggestions to support those use cases with a simpler API |
@lhorie Have you thought of making the router something like The stream would emit At the end, you could optionally do You could keep the current // The router implementation
function stream(defaultRoute, routes) {
var router = m.prop()
// router.set calls this automatically
function update() {
router(router())
}
// ...
router.link = link
router.prefix = prefix
router.get = get
router.set = set
router.update = update
return router
}
m.route = (function () {
var router
function route(elem, defaultRoute, routes) {
router = stream(defaultRoute, routes)
router.map(function (change) {
m.mount(elem, m(change.component, change.params))
})
router.update()
}
route.stream = stream
;["get", "set", "link", "prefix", "update"].forEach(function (prop) {
route[prop] = function () {
if (router == null) throw new TypeError("Router not initialized yet")
return router[prop].apply(router, arguments)
}
})
return route
})() |
As an added bonus, it's harder to screw up with not having the router initialized yet, because things won't work if you don't have them set up. Obviously, the above doesn't include the dependency injection, but that would be pretty straightforward to do: // api/router.js would look like this instead:
var coreRenderer = require("../render/render")
var coreRouter = require("../router/router")
var autoredraw = require("../api/autoredraw")
module.exports = function($window, renderer, pubsub) {
var stream = coreRouter($window)
var router
function route(root, defaultRoute, routes) {
if (arguments.length === 0) {
if (router == null) throw new TypeError("Router not initialized yet")
return router
}
router = stream(defaultRoute, routes)
router.map(function (change) {
if (change.matched) {
renderer.render(elem, m(change.component, change.params))
} else {
router.set(path)
}
})
router.update()
autoredraw(root, renderer, pubsub, router.update)
}
route.stream = stream
;["get", "set", "link", "prefix", "update"].forEach(function (prop) {
route[prop] = function () {
if (router == null) throw new TypeError("Router not initialized yet")
return router[prop].apply(router, arguments)
}
})
return route
} And, as another bonus, the internal API almost exactly mirrors the external one, so if you need the extra flexibility, there's minimal impact on your app beyond possibly an extra import when you need to switch. |
@isiahmeadows the core router is already decoupled from everything else. The aim of the public router API is to be easy to use for the 99% most common use case. I think if the user needed to manually call m.mount or m.render, it would detract from its usability. |
@lhorie The primary API implementation of my idea already does that, so it won't affect the 99% use case (note the |
why not handling route events in component? "onRouteInit" "onRouteChange"? |
@khades That wouldn't make sense IMO. Routing has little to do with the components underneath. Conceptually, routing has little to do with components, anyways, unless you say "render this component for this route". |
update: I added experimental support for Usage: var MyLayout = {
view: function(vnode) {
return m(".layout", vnode.attrs.body)
}
}
var MyComponent = {
view: function() {return "hello"}
}
m.route(document.body, "/", {
"/": {
resolve: function(use, args, path, route) { //runs on route change
use(MyComponent) //may be called asynchronously for code splitting
},
render: function(vnode) { //vnode is m(MyComponent, routerArgs) where MyComponent is the value passed to `use()` above
return m(Layout, {body: vnode}) //runs on every redraw, like component `view` methods
}
},
"/foo": {
render: function() {
return m(Layout, {body: MyComponent}) // `resolve` method is optional
}
},
"/bar": {
resolve: function(use) {
use(MyComponent) // `render` method is optional too, so this renders `m(MyComponent, routeArgs)` without layout
}
}
}) Redirections can be done this way: m.route(document.body, "/", {
"/": {
resolve: function(next) {
if (!loggedIn) m.route.set("/login")
else next() //calling `use` with a different name here, but basically, resolve to undefined...
},
render: function() {
return m(Layout, {body: MyComponent}) //...and hardcode the component here to save some typing
}
},
"/test": {
resolve: function(use) {
if (!loggedIn) m.route.set("/login")
else use(MyComponent) //again, if no custom layout composition needed, you can omit `render`
}
},
} |
For the docs, |
@lhorie a nit, but for consistency, assuming the component hooks keep their current names, one may want to add {
onresolve: function (render, args, path, route){render(Component)},
onrender: function(vnode) {return vnode}
} One potential source of confusion in calling the first argument of Of note, your snippets above contains two occurences of the same bug (in case you were tempted to copy/paste them to the future docs): render: function() {
return m(Layout, {body: MyComponent})
}
Also, in both cases the parametrization of the component with |
@pygy if we're going to follow the |
Change the signature of what resolve expects, so you can do The linked refactor looks cleaner, similar changes to that would be:
Remove the |
Which async module loaders are we talking about? It might very well be possible to use async modules without any need to build |
@orbitbot The main purpose of |
@mindeavor In the docs, I have a webpack example, but presumably anything that can do code splitting would work |
@lhorie The wrapper scenario is also made easier. I don't understand what |
Ok, so here's a simple & easy way of doing async modules without explicit support from mithril. All you need is a mithril-friendly async load wrapper. Let's call it m.route(document.getElementById('app'), '/', {
'/': {
view: function (vnode) {
var Home = myRequire('Home.js')
return Home || m('p', "Loading...")
}
}
}) And that's it! Here is the definition: var loaded = {}
function load (moduleName) {
if ( loaded[moduleName] ) {
return loaded[moduleName]
}
else if ( loaded[moduleName] === undefined ) {
// Load asynchronously
someAsyncModuleLoader(moduleName, function (module) {
loaded[moduleName] = module
m.redraw()
})
loaded[moduleName] = false
}
else {
// Async loading is currently in progress.
// Do nothing.
}
} |
@pygy re: render vs view, render has diff semantics, view is wrapped in component semantics
In the example above, if all your routes use RouteResolvers, then Layout isn't recreated from scratch. If you use components, then since the top level component is different for each route, the Layout gets recreated from scratch on route changes @mindeavor |
I know the general semantic differences (I've re-implemented |
@lhorie Yes, and I think pre-render redirects are great :) I'm trying to remove the responsibility of choosing which vdom to render from |
@pygy for route args, render and view have the same behavior (but they can't be collapsed into one thing because of the difference I pointed out above). I know you're more familiar w/ the code, I'm just repeating for the benefit of others who may not be as familiar w/ the various use cases @mindeavor you don't necessarily need to resolve to anything:
|
@lhorie True,but I'm addressing many of the I think we need a new GitHub issue with a concrete list of requirements / desired use cases. |
afaik what is currently in the repo addresses all the use cases I want, except #1180 other than that there's people giving various suggestions to try to conceptually simplify the API, but most of those suggestions don't address all the use cases The use cases that I'd like to have are: 1 - some way to compose components with diff semantics (currently |
I agree with you that it doesn't seem possible to simplify the API further. Instead, Ill suggest an addition: a default, user-definable |
Can you give an example of usage? |
route.render = function(vnode){return m('.defaultlayout', vnode)}
m.route(root, default, {
'/foo': Component, // wrapped in the default layout
'/bar': {onmatch: function(resolve){...}}, // ditto
'/noDefaultLayout': {render: function(){return m('.bare')}} // just a div, no layout.
}) |
Thanks for the crucial feature list @lhorie. Here's a bin I believe ticks it all off in Mithril v0. You can turn faked random XHR errors on and off on the initial page - the dynamic component either defers draw til it can resolve the view with deferred resources, or redirects in the case of an error. The dynamic page opts to diff on route match. The error page mandates a full nuke of the DOM. AFAICT hitting all of these in any given route with Mithril v1's clumsy distinction between RouteResolvers and Components is not possible to any useful degree. To wit:
|
@barneycarroll regarding your very last point, #1268 means that the UI is not responsive while the resolution is pending right now. If it is fixed (using either #1262 or the mount-based solution), then the previous component would remain live while the new one is loading. A spinner may be activated from the |
@barneycarroll I'm not really sure what you are trying to get at. A large part of the rewrite effort in general is to modularize, which implies that each module has to have clear and localized semantics.
That's not what code splitting is. Code splitting is when you have an ERP system that serves a 1MB+ javascript bundle that contains code for several sub-applications, but you want to load each sub-application on demand to reduce initial page load time. But again, saying Let me put things this way: given how obsessive I am about making a framework whose sell point is size, do you think I wouldn't have discarded RouteResolvers if I could solve all of its use cases elsewhere without relying back on semantics that were previously considered problematic? |
@lhorie what I'm getting at is that under v1 the router API is more complicated and less powerful. It is possible to achieve the effect of some of the patterns in v0.2, but these are often mutually exclusive. The API design aims for a surface consistency with other APIs (vnode, attrs, component awareness) which give the illusion of a holistic top level controller, but because of the vagaries of the modularity dogma end up disappointing in their limitations. It is possible to achieve all the stuff in the list given the full I went into great depths about the practical application of code splitting. I know exactly what you mean and addressed it with code samples acknowledging the practical reality and described deep shortcomings in that area too. In order to make the argument for authors who benefit from piecemeal Mithril modularity and dynamic module resolution, you must at least present a scenario whereby this would be the case. I can't see it. The authentication scenario falls short at the same hurdle. Without building their own infrastructure to store the data relating to that authentication call, or the data that might populate any other arbitrary webservice request, authors are not going to thank you for the fact they had to manually compose their own modular Mithril build and write their own persistence layer to interact with component state, all for the benefit of being able to avoid the user having to download the redraw API. It doesn't add up in any practical scenario. I appreciate the modularity requirement is a huge achievement, but personally I think this is appealing to the wrong crowd at the expense of engaged authors who want a decent API. |
BTW this conversation is constant in that I'm illustrating all the use case scenarios and we end up talking at cross purposes because I'm doing the legwork of imagining, validating and explaining the use cases you're implying, but these end up being ignored because it's come down to an adversarial thing where everything I put up can be considered a straw man. It would really help if others wanted to put up examples of the current API dealing with Leo's matrix of features above. |
I don't understand what this means. Are you trying to say that this isn't possible: var Layout = {view: function(vnode) {return m(".layout", vnode.children)}}
var Foo = {view: function() {return m("div", "hi from foo")}}
m.route(document.body, "/123", {
"/:id": {
onmatch: function(vnode, resolve) {
resolve(Foo) // or require(["Foo.js"], resolve)
},
render: function(vnode) {
return m(Layout, [
m("div", "hi w/ id: " + vnode.attrs.id), // <- are you saying this is not populated because of the presence of an onmatch above?
vnode,
])
}
}
})
/* yields:
<div class="layout">
<div>hi w/ id: 123</div>
<div>hi from foo</div>
</div>
*/
I think you're missing my point when I mentioned modularity. Whether people use stuff piecemeal is completely irrelevant here. My point is that the semantics of the v0.2 APIs that you say are holistic are - in my personal opinion after using them in some real life projects - brittle. For example, in v0.2 you can't just add a m.redraw() call anywhere. Depending on where you put, it can cause a null reference exception in a view in a completely different component. Why? Because you used My understanding of what you've been saying so far is that RouteResolver sucks, and that its behavior can be achieved in v0.2 if we rely on semantics that are explicitly removed in v1 due to being problematic. Ok, that's true, but it's not actionable. Say we go ahead with your suggestion and remove RouteResolver. Then what? |
@barneycarroll http://jsbin.com/jebusamibe/edit?js,console <= this may help... Edit, BTW, |
There's a Layout component already loaded at page load and you want to render that plus some component that is not part of the initial bundle.
Why is this a choice? Both are happening, right? |
@lhorie I'd like a description of what that code is doing and why it's useful. It's evidently possible, but it's not at all clear what it means, why I would want to do it, and what happens when Foo renders. Surely that last part is kind of pertinent? Either the view cares about route (as in the render function) or the view is determined by the route (resolve Foo), but the two are mutually exclusive. That doesn't make any sense from an application developer's perspective - it only makes sense from the perspective of justifying previous API decisions. Let's look at a credible scenario: A huge amount of this debate relies on vague "yeah but it was a problem for some people I worked with". My contention is that coming up with new APIs does not in and of itself magic away those problems. Unless you can show me how you solve them! Finally, you talk about |
You pass both, by default. Look again at the JSBin I posted above... In And I'm not modifying anything in |
Not sure what happened with the order of the posts, but I already explained what the code does above. I still don't understand what is "mutually exclusive". You say you want a "view to care about a route", but why? This is what that statement means:
I also don't understand what is obnoxious. Show me some v1 code and tell what it's supposed to do vs what it's doing. Or some code that should work but doesn't.
Well, read the docs. Though their intricacies may be currently under-explained, all the use cases I care about and that have solutions are there. Again, if there are use cases that are not being satisfied by the current API, the proper way to address that is to open an issue describing actual vs expected behavior If you have issues with the RouteResolver API, you're also welcome to open an issue with a proposal for an alternative that a) solves all the currently documented use cases, and b) highlights the problem you're trying to solve and how it solves it |
Personally, I find it rather awkward to mix routing concerns into the view layer. React does it out of necessity because Declarative All The Things! (tm), but it seems unnecessary to add this coupling into the core of more js-centric libs. It introduces a huge surface area for odd/unexpected behavior and bugs. In domvm, I chose to sidestep all these philosophical arguments and edge-case handling by letting the user define the coupling through a bit of router boilerplate that could easily be conventionalized as needed in app-space while keeping concerns fully decoupled in the core. Maybe something to think about. /$0.02 |
@lhorie I realise I'm just complaining without offering solutions. The terminal problem with this conversation is that the defence of the current API is circular. It is the way it is because the rewrite has a mandate to separate concerns in a way that makes it impossible to bring those concerns back together for the router API, and once you've written it it's self justifying because whatever it does is whatever is possible. My parting message is that the routing docs address concerns which it presents as separate, but are intuitively holistic for authors, and these turn out to be mutually incompatible: you can't use the router's diffing strategy in combination with its asynchronous deferral. You do one or the other, or one then the other. This distinction, and the roundabout way it's handled, is of no benefit to authors. It may be the way the code 'has to work' for internal reasons, but the fact you can't mix and match with a single declarative object is frustrating. I still contend that the idea 'routed component' that takes the best of both worlds - RouteResolver (diffing, deferred resolution) and Component (lifecyle) - which in layman's terms is essentially saying that route endpoint components can benefit from Consider this as my trying to bow out gracefully from the confrontational aspect of 'change the API'. @leeoniya what you say about 'declarative all the things' is interesting (and BTW your thinking outside the box with DOMVM is really refreshing). Do you think we're being too obsessive about a view-focused paradigm in this conversation? And / or do you think we're being too drawn in to a desire for holistically static object APIs? How do you deal with concerns for deferred resource loading and diffing concerns with DOMVM? |
For my personal taste, yes. I tend to design API/model first and then optionally introduce a view layer and router as consumers of this API, coupling them as needed. View-centric app construction (a la React) encourages everything to be stuffed into the view. If you keep your state dumb and immutable, where does your API live? Is it on the views and can only be invoked via the UI? Why should the router config live within the views? Why should a route definition reference a DOM element shudders? React is declarative all the way down, so we now have ever-growing declarative apis to accommodate all use cases when imperative code could solve things more obviously with less/no hacks. Despite writing domvm (a view layer), my preference is to have it simply be a UI for my domain model's APIs, which means it needs the ability to be maximally decoupled. The only surface area where the router and view interact is eg I don't think that replicating in-view/in-vtree routing is a good idea, not in React, not in Mithril. It looks pretty and concise but introduces coupling and a large surface area for surprises, UB, hacking and bugs. If you guys can get it working well, great! But it's not my cup of tea. At the end of the day it's good to have multiple frameworks with different philosophies. |
A feature request for the Mithril rewrite:
I love the simplicity of the current router, but I would like to expose a hook that runs after a route has been resolved but before the associated component has been rendered. The feature would be useful for implementing a few common patterns:
/
routes to a landing page if the user is logged out, but/dashboard
if the user is logged in/dashboard
routes to a dashboard if user is logged in, but/login
if user is logged outThere is lots of precedent for this type of hook in other routers. See, for example,
onenter
in react-router and the ability to extendrouter.execute
in Backbone.Current solution in Mithril
Unfortunately, since
setPath
asynchronously triggers a reroute only after thewindow.onpopstate
event fires, the dashboard component above renders to the screen for a split second before the login page is loaded.Propsed API Options
Add an
onroute
hook to components that runs before the component is rendered and has the option to redirect to another route without rendering the component?Alternatively, if
oninit
returns false, prevent rendering of the component?The text was updated successfully, but these errors were encountered: