Best way to handle nested routers with shared objects

The bulk of my project relies on pages that contain 2 child routers. The first index page defines the sub navigation and it has “header” component that is visible for all sub navigation. Each sub page generally relies on data that can be shared with all sub pages within that main page.

I’m currently relying on passing a context that is injected to each page. What I don’t like about this approach is it makes re-use of any sub pages difficult as the context is tied to the main entry point.

Should i switch to a store approach where each page grabs an object from a “store” during the activate event?

Is there a better way of passing information with the router (most events are triggered via Href)

1 Like

I share common service (data + methods) through route config settings.

config.map([
  {
    route: '', name: 'details',
    title: 'Details',
    nav: true,
    moduleId: './details',
    settings: {someService: this.someService}
  },
  {
    route: 'policies', name: 'policies',
    title: 'Policies',
    nav: true,
    moduleId: './policies',
    settings: {someService: this.someService}
  },
]);

Then in each sub component

  activate(params, routeConfig) {
    this.someService = routeConfig.settings.someService;
  }
2 Likes

Note my approach still works when you have href pointing to a deep route.

Aurelia will run through parent route and child route full lifecycle in order to render the new page. The parent route config.map wouldn’t be missed wherever you navigate from.

2 Likes

Thanks,

I’m just trying to wrap my head around what is really different from your approach over just injecting someService at the constructor other than keeping the constructor signature cleaner.

1 Like

My approach is easier to handle new Service(id) which is difficult for injected object using Factory.of to maintain singleton instance.

2 Likes

Would you be able to quickly explain how you do this…

This would be a great How To article.

1 Like

Sure. Let’s take an example. The top route is 'product/:id', it got two child routes '' (details page), and 'comments' page.

We want to share a common object, but the object is related to a single product id. new ProductService(id). If you use normal DI injection in constructor, you have to do

@inject(Factory.of(ProductService))
export class Details {
  constructor(getProductService) {
    this.getProductService = getProductService;
  }
  activate(params) {
    this.id = params.id;
    this.productService = this.getProductService(this.id);
  }
}

The problem is that this.productService produced by Factory.of is not a singleton, hence it’s not “shared” among details and comments pages. I used to have a special implementation SingletonFactory to guarantee singleton for same arguments (same input id in this case), but it was bit unnecessarily complex.

If you share object through route settings, it’s for sure the singleton (you created manually).

export class Product {
  configureRouter(config, router, params) {
    this.router = router;
    // Note the special 3rd param "params", it's largely undocumented.
    this.id = params.id;
    this.productService = new ProductService(this.id);
    // Load product before doing anything.
    return this.productService.loadProduct()
    .then(product => {
      config.map([
       {
          route: '', name: 'details',
          title: 'Details',
          nav: true,
          moduleId: './details',
          settings: {productService: this.productService}
        },
        {
          route: 'comments', name: 'comments',
          title: 'Comments',
          nav: true,
          moduleId: './comments',
          settings: {productService: this.productService}
        }
      ]);
   });
  }
}

FYI the 3rd param on configureRouter The easy way to customize child router based on parent router param

4 Likes

I’m going to try working with this on a new route I’m working with. If it solves all my issues then I’ll be refactoring the rest. Thank you

1 Like

So maybe I’m missing something but how does this work when “id” changes without doing a complete route change.

What I’ve found to work from my quick experiment is tying back into the activate method and using an object.assign to update property values. A complete object swap doesn’t work, but updating the values of the existing referenced object does.

1 Like

Oh yes, try this on top to force a total reload when switching id.

import {activationStrategy} from 'aurelia-router';

export class Product {
  determineActivationStrategy() {
    return activationStrategy.replace;
  }
}
1 Like

To avoid performance penalty of the above, you can try alternative way.

In Product class activate(params) update productService.id to latest id.

Then productService reacts.

import {observable} from 'aurelia-framework';
class ProductService {
  @observable id;

  idChanged(newId) {
    // load new product
  }
}
1 Like

That is exactly what I ended up doing. The only issue I’m running into right now (Probably my own problem) is that deep linking isn’t working, but when the route Id changes after the page has been loaded everything is working.

Thank you for your help. This is certainly not explained well enough in the official docs

1 Like

is there a good way to get the service down to a 2nd child router?

Page activation is too late and currentInstruction is not available on router when configureRouter is called

1 Like

Besides params, configureRouter actually has two more undocumented arguments.

configureRouter(config, router, params, routeConfig, currentInstruction) {}

In child router, the routeConfig is the same thing you got in activate callback. You can get route settings from it. I have not tested it, but it should work.

2 Likes

yep… that does it. I was actually getting it from the router.parent.routes way, but that is what I want.

Hoping Aurelia 2 fixes all the doc issues :slight_smile:

1 Like

This is a very clever and interesting approach to sharing objects among child routes. Would the Aurelia team consider this the official “Aurelia Way”? I don’t see anything covering this topic in the documentation.

More on this: Dependency Injection: Basics | Aurelia

This seems to be the official approach. Correct me if I’m wrong:

import { inject } from 'aurelia-framework';
import { HttpClient, HttpClientConfiguration } from '@aurelia/fetch-client';
import { EventAggregator } from 'aurelia-event-aggregator';
import { Router } from 'aurelia-router';

@inject(HttpClient, Router, EventAggregator)
export class ProductService {

  product: Product;

  constructor(private http: HttpClient, private router: Router, private eventAggregator?: EventAggregator) {
    http.configure((config: HttpClientConfiguration) => config.useStandardConfiguration());
  }

  async setProduct(productId: string): Promise<Product> {
    return this.http.fetch('api/v1/product/' + productId, { method: "GET" })
      .then(response => { return (response.status === 200) ? response.json() : undefined; })
      .then(product => { return new Product(product); })
      .then(product => { this.product = product; return product; });
  }

}
import {NewInstance, inject} from 'aurelia-framework';
import {ProductService} from './product-service';

@inject(NewInstance.of(ProductService))
export class ProductRoot {

    private router: Router;

    constructor(private productService: ProductService){
    }

    async activate(params: any, routeConfig: RouteConfig): Promise<any> {
      if (params.productId) {
        return this.setProduct(params.productId);
      }
      return;
    }

    configureRouter(config: RouterConfiguration, router: Router) {

      this.router = router;

      config.map([
        { route: [''], name: 'details', moduleId: PLATFORM.moduleName('./details.component'), nav: false, title: 'Details'},
        { route: ['comments'], name: 'comments', moduleId: PLATFORM.moduleName('./comments.component'), nav: false, title: 'Comments'}
      ]);

    }

    get product(): Product {
      return this.productService.product;
    }
}
import {Parent, inject} from 'aurelia-framework';
import {ProductService} from './product-service';
  
@inject(Parent.of(ProductService))
export class ProductDetails {

    constructor(private productService: ProductService){
    }

    get product(): Product {
      return this.productService.product;
    }

  }

import {Parent, inject} from 'aurelia-framework';
import {ProductService} from './product-service';
  
@inject(Parent.of(ProductService))
export class ProductComments {

    constructor(private productService: ProductService){
    }

    get product(): Product {
      return this.productService.product;
    }

  }