skip to content

In Angular, how do you open a compose panel in a named router-outlet beside the inbox, and what does the resulting URL look like?

level: middleimportance: should knowfreq 35%

answer

  1. name attribute on the outlet
  2. outlet property on the route
  3. outlets command object
  4. parentheses in the URL
  5. null closes it

basics

~10 s

Add <router-outlet name="side" />, give a route outlet: 'side', and navigate with an outlets command such as [{outlets: {side: ['compose']}}]. The URL becomes /inbox(side:compose); setting side to null closes the panel.

solid answer

~40 s

A named outlet is `<router-outlet name="side" />`, next to the unnamed primary outlet. Routes target it with the `outlet` property: `{path: 'compose', component: Compose, outlet: 'side'}`. To open it, navigate with an outlets command: `[routerLink]="[{outlets: {side: ['compose']}}]"` or `router.navigate([{outlets: {side: ['compose']}}])`. The serializer writes secondary outlets in parentheses after the primary path: `/inbox(side:compose)`; several are joined with `//`. Because the side panel is in the URL, it survives reload and deep links. Primary navigations built with `routerLink` or `navigate()` are deltas, so `/sent` from there keeps `(side:compose)`, while `navigateByUrl('/sent')` replaces the whole URL and closes it. `{outlets: {side: null}}` removes that outlet.

code

ts · 15 lines
ts
import {Component} from '@angular/core';
import {RouterLink, RouterOutlet} from '@angular/router';

// routes: [{path: 'inbox', component: Inbox}, {path: 'compose', component: Compose, outlet: 'side'}]
@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink],
  template: `
    <a [routerLink]="[{outlets: {side: ['compose']}}]">Compose</a>
    <a [routerLink]="[{outlets: {side: null}}]">Close panel</a>
    <main><router-outlet /></main>
    <aside><router-outlet name="side" /></aside>
  `,
})
export class App {}

go deeper

for a junior

Know that an outlet can have a name, that routes target it with the outlet property, and that it shows up in the URL.

for a middle

Write the outlets command to open and close a panel, read the parenthesised URL syntax, and place the outlet route at the right level.

for a senior

Explain why delta navigation keeps secondary outlets while navigateByUrl drops them, and fix panels that close unexpectedly.

for a principal

Judge whether a panel deserves URL state at all, weighing deep links and Back behaviour against unusual URLs and extra routing complexity.

## Why named outlets exist Most screens have one routed area, the **primary** outlet. Some need a second, independently routed area: a compose panel beside the inbox, a chat drawer, a help pane. Angular's router supports this with **named (secondary) outlets**: each one has its own route branch and its own part of the URL, so the panel can be opened, closed, reloaded and deep-linked without touching the main view. ## The three pieces 1. **The outlet** - in the template, `<router-outlet />` (primary, named `'primary'`) and `<router-outlet name="side" />`. 2. **The route** - `{path: 'compose', component: Compose, outlet: 'side'}` in the same `Routes` array level as the routes of the component that holds that outlet. 3. **The navigation** - an `outlets` command naming the outlet. ```ts // open this.router.navigate([{outlets: {side: ['compose']}}]); // close this.router.navigate([{outlets: {side: null}}]); ``` ## The URL syntax The default `UrlSerializer` writes secondary outlets in parentheses, as `name:path`, joined by `//`: | State | URL | |---|---| | inbox only | `/inbox` | | inbox plus compose panel | `/inbox(side:compose)` | | plus a second outlet `help` | e.g. `/inbox(side:compose//help:shortcuts)` | | outlets under a `mail` parent | `/mail/(inbox//side:compose)` | The last row appears when the named outlet lives inside a child component, `MailShell`, rather than the root: the primary child and the named child are grouped in parentheses under `mail/`. The route config mirrors it: the `outlet: 'side'` route goes into `mail`'s `children`. ## What keeps and what drops the panel `routerLink` and `Router.navigate()` build the new URL as a **delta** of the current URL tree. A command that changes only the primary outlet leaves other outlets alone: - `<a routerLink="/sent">` from `/inbox(side:compose)` goes to `/sent(side:compose)`: the panel stays open; - `router.navigateByUrl('/sent')` parses a complete URL with no secondary part: the panel closes; - `{outlets: {primary: ['sent'], side: null}}` changes both in one navigation. This is a common source of "the side panel randomly closes" bugs: some code paths navigate by URL string and others by commands. ## Why the URL encoding matters - **Reload and share.** The panel state is in the address, so a reload of `/inbox(side:compose)` restores both views. - **Back button.** Opening and closing the panel are navigations, so Back undoes them; use `replaceUrl: true` if a panel toggle should not add history entries. - **Guards and resolvers** apply to the outlet's route like any other, so a `canDeactivate` guard on `Compose` can protect an unsaved draft when the panel closes. ## Reading an outlet's state Inside the panel component, `inject(ActivatedRoute)` returns the route of the `side` branch, with its own params and data; the primary view's route is a sibling in the router state tree, reachable through `router.routerState.root.children`. Each outlet also has its own lifecycle: opening the panel creates `Compose` and fires the side outlet's `activate`, and closing it destroys `Compose` without touching the inbox. ## Limits and alternatives - Every outlet name at one level must be unique. - The parenthesised URLs look unusual to users and analytics; if the panel does not need to be linkable, a query param or plain component state may be simpler. - An outlet's route must be defined at the level of the component that renders that outlet; a route at the wrong level simply never matches, which surfaces as a navigation error whose message starts "Cannot match any routes. URL Segment:" and names the outlet's segment.

  • Why does the side panel stay open after clicking a routerLink to /sent but close after navigateByUrl('/sent')?
    routerLink and navigate() build a delta of the current URL tree, changing only the primary outlet and keeping others. navigateByUrl() takes a complete URL; '/sent' has no secondary part, so the side outlet is dropped.
  • Where must the route for a named outlet be declared when the outlet sits inside MailShell?
    In MailShell's route children, next to the primary child routes, with outlet: 'side'. The URL then groups them under the parent, as /mail/(inbox//side:compose).

saying these in an interview costs you the question

  • Opening a named outlet with routerLink="side:compose" as a plain string
  • Expecting named outlet state to be lost on page reload
  • Believing every primary navigation closes secondary outlets
  • Declaring the outlet route at the root when the outlet lives in a child component
  • Closing a panel by navigating to an empty path instead of null