You're building an internal business application. Your QA team needs to test what an Admin sees versus a regular user. Your stakeholder wants to demo the new dashboard that's still behind a feature flag. And IT wants to verify that the "Bulk Export" feature is properly gated to licensed users only.
Three requests. Normally, that's three database changes, three test accounts, and a lot of context switching.
Or... you could just use the Angular Toolbar.
In the previous article, we introduced ngx-dev-toolbar and why it's useful for internal business applications. Now let's get it running and set up Feature Flags, Permissions, and App Features—the three tools that make testing different scenarios instant.
Quick Setup
Initialize the toolbar in your main.ts:
import { bootstrapApplication } from '@angular/platform-browser';
import { isDevMode } from '@angular/core';
async function bootstrap() {
const appRef = await bootstrapApplication(AppComponent, appConfig);
if (isDevMode()) {
const { initToolbar } = await import('ngx-dev-toolbar');
initToolbar(appRef);
}
}
bootstrap();
The dynamic import ensures zero production bundle impact—the toolbar code only loads in development.
How It Works
All three tools work the same way:
-
Register your options with
setAvailableOptions() -
Read the values with
getValues()
The toolbar stores any overrides in localStorage, so they persist across page reloads. When you read values with getValues(), you get your original values with any toolbar overrides already applied.
Feature Flags
Remember that new dashboard your stakeholder wants to demo? That's a feature flag. Let's wire it up.
// 📃 feature-flags.service.ts
import { ToolbarFeatureFlagService } from 'ngx-dev-toolbar';
@Injectable({ providedIn: 'root' })
export class FeatureFlagsService {
private toolbarService = inject(ToolbarFeatureFlagService);
// 👇 Real flag values from your flag provider (LaunchDarkly, Split, etc.)
private realFlags = toSignal(this.flagProvider.getFlags(), { initialValue: [] });
// 👇 All flags with toolbar overrides already applied
private flags = toSignal(this.toolbarService.getValues(), { initialValue: [] });
constructor() {
// Register flags with the toolbar when they load
effect(() => {
const flags = this.realFlags();
if (flags.length) {
this.toolbarService.setAvailableOptions(
flags.map(f => ({
id: f.key,
name: f.name,
description: f.description,
isEnabled: f.value,
}))
);
}
});
}
getFlag(id: string): Signal<boolean> {
return computed(() =>
this.flags().find(f => f.id === id)?.isEnabled ?? false
);
}
}
The getValues() method returns all flags with any toolbar overrides already merged in—no manual merging required.
Your flag provider might return data in a different format than the toolbar expects. You can map the values when registering:
// LaunchDarkly returns: { key: 'dark-mode', value: true }
// Toolbar expects: { id: string, name: string, description: string, isEnabled: boolean }
this.toolbarService.setAvailableOptions(
flags.map(f => ({
id: f.key, // key → id
name: this.formatName(f.key), // 'dark-mode' → 'Dark Mode'
description: this.descriptions[f.key], // Add descriptions from a local map
isEnabled: f.value, // value → isEnabled
}))
);
Use it in your components:
// 📃 dashboard.component.ts
@Component({...})
export class DashboardComponent {
private featureFlagsService = inject(FeatureFlagsService);
showNewDashboard = this.featureFlagsService.getFlag('new-dashboard');
}
@if (showNewDashboard()) {
<app-new-dashboard />
} @else {
<app-classic-dashboard />
}
Permissions
Now for that QA scenario—testing what an Admin sees versus a regular user. That's all about permissions.
Permissions work the same way as feature flags, but with three possible states: original value, forced granted, or forced denied.
For permissions, you might want more control over how overrides are applied. Instead of getValues(), you can use getForcedValues() which returns only the permissions that were overridden in the toolbar:
// 📃 permissions.service.ts
import { ToolbarPermissionsService } from 'ngx-dev-toolbar';
@Injectable({ providedIn: 'root' })
export class PermissionsService {
private toolbarService = inject(ToolbarPermissionsService);
// 👇 Real permissions from your auth backend
private realPermissions = toSignal(this.authService.getPermissions(), { initialValue: [] });
// 👇 Only the permissions overridden in the toolbar
private forcedPermissions = toSignal(this.toolbarService.getForcedValues(), { initialValue: [] });
constructor() {
// Register permissions with the toolbar
effect(() => {
const perms = this.realPermissions();
if (perms.length) {
this.toolbarService.setAvailableOptions(
perms.map(p => ({
id: p.id,
name: p.name,
isGranted: p.granted,
}))
);
}
});
}
hasPermission(id: string): Signal<boolean> {
return computed(() => {
// 👇 Check toolbar override first
const forced = this.forcedPermissions().find(p => p.id === id);
if (forced) return forced.isGranted;
// 👇 Fall back to real permission
return this.realPermissions().find(p => p.id === id)?.granted ?? false;
});
}
}
This pattern gives you explicit control: real values from your backend, with toolbar overrides layered on top only when set.
getValues() vs getForcedValues()
Both methods work, but serve different needs:
getValues()— Returns all options with overrides already applied. Simpler to use when you want the toolbar to manage everything.getForcedValues()— Returns only the options that were manually overridden. Use this when your backend is the source of truth and you want to explicitly layer toolbar overrides on top.
Choose getValues() for simpler integrations. Choose getForcedValues() when you need to preserve the distinction between real values and test overrides.
App Features
And finally, that "Bulk Export" feature IT wants to verify? That's a license-gated app feature.
App Features are perfect for testing subscription tiers and license-based functionality:
// 📃 app-features.service.ts
import { ToolbarAppFeaturesService } from 'ngx-dev-toolbar';
@Injectable({ providedIn: 'root' })
export class AppFeaturesService {
private toolbarService = inject(ToolbarAppFeaturesService);
// 👇 Real features from your licensing backend
private realFeatures = toSignal(this.licensingService.getFeatures(), { initialValue: [] });
private features = toSignal(this.toolbarService.getValues(), { initialValue: [] });
constructor() {
effect(() => {
const features = this.realFeatures();
if (features.length) {
this.toolbarService.setAvailableOptions(
features.map(f => ({
id: f.id,
name: FEATURE_METADATA[f.id]?.name ?? f.id,
description: FEATURE_METADATA[f.id]?.description ?? '',
isEnabled: f.enabled,
}))
);
}
});
}
isEnabled(id: string): Signal<boolean> {
return computed(() => this.features().find(f => f.id === id)?.isEnabled ?? false);
}
}
Did you notice the FEATURE_METADATA mapping? Your licensing API typically returns just feature IDs and enabled status—no user-friendly names or descriptions. We added both name (to control how it appears in the toolbar) and description locally:
// 📃 app-features.models.ts
export enum Feature {
Analytics = 'analytics',
BulkExport = 'bulk-export',
WhiteLabel = 'white-label',
Notifications = 'notifications',
ApiAccess = 'api-access',
}
export const FEATURE_METADATA: Record<string, { name: string; description: string }> = {
[Feature.Analytics]: { name: 'Analytics Dashboard', description: 'Advanced reporting and visualization' },
[Feature.BulkExport]: { name: 'Bulk Export', description: 'Export datasets to CSV or Excel' },
[Feature.WhiteLabel]: { name: 'White Label', description: 'Custom branding with your logo' },
[Feature.Notifications]: { name: 'Notifications', description: 'Real-time push notifications' },
[Feature.ApiAccess]: { name: 'API Access', description: 'REST API for integrations' },
};
This keeps your feature definitions type-safe and your descriptions in one place.
Now you can test what users on different subscription tiers see—without touching the database.
Combining for Realistic Scenarios
Here's where it all comes together.
Remember those three requests from the beginning? With the toolbar set up, you can handle all of them without touching the database. Toggle the new dashboard flag for the demo. Grant "can-admin" permission to simulate an admin user. Enable "Bulk Export" to verify the license gate.
The real power comes when you combine all three tools. Want to test what an "Admin with Enterprise license" sees? Enable the app features and grant the permissions. Testing a "Regular user on Basic tier"? Adjust accordingly.
In the next article, we'll see how to save these combinations as Presets—so you can switch between user personas with a single click.
What's Next
Now that you know how to use the built-in tools, check out the next article on Presets to learn how to save and share your configurations with your team.
Happy developing!



Top comments (0)