Rapscallion
Rapscallion

Reputation: 717

Angular routerLink navigating to wrong path on 'nested' pages

I'm building a basic notes app in Angular 12 where the notes are stored in Firebase. Notes contain text but can also act as a parent node for more notes, e.g:

Note
Note
|- Note
  |- Note
  |- Note
    |- Note
|- Note
Note

You can view a list of a note's child notes from /notes/{{note.id}} and edit the note itself at /edit/{{note.id}}. If the id for /notes/ is null, it grabs all of the top level notes.

The problem: Everything works fine until I try to view the notes list of the second level of the hierarchy. The routerLink appears to add both /edit/{{note.id}} and /notes/{{note.id}} to the navigation stack, e.g:

Note1
|- Note2 <-- click '>' button on note2 in list view
  |- Note3 <-- expect to see note3 in list view

Instead of viewing notes/2, instead I'm taken to edit/2. Clicking back in the browser takes me to the proper route I wanted of notes/2, allowing me to see note3 in the list view. Why would /edit/{{note.id}} be added to the router when you click the icon button?

EDIT: StackBlitz link to demo the issue here: https://stackblitz.com/edit/angular-hcmvab

Relevant code snippets below:

app-routing.module.ts

const routes: Routes = [
  { path: '', redirectTo: 'notes', pathMatch: 'full' },
  { path: 'login', component: LoginComponent },
  { path: 'notes',
    children: [
      { path: '', component: NotesComponent },
      { path: ':id', component: NotesComponent },
    ],
  },
  { path: 'edit/:id', component: NoteDetailsComponent }
];

notes.component.ts

export class NotesComponent implements OnInit {
  parentID = this.route.snapshot.paramMap.get('id');
  notes: any;

  constructor(
    private route: ActivatedRoute,
    private notesService: NotesService
  ) {}

  ngOnInit(): void {
    this.retrieveNotes();
  }

  retrieveNotes(): void {
    this.notesService
      .getChildNotes(this.parentID)
      .snapshotChanges()
      .pipe(
        map((changes: any) =>
          changes.map((c: any) => ({
            id: c.payload.doc.id,
            ...c.payload.doc.data(),
          }))
        )
      )
      .subscribe((data) => {
        this.notes = data;
      });
  }
}

notes.component.html

<mat-list>
  <mat-list-item *ngFor="let note of notes" [routerLink]="['/edit', note.id]">
    <mat-icon fontSet="material-icons-outlined" mat-list-icon>description_outlined</mat-icon>
    <div mat-line>{{note?.title}}</div>
    <div class="spacer"></div>
    <button mat-icon-button *ngIf="note.children.length > 0" [routerLink]="['/notes', note.id]">
      <mat-icon>arrow_forward_ios</mat-icon>
    </button>
  </mat-list-item>
</mat-list>

Upvotes: 1

Views: 1281

Answers (2)

Aviad P.
Aviad P.

Reputation: 32629

What you're looking for is a route with a variable number of parameters. Something like /note/a/b/c/d/e. One way to achieve that is to use a route with a custom url matcher, that accepts the route if it begins with /note (or your prefix of choice) and collects all the rest of the segments and passes them to the component.

Here's how the route would be defined:

const variableRouteMatcher: UrlMatcher = (
  segments: UrlSegment[]
): UrlMatchResult => {
  if (segments.length >= 1 && segments[0].path === 'note') {
    const fullPath = segments
      .slice(1)
      .map(r => r.path)
      .join('/');
    return {
      consumed: segments,
      posParams: {
        fullPath: new UrlSegment(fullPath, {})
      }
    };
  }
  return null;
};

const routes: Routes = [
  { matcher: variableRouteMatcher, component: NoteComponent }
];

Working StackBlitz example

EDIT (based on comment) After looking at your stackblitz, I've seen two problems, one as Cantarzo pointed in his answer, the fact that there is a link element inside another link element.

But the more critical error is that your notes component doesn't listen to changes in its route parameters, it takes a snapshot of the route and then uses that forever. Remember that Angular reuses components in routes, so the very same NoteComponent instance is used whenever you navigate to a note.

This means that the first time you navigate into a note, it gets its parent id, and then regardless of where you go, the parent id stays the same.

To solve this, you have to subscribe to changes in the activated route's parameters and update your parent id (and also your child notes) accordingly

See your forked stacklitz

Upvotes: 1

Cantarzo
Cantarzo

Reputation: 31

The problem is that you're putting one [routerLink] inside another. You should move the [routerLink] attribute from <mat-list-item> to another place, like a button inside of it. The way you're doing it is probably activating first the button click and then the click at the whole list item.

Try to change to this:

     <mat-list-item *ngFor="let note of notes" >
        <mat-icon fontSet="material-icons-outlined" mat-list-icon>description_outlined</mat-icon>
        <div mat-line>{{note?.title}}</div>
        <div class="spacer"></div>
        <button mat-icon-button [routerLink]="['/notes', note.id]">
          <mat-icon>Some-Edit-Icon</mat-icon>
        </button>
        <button mat-icon-button *ngIf="note.children.length > 0" [routerLink]="['/notes', note.id]">
          <mat-icon>arrow_forward_ios</mat-icon>
        </button>
      </mat-list-item>
    </mat-list>

Than just adjust the way you like. Another problem you may have is that you're passing the notes.id to the [routerLink] that access the children note. Maybe you should put a *ngFor and pass children.id instead

If this solution does not work, please create a stackBlitz so we can see the problem more clearly.

Upvotes: 1

Related Questions