cheatcode / joystick

A full-stack JavaScript framework for building stable, easy-to-maintain apps and websites.
https://cheatcode.co/joystick
Other
202 stars 10 forks source link

Consider a browser router for SPAs #205

Open rglover opened 1 year ago

rglover commented 1 year ago

I'd want this to more or less mimic what we have on the server. Ideally, have a way to automate this via @joystick.js/node by passing an spa: true flag to node.app() on the server that would bundle a routes table on the client. The idea being that when that is true, all hrefs on the client are automatically treated as internal and route via the History object.

Need to think and play to see if this is the best way to handle SPAs. I really want to avoid the mess that pattern has created but there are some legitimate applications for it and it's ignorant to make it second-tier functionality.

rglover commented 1 year ago

Mock that just came to mind...

import SomeComponent from '/ui/blah';

joystick.spa.router({
  '/path/to/:thing': (req = {}, res = {}) => {
    // req?.params req?.query req?.url
    // res.render() is only res method defined.

    // Idea is to make it so that the above req and res mimic the relevant parts of those objects on the browser.
    // Not a 1:1 copy, just a way to keep routes consistent.
    return res.render(SomeComponent, {
      layout: SomeLayoutComponent,
      props: {
        someProp: 123,
      },
    });
  },
});

Usage of a .spa object in the chain there is intentional. Want to be certain that developers don't confused the above as the main router (and suggest that it has a specific purpose).

This could run in index.client.js identical to how node.app() and its routes run in index.server.js.

rglover commented 1 year ago

Thinking about this again and routing via links. Could do something like joystick.spa.location('/path/to/go/to') which only maps to routes defined via the routes table above.

Another option (though could confuse) is to have some sort of spa: true on the node.app() function server side which tells the client to automatically nab all <a></a> tag clicks and route them via joystick.spa.location().

rglover commented 1 year ago

I keep coming back to this and the solution seems to be doing a client-side router with dynamic fetches of pages. I just realized this isn't too far from how client-side hydration works now, so short of URL parsing wouldn't be too wild to implement. Instead of the above importing of components (messy), we can just rely on the existing public routes for components:

import { router } from '@joystick.js/ui';

router({
  '/tester': (req = {}, res = {}) => {
    // req?.params // req?.query
    res.render('ui/pages/index/index.js', {
      layout: 'ui/pages/layouts/app/index.js',
    });
  },
  '/tester/another': (req = {}, res = {}) => {
    // req?.params // req?.query
    res.render('ui/pages/index/index.js', {
      layout: 'ui/pages/layouts/app/index.js',
    });
  },
});
rglover commented 1 year ago

Another riff on this. You could also do an additional folder at /ui/spas and just have Joystick components that represent an SPA (designated by a routes option on the component). These could work identical to how layout components work with a few modifications:

import ui from "@joystick.js/ui";

const App = ui.component({
  routes: {
    '/tester': (req = {}, res = {}) => {
      // req?.params // req?.query
      res.render('ui/pages/index/index.js', {
        layout: 'ui/pages/layouts/app/index.js',
      });
    },
    '/tester/another': (req = {}, res = {}) => {
      // req?.params // req?.query
      res.render('ui/pages/index/index.js', {
        layout: 'ui/pages/layouts/app/index.js',
      });
    },
  },
  render: ({ props, component }) => {
    return `
      <div>
        ${component(props.page)}
      </div>
    `;
  },
});

export default App;
rglover commented 1 year ago

Was thinking about this the other night and there's potential to automate it. Basically, the same way that we handle a mount for layouts (dynamic import) could hypothetically be used for an SPA.

The trick would be to have a flag on the node.app() object like spa: true which would tell Joystick to bundle a client-side route listener. That listener would intercept all <a> clicks and instead of doing the browser default, call to the server endpoint, get the response, and dynamically render it in the browser.

The goal/advantage being to remove any need for separate client-side routes config like the above. Define your routes once on the server, flag the app as spa: true and you're done.

rglover commented 1 year ago

Thinking about an API for a programming location/push API for this and landed on this:

ui.location('/path/to/place', {
  queryParam: 'thing',
});
rglover commented 12 months ago

This should also have some sort of hook logic for things like analytics. Something like...

// index.client.js

import joystick from '@joystick.js/ui';

joystick.spa.on('navigate', (event = {}) => {
  // event.from (from path)
  // event.to (to path)
});
rglover commented 12 months ago

In terms of implementing the automated version of this, I think I can just steal from the new HMR stuff and how I do remounting there.

rglover commented 10 months ago

Note I wrote in passing:

For client render:

You will need to check if you're rendering a standalone page or a layout. If a standalone page, just hijack the <a>
tags and handle their redirects with push state.

If layout, you will need to add a method to the component class instance like handleSetProps() which sets the
props for the component and then triggers a re-render. This would allow you to just get the current layout
component instance, set a new props.page (dynamically fetched/imported version of the page being routed to),
and trigger a re-render.