How to delay the configureRouter()?


#1

Scenario:

AppRouter has a projects route that has a childRoute /projects and /projects/:project_id

The /projects/:project_id screen has multiple tabs.

I want to make each tab a route.

So when I load the page I declare configureRouter, and set child routes of /projects/:project_id for each tab.

So I have for example:

/projects/:project_id/general
/projects/:project_id/issues
/projects/:project_id/notes

When user access /projects/:projects_id I query the project information. All my other tabs need information from the project, so instead of querying the project information everytime I click in a tab, I want to pass it on the settings json from the route.

So my question is, how do I delay the route configuration, so it is configured just after I have gotten the response with the project information from my api?


#2

I encountered similar issue before.

You can return a promise from configureRouter() which will wait for the promise to finish.

But that doesn’t solve this issue. configureRouter() was called before the route /projects/:project_id was activated, it means you do not know about project_id when in configureRouter().

Instead of delay the child routes creation, you create an incomplete route table in configureRouter() without delay, then in activate(), you gather information about current project and mutate routing table.

  activate(params, routeConfig) {
    this.project_id = params.project_id;
    
    // it is not necessary to return the ajax promise
    return ajaxToGetProject(this.project_id).then(
      project => {

        // you can also update title of an existing route
        _.each(this.router.routes, route => {
          // exiting route with placeholder title
          if (route.title === '_project_name_') {
            route.navModel.setTitle(project.name);
          }
        });

        // mutate route table!
        this.router.addRoute({router: '...', moduleId: '...'});

        // remember to refresh navigation after mutation.
        this.router.refreshNavigation();
      }
    );
  }

  configureRouter(config, router) {
    this.router = router;

    config.map([/*...*/]);
  }

This solution has a bug, because some child routes are created asynchronously after configureRoute(), it doesn’t work when user reload the browser page under route /projects/:project_id/async_child_route.


#3

Sorry, I think I miss-understood you issue. You want to share information across multiple child routes, which I did too. I will share some code shortly.


#4

As I said before, configureRouter() doesn’t know about project_id, you have no way to put the project object into settings of every child route.

Here is a resolver we use internally. SingletonFactory guarantees if you call the factory method with same arguments, you always get the same instance back. So here you can share same projectService in all your child routes.

If more people find SingletonFactory useful, I will publish it as an Aurelia plugin.

singleton-factory.js

import {resolver} from 'aurelia-dependency-injection';

const FACTORY_MAP = new Map();

/**
* Used to allow injecting dependencies but also passing data to the constructor.
* if additional data are same, return same instance.
*/
@resolver()
export class SingletonFactory {
  /** @internal */
  _key;
  _map = new Map();

  /**
  * Creates an instance of the Factory class.
  * @param key The key to resolve from the parent container.
  */
  constructor(key) {
    this._key = key;
  }

  /**
  * Called by the container to pass the dependencies to the constructor.
  * @param container The container to invoke the constructor with dependencies and other parameters.
  * @return Returns a function that can be invoked to resolve dependencies later, and the rest of the parameters.
  */
  get(container) {
    return (...rest) => {
      let created;
      this._map.forEach((value, key) => {
        if (created) return;
        if (key.length !== rest.length) return;
        for (let i = 0, ii = rest.length; i < ii; i += 1) {
          if (key[i] !== rest[i]) return;
        }
        created = value;
      });

      if (created) {
        return created;
      } else {
        const instance = container.invoke(this._key, rest);
        this._map.set(rest, instance);
        return instance;
      }
    };
  }

  /**
  * Creates a Factory Resolver for the supplied key.
  * @param key The key to resolve.
  * @return Returns an instance of Factory for the key.
  */
  static of(key) {
    const created = FACTORY_MAP.get(key);

    if (created) {
      return created;
    } else {
      const instance = new SingletonFactory(key);
      FACTORY_MAP.set(key, instance);
      return instance;
    }
  }
}

project-service.js uses partial injection, projectId will be provided later.

@inject(HttpClient)
export class ProjectService() {
  project = null;

  constructor(client, projectId) {
    this.client = client
  }

  reloadProject() {
    // something like this
    return this.client.fetch(/*...*/)
    .then(json => this.project = json.project);
  }

  reloadOtherProjectRelatedInfo() {}
}

In /projects/:project_id

@inject(SingletonFactory.of(ProjectService))
export class Project {
  constructor(getProjectService) {
    this.getProjectService = getProjectService;
  }

  activate(params, routeConfig) {
    this.projectId = params.project_id;
    this.projectService = this.getProjectService(this.projectId);
    return this.projectService.reloadProject();
  }
}

In /projects/:project_id/other

@inject(SingletonFactory.of(ProjectService))
export class Other {
  constructor(getProjectService) {
    this.getProjectService = getProjectService;
  }

  activate(params, routeConfig) {
    this.projectId = params.project_id;
    this.projectService = this.getProjectService(this.projectId);
  }

  @computedFrom('projectService', 'projectService.project')
  get project() {
    return this.projectService && this.projectService.project;
  }
}

#5

Best solution so far was to follow the idea given to me by @bigopon on gitter channel

basically you pass a navigationStrategy for the route you want to have the data passed on settings, and return a promise once you have the data.

const navStrat = (instruction) => {

 return fetch(url)
     .then(response => response.json)
     .then(response => {
           instruction.settings = { /*pass your data here*/ };
      })
    .catch(console.error);
  }
};

config.map([
 { route : '', name : 'route-name', moduleId: './your-module-id', nav : false, title : 'Your Route Title', navigationStrategy : navStrat}
]);