Skip to content

feat: add lifecycle methods to route config #172

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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

matt-sneed
Copy link
Contributor

@matt-sneed matt-sneed commented May 17, 2025

This adds the beforeEnter lifecycle method as a top level lifecycle method in the router like routeEnter and routeExit. It also provides optionality to setup beforeEnter, routeEnter, routeExit methods in the route config and/or the overriden methods like we have today.

Testing:

  1. Create a router config that has the standard lifecycle hooks routeEnter, routeExit, beforeEnter and ensure they get called in proper sequence.
  2. Ensure that you can define both route config lifecycle hooks, and lifecycle hooks in LitElement and they will be called in the correct order. Additionally ensure that the LitElement callbacks are called after the route config callbacks.
  3. Ensure that the beforeEnter hooks now get called on root level nodes in addition to child nodes (previously restricted only to child nodes).

Example routing configuration with new lifecycle methods:

const ROOT_PATH = 'example-app';

/** @enum {string} */
const ROUTE_PATHS = {
  app: ROOT_PATH,
  dashboard: '/',
};

/** @enum {string} tag name mapping */
const ROUTE_IDS = {
  app: 'example-app',
  dashboard: 'example-app-dashboard'
};

const ROUTE_CONFIG = {
  id: ROUTE_IDS.app,
  path: ROUTE_PATHS.app,
  tagName: ROUTE_IDS.app,
  beforeEnter: async (currentNode, nextNodeIfExists, routeId, context) => {
    console.log('root beforeEnter');
    //do something
  },
  routeEnter: async (currentNode, nextNodeIfExists, routeId, context) => {
    console.log('root routeEnter');
    //do something
  },
  routeExit: async (currentNode, nextNode, routeId, context) => {
    console.log('root routeExit');
    //do something
  },
  subRoutes: [
    {
      id: ROUTE_IDS.dashboard,
      path: ROUTE_PATHS.dashboard,
      tagName: ROUTE_IDS.dashboard,
      beforeEnter: async (currentNode, nextNodeIfExists, routeId, context) => {
        console.log('dashboard beforeEnter');
        import('../../components/example-app-dashboard.js');
      },
      routeEnter: async (currentNode, nextNodeIfExists, routeId, context) => {
        console.log('dashboard routeEnter');
        //do something
      },
      routeExit: async (currentNode, nextNode, routeId, context) => {
        console.log('dashboard routeExit');
        //do something
      },
    },
};

@matt-sneed matt-sneed marked this pull request as ready for review May 19, 2025 14:51
@matt-sneed matt-sneed changed the title feat: add lifecycle methods to route config. feat: add lifecycle methods to route config May 19, 2025
Copy link
Contributor

@dethell dethell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matt-sneed with the additions of the life cycle functions to the route data, are we able to support a router use case where no router mixin is used? It seems like the changes allow for that, but without a concrete example I just wanted to verify.

* @param {string} routeId - The ID of the route being entered.
* @param {!Object} context - A context object, potentially containing shared state or utilities.
* @returns {Promise<boolean|void>} Should return a Promise.
* Resolves to `true` or `void` to allow navigation.
Copy link

@C-Duxbury C-Duxbury May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it semantically make sense to allow the routEnter() callback to have a return value? My mental model is that the transition has already occurred by the time that hook is called so the window to abort would have passed.

I also notice that we don't have symmetry between the exit and enter hooks (exit lacks a "before" flavor). Could these just be collapsed into one enter and one exit (the way the mixin does) and have them return boolean values to indicate if the transition can occur?

Copy link
Contributor Author

@matt-sneed matt-sneed May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@C-Duxbury i was just recreating the same logic that was currently there, but for the routeData version. i believe the intention is to prevent recursing down the node list so that if a parent returned false in it's lifecycle method it would prevent further tree navigation.

const shouldContinue = await routingElem.routeEnter(currentEntryNode, nextEntryNode, routeId, context);

Copy link

@C-Duxbury C-Duxbury May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I edited my comment to expand on this. Should we just have a single "enter" hook that occurs before and can return a boolean value to indicate if the transition can occur? I'm wondering if there's value in having distinct before/after hooks for route enter since the mixin doesn't.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a use case that needs both unless there is some case for performing since actions after committing to entering a route. Seems reasonable to have just the single case. @matt-sneed ?

Copy link
Contributor

@jrobinson01 jrobinson01 May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, this is what I'm picturing:

entry

if routeData.beforeEnter(...) !== false
  get or create route component
     if routeComponent.routeEnter
       routeComponent.routeEnter(...)

exit

if routeData.beforeExit(...) !== false
  get exit component
    if routeComponent.routeExit
      routeComponent.routeExit(...)

So the RouteData in the config only provides the beforeEnter and/or beforeExit hooks. The components can still implement routeEnter and/or routeExit but are not required to do so.

Copy link

@C-Duxbury C-Duxbury May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i might use routeEnter to navigate my page down to a anchor on the page based on the route path

@matt-sneed Do we need router-specific hooks to satisfy this use case? My thought is that connectedCallback() or firstUpdated() could be used for this purpose. The component doesn't need to have knowledge about routing to do something when it's placed into the DOM.

I think we can probably get away with just having one enter and exit hook each in the config to allow actions/guards that happen before the transition occurs and the element is placed in the DOM.

Copy link
Contributor Author

@matt-sneed matt-sneed May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, the router config shouldn't be directly using fetch and updating data, nor should it be manipulating the DOM

@jrobinson01 agree with that statement, i'm saying that the router provides the "hook" to let the app do that job. now part of the equation is what lifecycle hooks are needed to do all the use cases we might need.

Do we need router-specific hooks to satisfy this use case? My thought is that connectedCallback() or firstUpdated() could be used for this purpose. The component doesn't need to have knowledge about routing to do something when it's placed into the DOM.

I think we can probably get away with just having one enter and exit hook each in the config to allow actions/guards that happen before the transition occurs and the element is placed in the DOM.

@C-Duxbury here's a couple pointed examples of where that may not work, and using a routeEnter in a component is still needed:

  override async routeEnter(
    currentNode: RouteTreeNode,
    nextNodeIfExists: RouteTreeNode,
    routeId: string,
    context: Context,
  ): Promise<boolean | undefined> {
    // store path so it can be passed to sidebar
    this.routePath = context.path;
    await super.routeEnter(currentNode, nextNodeIfExists, routeId, context);
    if (context.hash) {
      await this.navToSection(`#${context.hash}`);
    }
    return true;
  }
  override async routeEnter(
    currentNode: RouteTreeNode,
    nextNodeIfExists: RouteTreeNode,
    routeId: string,
    context: Context,
  ) {
    // store path so it can be passed to sidebar
    await super.routeEnter(currentNode, nextNodeIfExists, routeId, context);
    this.reversalFlag = context.query.has('reversalFlag');
  }

Copy link

@C-Duxbury C-Duxbury May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matt-sneed We'd need a way to get at the route context directly from the Router instance to support this use case without hooks. You'd basically just move the logic from routerEnter() into firstUpdated():

protected firstUpdated() {
    const { hash } = Router.getInstance().getContext();
    if (hash) {
      await this.navToSection(`#${hash}`);
    }
}

This is one of the gaps I mentioned in the comparison document. Getting the context outside of hooks would be a separate enhancement PR, but I think it's orthogonal with the goal here of providing ways to route without inheritance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i think if we had access to the current route context in some consumable way at any point in the lifecycle, it does change the requirements

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The router currently reuses components that are already in the tree, so for an existing component, the router would need to have changed one of the component's properties in order to trigger the lit lifecycle events. That might be sufficient for most (all?) use cases where a component needs to react to a route change but maybe worth pointing out here.

@matt-sneed
Copy link
Contributor Author

@matt-sneed with the additions of the life cycle functions to the route data, are we able to support a router use case where no router mixin is used? It seems like the changes allow for that, but without a concrete example I just wanted to verify.

i think this is a first step towards that. i don't think it implicitly solves that problem yet.

@barronhagerman
Copy link
Contributor

@matt-sneed with the additions of the life cycle functions to the route data, are we able to support a router use case where no router mixin is used? It seems like the changes allow for that, but without a concrete example I just wanted to verify.

Technically, you don't have to use the mixin right now. The only requirement is that each routed element implements the BasicRoutingInterface (i.e. async routeExit(currentNode, nextNode, routeId, context) and async routeEnter(currentEntryNode, nextEntryNode, routeId, context) functions.

@chrisgubbels
Copy link
Contributor

I'm creating a demo app in the repo which will allow for better testing of improvements and changes. Lets hold off on PRs until we have that as a sandbox.

@C-Duxbury
Copy link

C-Duxbury commented May 20, 2025

Technically, you don't have to use the mixin right now.

@barronhagerman Is that true in practice? To the best of my ability, it looks like the routingMixin() contains the logic for placing a component into the DOM. I couldn't get any functionality without inheriting from it.

@barronhagerman
Copy link
Contributor

@barronhagerman Is that true in practice? To the best of my ability, it looks like the routingMixin() contains the logic for placing a component into the DOM. I couldn't get any functionality without inheriting from it.

The mixin's routeExit/routeEnter contains the logic to remove/add elements to the DOM. If you want to use it without the mixin, I think you'd just have to implement that on your own; you'd still get all the other router functionality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants