christianalfoni / reactive-router

A reactive wrapper around Page JS
MIT License
46 stars 1 forks source link

Changes to the project #7

Closed christianalfoni closed 9 years ago

christianalfoni commented 9 years ago

Okay, so me and @bfitch has been playing around with the project I think we cracked it!

Take a look at this reasoning:

So there are now two projects: addressbar and url-mapper (no repo yet). So conceptually this is what they do:

addressbar

Makes the addressbar in the browser work like an input. This means that you can listen to url changes and you can set the value of the addressbar without causing side effects. This has nothing to do with routing, it is just a prerequisite for a reactive approach.

url-mapper (Please suggest other names)

This library just takes a url and maps it to a function. Parsing out params, and soon queries.

So now we have the two building blocks we need to do something pretty awesome in Cerebral:

How it works with Cerebral

First we expose our url-mapper as a serivce.

controller.js

import Controller from 'cerebral';
import Model from 'cerebral-immutable-store';

import route from 'url-mapper';

const model = Model({
  url: '/'
});

const services = {
  route: route
};

export default = Controller(model, services);

Now we create ONE signal that will handle all the routing.

main.js

import controller from './controller.js';

// We define one action routing urls to outputs
function route (input, state, output, services) {
  state.set('url', input.url);
  services.route(input.url, {
    '/': output.home,
    '/messages': output.messages,
    '/messages/:id': output.message
  });
}
route.outputs = ['home', 'messages', 'message'];

// The one signal defining routes takes the routing action
// and handles the outputs
controller.signal('urlChanged', route, {

  // Just setting the page to home, like {currentPage: 'home'} etc.
  home: [...homeActionChain],

   // Action chain for setting page and loading message titles
  messages: [...messagesActionChain],

  // We reuse the previous action chain and add actions to load the
  // specific message
  message: [...messagesActionChain, ...messageActionChain]
});

// We listen to changes on the addressbar, preventing any default behaviour
// and trigger the signal
addressbar.on('change', function (event) {
  event.preventDefault();
  controller.signals.urlChanged({url: event.target.value});
});

// Whenever we trigger "urlChanged" signal manually or use the debugger
// the addressbar will still reflect the correct url in the addressbar
controller.on('change', function () {
  addressbar.value = controller.get('url');
});

So the beauty of this is:

  1. The addressbar is just a "special input"
  2. Routing is just mapping a url-string to some output
  3. With Cerebrals action chains it is very easy to compose them together
  4. You state is now FREE! You can set whatever state you want related to a url. Transitions and stuff is something you handle in a component, where it belongs!
  5. It can be used with any solution really

I am making a video on this today, please let me know if you have any initial thoughts. I will make sure to make an example with transitions :-)

You can test this by cloning the V2 branch, npm install and npm start

Guria commented 9 years ago

Let suppose we got /messages/42 as url:

Now how to proceed in reverse path? As I remember you declared that view shouldn't know anything about urls and routing. So I suppose that setting displayedMessageId or currentView state values from any action would change url accordingly. How would you suggest to achieve it?

garth commented 9 years ago

Sounds like a really great solution. Looking forward to swapping out v1 for v2.

Maybe it would be worth to borrow a couple of functions from page.js or similar to do the url parsing. Then you can maintain the existing functionality with regards to the url data being passed to the next action.

christianalfoni commented 9 years ago

Hi @Guria and thanks for the comment,

The "other way around" is calling signals.urlChanged manually. So:

controller.signals.urlChanged({url: '/someUrl'});

addressbar.on('change', function (event) {
  event.preventDefault();
  controller.signals.urlChanged({url: event.target.value});
});

What I meant about "VIEW layer should not know about urls" is that your VIEW layer does not check the URL to figure out what to render. It still uses a URL as input to output actual application state changes. And those mapped state changes are what the VIEW layer uses... if that makes sense :-)

So one way is:

  1. Url changes in addressbar (but is prevented)
  2. State changing logic triggers urlChanged-signal, where url and other state is set
  3. State updates and addressbar value is set based on url state

The other way is:

  1. You manually trigger urlChanged-signal with a url, where url and other state is set
  2. State updates and addressbar value is set based on url state

It would be interesting to look at solutions where certain state is related to certain parts of your url. So changes to currentPage would automatically be /pages/{currentPage} etc. I think that would quickly become quite complex, but url-mapper is just one way of handling urls and state. A different lib could use addressbar to achieve what you are saying here, though I am not quite sure how you would express that. Hm... you would probably have to do something like this:

So instead of speifically setting the URL in the urlChanged signal you would dynamically create it based on state inside your tree.

controller.on('change', function () {
  var url = createUrlByState(controller.get());
  addressbar.value = url;
});

It is a really interesting concept, but you would have to somewhere define what each "section" of the url means. So /messages/123 -> [['currentPage'], ['messageId']], would map the url and insert state directly into those paths. Though reversing is really hard I think... hm...

You know, it would be fantastic to automatically produce urls back/forth just based on state and with the addressbar it is possible, there just has to be a different implementation than url-mapper to do it. Have to think about it a lot more :-)

So the goal now was to map urls to application specific state, which is to be used by the VIEW layer. You still use URLs to map to correct state changes. But yeah, removing the url completely and just map state values to a url would be really cool!

christianalfoni commented 9 years ago

@garth @bfitch has been working on the url parsing stuff, not sure if he dived into some existing implementations, but yeah, that is a good idea :-)

bfitch commented 9 years ago

@garth @christianalfoni What's in the v2 branch right now are just a couple functions that can parse urls with dynamic segments /foo/:id/bar/:guid into an JS object {ud: '123', guid: 'abc'}. I think once we have a solution for parsing query params we should be set, I think? I assume there's a small, well tested library out there that just handles parsing query params into objects (including complex arrays and nested objects). If not, I could take a crack at writing one.

Re: mapping app state to url "sections".....what if you used query parameters!?!

http://myapp.io?currentPage=messages&messageId=123&.....

then each time you change the state with a signal, you serialize the current state into query params. Then the query parser can parse that query string back and set as your new app state. Maybe a silly idea...but I've actually been thinking about trying this for a bit. I think it's a possible solution to "automatically produce urls back/forth just based on state".

bfitch commented 9 years ago

Looks like this is a good option for query strings: https://github.com/hapijs/qs . Maintained by the hapi.js framework and with zero dependencies.

christianalfoni commented 9 years ago

https://github.com/christianalfoni/reactive-router/issues/8