diff --git a/packages/survey-angular-ui/src/components/list/list-item.component.html b/packages/survey-angular-ui/src/components/list/list-item.component.html index b6e8722215..f967248b6a 100644 --- a/packages/survey-angular-ui/src/components/list/list-item.component.html +++ b/packages/survey-angular-ui/src/components/list/list-item.component.html @@ -7,7 +7,7 @@
- +
\ No newline at end of file diff --git a/packages/survey-angular-ui/src/components/list/list-item.component.ts b/packages/survey-angular-ui/src/components/list/list-item.component.ts index 2be5426f46..26a83cd3e6 100644 --- a/packages/survey-angular-ui/src/components/list/list-item.component.ts +++ b/packages/survey-angular-ui/src/components/list/list-item.component.ts @@ -41,6 +41,10 @@ export class ListItemComponent extends BaseAngular implements AfterViewInit { get itemComponent(): string { return this.model.component || this.listModel.itemComponent; } + get itemComponentData(): any { + const extraData = this.listModel.onGetItemExtraComponentData ? this.listModel.onGetItemExtraComponentData(this.model) : undefined; + return { model: this.model, listModel: this.listModel, ...extraData }; + } getModel() { return this.model; diff --git a/packages/survey-core/src/list.ts b/packages/survey-core/src/list.ts index 572533b5e2..e334548243 100644 --- a/packages/survey-core/src/list.ts +++ b/packages/survey-core/src/list.ts @@ -71,6 +71,7 @@ export class ListModel extends ActionContainer public scrollHandler: (e?: any) => void; public loadingIndicatorVisibilityObserver: (isVisible: boolean) => void; public areSameItemsCallback: (item1: IAction, item2: IAction) => boolean; + public onGetItemExtraComponentData: (item: T) => any; private hasText(item: T, filterStringInLow: string): boolean { if (!filterStringInLow) return true; diff --git a/packages/survey-core/src/survey.ts b/packages/survey-core/src/survey.ts index 2ccea6598e..58b7615f3c 100644 --- a/packages/survey-core/src/survey.ts +++ b/packages/survey-core/src/survey.ts @@ -1688,6 +1688,7 @@ export class SurveyModel extends SurveyElementCore * * [View Demo](https://surveyjs.io/form-library/examples/toc-feature/ (linkStyle)) * @see tocLocation + * @see tocItemComponent */ @property() showTOC: boolean; /** @@ -1700,8 +1701,29 @@ export class SurveyModel extends SurveyElementCore * * [View Demo](https://surveyjs.io/form-library/examples/toc-feature/ (linkStyle)) * @see showTOC + * @see tocItemComponent */ @property() tocLocation: "left" | "right"; + /** + * The name of a component used to render item in the Table of Contents. Applies only when [`showTOC`](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model#showTOC) is `true`. + * + * The custom component receives the following props: + * + * - `item`: [`IAction`](https://surveyjs.io/form-library/documentation/api-reference/iaction)\ + * A TOC item. + * + * - `model`: `ListModel`\ + * A TOC item list model. + * + * - `survey`: [`SurveyModel`](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model)\ + * The survey model. + * + * - `page`: [`PageModel`](https://surveyjs.io/form-library/documentation/api-reference/page-model)\ + * The page model. + * @see showTOC + * @see tocLocation + */ + @property() tocItemComponent: string; /** * Specifies whether to display the [survey title](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model#title). * @@ -8558,6 +8580,10 @@ Serializer.addClass("survey", [ dependsOn: ["showTOC"], visibleIf: (survey: any) => { return !!survey && survey.showTOC; } }, + { + name: "tocItemComponent", + visible: false + }, { name: "readOnly:boolean" }, { name: "mode", visible: false, isSerializable: false }, { name: "storeOthersAsComment:boolean", default: true }, diff --git a/packages/survey-core/src/surveyToc.ts b/packages/survey-core/src/surveyToc.ts index 4139d9f91c..78b5286d2b 100644 --- a/packages/survey-core/src/surveyToc.ts +++ b/packages/survey-core/src/surveyToc.ts @@ -58,6 +58,18 @@ export function createTOCListModel(survey: SurveyModel, onAction?: () => void): }; var listModel = new ListModel(listOptions as any); listModel.allowSelection = false; + if (survey.tocItemComponent) { + listModel.itemComponent = survey.tocItemComponent; + } + listModel.onGetItemExtraComponentData = (item: Action) => { + return { survey: survey, page: item.data?.page }; + }; + const updateTocItemComponent = () => { + if (survey.tocItemComponent) { + listModel.itemComponent = survey.tocItemComponent; + } + }; + survey.registerFunctionOnPropertyValueChanged("tocItemComponent", updateTocItemComponent, "toc"); const updateSelectedItem = (currentPage: PageModel, defaultSelection?: IAction) => { listModel.selectedItem = !!currentPage && listModel.actions.filter(i => i.id === currentPage.name)[0] || defaultSelection; }; @@ -82,6 +94,7 @@ function getTOCItems(survey: SurveyModel, onAction: () => void) { return new Action({ id: page.name, locTitle: page.locNavigationTitle, + data: { page: page, survey: survey }, action: () => { DomDocumentHelper.activeElementBlur(); !!onAction && onAction(); @@ -189,6 +202,7 @@ export class TOCModel implements ILayoutElementModel { }; public dispose(): void { this.survey.unRegisterFunctionOnPropertyValueChanged("_isMobile", "toc"); + this.survey.unRegisterFunctionOnPropertyValueChanged("tocItemComponent", "toc"); const [handler] = this.survey.unRegisterFunctionOnPropertyValueChanged("pages", "toc"); this.survey.onEndLoadingFromJson.remove(handler); this.survey.onPageAdded.remove(handler); diff --git a/packages/survey-core/tests/surveyTOCTests.ts b/packages/survey-core/tests/surveyTOCTests.ts index 03ab400d72..6e14279d76 100644 --- a/packages/survey-core/tests/surveyTOCTests.ts +++ b/packages/survey-core/tests/surveyTOCTests.ts @@ -1180,3 +1180,102 @@ describe("TOC", () => { expect(!!survey.findLayoutElement("toc-navigation")).toBe(true); }); }); + +describe("TOC custom item component", () => { + test("tocItemComponent sets listModel.itemComponent", () => { + const survey = new SurveyModel({ + pages: [ + { + name: "page1", + elements: [{ type: "text", name: "question1" }] + }, + { + name: "page2", + elements: [{ type: "text", name: "question2" }] + } + ], + showTOC: true, + tocItemComponent: "sv-custom-toc-item" + }); + + const tocListModel = createTOCListModel(survey); + + expect(tocListModel.itemComponent).toBe("sv-custom-toc-item"); + }); + + test("tocItemComponent updates listModel.itemComponent dynamically", () => { + const survey = new SurveyModel({ + pages: [ + { + name: "page1", + elements: [{ type: "text", name: "question1" }] + }, + { + name: "page2", + elements: [{ type: "text", name: "question2" }] + } + ], + showTOC: true + }); + + const tocListModel = createTOCListModel(survey); + + expect(tocListModel.itemComponent).toBe("sv-list-item-content"); + + survey.tocItemComponent = "sv-custom-toc-item"; + + expect(tocListModel.itemComponent).toBe("sv-custom-toc-item"); + }); + + test("TOC items have page and survey in data", () => { + const survey = new SurveyModel({ + pages: [ + { + name: "page1", + elements: [{ type: "text", name: "question1" }] + }, + { + name: "page2", + elements: [{ type: "text", name: "question2" }] + } + ], + showTOC: true + }); + + const tocListModel = createTOCListModel(survey); + + expect(tocListModel.actions[0].data).toBeTruthy(); + expect(tocListModel.actions[0].data.page).toBe(survey.pages[0]); + expect(tocListModel.actions[0].data.survey).toBe(survey); + + expect(tocListModel.actions[1].data.page).toBe(survey.pages[1]); + expect(tocListModel.actions[1].data.survey).toBe(survey); + }); + + test("TOC listModel.onGetItemExtraComponentData returns survey and page", () => { + const survey = new SurveyModel({ + pages: [ + { + name: "page1", + elements: [{ type: "text", name: "question1" }] + }, + { + name: "page2", + elements: [{ type: "text", name: "question2" }] + } + ], + showTOC: true + }); + + const tocListModel = createTOCListModel(survey); + + expect(tocListModel.onGetItemExtraComponentData).toBeTruthy(); + + const extraData = tocListModel.onGetItemExtraComponentData!(tocListModel.actions[0]); + expect(extraData.survey).toBe(survey); + expect(extraData.page).toBe(survey.pages[0]); + + const extraData2 = tocListModel.onGetItemExtraComponentData!(tocListModel.actions[1]); + expect(extraData2.page).toBe(survey.pages[1]); + }); +}); \ No newline at end of file diff --git a/packages/survey-react-ui/src/components/list/list-item.tsx b/packages/survey-react-ui/src/components/list/list-item.tsx index eb9056fd72..72daa1d23c 100644 --- a/packages/survey-react-ui/src/components/list/list-item.tsx +++ b/packages/survey-react-ui/src/components/list/list-item.tsx @@ -33,7 +33,8 @@ export class ListItem extends SurveyElementBase { if (!this.item) return null; const className = this.model.getItemClass(this.item); const itemContent = this.item.component || this.model.itemComponent; - const newElement = ReactElementFactory.Instance.createElement(itemContent, { item: this.item, key: this.item.id, model: this.model }); + const extraData = this.model.onGetItemExtraComponentData ? this.model.onGetItemExtraComponentData(this.item) : undefined; + const newElement = ReactElementFactory.Instance.createElement(itemContent, { item: this.item, key: this.item.id, model: this.model, ...extraData }); const contentWrap =
@@ -56,6 +57,10 @@ const itemComponent = computed( () => props.item.component || props.model.itemComponent ); +const extraComponentData = computed( + () => props.model.onGetItemExtraComponentData ? props.model.onGetItemExtraComponentData(props.item as any) : undefined +); + onMounted(() => { setTimeout(() => { props.model.onItemRended(props.item as any, elementRef.value);