4

In our Angular-7-Application, we use @ngrx and @ngrx/router-store to get the query params into the state.

A few components of the application are paginated lists. We have every list as a component and the Pagination-Component included in every list.

The current page is stored in the URL as a query Parameter: user/:userId/agent?page=0 and the PaginationComponent gets the current page from state.router.state.queryParams.page. However, if a user accesses the URL user/:userId/agent, queryParams.page returns undefined.

We could solve this by using state.router.state.queryParams.page || 0 in every component but I wonder, if there is an easier way - can a Route without query params be redirect to a Route with query params?

I tried using the most obvious redirect:

{ path: 'user/:userId/agent', redirectTo: '/user/:userId/agent?page=0', pathMatch: 'full' },
{ path: 'user/:userId/agent?page=0', component: AgentListComponent },

but I get Error: Cannot match any routes. URL Segment: 'user/max/agent'.

The only feature request I found was this one where the error above appears.

Freestyle09
  • 4,894
  • 8
  • 52
  • 83
Florian Gössele
  • 4,376
  • 7
  • 25
  • 49
  • 1
    I feel this is a broad question, because there are many ways. You could use a guard that redirects, the component could do it, etc. etc. But, Angular can not match routes with query parameters. That's not a supported feature, and I don't think it ever will be. – Reactgular Jan 19 '19 at 15:26
  • To be honest, adding `|| 0` is *truly* the least effort and easiest to maintain. I think you're over thinking the issue, and shorter URLs are easier to read URLs. – Reactgular Jan 19 '19 at 15:28
  • Adding `|| 0` to every component in which it is used is the least effort, but it contradicts the DRY-principle. – Florian Gössele Jan 24 '19 at 12:49

4 Answers4

3

For your question specifically:

can a Route without query params be redirect to a Route with query params?

I don't think this can work because the ? in a query is a separator, which is not part of a URL's query string.

Alternative 1 - since you are using ngrx, one way to do it is to use the custom serializer. The docs from the ngrx.io site show an example of returning the params with serialization. This is where you can add logic to add a default to the params if it doesn't exist. I'll disclaim this may be less ideal because it'll fire on each route, but it can make your routes simpler.

import { Params, RouterStateSnapshot } from '@angular/router';
import { RouterStateSerializer } from '@ngrx/router-store';

export interface RouterStateUrl {
  url: string;
  params: Params;
  queryParams: Params;
}

export class CustomSerializer implements RouterStateSerializer<RouterStateUrl> {
  serialize(routerState: RouterStateSnapshot): RouterStateUrl {
    let route = routerState.root;

    while (route.firstChild) {
      route = route.firstChild;
    }

    const {
      url,
      root: { queryParams },
    } = routerState;
    const { params } = route;

    // Add here
    if (<insert url logic> && queryParams.page === undefined) {
        queryParams.page = 0;
    }

    // Only return an object including the URL, params and query params
    // instead of the entire snapshot
    return { url, params, queryParams };
  }
}

Alternative 2 - You can wrap the HttpClient or more preferably, create a generic page list method that checks for this and adds it to the request if there is no page. This answer shows an example of how to achieve adding params.

Alternative 3 - You can use the page as a part of the path and do work arounds/changes as needed to generate your requests.

{ path: 'user/:userId/agent', redirectTo: '/user/:userId/agent/0', pathMatch: 'full' },
{ path: 'user/:userId/agent/:page', component: AgentListComponent },
1

In our Angular-7-Application, we use @ngrx and @ngrx/router-store to get the query params into the state.

To have query params and state synchronized you need an effect that captures any action that results in a page change in your application. Inside the event, you'll have something like:

@Effect({dispatch:false})
setRouteParams = this.actions$.pipe(
    ofType<ActionCausingPageChange>("action name"),
    tap( action =>{

        let a = { page: action.payload.page };
        // or in case it's not part of action payload, get it from store
        this.router.navigate(
            [], {
                relativeTo: this.route,
                queryParamsHandling: 'merge',
                queryParams: a
            });
        }
    )
);

Then have a meta reducer to update state from query params on page reload:

export function initStateFromQueryParams(
    reducer: ActionReducer<AppState>
): ActionReducer<AppState> {
    return function(state, action) {
        const newState = reducer(state, action);
        if ([INIT.toString(), UPDATE.toString()].includes(action.type)) {
            const urlParams = new URLSearchParams(window.location.search);
            return { ...newState, page: urlParams.get("page") };
        }
        return newState;
    };
}

This way you always know if page number changes it's gonna be reflected in url consequently. So even if you go to a new route(component) after that route gets its initial data the effect will trigger which updates query params.

You may want to checkout out this phenomenal article about state management in angular apps

Wildhammer
  • 2,017
  • 1
  • 27
  • 33
  • Thanks for the link to that article. The following made me wonder whether the navigation should take place inside the effect or outside: "Since the user can always interact with the URL directly, we should treat the router as the source of truth and the initiator of actions." – david_i_smith Dec 08 '19 at 21:01
1

For me this worked on the root path:

{
  path: '',
  redirectTo: '/foo?bar=baz',
  pathMatch: 'full'
}

However when trying the same with a named param (like your :userId) it doesn't work

Ronin
  • 7,322
  • 6
  • 36
  • 54
0

What I have done for this functionality is in the routing module, create a route to a component:

{
    path: 'route-to-component-that-redirects-here',
    component: MyRedirectComponent
}

Then in the component, you can navigate with the queryParms set to what you want back to the route you want:

// MyRedirectComponent

ngOnInit(): void {
    this.router.navigate(['/route-i-want-here'], { queryParams: { p: 'value' } });
}
Sean Chase
  • 1,139
  • 1
  • 15
  • 23