From 8dc419d8ca5155aa5de02b66d2d6a6f55e25f81a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Sep 2025 11:16:32 +0000 Subject: [PATCH 1/4] Initial plan From 2383e282d0049100e3cb6d69dfae04b0fdbb6d91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Sep 2025 11:23:07 +0000 Subject: [PATCH 2/4] feat(Campaign): add createContext template method for extensibility Co-authored-by: timowestnosto <13622115+timowestnosto@users.noreply.github.com> --- src/components/Campaign/Campaign.ts | 26 ++++++++++++++++- test/components/Campaign.spec.tsx | 44 +++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/components/Campaign/Campaign.ts b/src/components/Campaign/Campaign.ts index f9847770..b655bdbd 100644 --- a/src/components/Campaign/Campaign.ts +++ b/src/components/Campaign/Campaign.ts @@ -67,6 +67,30 @@ export class Campaign extends NostoElement { async load() { await loadCampaign(this) } + + /** + * Extension point: override to enrich or modify the template context. + * This method is called during campaign rendering to create the context object + * that will be passed to the template. Subclasses can override this method + * to add custom properties, feature flags, or transform the data. + * + * @param raw - The raw JSON result from the Nosto API + * @returns The context object to be used in template rendering + * + * @example + * ```typescript + * class CustomCampaign extends Campaign { + * createContext(raw) { + * const context = super.createContext(raw); + * return { ...context, customProperty: 'value' }; + * } + * } + * ``` + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createContext(raw: JSONResult): any { + return getContext(raw) + } } export async function loadCampaign(element: Campaign) { @@ -85,7 +109,7 @@ export async function loadCampaign(element: Campaign) { if (rec) { if (useTemplate) { const template = getTemplate(element) - compile(element, template, getContext(rec as JSONResult)) + compile(element, template, element.createContext(rec as JSONResult)) api.attributeProductClicksInCampaign(element, rec as JSONResult) } else { await api.placements.injectCampaigns( diff --git a/test/components/Campaign.spec.tsx b/test/components/Campaign.spec.tsx index 5e4e70e2..328c1a1d 100644 --- a/test/components/Campaign.spec.tsx +++ b/test/components/Campaign.spec.tsx @@ -163,4 +163,48 @@ describe("Campaign", () => { expect(mockObserver.observe).not.toHaveBeenCalled() expect(mockBuilder.load).not.toHaveBeenCalled() }) + + it("should allow subclasses to override createContext method", async () => { + // Create a custom campaign that extends the base Campaign + class CustomCampaign extends Campaign { + createContext(raw: any) { + const context = super.createContext(raw) + return { ...context, customProperty: "customValue", modified: true } + } + } + + // Register the custom element + if (!customElements.get("custom-campaign")) { + customElements.define("custom-campaign", CustomCampaign) + } + + const templateId = "custom-template" + const template = document.createElement("template") + template.id = templateId + template.innerHTML = ` +
{{ customProperty }}
+
{{ modified }}
+
{{ title }}
+ ` + document.body.appendChild(template) + + const { mockBuilder } = mockNostoRecs({ + "custom-123": { + title: "Custom Campaign Test" + } + }) + + // Create custom element using JSX-like syntax + const customCampaign = document.createElement("custom-campaign") as Campaign + customCampaign.setAttribute("placement", "custom-123") + customCampaign.setAttribute("template", templateId) + document.body.appendChild(customCampaign) + + await customCampaign.connectedCallback() + + expect(customCampaign.innerHTML).toContain("customValue") + expect(customCampaign.innerHTML).toContain("true") + expect(customCampaign.innerHTML).toContain("Custom Campaign Test") + expect(mockBuilder.load).toHaveBeenCalledWith() + }) }) From c0f59a2940a884d70ebfbdb16624545d99b79c72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Sep 2025 11:32:43 +0000 Subject: [PATCH 3/4] fix: change createContext return type to object and use JSX syntax in test Co-authored-by: timowestnosto <13622115+timowestnosto@users.noreply.github.com> --- src/components/Campaign/Campaign.ts | 3 +-- test/components/Campaign.spec.tsx | 9 ++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/Campaign/Campaign.ts b/src/components/Campaign/Campaign.ts index b655bdbd..5e3413de 100644 --- a/src/components/Campaign/Campaign.ts +++ b/src/components/Campaign/Campaign.ts @@ -87,8 +87,7 @@ export class Campaign extends NostoElement { * } * ``` */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createContext(raw: JSONResult): any { + createContext(raw: JSONResult): object { return getContext(raw) } } diff --git a/test/components/Campaign.spec.tsx b/test/components/Campaign.spec.tsx index 328c1a1d..a875b732 100644 --- a/test/components/Campaign.spec.tsx +++ b/test/components/Campaign.spec.tsx @@ -1,6 +1,7 @@ /** @jsx createElement */ import { describe, it, expect, vi, Mock } from "vitest" import { Campaign } from "@/components/Campaign/Campaign" +import { JSONResult } from "@nosto/nosto-js/client" import { mockNostoRecs } from "../mockNostoRecs" import { createElement } from "../utils/jsx" @@ -167,7 +168,7 @@ describe("Campaign", () => { it("should allow subclasses to override createContext method", async () => { // Create a custom campaign that extends the base Campaign class CustomCampaign extends Campaign { - createContext(raw: any) { + createContext(raw: JSONResult) { const context = super.createContext(raw) return { ...context, customProperty: "customValue", modified: true } } @@ -194,10 +195,8 @@ describe("Campaign", () => { } }) - // Create custom element using JSX-like syntax - const customCampaign = document.createElement("custom-campaign") as Campaign - customCampaign.setAttribute("placement", "custom-123") - customCampaign.setAttribute("template", templateId) + // Create custom element using JSX syntax + const customCampaign = () as Campaign document.body.appendChild(customCampaign) await customCampaign.connectedCallback() From 3a7b2fcc8767f3628f2cfab714723c102d0f5e80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:24:00 +0000 Subject: [PATCH 4/4] feat(Campaign): make createContext async for better extensibility Co-authored-by: timowestnosto <13622115+timowestnosto@users.noreply.github.com> --- src/components/Campaign/Campaign.ts | 8 ++++---- test/components/Campaign.spec.tsx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Campaign/Campaign.ts b/src/components/Campaign/Campaign.ts index 5e3413de..3d03c222 100644 --- a/src/components/Campaign/Campaign.ts +++ b/src/components/Campaign/Campaign.ts @@ -80,14 +80,14 @@ export class Campaign extends NostoElement { * @example * ```typescript * class CustomCampaign extends Campaign { - * createContext(raw) { - * const context = super.createContext(raw); + * async createContext(raw) { + * const context = await super.createContext(raw); * return { ...context, customProperty: 'value' }; * } * } * ``` */ - createContext(raw: JSONResult): object { + async createContext(raw: JSONResult): Promise { return getContext(raw) } } @@ -108,7 +108,7 @@ export async function loadCampaign(element: Campaign) { if (rec) { if (useTemplate) { const template = getTemplate(element) - compile(element, template, element.createContext(rec as JSONResult)) + compile(element, template, await element.createContext(rec as JSONResult)) api.attributeProductClicksInCampaign(element, rec as JSONResult) } else { await api.placements.injectCampaigns( diff --git a/test/components/Campaign.spec.tsx b/test/components/Campaign.spec.tsx index a875b732..031e9fe6 100644 --- a/test/components/Campaign.spec.tsx +++ b/test/components/Campaign.spec.tsx @@ -168,8 +168,8 @@ describe("Campaign", () => { it("should allow subclasses to override createContext method", async () => { // Create a custom campaign that extends the base Campaign class CustomCampaign extends Campaign { - createContext(raw: JSONResult) { - const context = super.createContext(raw) + async createContext(raw: JSONResult) { + const context = await super.createContext(raw) return { ...context, customProperty: "customValue", modified: true } } }