cyclejs-community / cyclic-router

Router Driver built for Cycle.js
MIT License
109 stars 25 forks source link

Router outlet component? #195

Open kylecordes opened 7 years ago

kylecordes commented 7 years ago

Consuming cyclic-router is a relatively manual process, as shown in the README example. It provides mostly a driver. What are the thoughts on the idea of providing a higher level component, a "router outlet"? I have the following relatively simple implementation, which could be enhanced with something about parameters then land in a PR, if it seems likely to be considered and merged. Thoughts?

// Cycle cyclic-router RouterOutlet component This implements a relatively naive
// router outlet component, which pre-creates instances of all the destination
// route components, and supports only simple non-parameterized routing so far.

// Requires that incoming sources include a cyclic-router called router.
// Requires that application will consume a VDOM sink called DOM.

import xs, { Stream } from 'xstream';
import { VNode } from '@cycle/dom';
import isolate from '@cycle/isolate';
import { RouterSource, RouteMatcherReturn } from 'cyclic-router';

export interface RouteDef {
  urlPath: string;
  label: string;
  cssClass: string;
  componentFn: any;
}

interface Sources {
  router: RouterSource;
}

interface Sinks {
  DOM: Stream<VNode>;
}

export function RouterOutlet<S extends Sinks>(sources: Sources, defs: RouteDef[], sinkNames: string[]): S {

  const defsWithComps = defs.map(def => {
    const comp = isolate(def.componentFn)({
      ...sources,
      router: sources.router.path(def.urlPath) // scoped router
    });
    return { ...def, comp };
  });

  // Pass through all the non-DOM outputs

  const passThroughSinks = sinkNames
    .map(n => ({
      [n]: xs.merge(...defsWithComps
        .map(def => def.comp[n] || xs.never()))
    }))
    .reduce(Object.assign, {});

  // Switch between component DOM outputs:

  const match$ = sources.router.define(
    defsWithComps.reduce((result, item) => ({
      ...result,
      [item.urlPath]: item.comp
    }), {}));

  const dom$ = match$.map((m: RouteMatcherReturn) => m.value)
    .map((c: Sinks) => c.DOM).flatten();

  return {
    ...passThroughSinks,
    DOM: dom$
  };
}
ntilwalli commented 7 years ago

@kylecordes this is an interesting idea but the structure of this component seems app-specific. Is it true DOM sink will be the only non-merged (composed) sink? That may be true most of the time (I can't think of a scenario where that's not true), but that doesn't mean it is always true. This type of component template seems useful but opinionated in way that doesn't belong in the core lib.

I think it merits a blog post which serves as a possible guide and we could link to that post from the README or even publishing a different driver-lib which wraps this driver and enhances with this opinion.

kylecordes commented 7 years ago

@ntilwalli This is but my first attempt to make a sufficiently generic router outlet component. I attempted already to pull out everything app specific. It is boiled down to the minimum needed set of sources (one) and sinks (one) to do its job. I wasn't sure what the best way is to pass through other sinks. Sources already passed through unchanged as it sits.

What are the alternatives for passing through sinks? Something similar to this is under discussion over on onionify. The question there is how to make a generic container for many components in a list. The question here is how to make a single component that at any given moment contains only one from a list.

Over there:

https://github.com/staltz/cycle-onionify/pull/37

There is discussion of a generic/declarative way of asking for a certain set of sinks to be combined versus merged: pick({ DOM: 'combine', '*': 'merge' })

I wonder if @staltz or @jvanbruegge have thoughts on how that stuff might apply here. It seems very analogous to me.

It would be most disappointing if the ultimate answer is, that there is no sufficiently generic way to build either a router outlet or a collection container.

ntilwalli commented 7 years ago

I haven't kept up with the aforementioned discussion, but I do understand that it is useful to declare (at the root level) whether a sink is meant to be combined or merged so each individual component can take advantage of that info, but I don't consider this concern to be related specifically to routing. I do this in my application with a helper function which I use in all of my components - not just my routing components - while merging children components into the parent.

The question here is how to make a single component that at any given moment contains only one from a list.

Isn't this question precisely answered by isolate?

Are you asking if it's possible to construct a generic helper function that allows individual items in a collection to be targeted by different routes? If that's the question, I generally associate routes as sitting at a higher-level than an individual item in a collection. I can see a route mapping to a page which contains one or multiple collections. So I've never associated routing being a concern related to rendering collections.

That being said I could envision using routes to target individual items in a collection for highlighting/scrolling-to purposes... It might also be an interesting question to ask, can we take advantage of component isolation to automatically generate routes which allow individual components to be highlighted/scrolled-to within a collection?

Again, dunno if those questions relate to the problem you're trying to address. Can you give an example/scenario where this kind of component would be useful?

kylecordes commented 7 years ago

@ntilwalli and others,

No, isolate does not provide a router outlet. What I'm looking for is to not rewrite the router outlet functionality once per application, but rather to do it one time, and reuse across applications. This is analogous to how the router functionality works in numerous other SPA libraries/frameworks.

You are correct that routing has litte to do with rendering collections; the comparison I brought up is that the collection rendering mechanism has a generic way (in progress), to pass through the sinks of the components inside of it. The need to pass through a potentially different list of sinks per project, has not yet led to the conclusion that generic collection handling is unfeasible. I am suggesting here that a generic router outlet is a OK, much like generic collection handling is OK. Neither needs to be re-implemented per-application. (Edit: At least... I hope this is true for both.)

Here is an example application I have in progress, which uses cyclic-router:

https://github.com/kylecordes/cycle-example-1

The above first-draft router outlet code, in place in this project:

https://github.com/kylecordes/cycle-example-1/blob/master/src/routing.ts

Route definitions:

https://github.com/kylecordes/cycle-example-1/blob/master/src/routes.ts

Top level component, which uses the router outlet component to dynamically switch between various route destination components:

https://github.com/kylecordes/cycle-example-1/blob/master/src/app.tsx