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 extend router.execute in Backbone.
var Dashboard = {
oninit: function(vnode) {
if (!Auth.loggedIn()) {
m.route.setPath("/login");
}
}
// *** view code ***
}
Unfortunately, since setPath asynchronously triggers a reroute _only after the window.onpopstate event fires_, the dashboard component above renders to the screen for a split second before the login page is loaded.
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?
@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 (./router/router) that is really low level, and a wrapper module that exposes an API that is more similar to v0.2.x (./api/router)
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?
@andi1984 time-wise, I'm not sure. #1090 is keeping track of current status
I suggest keeping the router very minimal and not expanding to the full-blown concept of middleware.
What if m.route accepted a routes object that could have components as values or functions that allow the developer to manually resolve components:
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 setPath instead of resolving a component for the current route:
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?
@lhorie @tivac What do you all think 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?
@tivac I'm concerned how that would mesh with #1113. Classes are typeof C === "function", which might cause issues.
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)})
})
}}
})
resolve runs on every route change. view runs on every redraw.
The use cases that this pattern covers are:
render callback)m(Layout, ...) on line 5)args on line 4)resolve key on line 2)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 m.route.stream("/default", {"/path/:param": Component}), returning a stream?
The stream would emit {route, component, params, matched: true} objects on every route change, where route is the current route, component is the route's component, and params contains the route parameters as an object. In the event the route is invalid, you emit {route, matched: false} instead. If the default doesn't match any of the routes, you can (and should) error out early, so it can be assumed everything is in order.
At the end, you could optionally do m.mount(elem, component), and you now have infinite flexibility. Oh, and the router is fully decoupled from the rest of the API. To initialize the first route, you should call router(router()), but you can add a router.update() method to make this look less obscure.
You could keep the current m.route API as sugar, and just build the stream primitive quite simply.
// 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 router.update() right before the autoredraw call). But by the time you need to dive into internals like that, I don't think that one extra call is going to be much of a problem, and if you have a complex loading process, you might need to defer the initial load.
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 resolve and render hooks
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, use/next might as well be called render as you already do internally.
@lhorie a nit, but for consistency, assuming the component hooks keep their current names, one may want to add on to the router hooks as well.
So:
{
onresolve: function (render, args, path, route){render(Component)},
onrender: function(vnode) {return vnode}
}
One potential source of confusion in calling the first argument of onresolve render as I suggested is its signature: It takes a component whereas m.render and onrender only take vnodes.
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})
}
MyComponent should read m(MyComponent).
Also, in both cases the parametrization of the component with args is lost since args can't be reached in onrender. This means that onresolve is needed if one wants to access the parameters. Not a big deal as long as it is documented properly.
@pygy if we're going to follow the onfoo convention, the method names need to be something else. resolve and render are what they do when some event happens
@lhorie I think what @pygy is getting at is that resolve works closer to an event handler than an action. It'd be different if, say, resolve worked like this:
// Usage
m.route(document.body, "/", {
"/": {
// return value coerced to a stream
resolve(args, path, route) { return MyComponent },
// vnode is m(MyComponent, args) from above
render(vnode) { return m(Layout, {body: vnode}) },
},
"/foo": {
// `resolve` method is optional
render() { return m(Layout, {body: m(MyComponent)}) },
},
"/bar": {
// `render` method is optional too, so this renders
/ `m(MyComponent, args)` from above without layout
// streams are also accepted to enable async resolution
resolve() { return m.stream(MyComponent) },
}
})
// Redirections
m.route(document.body, "/", {
"/": {
// can return a string to redirect, and `undefined` is okay.
resolve() { if (!loggedIn) return "/login" },
render() { return m(Layout, {body: m(MyComponent)}) },
},
"/test": {
// again, if no custom layout composition needed, you can omit `render`
resolve() {
if (!loggedIn) return "/login"
else return MyComponent
}
},
}
(IMO it seems to work a little more concisely like the above as well. Just my opinion, though)
I took the words resolve and render from Vue Router and React respectively. resolve was also suggested by @s3ththompson above, which suggest to me that it's descriptive of its purpose.
I think onenter is not as clear a replacement for resolve and I haven't heard any other suggestions.
I think view is a confusing alternative to render because it may lead people to think a RouteResolver is a component or a special type of component. Also, it makes it difficult (probably impossible?) to distinguish a component from a RouteResolver in the router implementation. Also haven't heard any other suggestions.
So unless there's a strong argument in favor of different and specific method names, I think resolve and render will stay as is.
I think resolve and render are good names.
Does render get access to the same args that resolve does? I might have a minor suggestion to simplify the API, but I want to make sure I understand it first.
@mindeavor render gets a vnode whose attrs is args. It doesn't get path (which can be retrieved via m.route.get() and it doesn't get route (mostly because I don't see why it would need to)
So a route endpoint can be _either_ be an object consisting of at least one
of resolve / render — corresponding to v0's controller / view…
_or_ a component?
On Mon, 15 Aug 2016 at 16:09, Gilbert [email protected] wrote:
I think resolve and render are good names.
Does render get access to the same args that resolve does? I might have a
minor suggestion to simplify the API, but I want to make sure I understand
it first.—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/lhorie/mithril.js/issues/1095#issuecomment-239827425,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAFGq8F3Y3OWwnmKUTmVZBpoHn6Zg0XJks5qgIELgaJpZM4IuQal
.
@barneycarroll yes, I'm calling said object a RouteResolver: https://github.com/lhorie/mithril.js/blob/rewrite/docs/route.md#api
but resolve doesn't quite equate controller (e.g. no special this semantics, it takes a use callback, it may defer rendering by resolving asynchronously, it may decide what component to render, etc)
@lhorie Ah, I see. I'm not sure how important it is, but the thing that worries me is the use case for route params:
m.route(document.body, "/", {
"/users/:id": {
render(vnode) {
return m(Layout, {
body: m(ShowUser, { userId: vnode.attrs.id })
})
},
},
})
What is vnode is in this context? Or, is this the right way to do it?
@mindeavor it's a div vnode whose attrs field is set to the route params (See the first and last lines of the highlight).
So your snippet would work.
@pygy I see. I guess my main concern is how to explain these behaviors. Maybe renaming vnode to root in the docs might help? And that it's an empty div by default, so you normally just throw it away... unless it has attrs sourced from URL parameters, then you just use those, and throw the div away... ?
@mindeavor I suppose the empty vnode is provided for signature consistency (vnode in, vnode out, whereas resolve outputs a component throughuse(x)). You'd want to keep theattrsif they are set, and throw away thevnode`, indeed.
The distinction between
@lhorie, my understanding was that resolution was done when resolve is being called. I originally thought that "resolving" meant "path => route mapping", not a "path => component" as I know understand after your comment. When resolve is called, the path => route part has been done, and additional logic can refine the resolution.
The case for the render hook doing the rendering is IMO weaker, though. use triggers the render/render.js machinery, the hook presents the final vnode to be rendered... It's more of a wrapper.
Anyways, resolve and render are fine names, as is. There may be better ones, but I'm not inspired right now...
I've got a bit of a problem with the term resolve, because as a verb it signifies the conclusion of a resolution, whereas here we're using it to represent the context within which that resolution should happen. To put it in other terms, resolve is often used as the callback reference (what we're calling render here and done in eg onbeforeremove). You will often find resolve used as an internal reference in Promise definitions new Promise( resolve => { /**/ } ).
It makes sense in terms of Mithril's internals (the URI resolves to this route), but in author-land it doesn't feel right IMO.
Perhaps 'match' would be a better verb?
@pygy if we're going to follow the onfoo convention, the method names need to be something else. resolve and render are what they do when some event happens
@lhorie I'm in total agreement with @pygy about consistent event handler naming conventions. W3 DOM spec has onload, onhashchange. We have oncreate, and… onmatch?
The case for onrender isn't as clear though. All the event-response methods thus far emulate event handlers in that they, are triggered by external logic (lifecycle / event loop), execute arbitrary imperative code, and in the case of onbeforeupdate, can return false in order to prevent default behaviour. view and render are different in that they're exist to define declarative content and return it for consumption by Mithril's core renderer. view isn't a piece of arbitrary code to execute when the 'view' hook triggers, it _is_ the view.
onmatch is pretty good, and then the currently named use()/next() callback could be named resolve() whether or not it takes an argument, to finalize the path => component resolution.
The render hook really is indeed really a specialized view...
I do like onmatch(resolve)
Now that I think of it, aren't {view: () => "hi"} and {render: () => "hi"} redundant?
@pygy more specifically, render is React's name for what we've always called view — and as @lhorie says above, this is where he lifted the method name from.
I'm not convinced that 'routing is orthogonal to components' is that useful as an observation. Yes, it's essential for RouteResolvers to have a special async lifecycle hook that can redirect or defer execution. But other than that, I can't see why they shouldn't follow the conventions of vnodes.
:-1: to resolve
:+1: to onmatch
:-1: to render
:+1: to view, consistency means one less thing for me to remember!
Raises a question though:
m.route(document.body, "/", {
"/" : {
view : function() => "Hi"
}
});
Is that a Component or RouteResolver? Does it matter?
Doesn't matter AFAIC. It's a (totally legit) idempotent component which could be used anywhere.
@barneycarroll That's my assumption as well, dunno enough about the guts of router to say if it would need to care or not.
The api/router currently looks for view to tell components and RouteResolvers apart, but it could probably be fixed if they were to be consolidated into a single thing.
The trouble is, currently (also using current terminology), in render, the vnode argument is can be created based on what resolve passed with use(). That's not how views behave in components...
@pygy but that's not really a feature, is it? You've already got the state and attrs properties firmly ingrained (by the rest of the docs) as hooks to determine whatever view logic you want. I would say you should stick to mutating those in onmatch, and furthermore that the method (whatever you want to call it) should also match (haha) the signature of existing lifecycle methods: onbeforeremove has vnode first but gets a callback specified as the second argument. Aligning these things just keeps it simple IMO.
@barneycarroll Ooohh, yes, you were ahead of me here, now I get it.
So onmatch would be called, unconditionally, before either init or onbeforeupdate. Neat :-)
@tivac that was what I had just realized. I think it would have the same semantics if it were either, so it might as well just be a component in that case. My only concern was that I wanted to avoid having people think that components have a onmatch hook or that a RouteResolver can have oninit and friends.
@barneycarroll are you saying the signature should be something like onmatch(vnode, resolve)? where vnode.attrs is {path, route, ...args}?
Also you mentioned vnode.state. What would you expect to be in that object?
@lhorie Yeah I think them looking similar is nice, except for the potential confusion. May need to spend some time in the docs pointing out that as soon as you add onmatch it's not a component any more.
@lhorie yes, that signature is spot-on IMO.
WRT semantics, I for one can't conceive of an ambiguity problem for authors expecting an onmatch hook to fire on a nested vnode. It's exclusively considered as part of routing logic: unless the component is the end-point of a route key in an object supplied to m.route, it will never be invoked by Mithril. If we're just idling about what-ifs in the absence of use cases, you could argue the toss that such hooks be invoked recursively as part of a cycle triggered by a route match… But then you'd have to return to that rabbit hole of v0 placeholder components etc. Just say no: it's only ever invoked as part of top level route endpoints, and behaviour is clear-cut.
As regards the behaviour of state, @veloce might be able to chime in (since he's been actively engaged in route egde-casing through actual usage), but I would think that state reference is maintained so long as the route match is consecutive: thereby landing on /post/:postId twice in a row would result in previous state being exposed, but otherwise a fresh state would be provided. In author-land, the analogy would be that the route key is effectively the same as (and overrides) vnode attrs key: we're returning to the same thing.
Alternatively you could argue that state shouldn't persist at all between routes (difficult IMO, since the case of wanting to compare during onmatch/onbeforeupdate is pretty compelling), or you could say that state is totally persistent and maintained for the duration of the app's runtime (mildly convenient because it allows persistent session references, but ultimately horrible because without strict guidance you would typically end up with a huge God-model representing all sorts of orthogonal concerns that never gets GC'd).
I vote for persistence over the course of repeat matches. It's eminently easier to rationalise IMO.
@tivac I missed your last post. I think you're looking at it wrong: it _is_ a component to most intents and purposes, but it has a special inversion of control contract if it declares an onmatch, in which case it can have opinions on higher order concerns. TBH through all my support work (this comes up a lot on Stack Overflow) for Mithril v0 people had no problem grokking the fact that top level routed components behave differently in this practical respect — the only thing that messed with people was m.redraw.strategy.
Actually, I already went through all this (imagining RouteResolvers as components, before they had an official name), but had forgotten about the conclusions...
It would be hard to make it work with the way render/render is currently implemented, since components are instantiated deep down the renderer's gut, and it is 1) synchronous and 2) unaware of the router...
So as much as it would be nice to have RouteResolvers and components be one and the same thing, it can't probably be made to work without reworking things in depths... Ooooh, unless maybe we mount the RouteResolver on a detached element, with a parent node whose hooks call onmatch appropriately? Could that work? ... No, since we couldn't grab the vnode returned by the view and render it into the real root.
I missed @barneycarroll 's two posts before posting the last message. If we are to duplicate the component instantiation and lifecycle in the router, then it would make sense for the state to be preserved as long as the same component/RouteResolver is picked after matching a path to a route, even if different routes map to the same object.
It's not super-important to me if a RouteResolver is actually a Component. It sounds complex given feedback in the thread.
I do think that having the API be similar (by using view) is A) nice and B) maybe potentially confusing, but probably still worth it.
Yeah I don't think it's worth going down that rabbit hole. RouteResolvers are one thing, components are another, both just happen to have a method called view. If the object has an onmatch then it's treated as a RouteResolver, otherwise it's a component. If both types of entities are properly documented, I hope that will be enough to avoid confusion re: available hooks
I'd hope so.
I'm a bit wary of calling it plainly view, as I think it will cause confusion. Maybe we could qualify it?
I think calling it view is perfect if other lifecycle methods throw a helpful error if defined. For example, this would throw:
m.route(document.body, '/', {
'/': {
onmatch: () => ...,
oninit: () => ...,
view: () => ...,
}
})
...but removing either the oninit or onmatch would make it work again.
@mindeavor I don't understand the value proposition in that proposal. Having both onmatch & oninit is good. For example, I reckon the following code shouldn't pose any problems. Maybe others can tell me why it does?
m.route( document.body, '/profile', {
'/logout' : {
onmatch(){
user.loggedIn = false
m.route.set( '/login' )
}
},
'/login' : {
onmatch( vnode, resolve ){
if( user.loggedIn )
m.route.set( '/profile' )
else
resolve()
},
oninit( { state } ){
state.username = m.prop()
state.password = m.prop()
},
oncreate( { dom } ){
animate( dom, {
opacity : [ 0, 1 ],
transform : [ '-50%', '0%' ]
} )
},
view : ( { state } ) =>
m( '.App',
// etc
)
},
'/profile' : {
onmatch( { state }, resolve ){
if( user.loggedIn )
return m.route.set( '/profile' )
fetch( user.profile ).then( profile => {
state.profile = profile
resolve()
} )
},
view : ( { state } ) =>
m( '.App',
// etc
)
}
} )
I'd rather avoid objects with multiple "interfaces" (in this case IRouteResolver and IComponent) and instead have distinctly separate objects to have a better separation of concerns, e.g.
// login.js
var Login = {
oninit({state}) {
state.username = m.prop()
state.password = m.prop()
},
oncreate({dom}){
animate(dom, {
opacity: [0, 1],
transform: ['-50%', '0%']
})
},
view: ({state}) => m('.App', etc)
}
// profile.js
var Profile = {
oninit({state}) {
fetch(user.profile).then(profile => {
state.profile = profile
}).then(m.redraw)
},
view: ({state}) => m('.App', etc)
}
// routes.js
m.route(document.body, '/profile', {
'/logout': {
onmatch() {
user.loggedIn = false
m.route.set('/login')
}
},
'/login': {
onmatch(vnode, resolve) {
if(user.loggedIn) m.route.set('/profile')
else resolve(Login)
},
},
'/profile': {
onmatch(vnode, resolve){
if(!user.loggedIn) return m.route.set('/login')
else resolve(Profile)
},
}
})
This way, code related to routing and auto-redirecting stay with the router and away from components, and component code stays self-contained away from the routing logic
Btw, on a side note, I refactored RouteResolver into {onmatch(vnode,resolve), view(vnode)}. Docs are updated as well
@lhorie I don't get it. In this post you say 2 completely distinct interfaces are imperative. But then you say that these interfaces should both have view in common. It doesn't add up.
Is this a Component or a RouteResolver { view : () => { /**/ } }? Why is the distinction important, from an application authoring perspective? Why is the distinction important from a library logic perspective?
Is there any practical difference between these 2 routes, and if so, why is it useful or significant?
m.route( document.body, '/a', {
'/a' : { onmatch( {}, resolve ){ resolve( { view : Login.view } ) } },
'/b' : { view : Login.view }
Separation of concerns is not a good in itself; putting all your eggs in one basket is the rational thing to do unless there's a strong chance of the basket being dropped and some eggs is better than none. If the separation is clear and meaningful and brings grokkable benefits to the end user, that's great. I'm not seeing that here: we're looking at an increasingly large, complex and ambiguous API that hasn't brought any new functionality to the table since v0. Elsewhere we're already talking about introducing extra methods to workaround ambiguities laid by these unnecessarily dogmatic distinctions.
If the idea of onmatch as a special lifecycle hook for _components_ seems too much, we might look at onbeforeremove: it's optional; it will only be triggered when special conditions are met, and will never be triggered at all on most nodes; when it is triggered it can run arbitrary async logic that interrupts the natural flow of the rest of the lifecycle. The introduction of this very special logic doesn't merit a whole new class of entity with arbitrary limitations.
AFAICT not having a distinct RouteResolver entity reduces the number of entities and code paths in the library and makes things easier to extend for authors. I got confused when I found my nested onbeforeremoves weren't getting respected, but I got over that. I don't think anybody would get similarly put out when their nested onmatchs never trigger.
The difference between RouteResolver and Component is the fact that one is handled at the router layer, where object identity doesn't matter, and the other is handled by the render engine level, where it does.
Also, state and hooks are set up in the render engine, downstream of the router.
RouteResolvers live here.
api/router.js then calls renderer.render which handlers lifecycle events for the various phases in the corresponding sections. For example, the state is set up in the createComponent routine.
@pygy are all these details mentioned above good things? AFAICT it's all dubious complexity that I would rather forget in order to focus on legitimate application design concerns.
I understand where you're coming from, but it would be hard to implement based on the current design, where the router and the renderer are completely isolated. More in #1254.
In this post you say 2 completely distinct interfaces are imperative
No, what I meant is that an object that implements IRouteResolver should not also implement IComponent because then you're mixing different concerns and semantics. It makes no sense to have onmatch live in a component and have it stop firing in the off chance that it ends up nested in another component. You would ultimately need to refactor the onmatch logic out of said component. Or we could just have that logic always be at a top level thing. onbeforeremove has its own semantics (and to be honest, after our discussions, I'm now not that convinced that we can't do better there)
For what it's worth #1254 happens to be a good illustration of why the two are not reconcilable: component semantics are that changing one component to another recreates the subtree (otherwise you run into all kinds of ugly unexpected scenarios like DOM elements that have extra event handlers or classes and shouldn't, or whatever). The render/view/whatever-you-want-to-call-that method in RouteResolver is there to allow composing components without recreating the tree, which wouldn't be possible otherwise.
An alternative to support component composition is nested routes, but I feel that flavor of API is more complex (both implementation-wise and semantics-wise) and more restrictive.
Anyways, the bottom line is this: I think the default mode of operation for the router should be diff-by-default, the default mode of operation for components should be to recreate if diffing different components. It would be ideal to not break {"/": SomeComponent} as a shorthand for routing to a simple component, but it's also important to have a mechanism to properly support both diff-by-default layouts and route reloads.
It came up that {render: () => ...} and {view: () => ...} were different after all, so maybe I should go back and revert that name change (unless someone has a better idea)
It's funny that after crossing wires around different conceptions of separate concerns, it turns out the real practical concern mandating 2 separate entity types is user appreciation of the subtleties of diff algorithm :)
It makes no sense to have onmatch live in a component and have it stop firing in the off chance that it ends up nested in another component.
It makes sense in the case of onbeforemove, because this special asynchronous behaviour is the exception rather than the norm: it relies on that node itself being detached while its surroundings persist. Then special behaviour is engaged which deviates from the expected lifecycle continuation. The same applies to onmatch: it is only triggered under very specific conditions, and when that happens the normal lifecycle sequence is deferred until it resolves.
I think the default mode of operation for the router should be diff-by-default
This is a nice thought on some level. Back when I built my large Mithril v0 app I was frustrated by the default behaviour of total DOM trashing on route change, and we established the controller : function(){ m.redraw.strategy( 'diff' ) } pattern, which I've been recommending to other people coming with this problem for about a year. But at least in Mithril v0 we could set blanket redraw strategies. According to this new model, things aren't so simple: for instance, it breaks down as soon as you engage in component composition as suggested in the docs. If you load this fiddle, open the console (the real console, otherwise reading the logged mutations is a pain), and navigate between routes, we can see that effectively the whole tree is being trashed and regenerated anyway. If we go back to v0, we don't suffer from this.
Once again, I'd like to stress that I am desperately trying to make things _simpler_ and _more predictable_ for users. This isn't what we're seeing. I'd be mildly impressed if we could throw up a scenario with a coherent application code structure that benefits from RouteResolver view diffing, but I'm not holding out much hope.
component semantics are that changing one component to another recreates the subtree (otherwise you run into all kinds of ugly unexpected scenarios like DOM elements that have extra event handlers or classes and shouldn't, or whatever)
We've discussed this before, and I contend that the scenario above, while occasionally desirable, is generally an anti-pattern. onremove hooks allow for teardown mechanisms; if people don't want to or can't safely ensure the DOM is back in sync with the virtual tree before removal, they can always use keys to explicitly ensure this behaviour. All in all this is increasingly looking like a series of inter-dependent crutches that don't address any substantial user benefits.
Edit: what I'm saying above is wishful thinking based on naive intuition. It turns out it can't work like that in Mithril v0, since the vagaries of the diff mechanism can never be fully ensured https://jsbin.com/qeyiho/edit?js,output.
in Mithril v0 we could set blanket redraw strategies
Yes, but that usually had a large impact when suddenly you wanted to switch strategies mid-project. That's one of the things I'm hoping to fix (along w/ m.redraw.strategy being somewhat convoluted to understand)
while occasionally desirable, is generally an anti-pattern
A while back I ran into an issue w/ DOM recycling that was quite difficult to troubleshoot because it was falling into a scenario where the dev was responsible for cleaning up but didn't (I've changed the triggerability of recycling to avoid that now). Point being: if you leave it to the dev to fix it, it's gonna inflict unnecessary pain. I think that the library should not break if the developer forgot to clean up, so by extension, components diffed against different components should not recycle DOM.
I think reducing the number of types of entities only makes sense if we can generalize the problem. In this case, the problem is that reducing the number of entities to one (components) doesn't address the use cases I outlined and you'd end up needing some other API to cover for the shortcomings (and we already established that m.redraw.strategy wasn't a very good take on it)
we can see that effectively the whole tree is being trashed and regenerated anyway.
Right, and I acknowledged that this is not desirable (and that renaming back to {render: () => ...} would address that)
Maybe you see RouteResolver as a crutch, but honestly I don't see how merging everything into components is going to be a viable solution
I appreciate I'm kinda going all over the place with this. I'll bookend my 'intuitive idealism' tangent by saying that DOM plugin / imperative adhoc mutation teardown can be a tough thing to get your head around (it certainly felt like 'serious programming' when I first encountered the need for it as a nominal designer), but the concept needn't be rocket science. In my view the best way to address this requirement holistically is to say that any vnode whose control is ceded to non-Mithril code (ie anything in oncreate that touches the vnode.dom) would be the best place to declare that teardown requirement. Given that the specifics of _how_ to teardown in any given scenario is intrinsically dependent on potentially unknowable specifics, DOM nuking _does_ make sense as a generic method, but that should be addressable on a case by case basis because it's the exception rather than the general rule. A vnode hook to indicate this makes sense. There's a case to be made for key mismatch being the indicator (I've used incrementing guids on recurring components to do this in the past). If that's still too much author-land complexity, you could make the case for a teardown: true flag.
But I digress. I appreciate the virtual DOM diffing logic is incredibly sophisticated and what I'm describing above is a significant low level departure, and I realise this is a huge and far reaching topic with all sorts of implications beyond the scope of this issue. So let's leave that for the time being :)
WRT to the naming conventions and semantics implications (for internal purity AND user clarity), I think it's really contrived to think giving the authors a new term for the method is of any significant benefit. Regardless of the purported value in making RouteResolvers and Components intrinsically distinct entities, giving this method a different key seems silly.
The 'recyclable route view' is a point of convergence we can both agree is nice (in some cases essential) to have. I'm still not convinced it's essential to create a new API entity to benefit from this behaviour. As I've said before, onbeforeremove is a valuable special hook which is only triggered conditionally as a result of where the vnode is positioned. It can persist the underlying dom in flagrant exception to the normal rules under certain conditions, but if those conditions aren't matched it will never be triggered and that special async behaviour will never be triggered. Why are routed components so different? Surely it's easier for a developer to grok the special circumstance by virtue of context of a routed component than it is to appreciate that technically, the vnode in question was never removed — it simply disappeared as a result of one of its ancestors being removed (well, it is removed, it's just never beforeremoved).
Meanwhile, the internal semantics of RouteResolvers remains problematic. It's acceptable for a RouteResolver to have a view and an onmatch and yet for the onmatch resolution to make the view redundant. Following the 'inherently distinct entities' dogma, we should surely be saying there are 2 distinct flavours of RouteResolver: one which instantly unconditionally resolves to it's own view and another which eventually resolves to a component. The idea that you might want to perform async resource loading and provide new entities to resolve to is _exciting_ but weird. What if I match a previously matched path and resolve to a similar / identical component? Does that obviate & supersede the benefits of using RouteResolver views? IMO we'd be better off limiting complexity by saying resolve is simply done - a 0-argument function which results in the rest of the object being processed? This way we have 3 possibilities for routed entities:
onmatch. Consult the rest of the object's properties as normal, synchronously.onmatch: halt the lifecycle loop indefinitely and execute the function, awaiting…m.route.setonresolve execution, leading back to 1.All the benefits of async code execution can still be obtained via modifying attrs (currently feasible?) or, more appropriately, state (requires some rework AFAIK) — at which point it can be read by the view and dealt with accordingly.
I preferred the way data flowed in the original implementation.
Adding path and route to the attrs seems wrong for two reasons:
@pygy what was the original implementation?
WRT potential app space value for these properties, these are only be exposed to the route endpoint entity — there is no other mechanism for determining the input attrs for such a vnode, nor would they leak anywhere else unless the author explicitly passed them along. So in practice, I can't see how it would ever be a problem.
What landed in tree originally, and is demo'ed by Leo here: https://github.com/lhorie/mithril.js/issues/1095#issuecomment-231743262
I like that behavior coupled with the onmatch(resolve) terminology.
@barneycarroll I agree with Leo that components that clean up after themselves (hereafter "dense") are better by default. Adding hollow components (that can be swapped while preserving their content) as you suggest would be strictly more powerful since you could use keys to simulate the dense behavior, but they are tricky by nature.
If the diff engine treated the components as hollow, but the hyperscript factory added the component as key when no explicit key is set, we would keep the sane default while enabling hollowness by setting the key to Scratch that, keys must be unique in a given space, that would not allow to have lists of components.null. Possible gotcha: if m(Foo, {key: 1}) and m(Bar, {key: 1}) share the same keyspace (=> hollow behavior, children trees are diffed) and the user isn't aware of the hollow/dense difference. Dense behavior can be obtained by setting the keys to foo1 and bar1. But that means educating users.
Beside the layout wrapper, what uses do you have in mind for hollow components?
Would it be possible to resolve some issues by combining the onmatch and view functions, similarly to some of the earlier examples in this thread? Parts of the discussion above is circling around the issue that RouteResolvers currently are objects and share the naming of a key with Components which causes some confusion.
By combining onmatch and view, we could get something like this:
m.route(document.body, "/", {
"/": aComponent,
"/foo": function() {
return m(Layout, { body: MyComponent })
},
"/bar": function(resolve) {
resolve(load('MyComponent.js'))
}
})
So if the value of the route is an object, we have a Component, if it's a function, it should either return a composed component or eventually call resolve with a component to show.
Implementation idea (untested):
module.exports = function($window, renderer, pubsub) {
var router = coreRouter($window)
var route = function(root, defaultRoute, routes) {
var current = {path: null, component: "div"}
var replay = router.defineRoutes(routes, function(payload, args, path, route) {
args.path = path, args.route = route
if (typeof payload === "function") {
var resolve = function(component) {
current.path = path, current.component = component
renderer.render(root, Vnode(component, null, args, undefined, undefined, undefined))
}
if (path !== current.path) {
// not really sure what the first parameter should be here...
var result = payload(Vnode(null, null, args, undefined, undefined, undefined), resolve)
if (result) renderer.render(root, Vnode(result, null, args, undefined, undefined, undefined))
// else assume that the user will call resolve with something
}
else resolve(current.component)
}
else {
renderer.render(root, Vnode(payload, null, args, undefined, undefined, undefined))
}
}, function() {
router.setPath(defaultRoute, null, {replace: true})
})
autoredraw(root, renderer, pubsub, replay)
}
// ...
I may well be missing some point (diff/redraw logic?), since I'm not familiar with the mithril source in detail.
@orbitbot you probably missed it since this thread is pretty long, but Leo said that using view on RouteResolvers was a bad idea after all. I'm working with onmatch(resolve) and render(vnode) currently, even though wrap, dress or decorate may also work for the second method...
The drawback of your suggestion is that the function becomes either a onmatch or a render, you can't have both at the same time...
Also, while I'm at it, here's what a AFAICT race conditions resistant mount-based router would look like: https://github.com/pygy/mithril.js/blob/1fd7f7665904567082de3661530333a1b25bca1f/api/router.js
The drawback of your suggestion is that the function becomes either a onmatch or a render, you can't have both at the same time...
The main reason I was making the comment is that I can't really see the point of the render method to start with, so what would be the value of having both?
Unless I'm mistaken you can achieve the same result by returning a composed component _and_ calling resolve later, which should be functionally identical to if both methods exist as onmatch and render.
What if the component is created asynchronously, after onmatch returns?
Also, on redraw, you'd have to skip the onmatch logic and just return a node...
What if the component is created asynchronously, after onmatch returns?
Then the resolve function is called (line 8) and whatever was there is replaced (as before, if you have both onmatch and render).
Also, on redraw, you'd have to skip the onmatch logic and just return a node...
But isn't this already what happens with the caching in current inside the resolve function above? I guess there probably should be some caching after if (result) ... on line 15 as well.
What about this scenario?
{
onmatch: function (resolve) {
resolve(Foo ? Bar : Baz)
},
render: function (vnode) {
return m('.layout', vnode)
}
}
The current logic is messy because the whole route resolution logic (including route matching, i.e. generating regexps for each route to check if they match the proposed path) also runs on redraws. So you have to jump through hoops to determine if you really are resolving a route or just redrawing.
It is far less tangled if you mount a router component and redraw its root in the resolve callback, as shown here: https://github.com/pygy/mithril.js/blob/mount-based-router/api/router.js.
That version supports every scenario the current version does (including detecting third party history.pushState() calls), re-renders when route.set(route.get()) is called, and AFAICT doesn't suffer from race conditions when using delayed resolution.
Change the signature of what resolve expects, so you can do resolve(m('.layout', Foo ? Bar : Baz)).
The linked refactor looks cleaner, similar changes to that would be:
module.exports = function($window, mount) {
var router = coreRouter($window)
var currentComponent, currentArgs, currentPath
var RouteComponent = {
onbeforeupdate: function() {
var livePath = router.getPath()
if (livePath !== currentPath) {
route.setPath(livePath)
return false
}
},
view: function() {
return Vnode(currentComponent, null, currentArgs, undefined, undefined, undefined)
}
}
var globalId
var route = function(root, defaultRoute, routes) {
currentComponent = "div"
currentArgs = null
mount(root, RouteComponent)
router.defineRoutes(routes, function(payload, args, path, route) {
var id = globalId = {}
var resolved = false
function resolve (component) {
if (id !== globalId || resolved) return
resolved = true
currentComponent = component
currentArgs = args
currentPath = path
root.redraw(true)
}
if (typeof payload === "function") {
var result = payload.call(resolve, args, path, route)
if (result) resolve(result)
} else {
resolve(payload)
}
}, function() {
router.setPath(defaultRoute, null, {replace: true})
})
}
Remove the resolved check (might be dangerous?) and you can have easy placeholder elements while you're waiting for your async module to load... If the methods are renamed from view to something else then a source of confusion is definitely removed, but I think you could perhaps survive with only object vs function separation as an arguably nicer API. To be honest, I'd need to use either/both for a while to figure out what feels better.
Which async module loaders are we talking about? It might very well be possible to use async modules without any need to build resolve into mithril.
@orbitbot onmatch and render have fairly different semantics. I did try to merge the two but it ends up becoming fairly ugly to support all the use cases.
The main purpose of render that onmatch/resolve don't address is to recompute a view with new route parameters (for example when going from /grid?sort=asc to /grid?sort=desc). If resolve expected a vnode, then onmatch would need to be called on every redraw. That doesn't work because resolve can be called asynchronously, and then I'd need to pass some sort of flag to indicate a first-time resolve vs a redraw (or I would have to resolve a render-like function - at which point it might as well be a separate method)
@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 RouteResolver.render adds to component.view in the /grid?sort=asc to /grid?sort=desc scenario... Aren't these passed as args/attrs anyway?
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 myRequire. First, an example of using 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
routeResolver = {render: () => m(Layout, m(Foo))} // component structure is Layout > Foo
component = {view: () => m(Layout, m(Foo))} // component structure is AnonymousComponent > Layout > Foo
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 onmatch is also there for pre-render redirects (i.e. calling m.route.set before the first render pass, thus avoiding flickering, potential null ref, unwanted lifecycle method calls and other issues)
I know the general semantic differences (I've re-implemented api/router.js twice already, tests all green), I don't understand why they matter in the specific case of sorting based on args change...
@lhorie Yes, and I think pre-render redirects are great :) I'm trying to remove the responsibility of choosing which vdom to render from onmatch.
@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:
withFullSignature = {
onmatch: function(vnode, resolve) {
if (loggedIn) resolve() //resolve to nothing
else m.route.set("/login")
},
render: function() {return m(Foo)} //hard code it here
}
@lhorie True,but I'm addressing many of the resolve(vnode) examples I'm seeing.
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 {render: () => m(Layout, m(Foo))})
2 - some way to do code splitting (currently {onmatch: (vnode, resolve) => require(["Foo.js"], resolve)}
3 - some way to do pre-render redirects (currently {onmatch: () => m.route.set("/")})
4 - some way to recreate from scratch on route reload (currently not supported without custom state machine hacks)
5 - route args should always be available for the above use cases
6 - all of the features above must be possible to use at the same time within a single route
I agree with you that it doesn't seem possible to simplify the API further.
Instead, Ill suggest an addition: a default, user-definable route.render that can be superseded on a per route basis by RouteResolver.render. That would simplify route declarations by removing boilerplate.
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:
m.requests - but you could change that behaviour or avoid it entirely if you disagreed with that opinion. The classic example in Mithril v0 is that a route-level component halts view execution until it receives data which it then passes to the view. Mithril v1 turns this on its head by saying you can halt execution until it receives a glorified view structure which you can't pass anything into1.At the risk of boring everybody away from this thread forever I really must urge that we have a deep and honest think about this whole 'code-splitting' thing and its purported practical benefits, because I think there's some serious flaws in the value proposition here which are intrinsic to the confusion of concerns in RouteResolver / Component.
Consider that managing async interactions with webservices is an established, ubiquitous, mundane, regular & essential part of any holistic SPA front-end framework. Consider that Mithril provides a complete batteries-included API for this. Consider that the application code for that task involves a pattern that involves calling a method, returning a wrapper for an async value, and _binding it to a stateful object, which can later be interpolated to retrieve the value_.
Consider that splitting a front-end application's holistic structure into modules for piecemeal asynchronous invocation in a single runtime is a relatively recent phenomenon which is neither obvious in its application nor necessarily desirable, and requires architectural thinking beyond the scope of generic webservices and Mithril's immediate remit. Consider that Mithril provides none of the tooling necessary to implement code-splitting - only an API which can accept an object which could have been code split.
resolve doesn't do anything to help with code-splitting - it's just a function which consumes an object. You can't implement async component loading and be under the illusion that it is doing anything useful with this regard, because you will have to have done all the significant work yourself to get there. And even if we're just aiming to solve some arbitrary aesthetic issue - that resolve( myComponent ) feels nicer than Object.assign( state, { myComponent } ) - then we must accept that our target audience is going to be disappointed when they discover that every other method of passing async values from one context to another necessitates assignment to an object. Lifecycle deferral - the ability to pause the Mithril run loop - _is_ a feature, of course. But are the people making use of this feature really going to be impressed by the convenience of not having to write a placeholder div when they realise that this is the only place it's possible, and every async-data dependent view can't do this - and must explicitly write conditional views to accommodate pre-resolution state?.
@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 onmatch hook, no need to paint a placeholder while the delayed component is loading.
@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. m.redraw.strategy semantics and the start/endComputation semantics of v0.2's m.request are extremely disruptive; changing it risks breaking things in completely unrelated parts of the application. One can argue that this is powerful, but similar to C pointer arithmetic, it's also a huge foot gun, which hinders code scalability and requires an inordinate amount of discipline to manage.
The form of code-splitting most people writing SPAs will be familiar with involves separating a statically served front-end shell (app.js) from the data it populates
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 onmatch/resolve is only about code splitting is missing the big picture. In a real life situation, the auth example relies on RouteResolver as well. Saying that those were possible in v0.2 ignores the disruptive nature of v0.2 semantics, the dichotomy of request-dependent redraws being more suitable for toy apps vs request-independent redraws being more suitable for production apps, the DOM-dirtying behavior of placeholders, etc etc.
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 mithril.js package, but it would be incredibly convoluted compared to v0.2, where at least the APIs made holistic sense. You must be able to see that for authors to stomach the fact that they that _either_ dynamically load a lifecycle-aware module _or_ pass route-related attributes to the view is absolutely weird. The fact that this is a necessary trade off in order for another mythical demographic to be able to use Mithril's route engine without pulling in hyperscript or lifecycle modules is small comfort.
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.
the fact that they that either dynamically load a lifecycle-aware module or pass route-related attributes to the view
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>
*/
all for the benefit of being able to avoid the user having to download the redraw API
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 m.request (!). We can't really mount much of an argument about how these semantics are useful in cases A or B if these same semantics cause crazy issues like that.
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, vnode.tag being the route resolver is a bit weird, but it provides a window into the JSBin sandbox :-)
description of what that code is doing
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.
Let's look at a credible scenario: /issues/:issueId. As a Mithril v1 developer I am faced with a choice: do I resolve to to the issues component, or do I pass down the valuable information of the issueId to the view?
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: /issues/:issueId. As a Mithril v1 developer I am faced with a choice: do I resolve to to the issues component, or do I pass down the valuable information of the issueId to the view? I'm honestly interested in your opinion on which is better, and why it should matter in application development. My gut feeling is that I'll be telling StackOverflow adopters that "that's the way it has to work, regardless of your concerns. but here's a cool hack from @pygy whereby you modify the DOM in onmatch". For my part, I think the distinction between these choices is obnoxious, and I can't hear a single voice that says it resolves any given problem. Mark my words: no-one will ever engineer a system whereby they request a unique component that combines data and component. That is THE anti-pattern the entire web movement has been working against this whole time. If that's an honest suggestion from Mithril, I may as well go back to PHP pages. Seriously, who benefits from this?
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 m.request reference errors. What's the problem here? Have you solved it? Can you show me how things were a problem then and are no longer a problem now? Again, the nebulous justification seems completely disconnected from the purported solution.
Let's look at a credible scenario: /issues/:issueId. As a Mithril v1 developer I am faced with a choice: do I resolve to to the issues component, or do I pass down the valuable information of the issueId to the view?
You pass both, by default. Look again at the JSBin I posted above... In render() the component is in vnode.tag and the args in vnode.attrs.
And I'm not modifying anything in onmatch...
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:
var Issue = {
view: function() {
return m("div", "Issue id: " + m.route.param("id")) // this component would be more reusable if id was an argument
}
}
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.
Unless you can show me how you solve them
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 onmatch - is possible in v1: it simply requires a more open attitude to 'mixed concerns'. I'm going to pursue this as a plugin instead of just ranting :)
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?
@barneycarroll
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 ["a", {href: router.href("users", 123)}, "User 123"]. Since domvm views do not have auto-redraw, you can invoke it at any time on any view (or subview). There are some DRY helpers in the router (like a didEnter(route) handler, so you can set up redraw / logging / whatever on any route entry).
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.
Most helpful comment
update: I added experimental support for
resolveandrenderhooksUsage:
Redirections can be done this way: