Repository: taiga-family/taiga-ui
Pull request #10774: fix(legacy): reuse `identityMatcher` inside `tuiMultiSelectGroup`
Author: login: splincode
Created: 2025-04-09T07:53:10Z | Merged: 2025-04-10T06:32:27Z

Fixes #10721

Linked issue #10721: 🐞 - identityMatcher doesnot work in tuiMultiSelectGroup
### Reproduction url

https://stackblitz.com/edit/angular-rthgbrve?file=src%2Fapp%2Fapp.component.ts,src%2Fapp%2Fapp.template.html

### Description

if we have complex object in controk,  tui-data-list-wrapper with tuiMultiSelectGroup in template and provide identityMatcher in providers, the checkbox does not active in multi select

### Taiga UI version

4.31

### Browser Used

Chrome

### OS Used

macOS

Diff:
diff --git a/projects/cdk/utils/miscellaneous/array-toggle.ts b/projects/cdk/utils/miscellaneous/array-toggle.ts
index a0bac204ed87d..1323ad27abca8 100644
--- a/projects/cdk/utils/miscellaneous/array-toggle.ts
+++ b/projects/cdk/utils/miscellaneous/array-toggle.ts
@@ -1,7 +1,15 @@
+import type {TuiIdentityMatcher} from '@taiga-ui/cdk/types';
+
 import {tuiArrayRemove} from './array-remove';
 
-export function tuiArrayToggle<T>(array: readonly T[], item: T): T[] {
-    const index = array.indexOf(item);
+export function tuiArrayToggle<T>(
+    array: readonly T[],
+    item: T,
+    identity?: TuiIdentityMatcher<T>,
+): T[] {
+    const index = identity
+        ? array.findIndex((it) => identity(it, item))
+        : array.indexOf(item);
 
     return index === -1 ? [...array, item] : tuiArrayRemove(array, index);
 }
diff --git a/projects/demo-playwright/tests/legacy/multi-select/multi-select.pw.spec.ts b/projects/demo-playwright/tests/legacy/multi-select/multi-select.pw.spec.ts
index 74cac50d93c1b..0fadee28081f3 100644
--- a/projects/demo-playwright/tests/legacy/multi-select/multi-select.pw.spec.ts
+++ b/projects/demo-playwright/tests/legacy/multi-select/multi-select.pw.spec.ts
@@ -261,4 +261,17 @@ test.describe('MultiSelect', () => {
             );
         });
     });
+
+    test('Multi group with separate data list', async ({page}) => {
+        await tuiGoto(page, 'components/multi-select');
+
+        const example = new TuiDocumentationPagePO(page).getExample(
+            '#multi-group-with-separate-data-list',
+        );
+
+        await example.scrollIntoViewIfNeeded();
+        await example.locator('button').click();
+
+        await expect(page).toHaveScreenshot('11-multi-group-with-separate-data-list.png');
+    });
 });
diff --git a/projects/demo/src/modules/components/multi-select/examples/15/index.html b/projects/demo/src/modules/components/multi-select/examples/15/index.html
new file mode 100644
index 0000000000000..03fe8e106bee3
--- /dev/null
+++ b/projects/demo/src/modules/components/multi-select/examples/15/index.html
@@ -0,0 +1,21 @@
+<form [formGroup]="form">
+    <button
+        appearance="outline-grayscale"
+        tuiButton
+        tuiDropdownDirection="bottom"
+        type="button"
+        [tuiAppearanceMode]="length ? 'checked' : null"
+        [tuiDropdown]="dropdown"
+        [(tuiDropdownOpen)]="open"
+    >
+        Statuses
+    </button>
+
+    <ng-template #dropdown>
+        <tui-data-list-wrapper
+            formControlName="control"
+            tuiMultiSelectGroup
+            [items]="items"
+        />
+    </ng-template>
+</form>
diff --git a/projects/demo/src/modules/components/multi-select/examples/15/index.ts b/projects/demo/src/modules/components/multi-select/examples/15/index.ts
new file mode 100644
index 0000000000000..e4f51063246a2
--- /dev/null
+++ b/projects/demo/src/modules/components/multi-select/examples/15/index.ts
@@ -0,0 +1,49 @@
+import {Component} from '@angular/core';
+import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
+import {changeDetection} from '@demo/emulate/change-detection';
+import {encapsulation} from '@demo/emulate/encapsulation';
+import {TuiButton} from '@taiga-ui/core';
+import {TuiDataListWrapper, tuiItemsHandlersProvider} from '@taiga-ui/kit';
+import {TuiMultiSelectModule, TuiTextfieldControllerModule} from '@taiga-ui/legacy';
+
+interface Status {
+    name: string;
+}
+
+@Component({
+    standalone: true,
+    imports: [
+        ReactiveFormsModule,
+        TuiButton,
+        TuiDataListWrapper,
+        TuiMultiSelectModule,
+        TuiTextfieldControllerModule,
+    ],
+    templateUrl: './index.html',
+    encapsulation,
+    changeDetection,
+    providers: [
+        tuiItemsHandlersProvider({
+            identityMatcher: (controlValue: {name: string}, item: {name: string}) =>
+                controlValue.name === item.name,
+            stringify: (item: {name: string}): string => `${item.name}`,
+        }),
+    ],
+})
+export default class Example {
+    protected open = false;
+
+    protected readonly items: readonly Status[] = [
+        {name: 'Drafts'},
+        {name: 'In Progress'},
+        {name: 'Completed'},
+    ];
+
+    protected readonly form = new FormGroup({
+        control: new FormControl<readonly Status[]>([{name: 'Drafts'}]),
+    });
+
+    protected get length(): number {
+        return this.form.controls.control.value?.length || 0;
+    }
+}
diff --git a/projects/demo/src/modules/components/multi-select/index.ts b/projects/demo/src/modules/components/multi-select/index.ts
index e2af84545b14f..e094474eaff72 100644
--- a/projects/demo/src/modules/components/multi-select/index.ts
+++ b/projects/demo/src/modules/components/multi-select/index.ts
@@ -75,6 +75,7 @@ export default class PageComponent extends AbstractExampleTuiControl {
         'Nested datalist',
         'Long items',
         'Readonly',
+        'Multi group with separate data list',
     ];
 
     protected readonly items = [
diff --git a/projects/legacy/components/multi-select/multi-select-group/multi-select-group.directive.ts b/projects/legacy/components/multi-select/multi-select-group/multi-select-group.directive.ts
index 1be5faf7342ec..0ad4e133abf20 100644
--- a/projects/legacy/components/multi-select/multi-select-group/multi-select-group.directive.ts
+++ b/projects/legacy/components/multi-select/multi-select-group/multi-select-group.directive.ts
@@ -7,6 +7,7 @@ import {
     TUI_DATA_LIST_HOST,
     tuiAsOptionContent,
 } from '@taiga-ui/core/components/data-list';
+import {TUI_ITEMS_HANDLERS} from '@taiga-ui/kit/tokens';
 import {AbstractTuiControl} from '@taiga-ui/legacy/classes';
 import {TuiMultiSelectOptionComponent} from '@taiga-ui/legacy/components/multi-select-option';
 import {PolymorpheusComponent} from '@taiga-ui/polymorpheus';
@@ -28,16 +29,21 @@ export const TUI_MULTI_SELECT_OPTION = new PolymorpheusComponent(
                 const multiSelect = inject(TuiMultiSelectComponent, {optional: true});
                 const ngControl = inject(NgControl);
                 const host = inject(AbstractTuiControl, {optional: true});
+                const {identityMatcher, stringify} =
+                    inject(TUI_ITEMS_HANDLERS, {optional: true}) ?? {};
 
                 return (
                     multiSelect || {
+                        stringify,
+                        identityMatcher,
                         handleOption: (option) => {
+                            const value = host?.value ?? ngControl.control?.value ?? [];
+                            const result = tuiArrayToggle(value, option, identityMatcher);
+
                             if (host) {
-                                host.value = tuiArrayToggle(host.value, option);
+                                host.value = result;
                             } else {
-                                ngControl.control?.setValue(
-                                    tuiArrayToggle(ngControl.control.value || [], option),
-                                );
+                                ngControl.control?.setValue(result);
                             }
                         },
                     }


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 5 out of 6 changed files in this pull request and generated no comments.

<details>
<summary>Files not reviewed (1)</summary>

* **projects/demo/src/modules/components/multi-select/examples/15/index.html**: Language not supported
</details>



state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T07:54:48Z
user: login: waterplea
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-09T08:47:35Z
user: login: splincode
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T09:19:06Z
user: login: vladimirpotekhin
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-09T13:23:15Z

Review comments:
diff_hunk: @@ -28,16 +29,24 @@ export const TUI_MULTI_SELECT_OPTION = new PolymorpheusComponent(
                 const multiSelect = inject(TuiMultiSelectComponent, {optional: true});
                 const ngControl = inject(NgControl);
                 const host = inject(AbstractTuiControl, {optional: true});
+                const {identityMatcher, stringify} =
+                    inject(TUI_ITEMS_HANDLERS, {optional: true}) ?? {};
 
                 return (
                     multiSelect || {
+                        stringify,
+                        identityMatcher,
                         handleOption: (option) => {
+                            const state = tuiArrayToggle(
+                                host ? host.value : ngControl.control?.value || [],
path: projects/legacy/components/multi-select/multi-select-group/multi-select-group.directive.ts
user: login: waterplea
body: Looks like we should make a const for this line:
```ts
const value = host?.value ?? ngControl.control?.value ?? [];
```
created_at: 2025-04-09T08:46:37Z
author_association: MEMBER
diff_hunk: @@ -28,16 +29,24 @@ export const TUI_MULTI_SELECT_OPTION = new PolymorpheusComponent(
                 const multiSelect = inject(TuiMultiSelectComponent, {optional: true});
                 const ngControl = inject(NgControl);
                 const host = inject(AbstractTuiControl, {optional: true});
+                const {identityMatcher, stringify} =
+                    inject(TUI_ITEMS_HANDLERS, {optional: true}) ?? {};
 
                 return (
                     multiSelect || {
+                        stringify,
+                        identityMatcher,
                         handleOption: (option) => {
+                            const state = tuiArrayToggle(
+                                host ? host.value : ngControl.control?.value || [],
path: projects/legacy/components/multi-select/multi-select-group/multi-select-group.directive.ts
user: login: splincode
body: fixed
created_at: 2025-04-09T09:19:06Z
author_association: MEMBER

PR comments:
user: login: screenshot-report-bot[bot]
created_at: 2025-04-09T07:53:19Z
body: <!-- screenshot-bot-id: test-report -->

# Pull request was closed :heavy_check_mark:

All saved screenshots (for current PR) were deleted :wastebasket:
author_association: NONE
user: login: nx-cloud[bot]
created_at: 2025-04-09T07:54:10Z
body: 
View your [CI Pipeline Execution ↗](https://cloud.nx.app/cipes/67f63c15f5c1777cbe48d2ef?utm_source=pull-request&utm_medium=comment) for commit d679515d2049cabb9d43f5b434ee19af867078b9.

| Command | Status | Duration | Result |
|---------|--------|----------:|--------|
| `nx run-many --target test --all --output-style=...` | ✅ Succeeded | 3m 20s | [View ↗](https://cloud.nx.app/runs/3RKslLzs4T?utm_source=pull-request&utm_medium=comment) |

---

☁️ [Nx Cloud](https://cloud.nx.app?utm_source=pull-request&utm_medium=comment) last updated this comment at `2025-04-09 09:40:27` UTC

<!-- NX_CLOUD_APP_COMMENT_END -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-09T07:59:31Z
body: Visit the preview URL for this PR (updated for commit d679515):

[https://taiga-previews-demo--pr10774-splincode-fix-issues-1072-nfjo1oiz.web.app](https://taiga-previews-demo--pr10774-splincode-fix-issues-1072-nfjo1oiz.web.app)

<sub>(expires Thu, 10 Apr 2025 09:24:52 GMT)</sub>

<sub>🔥 via [Firebase Hosting GitHub Action](https://github.com/marketplace/actions/deploy-to-firebase-hosting) 🌎</sub>

<sub>Sign: 73dddc3c665194f3e11f18c16aeb71af4c289c37</sub>
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-09T08:01:06Z
body: ## [Codecov](https://app.codecov.io/gh/taiga-family/taiga-ui/pull/10774?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=taiga-family) Report
Attention: Patch coverage is `33.33333%` with `6 lines` in your changes missing coverage. Please review.
> Project coverage is 65.19%. Comparing base [(`707aaf4`)](https://app.codecov.io/gh/taiga-family/taiga-ui/commit/707aaf49dcb2b5b8fe99990193ce55e9bbe6dd69?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=taiga-family) to head [(`d679515`)](https://app.codecov.io/gh/taiga-family/taiga-ui/commit/d679515d2049cabb9d43f5b434ee19af867078b9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=taiga-family).
> Report is 2 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/taiga-family/taiga-ui/pull/10774?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=taiga-family) | Patch % | Lines |
|---|---|---|
| [...multi-select-group/multi-select-group.directive.ts](https://app.codecov.io/gh/taiga-family/taiga-ui/pull/10774?src=pr&el=tree&filepath=projects%2Flegacy%2Fcomponents%2Fmulti-select%2Fmulti-select-group%2Fmulti-select-group.directive.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=taiga-family#diff-cHJvamVjdHMvbGVnYWN5L2NvbXBvbmVudHMvbXVsdGktc2VsZWN0L211bHRpLXNlbGVjdC1ncm91cC9tdWx0aS1zZWxlY3QtZ3JvdXAuZGlyZWN0aXZlLnRz) | 16.66% | [5 Missing :warning: ](https://app.codecov.io/gh/taiga-family/taiga-ui/pull/10774?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=taiga-family) |
| [projects/cdk/utils/miscellaneous/array-toggle.ts](https://app.codecov.io/gh/taiga-family/taiga-ui/pull/10774?src=pr&el=tree&filepath=projects%2Fcdk%2Futils%2Fmiscellaneous%2Farray-toggle.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=taiga-family#diff-cHJvamVjdHMvY2RrL3V0aWxzL21pc2NlbGxhbmVvdXMvYXJyYXktdG9nZ2xlLnRz) | 66.66% | [1 Missing :warning: ](https://app.codecov.io/gh/taiga-family/taiga-ui/pull/10774?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=taiga-family) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main   #10774      +/-   ##
==========================================
- Coverage   65.20%   65.19%   -0.01%     
==========================================
  Files        1274     1274              
  Lines       16829    16833       +4     
  Branches     2446     2433      -13     
==========================================
+ Hits        10973    10974       +1     
- Misses       5632     5645      +13     
+ Partials      224      214      -10     
```

| [Flag](https://app.codecov.io/gh/taiga-family/taiga-ui/pull/10774/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=taiga-family) | Coverage Δ | |
|---|---|---|
| [summary](https://app.codecov.io/gh/taiga-family/taiga-ui/pull/10774/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=taiga-family) | `65.19% <33.33%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=taiga-family#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/taiga-family/taiga-ui/pull/10774?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=taiga-family).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=taiga-family).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-09T08:16:43Z
body: <!-- playwright-report-github-action -- ⚙️ E2E testing -->

### Playwright test results

![failed](https://icongr.am/octicons/stop.svg?size=14&color=da3633)  **1 failed**  
![passed](https://icongr.am/octicons/check-circle.svg?size=14&color=3fb950)  **1116 passed**    
![skipped](https://icongr.am/octicons/skip.svg?size=14&color=0967d9)  **1 skipped**

#### Details

![report](https://icongr.am/octicons/package.svg?size=14&color=abb4bf)  [Open report ↗︎](https://taiga-previews-demo-e2e-report--pr10774-splincode-fix-kou592wz.web.app)  
![stats](https://icongr.am/octicons/pulse.svg?size=14&color=abb4bf)  1118 tests across 74 suites  
![duration](https://icongr.am/octicons/clock.svg?size=14&color=abb4bf)  10 minutes, 51 seconds  
![commit](https://icongr.am/octicons/git-pull-request.svg?size=14&color=abb4bf)  [d679515](https://github.com/taiga-family/taiga-ui/commit/d679515d2049cabb9d43f5b434ee19af867078b9)

<details open><summary><strong>Failed tests</strong></summary>

chromium › tests/legacy/multi-select/multi-select.pw.spec.ts › MultiSelect › Multi group with separate data list

</details>
<details ><summary><strong>Skipped tests</strong></summary>

chromium › tests/addon-mobile/mobile-dropdown/mobile-dropdown-with-textfield.pw.spec.ts › DropdownMobile for textfields › with select

</details>
author_association: CONTRIBUTOR