Code Monkey home page Code Monkey logo

Comments (7)

smuemd avatar smuemd commented on July 23, 2024

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.

smuemd avatar smuemd commented on July 23, 2024

🤔 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.

smuemd avatar smuemd commented on July 23, 2024

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.

foxdonut avatar foxdonut commented on July 23, 2024

@smuemd fantastic work. I am putting a lot of thought into these excellent points and I hope to have an update soon :)

from meiosis.

foxdonut avatar foxdonut commented on July 23, 2024

@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.

foxdonut avatar foxdonut commented on July 23, 2024

@smuemd

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.

smuemd avatar smuemd commented on July 23, 2024

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 and leave, 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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.