Skip to content

feat(web-sdk_angular): Web SDK + Angular reference implementation [NT-3465]#316

Open
David Nalchevanidze (nalchevanidze) wants to merge 204 commits into
mainfrom
NT-3235
Open

feat(web-sdk_angular): Web SDK + Angular reference implementation [NT-3465]#316
David Nalchevanidze (nalchevanidze) wants to merge 204 commits into
mainfrom
NT-3235

Conversation

@nalchevanidze

@nalchevanidze David Nalchevanidze (nalchevanidze) commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements a complete client-side SPA reference implementation of the Angular adapter SDK integration path using public APIs only, against shared mocks and fixtures. All behavior runs entirely on the client — no SSR, no server-side rendering, no hydration.

Constraint (REFREQ-20): No local shims, casts, or adapter logic mask a missing SDK capability. All SDK interaction goes through public package APIs.

What this implements

Core setup

  • SDK initialisation — initialises once per page load via a module-level singleton; falls back to baseline on failure; service cleans up subscriptions and calls sdk.destroy() on teardown
  • Multi-route navigation — Home (/) and Page Two (/page-two); page event fires on every SPA route change; SDK not re-initialised across navigations; event history preserved
  • Consent — toggle grants/withdraws tracking; page events always fire regardless of consent state
  • Identify and resetidentify() and reset() transition profile state; both persist across reloads via SDK storage
  • Preview panel — lazy-loaded on first open; forces live re-resolution while open; restores per-entry lock settings on close; supports audience and variant overrides
  • Locale consistency — Contentful client wrapped with sdk.withOptimizationLocale() before every fetch
  • Feature flags — subscribes to a 'boolean' flag via the SDK flag state API; auto-emits a flag-view event on access; returns undefined for anonymous sessions, resolves after identify()

Tracking

  • Analytics event log — renders the SDK eventStream in real time; view/hover rows update in place; events blocked by consent are absent; history persists across routes
  • Page tracking — fires on first load and every route change, regardless of consent
  • Attribute-based (auto)data-ctfl-* DOM attributes observed by the SDK; view, click, hover events fire after consent; stop on withdrawal. Click scenarios: direct, descendant, and ancestor all emit component_click
  • Code-based (manual) — entries registered via enableElement; emits view events only; no click or hover events

Live updates

  • Global toggle — OFF (default): entry resolves once then freezes; ON: re-resolves on every profile change
  • Per-entry overrides — Always live, Always locked, and Default modes implemented

Content

  • Entry resolution — SDK resolves the correct variant; always falls back to baseline on invalid data; no variant selection logic in app code
  • Nested entries — each level resolves independently; arbitrary nesting depth handled by the SDK without app-level recursion
  • Merge tags — profile-resolved values rendered inline; [Merge Tag] fallback when no profile is active
  • Rich text — renders as formatted HTML; merge tag nodes resolved inline with the same fallback behaviour

Notable decisions

  • Zoneless change detectionzone.js removed; uses provideZonelessChangeDetection with signals and toSignal() throughout
  • Standalone components — no NgModules; inject() for DI, input()/output() for component I/O, @if/@for control flow syntax
  • No file-role suffixeshome.ts not home.component.ts; Optimization not OptimizationService
  • Module-level singletoninstance and attachmentStarted prevent double-init; ngOnDestroy resets both for clean teardown
  • CSR only — no provideClientHydration, no SSR

🤖 Generated with Claude Code

Adds implementations/angular-web-sdk — an Angular 22 CSR skeleton that
serves a Hello World page and establishes the project structure for future
@contentful/optimization-web integration.

## What was added

- angular.json — Angular CLI build config (@angular/build:application),
  dev server on port 3000 (matching other implementations), production +
  development configurations, analytics disabled, packageManager set to pnpm
- package.json — Angular 22 deps, standard implementation scripts (dev,
  build, typecheck, clean, serve:mocks, launch), no zone.js (zoneless)
- tsconfig.json — single config (no tsconfig.app.json split; no tests yet),
  strict mode, ES2022 target, moduleResolution: bundler
- pnpm-workspace.yaml — sharedWorkspaceLockfile: false plus SDK tarball
  overrides so the implementation resolves local pkgs/ tarballs
- src/main.ts — bootstrapApplication(App, appConfig)
- src/app/app.ts — minimal standalone root component (Hello World)
- src/app/app.config.ts — provideBrowserGlobalErrorListeners,
  provideZonelessChangeDetection, provideRouter; no zone.js, no
  provideClientHydration (CSR only), no provideHttpClient (too early)
- src/app/app.routes.ts — empty Routes array
- src/index.html — HTML shell with <app-root>
- src/styles.css — minimal global reset
- scripts/launch-reference-app.sh — one-shot launcher: builds SDK pkgs,
  installs deps, starts mock server in background, starts Angular dev server
  in foreground, cleans up on exit
- AGENTS.md — local rules and commands
- README.md — standard repo header, quick start, manual setup, project
  structure, related links

## Key decisions

- Zoneless change detection (provideZonelessChangeDetection) — zone.js
  removed from deps and angular.json polyfills entirely
- Single tsconfig.json instead of the Angular CLI default split
  (tsconfig.json + tsconfig.app.json) — the split only pays off when a
  tsconfig.spec.json for tests is also present
- standalone: true omitted from App — redundant in Angular 19+, all
  components are standalone by default
- No SDK integration yet — added only when the public Angular surface is ready
- Root package.json gets implementation:angular-web-sdk shortcut matching
  the pattern of other implementations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Not present in other implementation READMEs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add CONFIG InjectionToken with hardcoded mock defaults (grows per feature)
- Wire app routes: / → HomeComponent, /page-two → PageTwoComponent
- Extract app root template to app.html, add nav links and router-outlet
- Add stub HomeComponent and PageTwoComponent
- Update REQUIREMENTS.md with feature-oriented progress table
- Remove .env.example (no longer needed with hardcoded defaults)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sses

- Rename home.component.ts → home.ts, class HomeComponent → Home
- Rename page-two.component.ts → page-two.ts, class PageTwoComponent → PageTwo
- Update app.routes.ts imports accordingly
- Add naming and modern Angular patterns rules to AGENTS.md
- Add implementation:angular-web-sdk shortcut to root package.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add design system to styles.css: nav, typography, card, entry grid, utility panel
- Add RouterLinkActive to nav with active class and exact match on home link

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add @contentful/optimization-web dependency (resolved via pnpm-workspace override)
- Add SDK config fields to CONFIG token (clientId, sdkEnvironment, urls, logLevel)
- Create Optimization service with module-level singleton and graceful error handling
- Wire page tracking via Router NavigationEnd — fires on every route change incl. initial load
- Inject Optimization in App root to force instantiation on startup
- Mark features 1 and 2 as done in REQUIREMENTS.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…(feature 18)

- Add contentful and @contentful/rich-text-types dependencies
- Add Contentful fields to CONFIG token (spaceId, token, environment, host, basePath)
- Create ContentfulClient service wrapping CDA with sdk.withOptimizationLocale()
- Add types/contentful.ts — typed entry skeleton and RichTextDocument
- Add utils/type-guards.ts — isRecord and isEntry helpers
- Mark feature 18 as done in REQUIREMENTS.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…scenarios (features 3-6)

- Add OptimizationResolver service wrapping sdk.resolveOptimizedEntry with baseline fallback
- Add ContentEntry component with auto-tracking (data-ctfl-* attributes) and manual tracking
  (enableElement/clearElement via effect + OnDestroy) and all three click scenarios
  (direct/descendant/ancestor)
- Wire Home page to fetch all entries on init and pass selectedOptimizations down so entries
  re-resolve on profile changes
- Bridge SDK plain-function subscribe protocol to RxJS Observable for toSignal compatibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ual differentiation

- Add variant/baseline badge and green left-border accent to entry cards
- Restructure home page with page header, stat panel, and section headers
- Tune spacing: tighter entry-grid gap, larger section margins, bottom padding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ide (features 9-10)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eature 17)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…(feature 15)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ew event (feature 19)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n REQUIREMENTS.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ut (features 13-14)

- Add RichTextRenderer component with renderer-map approach (no switch/enum comparison)
- Add MergeTagPipe and isMergeTagEntry type guard for inline merge tag resolution
- Wire rich text + merge tag detection into ContentEntry with hasMergeTag computed
- Add rich text, merge tag, manual, and click scenario badges to entry cards
- Reorganise card header: IDs (base/var/exp) on left, badges on right, both top-aligned

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add NestedContentEntry and NestedContentItem components with forwardRef self-recursion
- Each level resolves its own SDK variant independently via OptimizationResolver
- Home page routes nestedContent entries to NestedContentEntry instead of ContentEntry
- Add nested badge style and indented border for child levels
- Reduce badge font size for a more compact card header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…features 11+16)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…MENTS.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@fmamud

Copy link
Copy Markdown
Contributor

lint is failing btw

…anges

Remove contentfulLocales and withOptimizationLocale() following the SDK's
locale ownership shift (#317). App now passes locale directly to CDA requests.
Rename NgContentfulLiveUpdates to NgLiveUpdates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Client.loadEntries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…GENTS.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…iles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ass names to components

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…omponents

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…esolved tracking

Remove generateMeta helper, inline meta object directly in resolved computed.
Drop isRecord/typeof guards for selectedOptimization in favour of optional chaining.
Track mergeTagResolved accurately via the resolver callback rather than hasMergeTag
check; true if any merge tag resolved, false if callback fired but none resolved,
undefined if no merge tag nodes present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…m @contentful/rich-text-types

Replace unknown-typed resolve function with typed RichTextNode union (Block | Inline | Text),
removing redundant isRecord guards. Use satisfies for structural compatibility without unsafe casts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove resolveRichTextMergeTags wrapper by inlining into resolveEntryMergeTags.
Drop redundant isEntry guard in _variant since _entry is already typed as Entry | undefined.
Keep isRecord(node.data) only where required by the unknown-typed recursive resolver.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… service

Only used in entry.ts, so no reason to export it from utils.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EntryMeta was only ever accessed via .meta, never used standalone.
Flatten the fields directly onto ResolvedEntryView and update all callsites.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tContentfulEntry function

Drop @Injectable class, providers array, and post-construction .with() setup in favour of
a plain inject-pattern function that takes signals upfront and returns the resolved signal directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eUpdates to isLive

Order members per Angular style guide across EntryCard, ControlPanel, and NgLiveUpdates.
Remove redundant inline section comments from ControlPanel.
Rename injectContentfulEntry param liveUpdates -> isLive since it receives the resolved
boolean signal, not the raw input.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…redundant guards

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…edEntryView to ResolvedEntry and align field names

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…g boolean signal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lTracking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ffect

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ng boolean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tly in template

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s CSS modifier directly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… and experienceId→optimizationId

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

2 participants