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);