kriasoft / universal-router

A simple middleware-style router for isomorphic JavaScript web apps
https://www.kriasoft.com/universal-router/
MIT License
1.71k stars 104 forks source link

How to activate nested components with ids from the entire chain of nested routes. #136

Open adrian-moisa opened 7 years ago

adrian-moisa commented 7 years ago

After implementing the demo example I have reached a roadblock. In the intro example the routes seem to have a 1 to 1 relation between route and action.

const routes = [
    {
        path: '', // optional
        action: () => `<h1>Home</h1>`,
    },
    {
        path: '/posts',
        action: () => console.log('checking child routes for /posts'),
        children: [
            {
                path: '', // optional, matches both "/posts" and "/posts/"
                action: () => `<h1>Posts</h1>`,
            },
            {
                path: '/:id',
                action: (context) => `<h1>Post #${context.params.id}</h1>`,
            },
        ],
    },
];

I am used to building nested components that have a router outlet (Angular 2) or match components (React). For example I would have a nesting like this:

 <chapter-cmp>
    <h1>Chapter title</h1>
    ...

    <lesson>
        <h1>Lesson title</h1>
        ...
    </lesson>

</chapter-cmp>
<sidebar>
    <p>Open almost all the time except for a few routes</p>
</sidebar>

How do I feed the corresponding data for each layer of the app? Currently I am not aware how can I get the ids from parents of nested routes without having to create monolitic routes like these path: '/:course/:chapter/:lesson'. I thought that I can just fire actions in the state store that will be received by the components. But than what if I move to another section of the app where I need to tear-down some components and instantiate others. '/blog/:category/:posts'`.

frenzzy commented 7 years ago

There are plenty ways how you can go. I believe that the right approach is to fetch the whole page data with a single http request (say hey to graphql) and use object composition approach for components, for example:

import UniversalRouter from 'universal-router';

const CourseLayout = (props, content) => `
  <section class="course">
    <h1>${props.title}</h1>
    ${content}
  </section>
  <aside class="sidebar">${props.details}</aside>
`;

const ChapterLayout = (props, content) => CourseLayout(props, `
  <section class="chapter">
    <h2>${props.title}</h2>
    ${content}
  </section>
`);

const LessonLayout = (props, content) => ChapterLayout(props, `
  <section class="lesson">
    <h3>${props.title}</h3>
    ${content}
  </section>
`);

const router = new UniversalRouter([
  {
    path: '/:course',
    async action({ params }) {
      const data = await fetch(`/api/course/${params.course}`);
      return CourseLayout(data, `<p>${data.description}</p>`);
    }
  },
  {
    path: '/:course/:chapter',
    async action({ params }) {
      const data = await fetch(`/api/course/${params.course}` +
        `?include-chapter=${params.chapter}`);
      return ChapterLayout(data, `<p>${data.chapter.description}</p>`);
    }
  },
  {
    path: '/:course/:chapter/:lesson',
    async action({ params }) {
      const data = await fetch(`/api/course/${params.course}` +
        `?include-chapter=${params.chapter}` +
        `&include-lesson=${params.lesson}`);
      return LessonLayout(data, `<p>${data.lesson.description}</p>`);
    }
  },
]);

router.resolve(location.pathname).then(html => {
  document.body.innerHTML = html;
});

this also will work with nested routes:

const routes = {
  path: '/:course',
  children: [
    { path: '', action(context, params) {/* course */} },
    { path: '/:chapter', children: [
      { path: '', action(context, params) {/* chapter */} },
      { path: '/:lesson', action(context, params) {/* lesson */} },
    ] },
  ],
};
adrian-moisa commented 7 years ago

Looks like I completely misunderstood the router architecture. I experimented a bit and I found that actually routes give plenty of control over what happens in the application. Here's a sample I used for experiments.

export const appRoutes = [

    {
        path: '',
        action: (context:any) => {
            console.log('App') 
            return {
                title: `Home`,
                component: `<app-cmp></app-cmp>`
            }
        },
    },
    {
        path: '/:course',
        action: (context:any) => {
            console.log(`GET course "${context.params.course}"`)
        },
        children: [
            {
                path: '',
                action: (context:any) => {
                    console.log('Course')
                    return {
                        title: `Course ${context.params.course}`,
                        component: `<course-cmp></course-cmp>`
                    }
                },
            },
            {
                path: '/:chapter',
                action: (context:any) => {
                    console.log(`GET chapter "${context.params.chapter}"`)
                },
                children: [
                    {
                        path: '',
                        action: (context:any) => {
                            console.log('Chapter') 
                            return {
                                title: `Chapter ${context.params.chapter}`,
                                component: `<chapter-cmp></chapter-cmp>`
                            }
                        },
                    },
                    {
                        path: '/:lesson',
                        action: (context:any) => {
                            console.log(`GET lesson "${context.params.lesson}"`)
                        },
                        children: [
                            {
                                path: '',
                                action: (context:any) => {
                                    console.log('Lesson') 
                                    return {
                                        title: `Lesson ${context.params.lesson}`,
                                        component: `<lesson-cmp></lesson-cmp>`
                                    }
                                },
                            }
                        ]
                    },
                ]
            },
        ],
    }
]

Accessing http://localhost:3000/apps/javascript/variables renders in console the following output. Basically instead of console.log I will have store.dispatch(getLesson(lessonId)).

GET course "apps"
GET chapter "javascript"
GET lesson "variables"
Lesson

An improvement will be to also prevent new GETs after first load for the parents that don't change ids. For example if I execute router.resolve('/apps/javascript/variables/primitives') I would like to see only the GET codeBlock request. Although it's debatable if this is a good approach. Data synchronization issues could creep in. Any advice on this point?

GET course "apps"
GET chapter "javascript"
GET lesson "variables"
GET codeBlock "primitives"
Code block component

Now I have to figure out how to emulate the <router-outlet> from angular. I'll post back when I have that part working properly.

frenzzy commented 7 years ago

getLesson may skip http request in case of fresh data, for example based on time of the latest update. Or you can fetch data only once (if not exist in store) and use sockets to be notified about updates.