vaadin / router

Small and powerful client-side router for Web Components. Framework-agnostic.
https://vaadin.com/router
Apache License 2.0
407 stars 49 forks source link

Feature Request: Route Metadata #444

Open bennypowers opened 4 years ago

bennypowers commented 4 years ago

Let's say we're implementing a material design nav drawer:

mio-components_assets_1nsuL8VDpBW_LZYXgabK1H0uq6icmmKYt_nav-drawer-intro

In such a case, I might want to keep a record of nav items something like this:

[{
  name: 'My Files',
  path: '/files',
  icon: 'folder',
  isNavRoute: true
}, {
  name: 'Shared with me',
  path: '/shared',
  icon: 'users',
  isNavRoute: true
}]

Currently, I would have to keep some of this information in two places at once - here in my record of nav routes, as well as in vaadin-router's route config.

I propose adding a metadata: Record<string, any> field to the Route interface, which would allow users to add arbitrary metadata (like icon and whether or not to include this route in the nav bar) to their routing table.

Before:

// app-shell.js
import { router } from './router.js';
const navRoutes = {/* ... some record of nav routes, as above */}
class AppShell extends LitElement {
  routes = somehowCollateRoutes(router.getRoutes(), navRoutes);
  render() {
    return html`
      <mwc-drawer>
        <mwc-list>${this.routes.map(routeTemplate)}</mwc-list>
      </mwc-drawer>
    `;
  }
}

After:

// app-shell.js
import { router } from './router.js';

class AppShell extends LitElement {
  routes = router.getRoutes();
  render() {
    return html`
      <mwc-drawer>
        <mwc-list>${this.routes.map(routeTemplate)}</mwc-list>
      </mwc-drawer>
    `;
  }
}

@web-padawan , in discussion on Polymer Slack, raised the possibility of adding that metadata to location as well, which could be useful for components

web-padawan commented 4 years ago

raised the possibility of adding that metadata to location as well, which could be useful for components

I'm not saying it should be there, that was just an idea on where to place it.

So the use case you are trying to achieve seems to be mostly about filtering routes? That sounds like a valid use case to me. Maybe we could think about more cases.

bennypowers commented 4 years ago

mostly about filtering routes

But also about providing other data, in this case icon.

One might imagine having metadata like requiredRoles or badgeContent as well

lpellegr commented 4 years ago

I have a similar use case where fields are added at the route definition and used before or on rendering. The purpose, for now, is to know if a route requires authentication checking (i.e. not all require that) and what parent layout to use.

web-padawan commented 4 years ago

Technically, any arbitrary properties can be added to the route object.

So I assume this is mostly about type definitions support, right?

lpellegr commented 4 years ago

In my case, yes.

bennypowers commented 4 years ago

Yeah. I'd rather not slather on a bunch of // @ts-ignore

As a user, I want to know what is and is not the intended use of the API.

Artur- commented 4 years ago

Is a generic metadata better than creating an app specific type variant to use, e.g.

interface MetadataRouteWithAction extends RouteWithAction {
  icon: string,
  isNavRoute: boolean
}
vlukashov commented 4 years ago

@bennypowers, what do you think of the custom Route type extension approach described above?

Can you please try it out in your app and see what are the pros and cons of each approach? That would be helpful!

I anticipate that with metadata the application code would look like:

const routes = router.getRoutes();
...
<mwc-list>${this.routes.map(routeTemplate)}</mwc-list>
...
funciton routeTemplate(route: Route) {
  const meta = route.metadata as { icon: string, isNavRoute: boolean };
  return meta.isNavRoute
    ? html`<my-icon icon=${meta.icon}>...</my-icon>`
    : html``;
}

With a custom Route type extension the application code would look like:

type MyRoute = Route & {
  icon: string,
  isNavRoute: boolean
}
...
const routes = router.getRoutes() as MyRoute[];
...
<mwc-list>${this.routes.map(routeTemplate)}</mwc-list>
...
funciton routeTemplate(route: MyRoute) {
  const meta = route.metadata;
  return meta.isNavRoute
    ? html`<my-icon icon=${meta.icon}>...</my-icon>`
    : html``;
}

Side note: I think it would be possible to get rid of the type cast if the Router type is generic and has the extended Route type as a type parameter.

bennypowers commented 4 years ago

Indeed, that's a better solution. here's my app code:

declare module '@vaadin/router/dist/vaadin-router' {
  export interface RouteMetadata {
    icon?: Icon;
    stage?: Stage;
    label?: string;
  }

  export interface BaseRoute {
    metadata?: RouteMetadata;
  }
}

I'm unavailable to do it today, but I'll redo this PR to be a docs-only PR that adds an example like this.

it would be nice if (in a later PR) these types were exported from @vaadin/router instead of @vaadin/router/dist/vaadin-router