Comments (7)
So I am using an additional utility to extract all params from the current route
array
export function getRouteParams (route = []) {
let res = {}
for (const segment of route) Object.assign(res, segment.params)
return res
}
... and then in the brewer module
// brewer/service.js
import O from 'patchinko/constant'
import { findRouteSegment, getRouteParams } from '../util/routing-state-helpers'
import { whenPresent } from '../util'
export function service ({ state, update }) {
const params = getRouteParams(state.route.current)
whenPresent(
findRouteSegment('Brewer', state.route.arrive),
arrive => {
const id = params.id
console.debug('[service :: brewer]', 'Set beverage', id, ' brewer description')
update({ brewer: O({ [id]: 'Brewer of beverage ' + id }) })
}
)
whenPresent(
findRouteSegment('Brewer', state.route.leave),
leave => {
const id = params.id
console.debug('[service :: brewer]', 'Tear down beverage', id, ' brewer description')
update({ brewer: O({ [id]: O }) })
}
)
}
... and then in the Brewer component
// brewer/view.js
import { createElement as el } from '../util/render'
export function Brewer ({ state, actions, routing, beverage }) {
return (
el('div',
null,
state.brewer[ beverage ],
el('button',
{
className: 'btn',
onClick: () =>
actions.navigateTo(routing.parentRoute())
},
'x'
)
)
)
}
... which gets passed the beverage id as a prop from the Beverage parent component
// beverage/view.js
import { createElement as el } from '../util/render'
import { Brewer } from '../brewer/view'
import { Route } from '../routes'
import { router } from '../router'
const componentMap = { Brewer }
export function Beverage ({ state, actions, routing }) {
const Component =
componentMap[ routing.childSegment.id ]
const id = // beverage id
routing.localSegment.params.id
return (
el('div',
null,
el('div',
null,
state.beverage[ id ]
),
el('div',
null,
el('a',
{ href: router.toPath(routing.childRoute([ Route.Brewer() ])) },
'Brewer'
)
),
Component &&
el(Component, {
state,
actions,
routing: routing.next(),
beverage: id }),
el('div',
null,
el('a',
{ href: router.toPath(routing.siblingRoute([ Route.Beverages() ])) },
'Back to list'
)
)
)
)
}
from meiosis.
🤔 an alternative would be to make the parent route params available on the child routes:
// helpers/routing/router-helper/index.js
export function createRouteMap (
routesConfig = {},
path = '',
fn = () => [],
acc = {},
parentParams = [] // <- new
) {
return Object
.entries(routesConfig)
.reduce((result, [ id, config ]) => {
const [ configPath, children ] = getConfig(config)
const routeParams = findPathParams(configPath).concat(parentParams, findQueryParams(configPath))
const localPath = path + getPathWithoutQuery(configPath)
function createRoute (params) {
return fn(params).concat({ id, params: pick(routeParams, params) })
}
result[localPath] = createRoute
createRouteMap(children, localPath, createRoute, result, routeParams) // <- routeParams as parentParams
return result
}, acc)
}
That way with an url path pattern like #/coffee/:id/brewer
, :id
would automatically also become available as a param for the brewer child route in state:
// state for location.hash === '#/beer/c2/brewer'
{
/* ... */
route: {
current: [ { id: 'Beer', params: {} }, { id: 'Beverage', { id: 'b2'} }, { id: 'Brewer, { id: 'b2' } } ],
}
}
I am not sure if auto insertion is what one would expect in all situations though. 🤔
After a bit more testing, everything else seems to be going very smooth. This routing solution is fantasic.
The only other thing that comes to mind, is that the routes
and router
modules could probably be consolidated into a single module. (not easy in this particular instance because of how you isolated the business logic in common-core
- very clever btw)
Also router.locationBarSync
could be a service
function as well, couldn't it? That would keep the index.js
a bit leaner.
Last but not least, also in index.js
/* ... */
import { createApp } from './app'
import { router } from './router'
const app = createApp(router.parsePath(window.location.hash))
/* ... */
... would land you right at your destination.
Also, ping me if you want help writing up the docs.
from meiosis.
Alright, found one other gotcha when working with query params, in general:
When changing query params in the browser bar it is possible that state.route.arrive
and state.route.leave
both reference the same route at the same time. This may require to be more specific when relying on route states, e. g. in a service.
So for example leave assertion in case of the brewer service it would look something like this:
whenPresent(
((!brewerArrive && brewerLeave) ? brewerLeave : null),
leave => {
const id = params.id
console.debug('[service :: brewer]', 'Tear down beverage', id, ' brewer description')
update({ brewer: O({ [id]: O }) })
}
)
Playing a bit more with automatically passing down params to child routes, I think the mechanism should only apply to path params and not include query strings
Here is an updated version of the createRouteMap
functions that reflects that:
// helpers/routing/router-helper/index.js
export function createRouteMap (
routesConfig = {},
path = '',
fn = () => [],
acc = {},
parentParams = []
) {
return Object
.entries(routesConfig)
.reduce((result, [ id, config ]) => {
const [ configPath, children ] = getConfig(config)
// const localPath = path + configPath
const pathParams = parentParams.concat(findPathParams(configPath))
const routeParams =
[ ...new Set(pathParams.concat(findQueryParams(configPath))) ]
const localPath = path + getPathWithoutQuery(configPath)
function createRoute (params) {
return fn(params).concat({ id, params: pick(routeParams, params) })
}
result[localPath] = createRoute
createRouteMap(children, localPath, createRoute, result, pathParams)
return result
}, acc)
}
from meiosis.
@smuemd fantastic work. I am putting a lot of thought into these excellent points and I hope to have an update soon :)
from meiosis.
@smuemd ok here is what I have:
one thing I noticed is that with the router's toPath functions all values that are not explicitly defined in routeConfig as path values or query params will be dropped
In practice this means that in the routing example this:
<a href={router.toPath(routing.childRoute([Route.Brewer({ id })]))}>Brewer
...will result in id === undefined at the brewer component for route #/coffee/c2/brewer, as currently specified.
Good catch! After considering passing parent parameters down to child routes, I'm thinking it might be preferable to keep routes "deterministic", so to speak, in that each route segment indicates precisely which parameters they want.
So, I've added a way in the route config to indicate which parameters a route segment wants to receive from parent routes:
const beverageRoutes = {
Beverages: "",
Beverage: [
"/:id",
{
Brewer: ["/brewer", ["id"]]
}
]
};
An array of parameters to receive from parent routes. That way, you can still be precise with route parameters for each segment.
Now a config can be:
key: path
key: [path, childConfig]
key: [path, parentParameters]
key: [path, parentParameters, childConfig]
I like how concise that is, but perhaps it is a little bit too cryptic. A more verbose and explicit config may be preferable, such as
const beverageRoutes = {
Beverages: "",
Beverage: {
path: "/:id",
children: {
Brewer: { path: "/brewer", parentParams: ["id"] }
}
}
};
With this, because the parameter is picked up from the parent, we don't need to specify it in the child route:
<a href={router.toPath(routing.childRoute([Route.Brewer()]))}>Brewer</a>
When changing query params in the browser bar it is possible that state.route.arrive and state.route.leave both reference the same route at the same time. This may require to be more specific when relying on route states, e. g. in a service.
Without getting all parent params and instead just the ones you specify, that should help avoiding a false route change. It's still possible to get the same route in arrive
and leave
, but that is legitimate, such as when going directly from one brewer to another. The service can both clean up the id
that we left, and the id
at which we are arriving:
export const service = ({ state, update }) => {
whenPresent(findRouteSegment(state.route.arrive, "Brewer"), arrive => {
const id = arrive.params.id;
update({ brewer: O({ [id]: `Brewer of beverage ${id}` }) });
});
whenPresent(findRouteSegment(state.route.leave, "Brewer"), leave => {
const id = leave.params.id;
update({ brewer: O({ [id]: O }) });
});
};
The only other thing that comes to mind, is that the routes and router modules could probably be consolidated into a single module. (not easy in this particular instance because of how you isolated the business logic in common-core - very clever btw)
Indeed, my example is somewhat particular because of what you've indicated, that I'm isolating and re-using code in common-core. In an app where that is not necessary, some consolidation can certainly be done.
Also router.locationBarSync could be a service function as well, couldn't it? That would keep the index.js a bit leaner.
I did have it as a service initally, but then I ran into a chicken-and-egg problem. I think that is no longer the case, I'll verify if I can reorganize that.
Last but not least, also in index.js
/* ... */ import { createApp } from './app' import { router } from './router' const app = createApp(router.parsePath(window.location.hash)) /* ... */... would land you right at your destination.
I already have
const app = createApp(router.initialRoute);
which arrives directly at the url... did I misunderstand?
After a bit more testing, everything else seems to be going very smooth. This routing solution is fantastic.
That is really great to hear. Thank you so much for trying this out, testing scenarios, and sharing your ideas, it's very helpful and much appreciated, keep them coming :)
Let me know what you think of the above. Cheers 🍻
from meiosis.
Also, ping me if you want help writing up the docs.
Actually, I could use some suggestions on what to use to document the API. jsdoc?
from meiosis.
So, I've added a way in the route config to indicate which parameters a route segment wants to receive from parent routes
An array of parameters to receive from parent routes. That way, you can still be precise with route parameters for each segment.
Brilliant. That works really well.
I like how concise that is, but perhaps it is a little bit too cryptic. A more verbose and explicit config may be preferable, such as
I would stick with the array based config. I think ledibility is fine, especially if you force yourself to always declare all three elements in an array, even if not needed
// routes/index.js
const beverageRoutesA = {
Beverages: '',
Beverage: [ '/:id', [], {
Brewer: [ '/brewer?location?misc', [ 'id', 'type' ], {} ]
} ]
}
export const routesConfig = {
Home: '/',
Login: '/login?returnTo',
Settings: '/settings',
Tea: [ '/tea', [], {
TeaDetails: '/:id'
} ],
Coffee: [ '/coffee?type', [], beverageRoutesA ],
Beer: [ '/beer?type', [], {
Beverages: '',
Beverage: [ '/:id?flavor', [], {
Brewer: [ '/brewer?location?misc', [ 'id' ], {} ]
}]
} ]
}
☝️ this is perfectly explicit
It's still possible to get the same route in
arrive
andleave
, but that is legitimate
I agree. This is resolved by having increased the precision of passing params.
I already have
const app = createApp(router.initialRoute);
Yes, I totally missed that one. False alarm here, sorry.
Thank you so much for trying this out, testing scenarios, and sharing your ideas
Yeah, a while back I was working on an app that had to heavily rely on the location bar as a data source and automatic processing (i. e. services). I felt compelled to run this against some of my pain points from then, that I still remember. The routing and service patterns have really come along way in accommodating those kind of more involved cases, without making you want to pull your hair out. It's very powerful and ergonomic, I think. Well done!
Actually, I could use some suggestions on what to use to document the API. jsdoc?
There is also ESDoc
...and Documentation.js (which uses jsdoc notation)
Mapbox uses Documentation.js. You may wanna take a look at this for inspiration. I haven't used it myself, but if I had to, I would start with consulting the mapbox implementation.
from meiosis.
Related Issues (20)
- How to integrate it into a redux app HOT 4
- All the documentation removed? HOT 1
- Tutorials for react deleted HOT 1
- Example Implementation HOT 1
- About React Hooks and meiosis state HOT 1
- SAM Pattern nap issue HOT 6
- Brilliant! HOT 5
- more food for thought HOT 3
- Error on auth redirect HOT 2
- subtle routing bug HOT 6
- Convert to Mergerino HOT 2
- Routing helper broken when used with mithril router without the prefix
- Feature request: bind actions to Actions object HOT 3
- examples with react-hook (or solidjs state) HOT 4
- meiosis-setup with snowpack? HOT 1
- How to turn off a "map" callback? HOT 5
- Alternative way of connecting stream to React function components HOT 2
- Question on initialRoute HOT 8
- meiosis-setup with typescript: some suggestions HOT 7
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from meiosis.