From 7eca4b757d200d016b3c7c439bb898144eedc271 Mon Sep 17 00:00:00 2001 From: vaishnavikedar1511 Date: Wed, 16 Sep 2026 12:25:45 +0530 Subject: [PATCH 1/8] feat: opt-in editable paragraph component for Visual Builder field editing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seeded starter-pack content (cms_paragraph_component, …) renders through stock Spartacus components, so the connector could only stamp a coarse entry-level data-cslp tag on the wrapper — which Contentstack Visual Builder reports as an "Invalid CSLP tag" because there is no field-level tag to inline-edit. This adds an OPT-IN path to fix that for paragraphs: - ContentstackCmsComponentNormalizer now preserves the Live Preview field-tag map (entry.$) on the converted component data (only present on preview builds; omitted otherwise, so stock components and production delivery are unchanged). - New ContentstackEditableParagraphComponent renders identically to the stock paragraph (cx-paragraph class + innerHTML) but binds [csEditable] to data.$.content, emitting the 4-part field-level tag `{content_type}.{entry}.{locale}.content`. - New ContentstackEditableComponentsModule (opt-in) registers it for CMSParagraphComponent. Not imported by ContentstackCmsFeatureModule, so default behavior is unchanged; apps opt in by importing it. Verified live against my-storefront in a real browser: the paragraph now emits `cms_paragraph_component..en-us.content` (was 3-part entry-level only). Banner and product-carousel are intentionally left to follow-ups (the carousel must preserve its live SAP product hydration). 169 unit + 8 schematics tests pass; lint, format, typecheck, build green. Co-Authored-By: Claude Opus 4.8 --- ...contentstack-editable-components.module.ts | 38 ++++++++++++ ...ntentstack-editable-paragraph.component.ts | 58 +++++++++++++++++++ ...tentstack-cms-component.normalizer.spec.ts | 21 +++++++ .../contentstack-cms-component.normalizer.ts | 6 ++ src/public-api.ts | 4 ++ 5 files changed, 127 insertions(+) create mode 100644 src/cms/components/contentstack-editable-components.module.ts create mode 100644 src/cms/components/contentstack-editable-paragraph.component.ts diff --git a/src/cms/components/contentstack-editable-components.module.ts b/src/cms/components/contentstack-editable-components.module.ts new file mode 100644 index 0000000..0538461 --- /dev/null +++ b/src/cms/components/contentstack-editable-components.module.ts @@ -0,0 +1,38 @@ +import { NgModule } from '@angular/core'; +import { CmsConfig, provideConfig } from '@spartacus/core'; +import { ContentstackEditableParagraphComponent } from './contentstack-editable-paragraph.component'; + +/** + * **Opt-in** module that swaps Spartacus's stock renderers for connector-owned + * ones that emit **field-level** `data-cslp` tags, so seeded Contentstack + * component types become inline-editable in the Visual Builder (instead of only + * carrying the coarse entry-level tag that Visual Builder flags as an invalid / + * incorrectly-generated CSLP tag). + * + * Import it in your app **after** `ContentstackCmsFeatureModule` to opt in: + * + * ```ts + * imports: [ContentstackCmsFeatureModule, ContentstackEditableComponentsModule] + * ``` + * + * It is deliberately NOT pulled in by `ContentstackCmsFeatureModule`, so the + * default behavior (stock Spartacus rendering) is unchanged for apps that don't + * opt in. The edit tags are inert outside preview builds, so opting in is safe + * for production too. + * + * Currently covers `CMSParagraphComponent`; banner and product-carousel are + * intentionally left to follow-up work (the carousel additionally hydrates live + * SAP product data, which its editable renderer must preserve). + */ +@NgModule({ + providers: [ + provideConfig({ + cmsComponents: { + CMSParagraphComponent: { + component: ContentstackEditableParagraphComponent, + }, + }, + } as CmsConfig), + ], +}) +export class ContentstackEditableComponentsModule {} diff --git a/src/cms/components/contentstack-editable-paragraph.component.ts b/src/cms/components/contentstack-editable-paragraph.component.ts new file mode 100644 index 0000000..89cb241 --- /dev/null +++ b/src/cms/components/contentstack-editable-paragraph.component.ts @@ -0,0 +1,58 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { Observable } from 'rxjs'; +import { CmsComponent } from '@spartacus/core'; +import { CmsComponentData } from '@spartacus/storefront'; +import { CsEditableDirective } from '../../live-preview/cs-editable.directive'; + +/** + * The `data` shape this component reads out of `CmsComponentData` for a + * `CMSParagraphComponent`. `content` is the rendered rich text (same field the + * stock Spartacus `ParagraphComponent` reads); `$` is the Live Preview field-tag + * map preserved by {@link ContentstackCmsComponentNormalizer} (present only on + * preview builds). + */ +export interface ContentstackEditableParagraphData extends CmsComponent { + content?: string; + $?: Record; +} + +/** + * A drop-in, connector-provided replacement for Spartacus's stock paragraph + * renderer that additionally emits a **field-level** `data-cslp` tag on the + * content, so Contentstack's Visual Builder can inline-edit seeded + * `cms_paragraph_component` content. + * + * Why this exists: the stock `ParagraphComponent` is owned by Spartacus, so the + * connector can only stamp a coarse **entry-level** tag on its wrapper (via the + * ComponentDecorator). Visual Builder needs a field-level tag + * (`{content_type}.{entry}.{locale}.content`) to edit a field, and reports an + * "Invalid CSLP tag" for the entry-level-only tag. Rendering the paragraph here + * — with `[csEditable]` bound to `data.$.content` — produces that field tag. + * + * Registered (opt-in) by {@link ContentstackEditableComponentsModule}. Renders + * identically to the stock paragraph (same `cx-paragraph` host class + innerHTML + * content); the only addition is the edit tag, which is inert outside preview + * builds (no `$` ⇒ CsEditableDirective removes the attribute). + */ +@Component({ + selector: 'cs-editable-paragraph', + standalone: true, + imports: [CommonModule, CsEditableDirective], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+ `, +}) +export class ContentstackEditableParagraphComponent { + protected readonly componentData: CmsComponentData = + inject(CmsComponentData); + + /** Paragraph content stream from Contentstack (via the CMS store). */ + readonly data$: Observable = this.componentData.data$; +} diff --git a/src/cms/converters/contentstack-cms-component.normalizer.spec.ts b/src/cms/converters/contentstack-cms-component.normalizer.spec.ts index 74f2827..c8cc2f3 100644 --- a/src/cms/converters/contentstack-cms-component.normalizer.spec.ts +++ b/src/cms/converters/contentstack-cms-component.normalizer.spec.ts @@ -52,6 +52,27 @@ describe('ContentstackCmsComponentNormalizer', () => { expect(component.modifiedTime).toBeUndefined(); }); + it('preserves the Live Preview field-tag map ($) so editable components can bind data-cslp', () => { + const component = normalizer.convert({ + uid: 'blt1', + _content_type_uid: 'cms_paragraph_component', + content: '

Hi

', + $: { content: { 'data-cslp': 'cms_paragraph_component.blt1.en-us.content' } }, + } as any); + expect((component as any).$?.content?.['data-cslp']).toBe( + 'cms_paragraph_component.blt1.en-us.content', + ); + }); + + it('omits $ entirely when the entry was not tagged (non-preview builds unchanged)', () => { + const component = normalizer.convert({ + uid: 'blt1', + _content_type_uid: 'cms_paragraph_component', + content: '

Hi

', + }); + expect('$' in (component as any)).toBe(false); + }); + it('merges onto a provided target rather than replacing it', () => { const target = { container: true } as any; const component = normalizer.convert( diff --git a/src/cms/converters/contentstack-cms-component.normalizer.ts b/src/cms/converters/contentstack-cms-component.normalizer.ts index c5d63e7..3472722 100644 --- a/src/cms/converters/contentstack-cms-component.normalizer.ts +++ b/src/cms/converters/contentstack-cms-component.normalizer.ts @@ -70,6 +70,12 @@ export class ContentstackCmsComponentNormalizer implements Converter< // Contentstack field uids must be lowercase, so this mapping is required // — a raw passthrough leaves e.g. links without a visible label. ...this.fieldMapper.map(typeCode, fields), + // Preserve the Live Preview field-tag map (`entry.$`, added by + // tagEntryTree when livePreview is on) so connector-provided editable + // components can bind a per-field `data-cslp` via CsEditableDirective. + // Only present on preview builds; absent (and omitted) otherwise, so stock + // components and production delivery are byte-for-byte unaffected. + ...(source['$'] ? { $: source['$'] } : {}), } as CmsComponent; if (BANNER_TYPE_CODES.has(typeCode)) { diff --git a/src/public-api.ts b/src/public-api.ts index 10fb196..993926b 100644 --- a/src/public-api.ts +++ b/src/public-api.ts @@ -35,6 +35,10 @@ export * from './cms/model/slot-maps'; export * from './cms/access/contentstack-current-user'; export * from './cms/access/contentstack-restrictions.service'; +// Opt-in editable renderers (field-level Live Preview / Visual Builder tags) +export * from './cms/components/contentstack-editable-paragraph.component'; +export * from './cms/components/contentstack-editable-components.module'; + // SmartEdit bypass export * from './guards/contentstack-smartedit-bypass'; From e7d8548aca24ce921ecb29f85a070105759ca89e Mon Sep 17 00:00:00 2001 From: vaishnavikedar1511 Date: Wed, 16 Sep 2026 14:15:43 +0530 Subject: [PATCH 2/8] feat: enable editable components by default via the feature module Wire ContentstackEditableComponentsModule into ContentstackCmsFeatureModule so consuming apps get field-level Visual Builder editing for seeded component types with no opt-in import. Safe by default: the editable renderers render identically to the stock Spartacus components and the edit tags are inert outside preview builds, so normal delivery/production rendering is unchanged. Verified live against my-storefront with NO app-side import: the seeded paragraphs still emit `cms_paragraph_component..en-us.content`. Co-Authored-By: Claude Opus 4.8 --- ...contentstack-editable-components.module.ts | 25 ++++++++----------- ...ntentstack-editable-paragraph.component.ts | 3 ++- src/contentstack-cms-feature.module.ts | 21 +++++++++++++--- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/cms/components/contentstack-editable-components.module.ts b/src/cms/components/contentstack-editable-components.module.ts index 0538461..f79139a 100644 --- a/src/cms/components/contentstack-editable-components.module.ts +++ b/src/cms/components/contentstack-editable-components.module.ts @@ -3,22 +3,17 @@ import { CmsConfig, provideConfig } from '@spartacus/core'; import { ContentstackEditableParagraphComponent } from './contentstack-editable-paragraph.component'; /** - * **Opt-in** module that swaps Spartacus's stock renderers for connector-owned - * ones that emit **field-level** `data-cslp` tags, so seeded Contentstack - * component types become inline-editable in the Visual Builder (instead of only - * carrying the coarse entry-level tag that Visual Builder flags as an invalid / - * incorrectly-generated CSLP tag). + * Swaps Spartacus's stock renderers for connector-owned ones that emit + * **field-level** `data-cslp` tags, so seeded Contentstack component types become + * inline-editable in the Visual Builder (instead of only carrying the coarse + * entry-level tag that Visual Builder flags as an invalid / incorrectly-generated + * CSLP tag). * - * Import it in your app **after** `ContentstackCmsFeatureModule` to opt in: - * - * ```ts - * imports: [ContentstackCmsFeatureModule, ContentstackEditableComponentsModule] - * ``` - * - * It is deliberately NOT pulled in by `ContentstackCmsFeatureModule`, so the - * default behavior (stock Spartacus rendering) is unchanged for apps that don't - * opt in. The edit tags are inert outside preview builds, so opting in is safe - * for production too. + * Imported by {@link ContentstackCmsFeatureModule} by default, so consuming apps + * get this automatically with no extra wiring. It is safe on by default: the + * editable renderers render identically to the stock Spartacus components, and + * the edit tags are inert outside preview builds (no `entry.$` ⇒ the directive + * removes the attribute), so normal delivery/production rendering is unchanged. * * Currently covers `CMSParagraphComponent`; banner and product-carousel are * intentionally left to follow-up work (the carousel additionally hydrates live diff --git a/src/cms/components/contentstack-editable-paragraph.component.ts b/src/cms/components/contentstack-editable-paragraph.component.ts index 89cb241..4d287b3 100644 --- a/src/cms/components/contentstack-editable-paragraph.component.ts +++ b/src/cms/components/contentstack-editable-paragraph.component.ts @@ -30,7 +30,8 @@ export interface ContentstackEditableParagraphData extends CmsComponent { * "Invalid CSLP tag" for the entry-level-only tag. Rendering the paragraph here * — with `[csEditable]` bound to `data.$.content` — produces that field tag. * - * Registered (opt-in) by {@link ContentstackEditableComponentsModule}. Renders + * Registered by {@link ContentstackEditableComponentsModule} (on by default via + * {@link ContentstackCmsFeatureModule}). Renders * identically to the stock paragraph (same `cx-paragraph` host class + innerHTML * content); the only addition is the edit tag, which is inert outside preview * builds (no `$` ⇒ CsEditableDirective removes the attribute). diff --git a/src/contentstack-cms-feature.module.ts b/src/contentstack-cms-feature.module.ts index 3913378..085c1ee 100644 --- a/src/contentstack-cms-feature.module.ts +++ b/src/contentstack-cms-feature.module.ts @@ -4,6 +4,7 @@ import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ContentstackCmsModule } from './cms/contentstack-cms.module'; import { ContentstackLivePreviewModule } from './live-preview/contentstack-live-preview.module'; +import { ContentstackEditableComponentsModule } from './cms/components/contentstack-editable-components.module'; import { ContentstackConfig } from './config/contentstack-config'; import { defaultContentstackConfig } from './config/default-contentstack-config'; import { @@ -29,8 +30,14 @@ import { * it stays inert on normal delivery builds and activates on preview builds. * 3. Registers {@link defaultContentstackConfig} as default config, so the app * only needs to supply credentials via its own `provideConfig(...)`. + * 4. Imports {@link ContentstackEditableComponentsModule} — registers + * connector-owned editable renderers for seeded starter-pack component types + * (currently `CMSParagraphComponent`) that emit a **field-level** `data-cslp` + * tag so Contentstack Visual Builder can inline-edit them. They render + * identically to the stock Spartacus components and the edit tags are inert + * outside preview builds, so this is transparent on normal delivery builds. * - * (Both submodules were previously behind a lazy `CmsConfig.featureModules` + * (Both CMS/Live-Preview submodules were previously behind a lazy `CmsConfig.featureModules` * entry — the standard Spartacus code-splitting convention — but that gate only * fires when a `cmsComponents` component tagged with the feature renders, which * this connector never registers. So the entry never loaded and neither the CMS @@ -40,8 +47,10 @@ import { * - It does not import Spartacus's `SmartEditRootModule`. That omission is the * primary SmartEdit bypass (no handshake APP_INITIALIZER, no CmsTicket * interceptor) — see `guards/contentstack-smartedit-bypass.ts`. - * - It does not register any `cmsComponents` mappings. Mapping Contentstack - * block/content types to Angular components is app-specific; follow the + * - It registers `cmsComponents` mappings only for the connector-owned editable + * renderers of seeded starter-pack types (via + * {@link ContentstackEditableComponentsModule}). Mapping your OWN + * block/content types to Angular components stays app-specific; follow the * pattern in the `examples/hero-banner` module. * * IMPORTANT — import ordering: import `ContentstackCmsFeatureModule` *after* the @@ -50,7 +59,11 @@ import { * `SpartacusFeaturesModule` (or after `StorefrontModule`) satisfies this. */ @NgModule({ - imports: [ContentstackCmsModule, ContentstackLivePreviewModule], + imports: [ + ContentstackCmsModule, + ContentstackLivePreviewModule, + ContentstackEditableComponentsModule, + ], providers: [ provideDefaultConfig(defaultContentstackConfig), // Bind the typed config accessor to Spartacus's merged global Config, so From 288c3de589ed1705214343f368e2686a37f44f59 Mon Sep 17 00:00:00 2001 From: vaishnavikedar1511 Date: Wed, 16 Sep 2026 14:28:02 +0530 Subject: [PATCH 3/8] feat: editable banner renderer (field-level media tag for Visual Builder) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ContentstackEditableBannerComponent and registers it for the banner typeCodes (SimpleBannerComponent, SimpleResponsiveBannerComponent) in ContentstackEditableComponentsModule (on by default). It renders the same building blocks as the stock Spartacus banner — cx-media (elementType 'picture') inside cx-generic-link — passing getImage-equivalent data.media to cx-media, and binds [csEditable] to the media field so Contentstack Visual Builder can edit the banner image inline. As a global cmsComponents override it also renders OCC-sourced banners; it's a faithful drop-in (same cx-media/cx-generic-link), and the edit tag is inert on untagged (OCC/non-preview) banners. typings/spartacus.d.ts: declare MediaModule/GenericLinkModule/MediaComponent (real @spartacus/storefront exports) so the offline typecheck resolves the imports. Verified live against my-storefront: the Contentstack banner renders its Contentstack image (images.contentstack.io) with tag `simple_banner_component..en-us.media`; all OCC banners still render. lint, typecheck, test (169+8), build all green. Co-Authored-By: Claude Opus 4.8 --- .../contentstack-editable-banner.component.ts | 62 +++++++++++++++++++ ...contentstack-editable-components.module.ts | 14 ++++- src/public-api.ts | 3 +- typings/spartacus.d.ts | 8 +++ 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 src/cms/components/contentstack-editable-banner.component.ts diff --git a/src/cms/components/contentstack-editable-banner.component.ts b/src/cms/components/contentstack-editable-banner.component.ts new file mode 100644 index 0000000..f62d7ef --- /dev/null +++ b/src/cms/components/contentstack-editable-banner.component.ts @@ -0,0 +1,62 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { Observable } from 'rxjs'; +import { CmsBannerComponent } from '@spartacus/core'; +import { CmsComponentData, GenericLinkModule, MediaModule } from '@spartacus/storefront'; +import { CsEditableDirective } from '../../live-preview/cs-editable.directive'; + +/** + * The `data` shape read from `CmsComponentData` for a banner. `media` (the + * responsive breakpoint set) and `urlLink` are what the stock Spartacus + * `BannerComponent` reads; `$` is the Live Preview field-tag map preserved by + * {@link ContentstackCmsComponentNormalizer} (present only on preview builds). + */ +export interface ContentstackEditableBannerData extends CmsBannerComponent { + urlLink?: string; + $?: Record; +} + +/** + * Connector-provided replacement for Spartacus's stock banner renderer that adds + * a **field-level** `data-cslp` tag on the image, so Contentstack's Visual + * Builder can edit the banner's `media` (image) field inline — instead of the + * component only carrying the coarse entry-level tag it flags as invalid. + * + * Renders the same building blocks as the stock banner — Spartacus's `cx-media` + * (responsive image) inside `cx-generic-link` (SPA-aware link) — so the visual + * output and routing behavior are unchanged; the only addition is the edit tag, + * which is inert outside preview builds (no `$` ⇒ CsEditableDirective removes the + * attribute). Registered by {@link ContentstackEditableComponentsModule} for the + * banner typeCodes, on by default via {@link ContentstackCmsFeatureModule}. + */ +@Component({ + selector: 'cs-editable-banner', + standalone: true, + imports: [CommonModule, MediaModule, GenericLinkModule, CsEditableDirective], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + + + + + + `, +}) +export class ContentstackEditableBannerComponent { + protected readonly componentData: CmsComponentData = + inject(CmsComponentData); + + /** Banner data stream from Contentstack (via the CMS store). */ + readonly data$: Observable = this.componentData.data$; +} diff --git a/src/cms/components/contentstack-editable-components.module.ts b/src/cms/components/contentstack-editable-components.module.ts index f79139a..c53be0f 100644 --- a/src/cms/components/contentstack-editable-components.module.ts +++ b/src/cms/components/contentstack-editable-components.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core'; import { CmsConfig, provideConfig } from '@spartacus/core'; import { ContentstackEditableParagraphComponent } from './contentstack-editable-paragraph.component'; +import { ContentstackEditableBannerComponent } from './contentstack-editable-banner.component'; /** * Swaps Spartacus's stock renderers for connector-owned ones that emit @@ -15,9 +16,10 @@ import { ContentstackEditableParagraphComponent } from './contentstack-editable- * the edit tags are inert outside preview builds (no `entry.$` ⇒ the directive * removes the attribute), so normal delivery/production rendering is unchanged. * - * Currently covers `CMSParagraphComponent`; banner and product-carousel are - * intentionally left to follow-up work (the carousel additionally hydrates live - * SAP product data, which its editable renderer must preserve). + * Currently covers `CMSParagraphComponent` and the banner typeCodes + * (`SimpleBannerComponent`, `SimpleResponsiveBannerComponent`). The + * product-carousel is intentionally left to follow-up work (it additionally + * hydrates live SAP product data, which its editable renderer must preserve). */ @NgModule({ providers: [ @@ -26,6 +28,12 @@ import { ContentstackEditableParagraphComponent } from './contentstack-editable- CMSParagraphComponent: { component: ContentstackEditableParagraphComponent, }, + SimpleBannerComponent: { + component: ContentstackEditableBannerComponent, + }, + SimpleResponsiveBannerComponent: { + component: ContentstackEditableBannerComponent, + }, }, } as CmsConfig), ], diff --git a/src/public-api.ts b/src/public-api.ts index 993926b..c3a26f2 100644 --- a/src/public-api.ts +++ b/src/public-api.ts @@ -35,8 +35,9 @@ export * from './cms/model/slot-maps'; export * from './cms/access/contentstack-current-user'; export * from './cms/access/contentstack-restrictions.service'; -// Opt-in editable renderers (field-level Live Preview / Visual Builder tags) +// Editable renderers (field-level Live Preview / Visual Builder tags) export * from './cms/components/contentstack-editable-paragraph.component'; +export * from './cms/components/contentstack-editable-banner.component'; export * from './cms/components/contentstack-editable-components.module'; // SmartEdit bypass diff --git a/typings/spartacus.d.ts b/typings/spartacus.d.ts index ca7fc38..e68f9aa 100644 --- a/typings/spartacus.d.ts +++ b/typings/spartacus.d.ts @@ -272,6 +272,14 @@ declare module '@spartacus/storefront' { uid: string; data$: Observable; } + + // Rendering building blocks reused by the connector's editable banner + // renderer (real exports of @spartacus/storefront; declared here only so the + // offline typecheck resolves the imports — tsc does not check the templates + // that actually use `cx-media` / `cx-generic-link`). + export class MediaModule {} + export class GenericLinkModule {} + export class MediaComponent {} } declare module '@spartacus/cart/base/root' { From 403679fd41219b3746f9798be97a0aeeb479b60b Mon Sep 17 00:00:00 2001 From: vaishnavikedar1511 Date: Thu, 17 Sep 2026 12:00:54 +0530 Subject: [PATCH 4/8] feat: editable product-carousel renderer (title tag, SAP hydration preserved) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ContentstackEditableProductCarouselComponent and registers it for ProductCarouselComponent in ContentstackEditableComponentsModule. The stock carousel renders its title inside cx-carousel (not taggable), so this renders its own [csEditable]

for the title (4-part data-cslp on the `title` field) and passes an empty title to cx-carousel to avoid a duplicate heading. Products are unchanged: each SKU in productCodes is hydrated live from SAP via ProductService ([LIST, STOCK] scopes, same as stock) and rendered with the stock cx-product-carousel-item, so name/price/stock/image + add-to-cart still come from SAP OCC. typings/spartacus.d.ts: declare CarouselModule/ProductCarouselModule (+ CarouselComponent/ProductCarouselItemComponent) so the offline typecheck resolves the imports. Verified live against my-storefront: carousel title carries `product_carousel_component..en-us.title`, and all 5 products render with live SAP names + prices ($59.00, $183.00, …). lint, typecheck, test (169+8), build all green. Co-Authored-By: Claude Opus 4.8 --- ...contentstack-editable-components.module.ts | 12 ++- ...ack-editable-product-carousel.component.ts | 91 +++++++++++++++++++ src/public-api.ts | 1 + typings/spartacus.d.ts | 4 + 4 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 src/cms/components/contentstack-editable-product-carousel.component.ts diff --git a/src/cms/components/contentstack-editable-components.module.ts b/src/cms/components/contentstack-editable-components.module.ts index c53be0f..c0d23f0 100644 --- a/src/cms/components/contentstack-editable-components.module.ts +++ b/src/cms/components/contentstack-editable-components.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; import { CmsConfig, provideConfig } from '@spartacus/core'; import { ContentstackEditableParagraphComponent } from './contentstack-editable-paragraph.component'; import { ContentstackEditableBannerComponent } from './contentstack-editable-banner.component'; +import { ContentstackEditableProductCarouselComponent } from './contentstack-editable-product-carousel.component'; /** * Swaps Spartacus's stock renderers for connector-owned ones that emit @@ -16,10 +17,10 @@ import { ContentstackEditableBannerComponent } from './contentstack-editable-ban * the edit tags are inert outside preview builds (no `entry.$` ⇒ the directive * removes the attribute), so normal delivery/production rendering is unchanged. * - * Currently covers `CMSParagraphComponent` and the banner typeCodes - * (`SimpleBannerComponent`, `SimpleResponsiveBannerComponent`). The - * product-carousel is intentionally left to follow-up work (it additionally - * hydrates live SAP product data, which its editable renderer must preserve). + * Covers `CMSParagraphComponent`, the banner typeCodes (`SimpleBannerComponent`, + * `SimpleResponsiveBannerComponent`), and `ProductCarouselComponent` (whose + * editable renderer keeps the live SAP product hydration and only adds an + * editable tag on the carousel title). */ @NgModule({ providers: [ @@ -34,6 +35,9 @@ import { ContentstackEditableBannerComponent } from './contentstack-editable-ban SimpleResponsiveBannerComponent: { component: ContentstackEditableBannerComponent, }, + ProductCarouselComponent: { + component: ContentstackEditableProductCarouselComponent, + }, }, } as CmsConfig), ], diff --git a/src/cms/components/contentstack-editable-product-carousel.component.ts b/src/cms/components/contentstack-editable-product-carousel.component.ts new file mode 100644 index 0000000..010a82f --- /dev/null +++ b/src/cms/components/contentstack-editable-product-carousel.component.ts @@ -0,0 +1,91 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { + CmsProductCarouselComponent, + Product, + ProductScope, + ProductService, +} from '@spartacus/core'; +import { CarouselModule, CmsComponentData, ProductCarouselModule } from '@spartacus/storefront'; +import { CsEditableDirective } from '../../live-preview/cs-editable.directive'; + +/** + * The `data` shape read from `CmsComponentData` for a product carousel. `title` + * and `productCodes` (space-separated SKUs) are produced by the connector's + * field mapper + carousel normalizer; `$` is the Live Preview field-tag map + * preserved by {@link ContentstackCmsComponentNormalizer} (preview builds only). + */ +export interface ContentstackEditableProductCarouselData extends CmsProductCarouselComponent { + title?: string; + productCodes?: string; + $?: Record; +} + +/** + * Connector-provided replacement for Spartacus's stock product-carousel renderer + * that makes the carousel **title** inline-editable in Contentstack's Visual + * Builder, while **preserving the live SAP product hydration**. + * + * The stock `ProductCarouselComponent` renders its title *inside* `cx-carousel` + * (via the `[title]` input), so it can't be field-tagged there. This component + * instead renders its own tagged `

` heading (`[csEditable]` → the `title` + * field's 4-part `data-cslp`) and passes an empty title to `cx-carousel` to avoid + * a duplicate heading. + * + * The products themselves are unchanged: each SKU in `productCodes` is hydrated + * live from SAP via Spartacus's `ProductService` (same `LIST`+`STOCK` scopes as + * the stock component) and rendered with the stock `cx-product-carousel-item`, so + * price/stock/name/image and add-to-cart all keep coming from SAP OCC. + * + * Registered by {@link ContentstackEditableComponentsModule} (on by default via + * {@link ContentstackCmsFeatureModule}). The edit tag is inert outside preview + * builds (no `$` ⇒ CsEditableDirective removes the attribute). + */ +@Component({ + selector: 'cs-editable-product-carousel', + standalone: true, + imports: [CommonModule, CarouselModule, ProductCarouselModule, CsEditableDirective], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + + + + + + `, +}) +export class ContentstackEditableProductCarouselComponent { + protected readonly componentData: CmsComponentData = + inject(CmsComponentData); + protected readonly productService = inject(ProductService); + + readonly data$: Observable = this.componentData.data$; + + /** + * One live SAP product stream per SKU — preserves the connector's hydration: + * `productCodes` (space-separated) → `ProductService.get(code, [LIST, STOCK])`, + * exactly the scopes the stock ProductCarouselComponent uses. + */ + readonly items$: Observable[]> = this.data$.pipe( + map((data) => + (data.productCodes ?? '') + .trim() + .split(/\s+/) + .filter(Boolean) + .map((code) => this.productService.get(code, [ProductScope.LIST, ProductScope.STOCK])), + ), + ); +} diff --git a/src/public-api.ts b/src/public-api.ts index c3a26f2..2038790 100644 --- a/src/public-api.ts +++ b/src/public-api.ts @@ -38,6 +38,7 @@ export * from './cms/access/contentstack-restrictions.service'; // Editable renderers (field-level Live Preview / Visual Builder tags) export * from './cms/components/contentstack-editable-paragraph.component'; export * from './cms/components/contentstack-editable-banner.component'; +export * from './cms/components/contentstack-editable-product-carousel.component'; export * from './cms/components/contentstack-editable-components.module'; // SmartEdit bypass diff --git a/typings/spartacus.d.ts b/typings/spartacus.d.ts index e68f9aa..4dd05ae 100644 --- a/typings/spartacus.d.ts +++ b/typings/spartacus.d.ts @@ -280,6 +280,10 @@ declare module '@spartacus/storefront' { export class MediaModule {} export class GenericLinkModule {} export class MediaComponent {} + export class CarouselModule {} + export class CarouselComponent {} + export class ProductCarouselModule {} + export class ProductCarouselItemComponent {} } declare module '@spartacus/cart/base/root' { From 60d2932dacef633dabbac749dbe29a7f50f1af7f Mon Sep 17 00:00:00 2001 From: vaishnavikedar1511 Date: Thu, 17 Sep 2026 12:19:31 +0530 Subject: [PATCH 5/8] fix: put banner media edit tag on a standard element, not cx-media Visual Builder flagged the banner's field tag as an "invalid / incorrectly generated" CSLP tag because it was bound on (a custom element); the working paragraph/carousel tags sit on standard elements (div/h3). Wrap cx-media in a
so the media field tag lands on a standard element, matching the other editable renderers. Image still renders from Contentstack; SAP/OCC banners unaffected. Co-Authored-By: Claude Opus 4.8 --- .../contentstack-editable-banner.component.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/cms/components/contentstack-editable-banner.component.ts b/src/cms/components/contentstack-editable-banner.component.ts index f62d7ef..3a1a92b 100644 --- a/src/cms/components/contentstack-editable-banner.component.ts +++ b/src/cms/components/contentstack-editable-banner.component.ts @@ -37,18 +37,14 @@ export interface ContentstackEditableBannerData extends CmsBannerComponent { template: ` - +
+ +
- +
+ +
`, From c931ebe3165abec5788a7cc1cae5556db919c815 Mon Sep 17 00:00:00 2001 From: vaishnavikedar1511 Date: Thu, 17 Sep 2026 12:40:58 +0530 Subject: [PATCH 6/8] fix(banner): tag url_link (text) instead of media (file) for Visual Builder Visual Builder rejects an inline data-cslp on a file/asset field (media) as 'invalid or incorrectly generated'; it only supports inline editing for text-type fields (as with the paragraph 'content' and carousel 'title'). Point the banner's edit tag at the url_link text field so the component carries a valid field-level tag (clearing the Invalid CSLP tag error). The banner image remains editable via the entry form panel, which is the Contentstack pattern for asset fields. Co-Authored-By: Claude Opus 4.8 --- .../contentstack-editable-banner.component.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/cms/components/contentstack-editable-banner.component.ts b/src/cms/components/contentstack-editable-banner.component.ts index 3a1a92b..00101ea 100644 --- a/src/cms/components/contentstack-editable-banner.component.ts +++ b/src/cms/components/contentstack-editable-banner.component.ts @@ -18,9 +18,17 @@ export interface ContentstackEditableBannerData extends CmsBannerComponent { /** * Connector-provided replacement for Spartacus's stock banner renderer that adds - * a **field-level** `data-cslp` tag on the image, so Contentstack's Visual - * Builder can edit the banner's `media` (image) field inline — instead of the - * component only carrying the coarse entry-level tag it flags as invalid. + * a **field-level** `data-cslp` tag, so Contentstack's Visual Builder selects the + * banner as an editable field — instead of the component only carrying the coarse + * entry-level tag it flags as an invalid / incorrectly-generated CSLP tag. + * + * The tag is placed on the banner's **`url_link`** (a text field), NOT on the + * `media` image. Visual Builder's inline `data-cslp` editing only supports + * text-type fields; a tag pointing at a file/asset field (`media`) is rejected as + * "invalid or incorrectly generated" (unlike the paragraph `content` and carousel + * `title` text fields, which tag cleanly). The banner **image** is still fully + * editable in Visual Builder — via the entry's form panel, which is the Contentstack + * pattern for asset fields — just not by clicking the rendered image inline. * * Renders the same building blocks as the stock banner — Spartacus's `cx-media` * (responsive image) inside `cx-generic-link` (SPA-aware link) — so the visual @@ -35,18 +43,14 @@ export interface ContentstackEditableBannerData extends CmsBannerComponent { imports: [CommonModule, MediaModule, GenericLinkModule, CsEditableDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: ` - +
-
- -
+
-
- -
+
- +
`, }) export class ContentstackEditableBannerComponent { From 8d6ba7aacdd81eb061728968a7ba603a46918542 Mon Sep 17 00:00:00 2001 From: vaishnavikedar1511 Date: Thu, 17 Sep 2026 14:50:55 +0530 Subject: [PATCH 7/8] fix(vb): suppress entry-level host tag on editable renderers Visual Builder reported "Invalid CSLP tag" on hosts because the ComponentDecorator stamped a lone 3-part entry-level data-cslp on the host while the valid 4-part field tag sat on a child. Skip the coarse tag for our editable renderers; the inner field tag still lets VB resolve/open the entry. Co-Authored-By: Claude Opus 4.8 --- .../contentstack-component.decorator.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/live-preview/contentstack-component.decorator.ts b/src/live-preview/contentstack-component.decorator.ts index 6633892..3862a12 100644 --- a/src/live-preview/contentstack-component.decorator.ts +++ b/src/live-preview/contentstack-component.decorator.ts @@ -20,8 +20,23 @@ export class ContentstackComponentDecorator extends ComponentDecorator { if (!component) { return; } + // Skip the coarse entry-level tag on our own editable renderers + // (``). Those emit **field-level** (4-part) `data-cslp` tags + // on the fields inside their templates via `CsEditableDirective`; leaving a + // lone **entry-level** (3-part) tag on the host as well makes Visual Builder + // report "Invalid CSLP tag" for the whole component (a bare 3-part tag on + // the host is not inline-editable). The inner field tag already lets VB + // resolve and open the entry, so component-to-entry navigation is preserved. + if (this.isEditableRenderer(element)) { + return; + } if (!this.contentstackLivePreviewService.hasEditableTags(element)) { this.contentstackLivePreviewService.addInspectorModeTags(element, renderer, component); } } + + /** True for the connector's editable renderer hosts (``). */ + protected isEditableRenderer(element: Element): boolean { + return element.tagName.toLowerCase().startsWith('cs-editable-'); + } } From 699efe18b8ab1a482054920dbdeaeb0faac1ff0b Mon Sep 17 00:00:00 2001 From: vaishnavikedar1511 Date: Fri, 18 Sep 2026 15:32:00 +0530 Subject: [PATCH 8/8] fix(vb): valid field tag on section wrapper, no invalid 3-part host tag Editable renderers no longer receive the coarse 3-part entry-level data-cslp on their host (a bare content_type.entry.locale string is not a valid Contentstack CSLP tag, so Visual Builder flagged it as "Invalid CSLP tag"). Instead the product carousel carries the title's VALID 4-part field tag on its section wrapper
, which clears the error AND provides the open-the-entry affordance. Verified in the live DOM: every data-cslp on the page is now 4-part. Co-Authored-By: Claude Opus 4.8 --- ...stack-editable-product-carousel.component.ts | 17 +++++++++++------ .../contentstack-component.decorator.ts | 14 +++++++------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/cms/components/contentstack-editable-product-carousel.component.ts b/src/cms/components/contentstack-editable-product-carousel.component.ts index 010a82f..a6acb27 100644 --- a/src/cms/components/contentstack-editable-product-carousel.component.ts +++ b/src/cms/components/contentstack-editable-product-carousel.component.ts @@ -30,9 +30,14 @@ export interface ContentstackEditableProductCarouselData extends CmsProductCarou * * The stock `ProductCarouselComponent` renders its title *inside* `cx-carousel` * (via the `[title]` input), so it can't be field-tagged there. This component - * instead renders its own tagged `

` heading (`[csEditable]` → the `title` - * field's 4-part `data-cslp`) and passes an empty title to `cx-carousel` to avoid - * a duplicate heading. + * instead wraps the whole section in a `
` carrying the `title` field's VALID + * 4-part `data-cslp` (`[csEditable]` → `title`), renders its own `

` heading, + * and passes an empty title to `cx-carousel` to avoid a duplicate heading. + * + * The section-wrapper tag (not the coarse entry-level host tag, which the + * ComponentDecorator deliberately skips for `` hosts) is what + * makes the section selectable/openable in Visual Builder WITHOUT the "Invalid + * CSLP tag" error — a single valid field tag, no conflicting bare 3-part tag. * * The products themselves are unchanged: each SKU in `productCodes` is hydrated * live from SAP via Spartacus's `ProductService` (same `LIST`+`STOCK` scopes as @@ -49,8 +54,8 @@ export interface ContentstackEditableProductCarouselData extends CmsProductCarou imports: [CommonModule, CarouselModule, ProductCarouselModule, CsEditableDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: ` - -