Skip to main content

Angular

An Angular SDK for integrating feature flags into your application with real-time updates, structural directives, pipes, and route guards.

Table of Contents​

Features​

  • 🚀 Real-time Updates: WebSocket integration for instant feature flag changes
  • 🧩 Structural Directive: *flagpoleFeature for declarative show/hide with fallback templates
  • 🔧 Pipe: featureFlag pipe for inline checks and conditional classes
  • 🛡️ Route Guards: FeatureFlagGuard to protect routes behind flags
  • 🌍 Environment Support: Filter flags by environment (development, staging, production)
  • 📊 TypeScript: Full type safety with RxJS observables
  • ⚡ Zero Config: Works out of the box with sensible defaults

Installation​

NPM​

npm install @flagpole/angular socket.io-client

Yarn​

yarn add @flagpole/angular socket.io-client

Requirements​

  • Angular >= 15.0.0
  • RxJS >= 7.5.0
  • TypeScript >= 4.8.0
  • socket.io-client >= 4.7.2

Quick Start​

1. Register the Module​

Import FeatureFlagModule.forRoot() in your root module with your project's API key (available from the FlagPole dashboard):

// app.module.ts
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FeatureFlagModule } from "@flagpole/angular";

import { AppComponent } from "./app.component";

@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
FeatureFlagModule.forRoot({
apiKey: "fp_live_your_api_key",
environments: ["development"], // optional, if nothing is passed, then all environments will be shown (production, staging and development)
}),
],
bootstrap: [AppComponent],
})
export class AppModule {}

2. Use Feature Flags​

Template Usage​

<!-- feature.component.html -->
<div>
<h2>Feature Flags Test</h2>

<!-- Loading state -->
<div *ngIf="isLoading$ | async">Loading flags...</div>

<!-- Error state -->
<div *ngIf="error$ | async as error" class="error">
Error: {{ error.message }}
</div>

<!-- Use structural directive for specific flag -->
<div *flagpoleFeature="'newFeature'">
<app-new-feature></app-new-feature>
</div>

<!-- Use directive with fallback -->
<div *flagpoleFeature="'betaFeature'; else oldFeature">
<h3>Beta Feature Content</h3>
</div>
<ng-template #oldFeature>
<h3>Old Feature Content</h3>
</ng-template>

<!-- Use pipe for inline checks -->
<button *ngIf="'premiumFeature' | featureFlag" class="premium-btn">
Premium Action
</button>

<!-- Display all available flags -->
<h3>All Flags:</h3>
<div *ngFor="let flag of (flags$ | async) | keyvalue" class="flag-item">
<strong>{{ flag.key }}:</strong>
<span [class]="flag.value.isEnabled ? 'enabled' : 'disabled'">
{{ flag.value.isEnabled ? 'Enabled' : 'Disabled' }}
</span>
</div>
</div>

Component Usage​

// feature.component.ts
import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs";
import { FeatureFlagService, FeatureFlag } from "@flagpole/angular";

@Component({
selector: "app-feature",
templateUrl: "./feature.component.html",
styleUrls: ["./feature.component.css"],
})
export class FeatureComponent implements OnInit {
flags$: Observable<Record<string, FeatureFlag>>;
isLoading$: Observable<boolean>;
error$: Observable<Error | null>;

constructor(private featureFlagService: FeatureFlagService) {
this.flags$ = this.featureFlagService.flags$;
this.isLoading$ = this.featureFlagService.isLoading$;
this.error$ = this.featureFlagService.error$;
}

ngOnInit(): void {
// Check if a specific feature is enabled
if (this.featureFlagService.isFeatureEnabled("newDashboard")) {
console.log("New dashboard is enabled");
}

// Get complete flag details
const flag = this.featureFlagService.getFlag("betaFeature");
if (flag) {
console.log("Flag details:", flag);
}
}

onButtonClick(): void {
const isEnabled = this.featureFlagService.isFeatureEnabled("buttonFeature");
if (isEnabled) {
console.log("Button feature is enabled - executing action");
// Your feature logic here
}
}
}

3. Handle Loading & Error States​

Subscribe to the service observables and gate your feature content until flags are ready:

<!-- Loading state -->
<div *ngIf="isLoading$ | async" class="loading">
<mat-spinner></mat-spinner>
Loading feature flags...
</div>

<!-- Error state -->
<div *ngIf="error$ | async as error" class="error-banner">
<mat-icon>error</mat-icon>
Failed to load feature flags: {{ error.message }}
</div>

<!-- Content when loaded -->
<div *ngIf="!(isLoading$ | async) && !(error$ | async)">
<div *flagpoleFeature="'newFeature'">
<app-new-feature></app-new-feature>
</div>
</div>

API Reference​

FeatureFlagService​

Methods​

// Check if a feature flag is enabled
isFeatureEnabled(flagName: string): boolean

// Get complete flag details
getFlag(flagName: string): FeatureFlag | null

// Get all flags
getAllFlags(): Record<string, FeatureFlag>

Observables​

// All feature flags
flags$: Observable<Record<string, FeatureFlag>>;

// Loading state
isLoading$: Observable<boolean>;

// Error state
error$: Observable<Error | null>;

Structural Directive​

Use the *flagpoleFeature directive to conditionally show/hide content:

<!-- Basic usage -->
<div *flagpoleFeature="'featureName'">Feature content here</div>

<!-- With else template -->
<div *flagpoleFeature="'featureName'; else fallback">New feature content</div>
<ng-template #fallback> Old feature content </ng-template>

Pipe​

Use the featureFlag pipe for inline flag checks:

<!-- Show element if flag is enabled -->
<button *ngIf="'featureName' | featureFlag">Action</button>

<!-- Conditional classes -->
<div [class.premium]="'premiumFeature' | featureFlag">
Content with conditional styling
</div>

Route Guards​

Protect routes based on feature flags with FeatureFlagGuard:

// app-routing.module.ts
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { FeatureFlagGuard } from "@flagpole/angular";

const routes: Routes = [
{
path: "beta-feature",
component: BetaComponent,
canActivate: [FeatureFlagGuard],
data: {
featureFlag: "betaAccess",
redirectTo: "/home", // Optional: redirect if flag is disabled
},
},
{
path: "admin",
loadChildren: () =>
import("./admin/admin.module").then((m) => m.AdminModule),
canActivate: [FeatureFlagGuard],
data: {
featureFlag: "adminPanel",
redirectTo: "/unauthorized",
},
},
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}

Advanced Usage​

Reactive Patterns​

Combine feature flags with other observables using RxJS:

import { Component } from "@angular/core";
import { combineLatest, map } from "rxjs";

@Component({
selector: "app-dashboard",
template: `
<div *ngIf="showAdvancedDashboard$ | async; else basicDashboard">
<app-advanced-dashboard></app-advanced-dashboard>
</div>
<ng-template #basicDashboard>
<app-basic-dashboard></app-basic-dashboard>
</ng-template>
`,
})
export class DashboardComponent {
showAdvancedDashboard$ = combineLatest([
this.featureFlagService.flags$,
this.userService.currentUser$,
]).pipe(
map(
([flags, user]) =>
flags["advancedDashboard"]?.isEnabled && user?.isPremium
)
);

constructor(
private featureFlagService: FeatureFlagService,
private userService: UserService
) {}
}

A/B Testing Implementation​

Read the flag's conditions to branch between experiment variants:

import { Component } from "@angular/core";
import { map } from "rxjs";

@Component({
selector: "app-checkout",
template: `<ng-container [ngSwitch]="variant$ | async">
<app-checkout-v2 *ngSwitchCase="'variantA'"></app-checkout-v2>
<app-checkout-v3 *ngSwitchCase="'variantB'"></app-checkout-v3>
<app-checkout-v1 *ngSwitchDefault></app-checkout-v1>
</ng-container>`,
})
export class CheckoutComponent {
variant$ = this.featureFlagService.flags$.pipe(
map((flags) => flags["checkoutExperiment"]?.conditions?.variant ?? "control")
);

constructor(private featureFlagService: FeatureFlagService) {}
}

Real-time Updates​

The SDK keeps a WebSocket connection open and pushes flag changes into flags$ automatically — any component using the async pipe or the *flagpoleFeature directive re-renders without a page reload.

Configuration​

FeatureFlagConfig​

interface FeatureFlagConfig {
apiKey: string;
environments?: Environment[]; // 'development' | 'staging' | 'production'
}

If environments is omitted, flags from all environments are loaded.

Environment Variables​

Keep API keys out of source control by reading them from the Angular environment files:

// environment.ts
export const environment = {
production: false,
flagpoleApiKey: "fp_dev_your_dev_api_key",
};

// app.module.ts
import { environment } from "../environments/environment";

@NgModule({
imports: [
FeatureFlagModule.forRoot({
apiKey: environment.flagpoleApiKey,
environments: ["development"],
}),
],
})
export class AppModule {}

Error Handling​

Loading and Error States​

Always render loading and error states from the service observables:

<!-- Loading state -->
<div *ngIf="isLoading$ | async" class="loading">
<mat-spinner></mat-spinner>
Loading feature flags...
</div>

<!-- Error state -->
<div *ngIf="error$ | async as error" class="error-banner">
<mat-icon>error</mat-icon>
Failed to load feature flags: {{ error.message }}
</div>

<!-- Content when loaded -->
<div *ngIf="!(isLoading$ | async) && !(error$ | async)">
<!-- Your feature content here -->
</div>

Flag Not Found​

isFeatureEnabled returns false for flags that don't exist, and getFlag returns null — no extra error handling is required for missing flags. The SDK also fails safe (all flags false) when the connection drops or the API key is invalid.

Best Practices​

1. Secure Your API Key​

  • Store API keys in environment variables, never in source control
  • Use different keys for development, staging, and production
  • Rotate keys if they're ever exposed

2. Always Handle Loading and Error States​

Use the async pipe with isLoading$ and error$ so components react to connection changes instead of assuming flags are ready.

3. Use Descriptive Flag Names​

Use consistent, specific camelCase names:

  • ✅ newDashboard, betaUserProfile, experimentalSearch
  • ❌ flag1, test

4. Optimize Change Detection​

  • Use the OnPush change detection strategy where possible
  • Prefer the async pipe for automatic subscription management
  • Unsubscribe from manual subscriptions to prevent memory leaks
@Component({
selector: "app-feature",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div *flagpoleFeature="'newFeature'">
<!-- Content automatically updates when flag changes -->
</div>
`,
})
export class FeatureComponent implements OnDestroy {
private destroy$ = new Subject<void>();

constructor(private featureFlagService: FeatureFlagService) {
this.featureFlagService.flags$
.pipe(takeUntil(this.destroy$))
.subscribe((flags) => {
// Handle flags update
});
}

ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

5. Test Both Flag States​

Test enabled and disabled paths by mocking FeatureFlagService:

// feature.component.spec.ts
describe("FeatureComponent", () => {
let component: FeatureComponent;
let featureFlagService: jasmine.SpyObj<FeatureFlagService>;

beforeEach(() => {
const spy = jasmine.createSpyObj("FeatureFlagService", [
"isFeatureEnabled",
]);

TestBed.configureTestingModule({
declarations: [FeatureComponent],
providers: [{ provide: FeatureFlagService, useValue: spy }],
});

featureFlagService = TestBed.inject(
FeatureFlagService
) as jasmine.SpyObj<FeatureFlagService>;
});

it("should show new feature when flag is enabled", () => {
featureFlagService.isFeatureEnabled.and.returnValue(true);
// Test enabled state
});

it("should hide new feature when flag is disabled", () => {
featureFlagService.isFeatureEnabled.and.returnValue(false);
// Test disabled state
});
});

Troubleshooting​

Common Issues​

1. *flagpoleFeature or featureFlag pipe is not recognized​

Solution: Ensure FeatureFlagModule.forRoot({ ... }) is imported in your root module, and FeatureFlagModule is imported in any feature module that uses the directive or pipe.

2. Flags Always Return False​

Possible causes:

  • Invalid API key
  • Wrong environment configuration
  • Network connectivity issues

Solution: Log the service state to confirm what the SDK received:

this.featureFlagService.flags$.subscribe((flags) => console.log("Flags:", flags));
this.featureFlagService.error$.subscribe((error) => console.log("Error:", error));

3. WebSocket Connection Issues​

Ensure the WebSocket endpoints are reachable from your environment:

Development: ws://localhost:5000
Production: wss://useflagpole-api.onrender.com

4. Memory Leaks / Stale Subscriptions​

Use the async pipe, or takeUntil(this.destroy$) with ngOnDestroy, for every manual subscription to flags$, isLoading$, or error$.

Contributing​

We welcome contributions! To set up the SDK locally:

# Clone the repository
git clone https://github.com/flagpole-corp/flagpole-client-sdk-angular.git
cd flagpole-client-sdk-angular

# Install dependencies
npm install

# Build the SDK
npm run build

Submitting Changes​

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License​

MIT

Support​