Context Navigation Plugin
Context Navigation Plugin (@equinor/fusion-framework-plugin-context-navigation)
Event-driven context-to-URL reconciler plugin for Fusion Framework portals. It keeps the active context synchronized with the browser URL by reacting to context changes and app switches, encoding context into the URL, and optionally guarding against manual URL edits that drop context.
When to Use
Use this plugin when your portal needs to:
- Reflect the active context (project, facility, etc.) in the browser URL automatically
- Support multiple URL encoding strategies (path segment, query parameter, or app-defined custom encoding)
- Preserve context across app switches and page reloads
- Prevent users from accidentally losing context by editing the URL manually
Note
This plugin is intended for portal hosts, not individual applications.
Applications declare their preferred routing strategy via the app manifest'sbuild.options.contextRouting field β the portal's context-navigation plugin
picks up that declaration and applies the correct URL encoding automatically.
For App Developers
You don't need to install or configure this plugin. The portal handles it.
Your only responsibility is to declare how context should appear in your URL.
Declare a routing strategy
Set contextRouting in your app manifest's build.options:
// app.manifest.config.ts
export default defineAppManifest((env) => ({
// ...
build: {
options: {
contextRouting: 'query', // or 'path', or omit for default path behavior
},
},
}));| Value | URL Shape | When to Use |
|---|---|---|
'path' (or omitted) | /apps/{appKey}/{contextId}/sub-route | Default β simple apps without complex routing |
'query' | /apps/{appKey}/route?$contextId={id} | Apps with path-based sub-routes that conflict with a context segment |
Apps with custom URL shapes should omit contextRouting and register custom hooks instead (see below).
Note
When contextRouting is not set (or set to null), the portal defaults to the
path adapter which encodes context as a path segment after the app key.
If the app registers custom hooks (setContextPathExtractor /setContextPathGenerator), the custom adapter takes priority over the path
adapter regardless of the contextRouting value.
Custom URL shapes
If your app encodes context in a non-standard position (e.g. /route-a/{contextId}
instead of /{contextId}/route-a), register custom hooks in your app's context
configuration:
// app config
builder.setContextPathExtractor((pathname) => {
// Extract context id from your custom URL position
const segments = pathname.split('/').filter(Boolean);
return segments[1]; // e.g. /route-a/{contextId} β contextId
});
builder.setContextPathGenerator((context, pathname) => {
// Generate a URL with context in your custom position
const segments = pathname.split('/').filter(Boolean);
const route = segments[0] ?? '';
return `/${route}/${context.id}`;
});When these hooks are registered, the plugin's custom adapter picks them up
automatically β no manifest contextRouting declaration needed.
For Portal Developers
The rest of this README covers portal-level setup and configuration.
Key Concepts
| Concept | Description |
|---|---|
| Adapter | A self-selecting URL encoder/decoder. Each adapter declares canHandle() and provides encode() / decode() methods. |
| Reconciler | The reactive loop that watches context + app changes and triggers navigation when the URL is out of sync. |
| URL Guard | An optional secondary subscription that re-applies context encoding if an external navigation drops the context from the URL. |
| Source | The observable factory that drives the reconciler β determines whether app switches or context changes take priority. |
How It Works
- Observe β The reconciler watches the current app and current context via a configurable source factory.
- Resolve adapter β For each app, the first adapter whose
canHandle()returnstrueis selected. - Encode β The selected adapter encodes the context into a target URL.
- Compare β If the target URL differs from the current URL, navigation proceeds.
- Dispatch β A cancelable
onContextNavigationNavigateevent fires. Listeners can callpreventDefault()to abort. - Navigate β The framework navigation module performs the URL update.
- Confirm β An
onContextNavigationNavigatedevent fires after navigation completes.
Installation
pnpm add @equinor/fusion-framework-plugin-context-navigationQuick Start
App-portal (app switches lead)
An app-portal shows one app at a time. When the user switches apps, the reconciler
picks up the new app's context and encodes it into the URL. This is the default behavior.
import { enableContextNavigation } from '@equinor/fusion-framework-plugin-context-navigation';
export const configure = (configurator) => {
enableContextNavigation(configurator, (builder) => {
builder.setPortalName('app-portal');
builder.setDebug(true);
});
};The default source factory (createAppFirstSource) watches app.current$ and
resolves the app's context modules before emitting to the reconciler. When an app
switch happens, the new app's active context drives the URL update.
Context-portal (context changes lead)
A context-portal is organized around a shared context (e.g. a project). When the
user selects a context, navigation updates the URL immediately β regardless of
which app is active. Clearing context navigates back to the portal root.
import { enableContextNavigation } from '@equinor/fusion-framework-plugin-context-navigation';
import { createContextFirstSource } from '@equinor/fusion-framework-plugin-context-navigation/sources';
export const configure = (configurator) => {
enableContextNavigation(configurator, (builder) => {
builder.setPortalName('context-portal');
builder.setSourceFactory(createContextFirstSource());
builder.setNullContextUrl('/');
builder.setDebug(true);
});
};Key differences from app-portal:
createContextFirstSource()β context changes are the primary trigger; app modules are resolved as a dependency.setNullContextUrl('/')β when context is cleared, navigate to the portal landing page instead of delegating to the adapter.
Built-in Adapters
The plugin ships with three adapters evaluated in priority order:
| Adapter | URL Shape | When Selected |
|---|---|---|
| custom | App-defined (via generatePathFromContext / extractContextIdFromPath hooks) | App provides generatePathFromContext and/or extractContextIdFromPath hooks on its context provider |
| query | /apps/{appKey}/route?$contextId={id} | App manifest declares build.options.contextRouting: 'query' |
| path | /apps/{appKey}/{contextId}/sub-route | Default fallback β matches when contextRouting is 'path', null, or omitted |
When no custom adapters are registered, all three built-in adapters are available. The first whose canHandle() returns true for the current app wins.
Registering a Custom Adapter
You can register your own adapters for URL shapes not covered by the built-in set.
The canHandle predicate controls when your adapter is selected β use any signal
available in AdapterResolutionContext (app key, URL, context provider, routing strategy).
import type { ContextNavigationAdapter } from '@equinor/fusion-framework-plugin-context-navigation/adapters';
const hashAdapter: ContextNavigationAdapter = {
id: 'hash',
// Select this adapter for a specific app that uses hash-based context
canHandle: ({ appKey }) => appKey === 'my-hash-app',
encode: ({ context, currentURL }) => {
const url = new URL(currentURL.href);
url.hash = context ? `#ctx=${context.id}` : '';
return url;
},
decode: (url) => {
const match = url.hash.match(/^#ctx=(.+)$/);
return match?.[1] ?? null;
},
};
enableContextNavigation(configurator, (builder) => {
builder.registerAdapter(hashAdapter);
// NOTE: registering any adapter disables built-in defaults.
// Register all adapters you need explicitly.
});Configuration
| Builder Method | Default | Description |
|---|---|---|
registerAdapter(adapter) | Built-in set | Register a navigation adapter (object or factory). First match wins. |
setPortalName(name) | 'Portal' | Name used in debug log output. |
setOrigin(origin) | window.location.origin | Origin for constructing absolute URLs. |
setUrlGuard(enabled) | true | Re-sync context if an external navigation drops it from the URL. With replace: false, back/forward navigations update context from the URL instead of overwriting it. |
setDebug(enabled) | false | Enable verbose console.debug output. |
setNullContextUrl(urlOrFn) | β | Function (or static string) that returns the URL to navigate to when context is cleared. Receives { appKey, currentURL }. |
setRequireValidContext(enabled) | false | Validate the context against the app's context module (validateContext) before navigating. If validation fails, navigation is skipped. |
setNavigationOptions(options) | { replace: true } | Options passed to navigation.navigate() during URL updates. Set { replace: false } to push history entries β back/forward will then sync context from the URL rather than re-asserting the active context. |
setOnTransition(fn) | β | Side-effect hook called after each successful navigation. |
setSourceFactory(factory) | createAppFirstSource() | Observable source factory that drives the reconciler. |
Events
The plugin dispatches events through the Fusion Framework event system. Subscribe via framework.event.addEventListener().
| Event | When | Cancelable |
|---|---|---|
onContextNavigationNavigate | Before navigation β adapter resolved, target URL computed | Yes |
onContextNavigationNavigated | After navigation completes | No |
onContextNavigationAdapterResolved | When an adapter is selected for an app | No |
onContextNavigationSkipped | When reconciliation decides NOT to navigate | No |
Skip Reasons
The onContextNavigationSkipped event includes a reason field:
| Reason | Meaning |
|---|---|
'url-matches' | Target URL already matches the current URL |
'no-context' | Context is undefined (still initializing) |
'no-adapter' | No adapter could handle the current app |
'invalid-app-context' | setRequireValidContext(true) is set and the app's context module rejected the context |
'encode-returned-null' | Adapter's encode() returned null |
'canceled' | A listener called preventDefault() on the navigate event |
Intercepting Navigation
framework.event.addEventListener('onContextNavigationNavigate', (event) => {
console.log('About to navigate:', event.detail.targetURL.pathname);
// Cancel navigation conditionally
if (shouldBlock(event.detail)) {
event.preventDefault();
}
});Adapter Interface
interface ContextNavigationAdapter {
/** Unique identifier for logging and diagnostics. */
id: string;
/** Return `true` if this adapter handles the given app/URL combination. */
canHandle(ctx: AdapterResolutionContext): boolean;
/** Encode context into a URL. Return `null` to skip navigation. */
encode(args: { context: ContextItem | null; currentURL: URL }): URL | null;
/** Decode a context ID from a URL. Return `null` if not present. */
decode(url: URL): string | null;
}
interface AdapterResolutionContext {
appKey: string;
appContext: IContextProvider;
routingStrategy?: 'path' | 'query' | null;
currentURL: URL;
}Exports
| Specifier | Description |
|---|---|
@equinor/fusion-framework-plugin-context-navigation | Plugin runtime, configurator, enable helper, types, events |
@equinor/fusion-framework-plugin-context-navigation/adapters | Built-in adapter factories (createPathAdapter, createQueryAdapter, createCustomAdapter) |
@equinor/fusion-framework-plugin-context-navigation/sources | Source factories (createAppFirstSource, createContextFirstSource) and reconciler types |
@equinor/fusion-framework-plugin-context-navigation/utils | URL utility functions |
Peer Dependencies
| Package | Purpose |
|---|---|
@equinor/fusion-framework-module | Base module contract |
@equinor/fusion-framework-module-app | App switching and instance loading |
@equinor/fusion-framework-module-context | Context state management |
@equinor/fusion-framework-module-navigation | URL navigation |
@equinor/fusion-framework-module-event | Event dispatch |
rxjs | Reactive streams |
See Also
@equinor/fusion-framework-module-contextβ context module for query, validation, and resolution@equinor/fusion-framework-module-appβ app module definingFrameworkOptions.contextRoutingin build manifest