Skip to content

feat: add custom context service and initializer with URL parameter support#21152

Open
npapp-dev002 wants to merge 7 commits intodevelopfrom
feat/CSXPA-12011-custom-context-service
Open

feat: add custom context service and initializer with URL parameter support#21152
npapp-dev002 wants to merge 7 commits intodevelopfrom
feat/CSXPA-12011-custom-context-service

Conversation

@npapp-dev002
Copy link
Contributor

@npapp-dev002 npapp-dev002 commented Feb 12, 2026

Claude Sonnet generated summary

[DEMO] Custom Site Context Implementation - Example for SiteContextRoutesHandler.initOnce() Implementations

Summary

This PR demonstrates how custom site contexts work with the new SiteContextRoutesHandler.initOnce() pattern. This is a reference implementation and should NOT be merged.


Background

This PR was created as a response to the following improvement proposal:

"As an improvement, we can call the method SiteContextRoutesHandler.init() from LanguageInitializer (and all other specific context initializers), so we no longer depend on the order of APP_INITIALIZERs. But as prerequisite, we need to make the method SiteContextRoutesHandler.init(contextId: string) parametrizable with a specific site context ID."


✅ Key Finding: Parametrization is NOT Required

After analysis, we determined that initOnce() does NOT need a contextId parameter because:

Reason Description
Global Handling SiteContextRoutesHandler.initOnce() handles ALL URL parameters globally by reading from context.urlParameters config
Idempotent The method is safe to call from multiple initializers - it only executes once
Automatic Custom Context Support Custom contexts are automatically handled if registered in ContextServiceMap and added to context.urlParameters

What This PR Demonstrates

1. Custom Context Service (CustomContextService)

A derived context that wraps LanguageService with uppercase transformation:

@Injectable({ providedIn: 'root' })
export class CustomContextService implements SiteContext<string> {
  constructor(protected langService: LanguageService) {}

  getActive(): Observable<string> {
    return this.langService.getActive().pipe(map(lang => lang.toUpperCase()));
  }

  setActive(value: string): void {
    this.langService.setActive(value.toLowerCase());
  }
}

2. Why Custom Contexts Don't Always Need an Initializer

Two types of custom contexts:

Type Example Needs Initializer?
Derived Context CustomContextService (wraps LanguageService) ❌ NO
Independent Context Context with its own NgRx state ✅ YES

For derived contexts like CustomContextService:

  • SiteContextRoutesHandler.initOnce() already handles URL synchronization
  • ✅ The underlying context (LanguageService) is already initialized
  • getActive() automatically reflects the correct value

3. Reference Implementation (CustomContextInitializer)

Included as documentation for customers who need initializers for independent custom contexts.


Files Changed

File Change
custom-context-service.ts Custom context wrapping LanguageService
context-service-map.ts Added CustomContextService registration
context-ids.ts Added CUSTOM constant
app.module.ts Added custom context config with urlParameters
site-context-params.service.spec.ts Fixed test to use non-existent param
context-initializer-providers.ts Added documentation about custom contexts

How Custom Contexts Work (No Parametrization Needed)

┌─────────────────────────────────────────────────────────────────┐
│                    Application Bootstrap                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  LanguageInitializer ──┐                                        │
│                        │                                        │
│  CurrencyInitializer ──┼──► initOnce() ──► Handles ALL params:  │
│                        │         │         - language           │
│  BaseSiteInitializer ──┘         │         - currency           │
│                                  │         - baseSite           │
│                                  │         - custom  ◄── Auto!  │
│                                  │                               │
│                                  ▼                               │
│                         ┌─────────────────┐                     │
│                         │ isInitialized?  │                     │
│                         └────────┬────────┘                     │
│                                  │                               │
│                    ┌─────────────┴─────────────┐                │
│                    │                           │                │
│                    ▼                           ▼                │
│              First call:              Subsequent calls:         │
│              Execute init             Return immediately        │
│                                       (idempotent)              │
└─────────────────────────────────────────────────────────────────┘

Implementation Details

📁 SiteContextRoutesHandler.initOnce()
initOnce(): Observable<unknown> {
  if (!this.isInitialized) {
    this.isInitialized = true;
    this.initInternal();
  }
  return this.initialized$.asObservable();
}

private initInternal(): void {
  const routingParams = this.siteContextParams.getUrlEncodingParameters();
  // Handles ALL URL parameters including custom ones
  if (routingParams.length) {
    this.setContextParamsFromRoute(this.location.path(true));
    this.subscribeChanges(routingParams);
    this.subscribeRouting();
  }
  this.initialized$.next(undefined);
  this.initialized$.complete();
}
📁 LanguageInitializer (and other initializers)
initialize(): Observable<unknown> {
  const init$ = this.configInit
    .getStable('context')
    .pipe(
      switchMap(() => this.siteContextRoutesHandler.initOnce()),
      switchMap(() => this.languageStatePersistenceService.initSync()),
      switchMap(() => this.setFallbackValue())
    );

  this.subscription = init$.subscribe();
  return init$;
}
📁 Configuration Example
// app.module.ts
provideConfig({
  context: {
    custom: ['EN', 'DE', 'JA', 'ZH'],
    urlParameters: ['baseSite', 'language', 'currency', 'custom'],
  },
}),

// context-service-map.ts
export function serviceMapFactory() {
  return {
    [LANGUAGE_CONTEXT_ID]: LanguageService,
    [CURRENCY_CONTEXT_ID]: CurrencyService,
    [BASE_SITE_CONTEXT_ID]: BaseSiteService,
    [CUSTOM]: CustomContextService,  // Custom context registered here
  };
}

Conclusion

The current initOnce() implementation without parameters is the correct design:

  • ✅ Simpler implementation
  • ✅ No breaking changes for customers
  • ✅ Custom contexts work automatically
  • ✅ Backward compatible

Note: Customers only need to create a custom initializer when they have an independent context with its own state that requires special fallback logic or persistence.


⚠️ Reminder

This PR is for demonstration purposes only and should NOT be merged.


Related Links

@npapp-dev002 npapp-dev002 requested a review from a team as a code owner February 12, 2026 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant