New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Router demos #367

Open
jwx opened this Issue Jan 8, 2019 · 13 comments

Comments

Projects
None yet
2 participants
@jwx
Copy link
Member

jwx commented Jan 8, 2019

馃挰 Demo

An issue for various router demos. Question, thoughts and opinions are always welcome.

@jwx

This comment has been minimized.

Copy link
Member

jwx commented Jan 8, 2019

Following up on the demo in the Router parameters PR I've made a small addition to demonstrate one of the ways to alter the "url syntax".

navigation example - customized syntax

The links in the files have now changed to
app

<li><a href="authors/about">Authors</a></li>
<li><a href="books/about">Books</a></li>
<li><a href="about">About</a></li>

authors (and books is the same)

<a href="author/${author.id}">${author.name}</a>

author (and book is the same)

<li repeat.for="book of author.books"><a href="book/${book.id}">${book.title}</a></li>

There's still no route configuration, but the custom syntax has been hooked into the router like this
app

    this.router.activate({
      transformFromUrl: (path, router) => {
        if (!path.length) {
          return path;
        }
        if (path.startsWith('/')) {
          path = path.slice(1);
        }
        // Fetch components for the "lists" viewport
        const listsComponents = router.rootScope.viewports.lists.options.usedBy.split(',');
        const states = [];
        const parts = path.split('/');
        while (parts.length) {
          const component = parts.shift();
          const state: IComponentViewportParameters = { component: component };
          // Components in "lists" viewport can't have parameters so continue
          if (listsComponents.indexOf(component) >= 0) {
            states.push(state);
            continue;
          }
          // It can have parameters, but does it?
          if (parts.length > 0) {
            state.parameters = { id: parts.shift() };
          }
          states.push(state);
        }
        return states;
      },
      transformToUrl: (states: IComponentViewportParameters[], router) => {
        const parts = [];
        for (const state of states) {
          parts.push(state.component);
          if (state.parameters) {
            parts.push(state.parameters.id);
          }
        }
        return parts.join('/');
      }
    });

Now, this is example code to customize this specific example app, it's not intended as a generic syntax transformer. It's to illustrate one of the ways to customize. This code is not intended to be a part of the router. @fkleuver#1873 has since long expressed his wish to do the traditional route recognizer (which could be hooked in like this) and a route table lookup could also easily be thrown into the mix here (or at some other connection point).

@jwx

This comment has been minimized.

Copy link
Member

jwx commented Jan 8, 2019

In this demo I've updated the example app to have default components in the viewports and also added a child viewport, with a default component, in the Book component. Manual nav elements have been replaced with the au-nav and the addNav api.

viewports - defaults and nav

Test it yourself here.

With the help of the au-nav there's almost less code even though there's more features:
app

<template>
  <au-nav name="app-menu"></au-nav>
  <au-viewport name="lists" used-by="authors,books" default="authors"></au-viewport>
  <au-viewport name="content" default="about"></au-viewport>
</template>
@inject(Router)
export class App {
  constructor(private router: Router) {
    this.router.activate({});
    this.router.addNav('app-menu', [
      {
        title: 'Authors',
        components: [Authors, About],
        consideredActive: [Authors],
      },
      {
        title: 'Books',
        components: [Books, About],
        consideredActive: Books,
      },
      {
        components: 'about',
        title: 'About',
      },
    ]);
  }
}

books

<template>
<h3>Books</h3>
<ul>
  <li repeat.for="book of books">
    <a href="book=${book.id}">${book.title}</a>
    <ul><li repeat.for="author of book.authors">${author.name}</li></ul>
  </li>
</ul>
</template>
@inject(BooksRepository)
export class Books {
  constructor(private booksRepository: BooksRepository) { }

  get books() { return this.booksRepository.books(); }
}

book

<template>
<h3>${book.title}</h3>
<div>Published: ${book.year}</div>
<div>Author(s):
  <ul>
    <li repeat.for="author of book.authors"><a href="author=${author.id}">${author.name}</a></li>
  </ul>
</div>
<au-nav name="book-menu"></au-nav>
<au-viewport name="book-tabs" default="book-details=${book.id}" used-by="about-books,book-details"></au-viewport>
</template>
@inject(Router, BooksRepository)
export class Book {
  public static parameters = ['id'];

  public book: {id: number};

  constructor(private router: Router, private booksRepository: BooksRepository) { }

  public enter(parameters) {
    if (parameters.id) {
      this.book = this.booksRepository.book(+parameters.id);
    }
    this.router.setNav('book-menu', [
      {
        title: 'Details',
        components: `book-details=${this.book.id}`
      },
      {
        title: 'About books',
        components: 'about-books'
      },
    ]);
  }
}

book-details

<template>
<h3>Details about the book</h3>
<p>Here's details about <strong>${book.title}</strong>...</p>
</template>
export class BookDetails {
  public static parameters = ['id'];

  public book = {};
  constructor(private booksRepository: BooksRepository) { }

  public enter(parameters) {
    if (parameters.id) {
      this.book = this.booksRepository.book(+parameters.id);
    }
  }
}

And that's all. I think that's quite a lot of nice functionality for a small amount of code/work. Any questions or thoughts?

@shahabganji

This comment has been minimized.

Copy link

shahabganji commented Jan 8, 2019

Dear @jwx ,

That looks quite great an effort has been don on routing, well done 馃憤 and keep up the good work 馃

In Aurelia vCurrent we can have multiple named router-view as described here, however, they are in sync with one another via route configuration. What I want to know is that whether it's possible to have routes like secondary-routes of angular or not; for AFAIK, in an angular app one can optionally switch secondary routes off and on, on demand.

I have somehow explained a usecase here

Thanks and well done again.

@jwx

This comment has been minimized.

Copy link
Member

jwx commented Jan 9, 2019

Thanks, @shahabganji!

The routing demonstrated above doesn't used configured routes at all. Instead, each navigation contain a set of viewport states that form a navigation instruction. Sync is maintained within a navigation instruction so a navigation that contains three viewports will keep those three in sync while a navigation with a single viewport state will be totally independent from all other viewports. This means that while using the router like this doesn't support secondary-routes (since there are no configured routes) it doesn't need them: what's achieved with secondary routes is already a part of the router's core behavior.

The use case you referred to could be achieved in quite a few ways, but for example:

app

<au-viewport name="main" used-by="announcements" default="announcements"></au-viewport>
<au-viewport name="details"></au-viewport>

<!-- Styling should make this viewport floating -->
<au-viewport name="chat" used-by="chat" default="chat"></au-viewport>

chat

<au-viewport name="chat-main" used-by="chat-users" default="chat-users"></au-viewport>
<au-viewport name="chat-details" used-by="chat-user"></au-viewport>

With these two components handling viewports, you could have the following links:

<a href="2-aurelia-routing+chat+chat-user=davismj">This sets up the entire first image you referred to</>
<a href="3-advanced-aurelia">Changes the details component and leaves everything else as it is</>
<a href="chat-user=jwx">Changes the chat user to jwx and leaves everything else as it is</>
<a href="-@chat">Clears the content of the chat viewport (making it possible to hide with css) and leaves everything else as it is</>
<a href="chat">Shows the chat viewport with the chat users list and leaves everything else as it is</>

And just to be clear: the above html is everything you'd need to do when it comes to routing for your use case. There's no additional configuration, of routes or anything else, to get this working.

If you want to read more about the basic principles, I've put up my first document as an RFC here.

I hope I understood your use case correctly. Did that answer your questions? If not, or you have more questions or opinions, please do let me know!

@shahabganji

This comment has been minimized.

Copy link

shahabganji commented Jan 9, 2019

Thanks @jwx ,

You understood my use case correctly, and I knew this is different than configured routes. I still have some questions, yet not sure to ask them on this thread or on some RFC on the routing. anyway I'll ask them here and if you think we can move them into an appropriate thread.

  1. Is au-viewport a replacement for router-view, or just another way to configure routes fast and simple? Can I use compose instead like what is suggested here?
  2. Is this approach suitable for simpler scenarios that we require to setup without the complexities of routing or we can utilize that for enterprise applications too? ( Kinda, intuitively I think this approach is better for the former scenario, am I right? )
  3. How does these routes get along with routing steps(route guards), such a Authorize step?
<au-viewport name="chat" used-by="chat" default="chat" steps="Authorize"></au-viewport>

or

<au-viewport name="chat" used-by="chat" default="chat" steps="Authorize,IsAdmin"></au-viewport>
@jwx

This comment has been minimized.

Copy link
Member

jwx commented Jan 9, 2019

@shahabganji First of all, let me point out that none of this is completely set yet. A lot of these posts express ideas and opinions from members of the team and community as a part of the development process, not documentation on how it will actually be in vNext. Having said that

  1. My view is that au-viewport will replace router-view, containing the functionality necessary for both the traditional routes (router-view) and these convention-based ones. This also does track with vCurrent in the sense that in a route you configure which viewport the content should go to (if you have more than one router-view). The post you refer to about compose is primarily about using compose for the vCurrent layout feature, but we have talked about using compose for routing as well. However, since routing among other things involves navigation history, I think we'll end up with a separate au-viewport element for it.
  2. I think the answer here is very subjective. My ambition is to get this into such a state that it can be used in any kind of application. If it had existed when I made my last enterprise application, I would've used it for it.
  3. Routing steps/route guards will definitely be supported with the convention-based routing as well. While work on it hasn't started yet, here's what I'm thinking.

Thanks for taking an interest and providing questions and feedback!

@shahabganji

This comment has been minimized.

Copy link

shahabganji commented Jan 9, 2019

@jwx thanks for your prompt answer. The direction of routing sounds promising and that's awesome. Keep up the good work 馃憤

As the last question is it possible to have access to routes and their children before navigation to them as some sort of meta data to generate menus or site maps based on them? any work on that? both for when one uses convention-based routes or/and configured routes.

@jwx

This comment has been minimized.

Copy link
Member

jwx commented Jan 9, 2019

@shahabganji There's been talk about "going through" all the components of an application for that kind of meta data, but as far as I know it's not been specified exactly how that would be implemented.

The only thing I've done with relation to menues is the addNav, setNav and au-nav in the demo above. In most projects I've worked, menu structure and contents are decided by someone (who's not implementing the actual components) saying things like "and under the (menu) item 'Administration' we want the items 'Authors and 'Books' that shows their lists" so it just made sense to me to start an implementation from that point of view.

@jwx

This comment has been minimized.

Copy link
Member

jwx commented Jan 9, 2019

@shahabganji Since it's such a small amount of work, I added your chat use case to the previous demo.

The resulting demo is here.

To get the this functionality, I simply added three small components:

chat

<template>
  <h3>Chat <a href="-@chat" class="close">X</a></h3>
  <au-viewport name="chat-main" used-by="chat-users" default="chat-users"></au-viewport>
  <au-viewport name="chat-details" used-by="chat-user"></au-viewport>
</template>
export class Chat { }

chat-users

<template>
  <ul>
    <li repeat.for="user of users">
      <a href="chat-user=${user.id}">${user.id} (${user.name})</a>
    </li>
  </ul>
</template>
@inject(UsersRepository)
export class ChatUsers {
  constructor(private usersRepository: UsersRepository) { }

  get users() { return this.usersRepository.users(); }
}

chat-user

<template>
  <p>Chatting with <strong>${user.id} (${user.name})</strong><p>
  <p>You: Hello!</p>
  <p><input></p>
</template>
@inject(UsersRepository)
export class ChatUser {
  public static parameters = ['id'];

  public user = {};
  constructor(private usersRepository: UsersRepository) { }

  public enter(parameters) {
    if (parameters.id) {
      this.user = this.usersRepository.user(parameters.id);
    }
  }
}

and added the Chat link to the addNav:

app

    this.router.addNav('app-menu', [
      {
        title: 'Authors',
        components: [Authors, About],
        consideredActive: [Authors],
      },
      {
        title: 'Books',
        components: [Books, About],
        consideredActive: Books,
      },
      {
        components: About,
        title: 'About',
      },
      {
        components: 'chat',
        title: 'Chat',
      },
    ]);

And that's it! Any questions or thoughts?

@shahabganji

This comment has been minimized.

Copy link

shahabganji commented Jan 12, 2019

@jwx

Sounds easy to bootstrap, Is there any way that we cant test the above-mentioned sample online by ourselves, such as code sandbox or stackblitz?

@jwx

This comment has been minimized.

Copy link
Member

jwx commented Jan 18, 2019

Local components

So far the demos have contained globally registered (uniquely named) components, but of course Aurelia gives you the option to specify locally registered components. And the router will play along. Let's expand our previous demo with the following structure
image
so that the book component and the author component should have their own local information component.

Let's first look at how you'd declare that dependency. We've got the html require way

book

<require from="./information"></require>

or the code way

book

import { Information } from './information';

@customElement({ dependencies: [Information] }) // This api might change but principle will remain
export class Book { ... }

Once the Information component (which is just an export class Information {} with some html) has been registered in any of the above ways it's now available for navigation for book and anything in the au-viewport within book. However, since that viewport in book is specifying used-by (which it doesn't need to, it's just to limit the components that can be loaded into it) we need to add the component to the list of accepted components

book

<au-viewport name="book-tabs" used-by="about-books,book-details,information" default="book-details=${book.id}"></au-viewport>

Now information is available for us in links

<a href="information">Book information</a>

or code

this.router.goto("information");

and we can also add it to our already existing nav

this.router.setNav('book-menu', [
  { title: 'Details', components: `book-details=${this.book.id}` },
  { title: 'About books', components: 'about-books' },
  { title: 'Book information', components: 'information' },
]);

And when we do exactly the same thing for the author component and its own information component, we end up with this

router - local components

The demo can be found here.

Questions and opinions are highly appreciated!

@jwx

This comment has been minimized.

Copy link
Member

jwx commented Jan 18, 2019

Is there any way that we cant test the above-mentioned sample online by ourselves, such as code sandbox or stackblitz?

Sorry for not replying sooner, @shahabganji. (I honestly thought I already had replied.) Unfortunately, as far as I know vNext isn't up on either code sandbox or stackblitz. You can always clone the repo, though, and play around with the test/demo application.

@shahabganji

This comment has been minimized.

Copy link

shahabganji commented Jan 18, 2019

@jwx Not a problem at all, there's no rush.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment