ractivejs / ractive

Next-generation DOM manipulation
http://ractive.js.org
MIT License
5.94k stars 397 forks source link

RFC: The Official Router™ #3134

Open fskreuz opened 6 years ago

fskreuz commented 6 years ago

Description:

By popular demand, this is a placeholder issue to discuss The Official Router™ for Ractive.

There's two schools of thought when it comes to routing:

Config-based (Angular and family) which defines a list of paths and the matching component to render. There's usually a special element-like item (I think they call it "outlet") defined in the template where the component will be rendered.

const routes = [
  { path: /some pattern/, component: MyComponentConstructor, ...otherOptions },
]

Component-based (React and friends) which defines the routing on the template itself. Feels very natural to Ractive, inner-HTML + yielders and all.

<Route path="some pattern" ...otherOptions>
  <MyComponent ...someProps />
</Route>

I'm no expert in routing (I personally use page.js/history API + redux-like state management) so I'm not entirely sure why one framework did it this way, but the other that way. I'd like to know how people prefer their routing.

ceremcem commented 6 years ago

I've applied a router in ScadaJS, by using the "Component-based" approach. At the end, a router design will have to answer the following questions at least:

  1. Which routing approach will it choose? History API or hash routing? I prefer hash routing since it's mandatory if you don't want to wrestle with the reverse proxy setups.
  2. What will it do with the temporary parameters (like tokens that should be deleted from address bar after processing)?
  3. How will it communicate with other components and non-Ractive Javascript instances (and vice versa)? (Many components might need to interact with current address, such as a data table to determine the opened row, and any of them might want to change its responsible part (like a login code) )
  4. Bonus: Will it replace the current <a></a> tag in order to become interoperable with 3rd party libraries?

IMHO, a router is both easy to implement (either from scratch or by integrating a 3rd party router) and very application specific. The hard part is making the design decisions. So it won't worth it.

I'm not sure, but it might add a little more complexity to the design decisions (which makes it more application specific) when it comes to async components. (eg. How will it handle the async component fetching process?)

paulocoghi commented 6 years ago

I will answer the question how people prefer their routing showing a humble approach that I have made years ago that proposes a solution to both routing and async component load in Ractive.

I have integrated Page.js with Ractive, bundling the two as a single tool*.

*although always available, it was never really officially launched, and today I'm remaking it with Svelte before finally launching, transforming it into a development tool

It followed the "config-based" routing approach, and the syntax for the (still available) Ractive version was:

...
    <body>
      <script src='js/altiva.min.js'></script>
      <script>

        var app = new Altiva ()

        /* Here you define the dynamic areas that will host comonents  */
        app.sessions( [ 'header', 'content', 'footer', 'modals' ] )

        app.route( '/',
        {
            header: 'Menu'
            content: 'Feed'
            footer: 'NewFooter'
        });

        app.route( '/login',
        {
            content: 'Login'
            modals: 'ErrorModal'
        });

        app.start();

        </script>
    </body>
...

Internally, it registers all the components used in each route and create functions (only once in runtime) that asynchronously load every (not already loaded) component for each route.

Each of these functions are passed to page.js, so after this integration, everything runs automatically.


Obs 1: my code level if far lower than yours, even more two years ago when I have made this tool, so if you take a look in my code, do not be scared. I know that I have a lot to learn and improve :grimacing:

I'm working to do something better in the second version, with everything that I have learned since then, but it's based on Svelte.


Obs 2: The tool isn't small (82 kB gzipped) since there are also other features in it, like code compatibility with both browsers and mobile environments (including Cordova, Cocoon and others), already included and transparent PouchDB functionality with Ractive variables, including auto sync with CouchDB servers, ajax functionality, JWT authentication, and other things.

evs-chris commented 6 years ago

This is one of those things that's going to vary pretty widely based on preferences, but I think we should probably have something that's set up to be beginner friendly. At this point, Ractive's biggest draw is probably how easy it is to get started, while still being not too overly huge and still reasonably fast. It also scales pretty well from hello world to medium-sized app (on an enterprise scale - up to a hundred or so major views pulled in async), though I'd be surprised if a generic router would make it that far.

Given that, I really like the declarative approach where you start with a shell and use sub-component-ish things to wire up routes. Here's what just popped out of my head, so it may be crazy:

<shell>
  <left>
    {{#if ~/user.loggedIn}}
      <menu>...base menu...</menu>
      <route bind-match='/^\/foo/'>...menu to show if in /foo/ sub...</route>
    {{else}}
      <route not-match="/log/in" redirect="/log/in" />
    {{/if}}
  </left>
   <center>
     <route match="/log/in">
        ...login form...
     </route>
      <route match="/foo/bar" view="./foo/BarList" bind-list="~/bars" />
      <route match="/foo/bar/:id" view="./foo/Bar" bind-bar="getBar(route.id)" />
      <route-target />
      <footer>
        <route match="/">...footer content when at root route...</route>
      </footer>
   </center>
</shell>

That example's a bit all over the place, but it illustrates content rendered on route match, (possibly) async loading of view components, and binding view data based on a route-local variable. I don't think it would be particularly hard for all of that to be feasible, and I may try plop it into an extension of my Shell component if I get some free cycles. Macro partials and pre-processing component templates can go a really long way to making declarative components quite comfortable to use.

I think it's pretty obvious that declarative routing isn't going to scale beyond a few tens of routes at the most, so I think the routing component should also have an API that allows defining routes similar to page.js so that other middleware-ish things can be accomplished too. The only major difference between declarative routes and the API, aside from lower-level access for the API, would be that the API version wouldn't get automatic data binding, so there'd have to be special config for setting up links.

Another thing I think would be really cool to have would be a way to anchor a set of routes into their own container so that they could operate independently of other containers. I think most moderately complex apps are too limited by a single route destination, so having multiple containers that could share the url would allow pseudo-independent portions of a page to be in different states at the same time while still having a representative url. That's probably beyond the scope of an Official™ router, though.

kouts commented 6 years ago

+1 to what @evs-chris said, plus I really like some concepts of Vue router like In-Component Guards, Lazy Loading Routes, Scroll Behavior etc and the way route instance is available to all child components.

PaulMaly commented 6 years ago

There's two schools of thought when it comes to routing:

Maybe it will be a little bit impudently, but I try to suggest one more way to implement navigation and routing. I believe that is the best way for Ractive, too.

A few years ago, I've touched many-many solutions for routing, including this two ways you described. My main conclusions:

"Config-based (Angular and family) " - too many boilerplate. "Component-based (React and friends)" - not appropriate things in templates.

And one more that is peculiar to both - low flexibility, you know: "this route = this component, that route = this one." If we need a little bit complex condition - even more boilerplate.

So, I decided to find the way, how to make it more simple and in too time more flexible. The main idea is that the client "state" is more complicated than just URL you have.

Simple example, you've a modal and you want to show it by fragment in your URL "/#myModal", but you also have one more condition - only authorized users can see it. And what are we gonna to do? Include this info into URL or check it inside the component? But Modal-component has a general purpose, so basically, we shouldn't specify it with such things.

So, I've thought why not to make router state is just a usual reactive state? To make it, I use Page.js - most simple but powerful router I ever used.

My first, most simple solution was something like this:

const qs = require('qs');
const page = require('page');
const Ractive = require('ractive');

const $$app = new Ractive({
      el: '#app',
      template: require('./tempalates/parsed/app'),
      enhance: true,
      append: false,
      lazy: true,
      adapt:  [
           require('./adaptors/context') // first I wrote page.Context adaptor
      ],
      components: {
          products: require('./components/list'),
          product: require('./components/entry')
      },
      decorator: {
          modal: require('./decorators/modal') // just decorate jquery plugin
      },
      data: {
          loggerIn: false,
          products: []
      }
});

// and further is very simple

$$app.set('$route', page.current);
$app.on('teardown', () => page.stop());
page((ctx, next) => {
    ctx.query = qs.parse(ctx.querystring);
    return next();
});
page((ctx)  => $$app.set('$route', ctx));
page.start({
    click: true,
    popstate: true,
        hashchange: true,
    dispatch: true
});

// actually, it's all that we need to set up routing

now we have reactive url state and can use it in different ways:

<!--  in templates -->

<!-- just use this state in any regular conditions -->
{{#if $route.match('/products/:id') }} 
      <!-- use router params, querystring, and event history api state as you want -->
     <product id="{{$route.params.id}}" cart="{{$route.state.cart}}"></product>

     <! -- links will be automatically handled -->
     {{#if ! loggerIn }}
      <a href="#login">Login to buy it</a>
     {{/if}}

{{elseif $route.match('/products') }}
     <products filters="{{$route.query}}"></products>
{{else}}
      <p>404 - Not found</p>
      <a href="/products">Go to search the best products</a>
{{/if}}

<!-- show this modal on any page if url has #login and user not loggerIn -->
<!-- e.g. /products#login || /products/1#login, etc. -->
{{#if $route.match('/*#login') && ! loggerIn  }}
<div id="login" as-modal>
<!-- modal implementation -->
</div>
{{/if}}

Besides, we can use all built-in opportunities of Ractive:

// or programmatically

// get route or a parts
$$app.get('$route');
$$app.get('$route.pathname');
$$app.get('$route.query');
$$app.get('$route.params');
$$app.get('$route.state');

// navigate to another route
$$app.set('$route.pathname', '/product/1');

// set history state
$$app.set('$route.state', state);

// listen route changes
$$app.observe('$route', () => {});

//  etc.

One more awesome thing is that this solution isomorphic, so we can simply use it on both - client and server.

This's an example of minimum implementation: page.js, 1 adaptor, about 10 line of code.

As I use my own extension above Ractive called 'ractive-app' module (200 line of code with few useful features and boring boilerplate), I wrap this router solution in plugin and use it like this:

const App = require('ractive-app')();

const $$app = new App({...});

$$app.use(require('ractive-page'));

...

$$app.get('$route'); // now use it
$$app.page(); // if you need to use page.js dirrectly

It's similar to the Vue's plugins: https://vuejs.org/v2/guide/plugins.html, but improved. Using the plugin, we have collected solution in one separated module.

p/s I know that page.js not maintained now (https://github.com/visionmedia/page.js/issues/384) and it's very sad. (((( In my work, I use my own version with few insignificant improvements.

If you guys will choose this solution, I think it would be great to appropriate page.js (or it's fork) to Ractive's family. Or you can replace page.js with any other router module in this solution, but my request - let's it will be isomorphic. And sorry for my bad English.

update:

Almost forgot to say about one more advantage - Ractive's transitions work out-of-box. ))))) So, we can use them to switch between pages:

{{#if $route.match('/products/:id') }} 
<div fade-in-out>
     <product id="{{$route.params.id}}" cart="{{$route.state.cart}}"></product>

     {{#if ! loggerIn }}
      <a href="#login">Login to buy it</a>
     {{/if}}
</div>
{{elseif $route.match('/products') }}
<div fade-in-out>
     <products filters="{{$route.query}}"></products>
</div>
{{else}}
      <p>404 - Not found</p>
      <a href="/products">Go to search the best products</a>
{{/if}}
TotallyInformation commented 6 years ago

Has anyone used Ractive with flatiron/director - it seems mature and well supported.

Or maybe riot/route?

There is also Roadtrip

PaulMaly commented 6 years ago

Hi, Julian! I'm glad you so active recently!

Yep, I tried to use Director as an isomorphic router, but it works really bad for this purpose, I think. Because it has two separate routers inside of it, for client and server sides. It's weird. Also, Flatron and Director not maintained about 2 years.

Or maybe riot/route?

It's a nice thing, but I'm not using it yet. But I plan to test could it work on the server and if yes, maybe I switch from pagejs to this one.

There is also Roadtrip

Nothing know about it.

Also, you can try Ractive's integrated routers like: https://github.com/MartinKolarik/ractive-route https://github.com/fayway/ractive-router https://github.com/ElliotChong/ractive-router https://github.com/PaquitoSoft/ps-ractive-router

Cheers!

TotallyInformation commented 6 years ago

Hi again Paul. No problem - just trying to learn quickly. Thanks for listing those options. I've not got to FE routing yet in my project but I will do.

dagnelies commented 6 years ago

Oh, well, since it's full of RFCs here, I'll add my opinion to this one too. I'd suggest a radically different approach: to not tie it to components.

To me, routing is most useful if the URLs and navigation history reflect the particular state we are in. Therefore, I would tie it to the data, not the components. Something like:

ractive = new Ratcive({
    ...
    routing: ['view','foo'],
    data: {
         view: 'details',
         foo: 'foooooooooo!',
         bar: 'baaaaaaaaar'
    }
})

Everytime the value of view or foo would be changed, it would affect the URL:

https://.../...#view=...&foo=...

The history would be updated as well. The other way round too, upon loading the page, arguments would be read and the data initialized accordingly.

It would be trivial to then implement a template like:

{{#if view == 'details'}}
   ...
{{/if}}
{{#if view == 'listing'}}
   ...
{{/if}}
PaulMaly commented 6 years ago

@dagnelies I think your approach is very similar to mine I described above.

Btw, Page.js maintains again!