Skip to main content

Navigating Between Screens

Use NavController for a stack of screens and TabController for independent top-level sections. Both DSL functions create ViewStruct descriptions; call imperative methods on their mounted view instances, not on the structs.

Present and dismiss from a view

Most screens only need the BaseView convenience methods:

this.present(DetailsScreen({
id: 'details',
itemId,
}), { animated: true });

present() pushes into the nearest ancestor NavController. From the active screen, pop it with:

this.dismiss({ animated: true });

If the view is not part of the controller's active screen, dismiss() refuses the operation. Dialog presentation uses the same helper with { isDialog: true }; see the Dialogs guide.

Create a navigation stack

protected override getViews() {
return [
NavController({
id: 'mainNav',
initialView: HomeScreen({ id: 'home' }),
}).isInitialFocus(),
];
}

When the owner needs direct stack control, look up the mounted NavControllerView:

private get mainNav(): NavControllerView | undefined {
return this.getSubView<NavControllerView>('mainNav');
}

showDetails(itemId: string) {
this.mainNav?.push(
DetailsScreen({ id: 'details', itemId }),
true,
);
}

Do not call push() on NavController({...}); that value is only a ViewStruct.

Stack operations

nav.push(nextView, true, transitionStyleKey);
nav.pop(true, transitionStyleKey);
nav.replace(replacementView, false, transitionStyleKey);
nav.reset(0, newRootView, false, transitionStyleKey);
  • push(view, animated = true, key?) adds a screen.
  • pop(animated = true, key?) removes the current screen and returns it.
  • replace(view?, animated = false, key?) swaps the current screen and returns the removed one.
  • reset(endIndex = 0, newFirstScreen?, animated = false, key?) removes stack entries down to the requested boundary and optionally installs a new first screen. It returns false if a transition is already running.

Navigation calls made during an active transition are ignored. Disable the trigger or otherwise prevent double dispatch when a user can press repeatedly.

onLastViewRemoved is useful for an embedded flow that should notify its owner when its stack becomes empty.

Tabs with independent stacks

TabController owns a lazily created NavController and independent stack per tab, while NavController owns stack transitions and lifecycle completionTabController owns a lazily created NavController and independent stack per tab, while NavController owns stack transitions and lifecycle completion

Every TabController tab lazily creates its own NavController, so returning to a tab preserves that tab's stack:

const tabs: ITabControllerTab[] = [
{
id: 'home',
title: 'Home',
view: HomeScreen({ id: 'homeRoot' }),
},
{
id: 'settings',
title: 'Settings',
view: SettingsScreen({ id: 'settingsRoot' }),
transitionStyleKey: 'controls.NavController.transition.slide',
},
];

TabController({
id: 'mainTabs',
tabs,
selectedTab: this.selectedTabId,
})
.onTabSelected((tabId, nav) => {
this.selectedTabId = tabId;
console.info('Active tab stack', nav?.numberOfViews);
})
.onTabHidden((tabId) => {
console.info('Hidden tab', tabId);
});

The mounted TabControllerView also exposes selectTab(id), addTab(tab), and removeTab(id).

Tab selection itself is not animated. A tab's transitionStyleKey configures navigation within that tab's child NavController.

Avoid hibernateViewsWhenChangingTabs for production at present; the source marks the feature as implemented but still subject to rendering artifacts.

Talk to us