routerLink not updating url value when changed

Hello,

I have been blocking for three days on a problem that I cannot solve.

I tried several solutions that I could find on the forums but it did not change anything.

I am developing a PWA, I have two “menus”, one for the desktop version which is displayed just below the header and the second for the mobile version which is displayed by clicking on “ion-menu-button”.

To display the articles, the application must know what type of articles it should display, for adults or children.

Example, to see adult articles: https://www.example.com/articles
Example, to see children’s articles: https://www.example.com/kids/articles

You will notice that the “kids” segment has been added.

All the sections have the same thing.

In the menu, I have a button that allows you to switch between adults and children regardless of the section.

The switch system works very well on the menu of the desktop version but not the menu for the mobile version.

The segment returns a value of “NULL” in the “routerLink” and even when I switch between modes the value does not change. When I debug and look at the logs, the value has changed and I can even display it next to the title of the link.

I use a “SegmentPipe” to return the correct value.

I noticed one thing, when the segment changes if I concat the variable « pages »

this.pages = this.pages.concat(...this.pages, ...this.pages);

The new added values have the right segment.

Thanks for your help.

// app.components.html
...
<ion-menu-toggle auto-hide="false" *ngFor="let page of appPages">
	<ion-item [routerLink]="page.url | segment | async"
			  [routerLinkActive]="'active'"
			  [routerDirection]="'root'">
		<ion-icon slot="start" [name]="page.icon"></ion-icon>
		<ion-label>
			{{page.title | translate}} {{page.url | segment | async}}
		</ion-label>
	</ion-item>
</ion-menu-toggle>
...
import {Pipe, PipeTransform} from '@angular/core';

import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';

import {trimSlashes} from '../shared/utils';
import {SettingsFacade} from '../store/settings/settings.facade';

@Pipe({
    name: 'segment',
    // pure: false
})
export class SegmentPipe implements PipeTransform {
    segement$: Observable<string>;

    constructor(private settingsFacade: SettingsFacade) {
        this.segement$ = this.settingsFacade.segment;
    }

    transform(value: string): Observable<string> {
        return this.segement$
            .pipe(
                map((segment: string) => {
                    return `/${trimSlashes(segment + value)}`;
                })
            );
    }
}

While you wait for answers that will probably be more to your liking, I’m going to argue for a redesign. I would get all this segment mogrification out of pages and up into a service that provides appPages, so that it’s centralized.

Hello,
Thank you for your reply. Yes the segment is generated outside of appPages

The problem does not come from the generation of the segment, but from the component “App Component” which is loaded only once in the application and does not update the information when it changes.

Here are code snippets.

// settings.effects.ts
@Injectable()
export class SettingsEffects {
    ...

    constructor(
        private actions$: Actions,
        private store: Store<State>,
        private segmentRouterService: SegmentRouterService,
    ) {
    }

    initSegment$ = createEffect(() =>
            this.actions$.pipe(
                ofType(ROOT_EFFECTS_INIT),
                switchMap(() => {
                    return this.segmentRouterService.init();
                }),
                tap((segment: string) => {
                    if (segment) {
                        this.store.dispatch(SettingsActions.init({settings: {segment}}));
                    }
                })
            ),
        {dispatch: false}
    );

    ...
}
// settings.facade.ts
@Injectable()
export class SettingsFacade {
    ...

    private readonly segment$ = this.store.pipe(select(SettingsSelector.selectSegment));

    constructor(private store: Store<State>) {
    }

    changeSegment(segment: string): void {
        this.store.dispatch(SettingsActions.changeSegment({segment}));
    }

    get segment(): Observable<any> {
        return this.segment$;
    }

    ...
}
// segment-router.service.ts
@Injectable({
    providedIn: 'root'
})
export class SegmentRouterService implements OnDestroy {
    private value: string;
    private currentValue: string;
    private defaultValue = 'adults';

    subscription$ = new Subject<any>();
    routerEvents = new Subject<string>();

    constructor(
        private router: Router,
        private settingsFacade: SettingsFacade,
    ) {
        this.routerEvents
            .pipe(takeUntil(this.subscription$))
            .subscribe(async (segment: string) => {
                this.buildSegment(segment);
            });
    }

    async init(): Promise<any> {
        return new Promise((resolve) => {
            this.router.events
                .pipe(
                    filter((event: RouterEvent) => event instanceof ResolveStart),
                    map((event: ResolveStart) => {
                        let data = null;
                        let route = event.state.root;

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

                        return data.segment;
                    }),
                    distinctUntilChanged(),
                    takeUntil(this.subscription$),
                )
                .subscribe((segment: string) => {
                    this.routerEvents.next(segment);
                    this.onSegmentChangeEmitter.emit(segment);

                    resolve(this.currentSegment);
                });
        });
    }

    ngOnDestroy(): void {
        this.subscription$.next();
        this.subscription$.complete();
    }

    private buildSegment(segment: string) {
        this.value = segment;

        if (segment === this.defaultValue || segment === 'none') {
            this.currentValue = '';
        } else {
            this.currentValue = segment;
        }

        this.settingsFacade.changeSegment(this.currentValue);
    }
}

I’m wondering if all this complexity is really warranted. Just glancing at SegmentRouterService.init, I’m having a hard time convincing myself that the returned Promise can be guaranteed to resolve only once. I find pipes hard to debug, and this doesn’t seem like a great fit for one, because the stuff it does has a (relatively rare) well defined inflection point - when the user switches from adults to children and back. I would hang everything off that action, having somebody (SegmentRouterService, maybe) simply expose an Observable of whatever becomes AppComponent's appPages, subscribe to that in the app component, and get rid of the pipe entirely.

Hello,

Thank you for your answer.

I already tried what you just told me but it doesn’t work in the AppComponent.
The only solution I have made so far and which allows me to move forward because I lost a lot of time and reload the page when changing the language.
It’s not great, but I don’t have a choice, until I find another great solution.

The init function is called in the AppComponent and must return the promise before the platform is ready, otherwise the menu will be initialized with the value null before the routes are loaded.

The promise is executed only once and the resolve in the subscribe is ignored afterwards.

async initializeApp() {
	await this.segmentRouterService.init();

	this.platform.ready()
		.then(() => {
			...
		});
}

I have my best results with RxJS when I deal with it in as strictly a functional manner as I can.

I am seeing some imperative flavor in the code of this thread - especially the tap in SettingsEffects and the aforementioned Promise of SegmentRouterService.

I can tell you this: the basic idiom described in this post does indeed work when subscribing in the app component - I’ve been using it for quite a while to update the enabled/disabled status of common toolbar buttons.

I think you did not understand the problem, it does not come from the code I wrote, it is well functional and the values ​​are updated when I debug them, it is the routerLink in the AppComponent which do not update the value whatsoever using my code or another library.

To give an example, I start the application with the route “/articles

Here is the result :

Link [routerLink]=‘page.url’: /articles
Label {{page.title}} @ {{page.url}}: Articles @ /articles

Here is the result when I change segment and I go to the road “/kids/articles

Link [routerLink]=‘page.url’: /articles
Label {{page.title}} @ {{page.url}} => Articles @ /kids/articles

Here, we can see that the value of the url displayed in the label is updated and not that of the routerLink.

I noticed the same behavior when using a translation library which adds the language prefix in the url, the prefix is ​​not updated in the routerLink.

The following behaves as I think you’re expecting for me:

export class HomePage {
  link$ = new BehaviorSubject<string>("nowhere");
  link = this.link$.value;
  destination = "";

  constructor() {
    this.link$.subscribe(link => this.link = link);
  }

  moveRoad(): void {
    this.link$.next(this.destination);
  }
}
<ion-content>
    <a [routerLink]="link">road to nowhere</a>
    <ion-item>
        <ion-label>where to go?</ion-label>
        <ion-input [(ngModel)]="destination"></ion-input>
    </ion-item>
    <ion-item button (click)="moveRoad()">move road</ion-item>
</ion-content>

When I type something in the input box and click the “move road” button, the routerLink updates.

Yes the code works in the other components but not in the AppComponent where the ion-menu is located, that’s the problem I have.
I have another menu for the desktop version which is loaded outside of the AppComponent and there is no problem with that.

// app.component.ts
export class AppComponent implements OnInit {
    appPages: Page[] = [...];

    constructor() {
    }
}
// app.component.html
<ion-app>
    <ion-split-pane contentId="main-content">
        <ion-menu contentId="main-content" type="overlay">
            <ion-header>
                <ion-toolbar>
                    <ion-title>Menu</ion-title>
                </ion-toolbar>
            </ion-header>
            <ion-content>
                <ion-list lines="none">
                    <ion-menu-toggle auto-hide="false" *ngFor="let page of appPages">
                        <ion-item [routerLinkActive]="'active'"
                                  [routerLink]="page.url"
                                  [routerDirection]="'root'">
                            <ion-icon slot="start" [name]="page.icon"></ion-icon>
                            <ion-label>
                                {{page.title}}
                            </ion-label>
                        </ion-item>
                    </ion-menu-toggle>
                </ion-list>
            </ion-content>
        </ion-menu>
        <ion-router-outlet id="main-content"></ion-router-outlet>
    </ion-split-pane>
</ion-app>

This still WFM:

@Injectable()
export class LinkService {
  private link$ = new BehaviorSubject<string>("nowhere");

  watchLink(): Observable<string> {
    return this.link$;
  }

  pokeLink(link: string) {
    this.link$.next(link);
  }
}
export class HomePage {
  destination = "";

  constructor(private linker: LinkService) {
  }

  moveRoad(): void {
    this.linker.pokeLink(this.destination);
  }
}
<ion-content>
    <ion-item>
        <ion-label>where to go?</ion-label>
        <ion-input [(ngModel)]="destination"></ion-input>
    </ion-item>
    <ion-item button (click)="moveRoad()">move road</ion-item>
</ion-content>
export class AppComponent {
  link = "nowhere";

  constructor(
    private platform: Platform,
    private splashScreen: SplashScreen,
    private statusBar: StatusBar,
    private linker: LinkService,
  ) {
    this.initializeApp();
    linker.watchLink().subscribe(link => this.link = link);
  }

  initializeApp() {
    this.platform.ready().then(() => {
      this.statusBar.styleDefault();
      this.splashScreen.hide();
    });
  }
}
<ion-app>
    <ion-header>
        <a [routerLink]="link">road to nowhere</a>
    </ion-header>
    <ion-content>
        <ion-router-outlet></ion-router-outlet>
    </ion-content>
</ion-app>

Hello rapropos,

Thank you very much for your help and for the time you have spent solving my problem.

I tried to apply your code to test and I discovered that when I tried to clone the initial array of the appPages page list, the new variable contained a reference and not a copy, that’s why when I changed language or segment the value did not change because the new value generated does not correspond to any route.

I solved the problem by doing a deepCopy of the table

// Example
private pages = [
	{title: 'nav.home', url: '/home', icon: 'home'},
	{title: 'nav.list', url: '/list', icon: 'list'}
];

// Old
const pages = [...this.pages]

// New
const pages = JSON.parse(JSON.stringify(this.pages));

// Does not work for multi-dimensional arrays
const pages = [...this.pages]
const pages = this.pages.concat([]);
const pages = Object.assign([], this.pages);
const pages = this.pages.slice(0);

// There is also cloneDeep from the Lodash library which those who use this library
const pages = _.cloneDeep(this.pages);

Thanks again to you.

Have a good day.

1 Like

I think I see what is happening. I added a break-point in the algorithm that determines if the routerLink should be active; and, it looks like the local urlTree isn’t actually updated at the point that the NavigationEnd event is evaluated. In the following screenshot, I’ve navigated to ..../1/screens/1/.... ; but, the routerLink urlTree is still checking against the previous url, ..../1/screens/2/.... :

2017-11-18_06-16-07

Perhaps there is a race condition with the QueryList<RouterLink> in the RouterLinkActive directive - in which the DOM isn’t updated yet, at the time the NavigationEnd event has fired.