{"id":27827950,"url":"https://github.com/jean-merelis/angular-components","last_synced_at":"2026-02-20T21:14:59.386Z","repository":{"id":285234733,"uuid":"957461546","full_name":"jean-merelis/angular-components","owner":"jean-merelis","description":null,"archived":false,"fork":false,"pushed_at":"2025-04-26T01:49:55.000Z","size":1595,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-01T11:05:57.125Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://jean-merelis.github.io/angular-components","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jean-merelis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-03-30T12:41:09.000Z","updated_at":"2025-04-26T01:48:58.000Z","dependencies_parsed_at":"2025-03-30T14:22:35.669Z","dependency_job_id":"fe9988d1-cd02-4101-a3a3-c1345913cee1","html_url":"https://github.com/jean-merelis/angular-components","commit_stats":null,"previous_names":["jean-merelis/angular-components"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jean-merelis%2Fangular-components","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jean-merelis%2Fangular-components/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jean-merelis%2Fangular-components/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jean-merelis%2Fangular-components/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jean-merelis","download_url":"https://codeload.github.com/jean-merelis/angular-components/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251951278,"owners_count":21670236,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-05-01T22:02:01.463Z","updated_at":"2026-02-20T21:14:59.377Z","avatar_url":"https://github.com/jean-merelis.png","language":"TypeScript","readme":"# Merelis Angular Components\n\nA library of reusable Angular components and utilities that provides high-quality UI elements for your applications.\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n\n## Showcase\nhttps://jean-merelis.github.io/angular-components/\n\n## Installation\n\n```bash\nnpm install @merelis/angular --save\n```\n\n## Available Components\n\nCurrently, the library provides the following components:\n\n### MerSelect\n\nAn advanced select component with filtering and typeahead capabilities. Supports single or multiple selection, full customization, reactive forms integration, and conditional rendering.\n\n### MerProgressBar\n\nA progress bar component that can be used independently or integrated with other components.\n\n## Installation and Usage\n\n### Initial Setup\n\nAfter installing the package, you need to import the necessary styles in your application's `styles.scss` file:\n\n```scss\n@use '@angular/cdk/overlay-prebuilt.css';\n@use '@merelis/angular/select/styles';\n```\nThis will import both the CDK overlay styles (required for the dropdown functionality) and the component-specific styles.\n\nSince these are standalone components, you can import them directly in your components:\n\n#### Direct import in a component\n\n```typescript\nimport { Component } from '@angular/core';\nimport { MerSelect } from '@merelis/angular/select';\nimport { MerProgressBar } from '@merelis/angular/progress-bar';\n\n@Component({\n    selector: 'app-example',\n    standalone: true,\n    imports: [\n        MerSelect,\n        MerProgressBar\n    ],\n    template: `\n    \u003cmer-select [dataSource]=\"items\" [(value)]=\"selectedItem\"\u003e\u003c/mer-select\u003e\n    \u003cmer-progress-bar [value]=\"0.5\"\u003e\u003c/mer-progress-bar\u003e\n  `\n})\nexport class ExampleComponent {\n    // ...\n}\n```\n\n### MerSelect\n\nThe `MerSelect` offers a robust alternative to the native HTML select, with additional features like filtering and typeahead.\n\n#### Basic HTML\n\n```html\n\u003cmer-select\n    [dataSource]=\"optionsList\"\n    [(value)]=\"selectedValue\"\n    [placeholder]=\"'Select an option'\"\u003e\n\u003c/mer-select\u003e\n```\n\n#### Input Properties\n\n| Name | Type | Default | Description |\n|------|------|---------|-------------|\n| dataSource | Array\\\u003cT\\\u003e \u0026#124; SelectDataSource\\\u003cT\\\u003e | undefined | List of available options for selection or data source for the component |\n| value | T \\| T[] \\| null | undefined | Currently selected value |\n| loading | boolean | false | Displays loading indicator using MerProgressBar |\n| disabled | boolean | false | Disables the component |\n| readOnly | boolean | false | Sets the component as read-only |\n| disableSearch | boolean | false | Disables text search functionality |\n| disableOpeningWhenFocusedByKeyboard | boolean | false | Prevents the panel from opening automatically when focused via keyboard |\n| multiple | boolean | false | Allows multiple selection |\n| canClear | boolean | true | Allows clearing the selection |\n| alwaysIncludesSelected | boolean | false | Always includes the selected item in the dropdown, even if it doesn't match the filter. **Note: Only effective when using an array as dataSource, not when using a custom SelectDataSource.** |\n| autoActiveFirstOption | boolean | true | Automatically activates the first option when the panel is opened |\n| debounceTime | number | 100 | Debounce time for text input (in ms) |\n| panelOffsetY | number | 0 | Vertical offset of the options panel |\n| compareWith | Comparable\\\u003cT\\\u003e | undefined | Function to compare values |\n| displayWith | DisplayWith\\\u003cT\\\u003e | undefined | Function to display values as text |\n| filterPredicate | FilterPredicate\\\u003cT\\\u003e | undefined | Function to filter options based on typed text. **Note: Only effective when using an array as dataSource, not when using a custom SelectDataSource.** |\n| disableOptionPredicate | OptionPredicate\\\u003cT\\\u003e | () =\u003e false | Function to determine which options should be disabled |\n| disabledOptions | T[] | [] | List of options that should be disabled |\n| connectedTo | MerSelectPanelOrigin | undefined | Element to which the panel should connect |\n| panelClass | string \\| string[] | undefined | CSS class(es) applied to the options panel |\n| panelWidth | string \\| number | undefined | Width of the options panel |\n| position | 'auto' \\| 'above' \\| 'below' | 'auto' | Position of the panel relative to the input |\n| placeholder | string | undefined | Text to display when no item is selected |\n\n#### Output Events\n\n| Name | Description |\n|------|-------------|\n| opened | Emitted when the options panel is opened |\n| closed | Emitted when the options panel is closed |\n| focus | Emitted when the component receives focus |\n| blur | Emitted when the component loses focus |\n| inputChanges | Emitted when the text input value changes |\n\n#### Complete Example\n\n```typescript\nimport { Component } from '@angular/core';\n\ninterface User {\n    id: number;\n    name: string;\n}\n\n@Component({\n    selector: 'app-example',\n    template: `\n    \u003cmer-select\n      [dataSource]=\"users\"\n      [(value)]=\"selectedUser\"\n      [displayWith]=\"displayUserName\"\n      [compareWith]=\"compareUsers\"\n      [placeholder]=\"'Select a user'\"\n      [loading]=\"isLoading\"\n      (opened)=\"onPanelOpened()\"\n      (closed)=\"onPanelClosed()\"\n      (inputChanges)=\"onInputChanged($event)\"\u003e\n    \u003c/mer-select\u003e\n  `\n})\nexport class ExampleComponent {\n    users: User[] = [\n        { id: 1, name: 'John Smith' },\n        { id: 2, name: 'Mary Johnson' },\n        { id: 3, name: 'Peter Williams' }\n    ];\n    selectedUser: User | null = null;\n    isLoading = false;\n\n    displayUserName(user: User): string {\n        return user.name;\n    }\n\n    compareUsers(user1: User, user2: User): boolean {\n        return user1?.id === user2?.id;\n    }\n\n    onPanelOpened(): void {\n        console.log('Options panel opened');\n    }\n\n    onPanelClosed(): void {\n        console.log('Options panel closed');\n    }\n\n    onInputChanged(text: string): void {\n        console.log('Search text:', text);\n    }\n}\n```\n\n## Filtering Behavior and DataSource\n\nThe `MerSelect` supports two operation modes for data filtering:\n\n### 1. Automatic Filtering (Array as dataSource)\n\nWhen you provide an array as `dataSource`, the component performs automatic filtering based on the typed text. In this case, the following inputs control the filtering behavior:\n\n| Name | Type | Description |\n|------|------|-------------|\n| filterPredicate | FilterPredicate\\\u003cT\\\u003e | Custom function to filter options based on typed text. **Only applied when the dataSource is an array, not a custom SelectDataSource.** |\n| alwaysIncludesSelected | boolean | When true, always includes the selected item(s) in the dropdown, even if they don't match the filter. **Only applied when the dataSource is an array, not a custom SelectDataSource.** |\n\n### 2. Custom Filtering (SelectDataSource)\n\nWhen you implement and provide a custom `SelectDataSource`, the filtering behavior is determined by the implementation of the dataSource's `applyFilter` method. In this case:\n\n- The component invokes the `applyFilter` method when the user types\n- The `filterPredicate` and `alwaysIncludesSelected` inputs are ignored\n- The filtering logic is entirely controlled by the dataSource\n\n```typescript\nexport class CustomDataSource\u003cT\u003e implements SelectDataSource\u003cT\u003e {\n    // ...\n\n    async applyFilter(criteria: FilterCriteria\u003cT\u003e): void | Promise\u003cvoid\u003e {\n        // Here you implement your own filtering logic\n        // The criteria parameter contains:\n        // - searchText: the text typed by the user\n        // - selected: the currently selected item(s)\n\n        // You can decide to include selected items even if they don't match the filter\n        // (equivalent to the alwaysIncludesSelected behavior)\n\n        // You can also implement your own filtering logic\n        // (equivalent to the filterPredicate behavior)\n    }\n}\n```\n\n### Choosing Between Array and SelectDataSource\n\n- **Use a simple array** when you have a small set of static data that doesn't require server-side filtering.\n- **Implement a SelectDataSource** when you need complete control over filtering, especially for:\n    - Fetching data from the server based on typed text (typeahead)\n    - Handling large datasets\n    - Implementing complex filtering logic\n    - Showing loading indicators during asynchronous operations\n\n\n## Typeahead Functionality with TypeaheadDataSource\n\nThe `MerSelect` supports typeahead functionality, allowing you to search for options as you type. The library provides a generic `TypeaheadDataSource` implementation that handles common typeahead requirements including search request cancellation, loading states, and result management.\n\n### TypeaheadSearchFn Type\n\nA simple function type that can be used to perform typeahead searches:\n\n```typescript\nexport type TypeaheadSearchFn\u003cT\u003e = (query: string) =\u003e Observable\u003cT[]\u003e;\n```\n\n### TypeaheadSearchService Interface\n\nAlternatively, you can implement the `TypeaheadSearchService` interface to define how search operations will be performed:\n\n```typescript\nexport interface TypeaheadSearchService\u003cT\u003e {\n    /**\n     * Search method that takes a query string and returns an Observable of results\n     * @param query The search query string\n     * @returns Observable of search results\n     */\n    search(query: string): Observable\u003cT[]\u003e;\n}\n```\n\n### TypeaheadDataSourceOptions Interface\n\nThe `TypeaheadDataSource` accepts a configuration options object:\n\n```typescript\nexport interface TypeaheadDataSourceOptions\u003cT\u003e {\n    /**\n     * Whether to always include selected items in the results. Default false.\n     */\n    alwaysIncludeSelected?: boolean;\n\n    /**\n     * Whether to suppress loading events. Default false.\n     */\n    suppressLoadingEvents?: boolean;\n\n    /**\n     * Custom function to compare items for equality (defaults to comparing by reference)\n     * @param a First item to compare\n     * @param b Second item to compare\n     */\n    compareWith?: (a: T, b: T) =\u003e boolean;\n}\n```\n\n### Using the TypeaheadDataSource\n\nThe `TypeaheadDataSource` provides a robust solution for typeahead functionality with automatic cancellation of previous requests, which is essential for a smooth user experience.\n\n#### Implementation\n\nYou can implement typeahead functionality in two ways:\n\n##### 1. Using a Simple Search Function\n\n```typescript\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { delay } from 'rxjs/operators';\nimport { HttpClient } from '@angular/common/http';\nimport { TypeaheadDataSource, TypeaheadDataSourceOptions } from '@merelis/angular/select';\n\n// Define your data model\ninterface User {\n    id: number;\n    name: string;\n    email: string;\n}\n\n@Component({\n    selector: 'app-user-search',\n    standalone: true,\n    imports: [MerSelect],\n    template: `\n    \u003cmer-select\n      [(value)]=\"selectedUser\"\n      [dataSource]=\"userDataSource\"\n      [displayWith]=\"displayUserName\"\n      [placeholder]=\"'Search for users...'\"\u003e\n    \u003c/mer-select\u003e\n  `\n})\nexport class UserSearchComponent implements OnDestroy {\n    selectedUser: User | null = null;\n    userDataSource: TypeaheadDataSource\u003cUser\u003e;\n\n    constructor(private http: HttpClient) {\n        // Define a search function that returns an Observable\n        const searchFn = (query: string): Observable\u003cUser[]\u003e =\u003e {\n            return this.http.get\u003cUser[]\u003e(`/api/users?q=${query}`);\n        };\n\n        // Define options for the data source\n        const options: TypeaheadDataSourceOptions\u003cUser\u003e = {\n            compareWith: (a, b) =\u003e a.id === b.id\n        };\n\n        // Create the data source with the search function and options\n        this.userDataSource = new TypeaheadDataSource\u003cUser\u003e(searchFn, options);\n    }\n\n    // Display function for the select component\n    displayUserName(user: User): string {\n        return user?.name || '';\n    }\n}\n```\n\n##### 2. Using a TypeaheadSearchService\n\n```typescript\nimport { Injectable } from '@angular/core';\nimport { Observable, of } from 'rxjs';\nimport { delay } from 'rxjs/operators';\nimport { HttpClient } from '@angular/common/http';\nimport { TypeaheadSearchService, TypeaheadDataSource, TypeaheadDataSourceOptions } from '@merelis/angular/select';\n\n// Define your data model\ninterface User {\n    id: number;\n    name: string;\n    email: string;\n}\n\n// Implement TypeaheadSearchService for your data type\n@Injectable({ providedIn: 'root' })\nexport class UserSearchService implements TypeaheadSearchService\u003cUser\u003e {\n    constructor(private http: HttpClient) {}\n\n    search(query: string): Observable\u003cUser[]\u003e {\n        // Real implementation would use HttpClient\n        return this.http.get\u003cUser[]\u003e(`/api/users?q=${query}`);\n    }\n}\n\n@Component({\n    selector: 'app-user-search',\n    standalone: true,\n    imports: [MerSelect],\n    template: `\n    \u003cmer-select\n      [(value)]=\"selectedUser\"\n      [dataSource]=\"userDataSource\"\n      [displayWith]=\"displayUserName\"\n      [placeholder]=\"'Search for users...'\"\u003e\n    \u003c/mer-select\u003e\n  `\n})\nexport class UserSearchComponent implements OnDestroy {\n    selectedUser: User | null = null;\n    userDataSource: TypeaheadDataSource\u003cUser\u003e;\n\n    constructor(private userSearchService: UserSearchService) {\n        // Create the data source with the service and options\n        this.userDataSource = new TypeaheadDataSource\u003cUser\u003e(\n            userSearchService,\n            {\n                compareWith: (a, b) =\u003e a.id === b.id\n            }\n        );\n    }\n\n    // Rest of the component...\n}\n```\n\n### TypeaheadDataSource API\n\nThe `TypeaheadDataSource` constructor accepts the following parameters:\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| searchService | TypeaheadSearchFn\u003cT\u003e \\| TypeaheadSearchService\u003cT\u003e | Yes | A function or service that implements the search functionality |\n| options | TypeaheadDataSourceOptions\u003cT\u003e | No | Configuration options object |\n\n#### TypeaheadDataSourceOptions Properties\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| alwaysIncludeSelected | boolean | false | Whether to always include selected items in the results even if they don't match the search criteria |\n| suppressLoadingEvents | boolean | false | Whether to suppress loading event emissions |\n| compareWith | (a: T, b: T) =\u003e boolean | (a, b) =\u003e a === b | Custom function to determine equality between items |\n\n### How It Works\n\n1. **Efficient Request Handling**: When the user types in the search input, previous in-flight requests are automatically cancelled using RxJS `switchMap`, ensuring only the most recent search query is processed.\n\n2. **Loading State Management**: The data source emits loading states that the `MerSelect` can display as a progress indicator. This can be suppressed using the `suppressLoadingEvents` option.\n\n3. **Selected Items Preservation**: When `alwaysIncludeSelected` is true, selected items will always appear in the dropdown results even if they don't match the current search query.\n\n4. **Error Handling**: If the search service encounters an error, the data source will handle it gracefully, preventing the component from breaking and falling back to an empty result set.\n\n### Benefits of Using TypeaheadDataSource\n\n1. **Flexibility**: Supports two ways to implement search - through a simple function or a full service\n2. **Performance**: Efficiently handles rapid typing by cancelling outdated requests\n3. **User Experience**: Shows loading indicators at appropriate times\n4. **Resilience**: Provides graceful error handling\n5. **Adaptability**: Works with any data type and search implementation\n6. **Integration**: Seamlessly works with MerSelect's search capabilities\n\nThe `TypeaheadDataSource` implementation follows best practices for reactive programming with RxJS and works with both simple and complex typeahead scenarios.\n\n---\n\n## Custom Templates\n\nThe `MerSelect` allows customization of the trigger (clickable area) and options through templates.\n\n### Custom Trigger Template\n\n```html\n\u003cmer-select [dataSource]=\"users\" [(value)]=\"selectedUser\"\u003e\n    \u003cng-template merSelectTriggerDef\u003e\n        \u003cdiv class=\"custom-trigger\"\u003e\n            \u003cimg *ngIf=\"selectedUser?.avatar\" [src]=\"selectedUser.avatar\" class=\"avatar\"\u003e\n            \u003cspan\u003e{{ selectedUser?.name }}\u003c/span\u003e\n        \u003c/div\u003e\n    \u003c/ng-template\u003e\n\u003c/mer-select\u003e\n```\n\n### Custom Option Template\n\n```html\n\u003cmer-select [dataSource]=\"users\" [(value)]=\"selectedUser\"\u003e\n    \u003cng-template merSelectOptionDef let-option\u003e\n        \u003cdiv class=\"custom-option\"\u003e\n            \u003cimg *ngIf=\"option.avatar\" [src]=\"option.avatar\" class=\"avatar\"\u003e\n            \u003cdiv class=\"user-info\"\u003e\n                \u003cdiv class=\"name\"\u003e{{ option.name }}\u003c/div\u003e\n                \u003cdiv class=\"email\"\u003e{{ option.email }}\u003c/div\u003e\n            \u003c/div\u003e\n        \u003c/div\u003e\n    \u003c/ng-template\u003e\n\u003c/mer-select\u003e\n```\n\n---\n\n## Testing with Component Harnesses\n\nThe library provides testing harnesses for the `MerSelect` and its options, making it easier to test components that use these elements. These harnesses are built on top of Angular's Component Test Harnesses (CDK Testing) and provide a clean, implementation-detail-free way to interact with components in tests.\n\n### Installation\n\nThe testing harnesses are included in the package and can be imported from:\n\n```typescript\nimport { MerSelectHarness } from '@merelis/angular/select/testing';\nimport { MerSelectOptionHarness } from '@merelis/angular/select/testing';\n```\n\n### Setting Up Component Test Harnesses\n\nTo use the harnesses in your tests, you'll need to set up the Angular test environment with the harness environment:\n\n```typescript\nimport { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';\nimport { ComponentFixture, TestBed } from '@angular/core/testing';\nimport { MerSelectHarness } from '@merelis/angular/select/testing';\n\ndescribe('YourComponent', () =\u003e {\n    let fixture: ComponentFixture\u003cYourComponent\u003e;\n    let component: YourComponent;\n    let loader: HarnessLoader;\n\n    beforeEach(async () =\u003e {\n        await TestBed.configureTestingModule({\n            imports: [YourComponent],\n            // Include other necessary imports here\n        }).compileComponents();\n\n        fixture = TestBed.createComponent(YourComponent);\n        component = fixture.componentInstance;\n        loader = TestbedHarnessEnvironment.loader(fixture);\n    });\n\n    // Tests go here\n});\n```\n\n### MerSelectHarness API\n\nThe `MerSelectHarness` provides methods to interact with and query the state of a `MerSelect`:\n\n| Method | Description |\n|--------|-------------|\n| `static with(filters: MerSelectHarnessFilters)` | Gets a `HarnessPredicate` that can be used to find a select with specific attributes |\n| `click()` | Clicks on the select trigger to open/close the panel |\n| `clickOnClearIcon()` | Clicks on the clear icon to clear the selection |\n| `focus()` | Focuses the select input |\n| `blur()` | Removes focus from the select input |\n| `isFocused()` | Gets whether the select is focused |\n| `getValue()` | Gets the text value displayed in the select trigger |\n| `isDisabled()` | Gets whether the select is disabled |\n| `getSearchText()` | Gets the current text in the search input |\n| `setTextSearch(value: string)` | Sets the text in the search input |\n| `isOpen()` | Gets whether the options panel is open |\n| `getOptions(filters?: Omit\u003cSelectOptionHarnessFilters, 'ancestor'\u003e)` | Gets the options inside the panel |\n| `clickOptions(filters: SelectOptionHarnessFilters)` | Clicks the option(s) matching the given filters |\n\n### MerSelectOptionHarness API\n\nThe `MerSelectOptionHarness` provides methods to interact with and query the state of a select option:\n\n| Method | Description |\n|--------|-------------|\n| `static with(filters: SelectOptionHarnessFilters)` | Gets a `HarnessPredicate` that can be used to find an option with specific attributes |\n| `click()` | Clicks the option |\n| `getText()` | Gets the text of the option |\n| `isDisabled()` | Gets whether the option is disabled |\n| `isSelected()` | Gets whether the option is selected |\n| `isActive()` | Gets whether the option is active |\n| `isMultiple()` | Gets whether the option is in multiple selection mode |\n\n### Example Test\n\nHere's an example of testing a component that uses `MerSelect`:\n\n```typescript\nimport { Component } from '@angular/core';\nimport { ComponentFixture, TestBed } from '@angular/core/testing';\nimport { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';\nimport { HarnessLoader } from '@angular/cdk/testing';\nimport { MerSelectHarness, MerSelectOptionHarness } from '@merelis/angular/select/testing';\nimport { MerSelect } from '@merelis/angular/select';\n\n@Component({\n    template: `\n    \u003cmer-select\n      [dataSource]=\"fruits\"\n      [(value)]=\"selectedFruit\"\n      [placeholder]=\"'Select a fruit'\"\u003e\n    \u003c/mer-select\u003e\n  `,\n    standalone: true,\n    imports: [MerSelect]\n})\nclass TestComponent {\n    fruits = ['Apple', 'Banana', 'Orange', 'Strawberry'];\n    selectedFruit: string | null = null;\n}\n\ndescribe('TestComponent', () =\u003e {\n    let fixture: ComponentFixture\u003cTestComponent\u003e;\n    let component: TestComponent;\n    let loader: HarnessLoader;\n\n    beforeEach(async () =\u003e {\n        await TestBed.configureTestingModule({\n            imports: [TestComponent]\n        }).compileComponents();\n\n        fixture = TestBed.createComponent(TestComponent);\n        component = fixture.componentInstance;\n        loader = TestbedHarnessEnvironment.loader(fixture);\n        fixture.detectChanges();\n    });\n\n    it('should open the select and select an option', async () =\u003e {\n        // Get the select harness\n        const select = await loader.getHarness(MerSelectHarness);\n\n        // Check initial state\n        expect(await select.getValue()).toBe('');\n        expect(await select.isOpen()).toBe(false);\n\n        // Open the select\n        await select.click();\n        expect(await select.isOpen()).toBe(true);\n\n        // Get all options\n        const options = await select.getOptions();\n        expect(options.length).toBe(4);\n\n        // Click the \"Banana\" option\n        await select.clickOptions({ text: 'Banana' });\n\n        // Check that the panel is closed after selection\n        expect(await select.isOpen()).toBe(false);\n\n        // Check that the value is updated\n        expect(await select.getValue()).toBe('Banana');\n        expect(component.selectedFruit).toBe('Banana');\n    });\n\n    it('should filter options based on search text', async () =\u003e {\n        const select = await loader.getHarness(MerSelectHarness);\n\n        // Open the select\n        await select.click();\n\n        // Enter search text\n        await select.setTextSearch('ber');\n\n        // Get filtered options\n        const options = await select.getOptions();\n        expect(options.length).toBe(1);\n        expect(await options[0].getText()).toBe('Strawberry');\n\n        // Select the filtered option\n        await options[0].click();\n        expect(await select.getValue()).toBe('Strawberry');\n    });\n});\n```\n\n### Testing with Complex Data Structures\n\nWhen using objects as options, you can leverage the harness methods to test more complex scenarios:\n\n```typescript\nimport { Component } from '@angular/core';\nimport { ComponentFixture, TestBed } from '@angular/core/testing';\nimport { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';\nimport { HarnessLoader } from '@angular/cdk/testing';\nimport { MerSelectHarness } from '@merelis/angular/select/testing';\nimport { MerSelect } from '@merelis/angular/select';\n\ninterface User {\n    id: number;\n    name: string;\n    email: string;\n}\n\n@Component({\n    template: `\n    \u003cmer-select\n      [dataSource]=\"users\"\n      [(value)]=\"selectedUser\"\n      [displayWith]=\"displayUser\"\n      [compareWith]=\"compareUsers\"\n      [placeholder]=\"'Select a user'\"\u003e\n    \u003c/mer-select\u003e\n  `,\n    standalone: true,\n    imports: [MerSelect]\n})\nclass UserSelectComponent {\n    users: User[] = [\n        { id: 1, name: 'John Doe', email: 'john@example.com' },\n        { id: 2, name: 'Jane Smith', email: 'jane@example.com' },\n        { id: 3, name: 'Bob Johnson', email: 'bob@example.com' }\n    ];\n    selectedUser: User | null = null;\n\n    displayUser(user: User): string {\n        return user?.name || '';\n    }\n\n    compareUsers(user1: User, user2: User): boolean {\n        return user1?.id === user2?.id;\n    }\n}\n\ndescribe('UserSelectComponent', () =\u003e {\n    let fixture: ComponentFixture\u003cUserSelectComponent\u003e;\n    let component: UserSelectComponent;\n    let loader: HarnessLoader;\n\n    beforeEach(async () =\u003e {\n        await TestBed.configureTestingModule({\n            imports: [UserSelectComponent]\n        }).compileComponents();\n\n        fixture = TestBed.createComponent(UserSelectComponent);\n        component = fixture.componentInstance;\n        loader = TestbedHarnessEnvironment.loader(fixture);\n        fixture.detectChanges();\n    });\n\n    it('should select a user by name and update the component model', async () =\u003e {\n        const select = await loader.getHarness(MerSelectHarness);\n\n        // Open the select\n        await select.click();\n\n        // Click the option with Jane's name\n        await select.clickOptions({ text: 'Jane Smith' });\n\n        // Check that the select shows the correct text\n        expect(await select.getValue()).toBe('Jane Smith');\n\n        // Check that the component model is updated with the correct object\n        expect(component.selectedUser).toEqual(component.users[1]);\n        expect(component.selectedUser?.id).toBe(2);\n    });\n});\n```\n\n### Testing Multiple Selection\n\nYou can also test the multiple selection mode of the `MerSelect`:\n\n```typescript\n@Component({\n    template: `\n    \u003cmer-select\n      [dataSource]=\"colors\"\n      [(value)]=\"selectedColors\"\n      [multiple]=\"true\"\n      [placeholder]=\"'Select colors'\"\u003e\n    \u003c/mer-select\u003e\n  `,\n    standalone: true,\n    imports: [MerSelect]\n})\nclass ColorSelectComponent {\n    colors = ['Red', 'Green', 'Blue', 'Yellow', 'Purple'];\n    selectedColors: string[] = [];\n}\n\ndescribe('ColorSelectComponent', () =\u003e {\n    // Test setup...\n\n    it('should support multiple selection', async () =\u003e {\n        const select = await loader.getHarness(MerSelectHarness);\n\n        // Open the select\n        await select.click();\n\n        // Select multiple options\n        await select.clickOptions({ text: 'Red' });\n        await select.clickOptions({ text: 'Blue' });\n        await select.clickOptions({ text: 'Yellow' });\n\n        // Check component model\n        expect(component.selectedColors).toEqual(['Red', 'Blue', 'Yellow']);\n\n        // Verify that the selected options are marked as selected\n        const options = await select.getOptions();\n        for (const option of options) {\n            const text = await option.getText();\n            const isSelected = await option.isSelected();\n\n            if (['Red', 'Blue', 'Yellow'].includes(text)) {\n                expect(isSelected).toBe(true);\n            } else {\n                expect(isSelected).toBe(false);\n            }\n        }\n    });\n});\n```\n\n---\n\n## Integration with Angular Material\nThe MerSelect can be integrated with Angular Material's mat-form-field component through the @merelis/angular-material package. This integration allows you to use the select component within Material's form field, benefiting from features like floating labels, hints, and error messages.\n\n### Installation\n\n```bash\nnpm install @merelis/angular-material --save\n```\n\n### Usage with mat-form-field\n\n```typescript\nimport { Component } from '@angular/core';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MerSelect } from '@merelis/angular/select';\nimport { MerSelectFormFieldControl } from \"@merelis/angular-material/select\";\n\n@Component({\n    selector: 'app-material-example',\n    standalone: true,\n    imports: [\n        MatFormFieldModule,\n        MatInputModule,\n        MerSelect,\n        MerSelectFormFieldControl\n    ],\n    providers: [\n        provideMerMaterialIntegration() // Enable integration with Angular Material\n    ],\n    template: `\n    \u003cmat-form-field appearance=\"outline\"\u003e\n      \u003cmat-label\u003eSelect a user\u003c/mat-label\u003e\n      \u003cmer-select merSelectFormField\n        [dataSource]=\"users\"\n        [(value)]=\"selectedUser\"\n        [displayWith]=\"displayUserName\"\n        [compareWith]=\"compareUsers\"\u003e\n      \u003c/mer-select\u003e\n      \u003cmat-hint\u003eSelect a user from the list\u003c/mat-hint\u003e\n      \u003cmat-error\u003ePlease select a valid user\u003c/mat-error\u003e\n    \u003c/mat-form-field\u003e\n  `\n})\nexport class MaterialExampleComponent {\n    users = [\n        { id: 1, name: 'John Smith' },\n        { id: 2, name: 'Mary Johnson' },\n        { id: 3, name: 'Peter Williams' }\n    ];\n    selectedUser = null;\n\n    displayUserName(user: any): string {\n        return user?.name || '';\n    }\n\n    compareUsers(user1: any, user2: any): boolean {\n        return user1?.id === user2?.id;\n    }\n}\n```\n\n---\n\n## Component Integration\n\nThe `MerSelect` internally uses the `MerProgressBar` to display a loading indicator when the `loading` property is set to `true`.\n\n```html\n\u003cmer-select\n    [dataSource]=\"dataItems\"\n    [(value)]=\"selectedItem\"\n    [loading]=\"isLoadingData\"\u003e\n\u003c/mer-select\u003e\n```\n\n---\n\n### CSS Customization\n\nThe components can be customized using CSS variables. Below are the available variables for each component.\n\n#### MerSelect\n\n```scss\n.mer-select {\n    // Base select appearance\n    --mer-select-font: system-ui, Roboto, sans-serif;\n    --mer-select-font-size: 1em;\n    --mer-select-font-weight: normal;\n    --mer-select-line-height: 1em;\n    --mer-select-letter-spacing: normal;\n    --mer-select-min-height: 32px;\n    --mer-select-side-padding: 8px;\n    --mer-select-input-height: 100%;\n    --mer-select-input-width: 100%;\n    --mer-select-trigger-wrapper-gap: 4px;\n\n\n    // multiple select  \n    --mer-select-multiple-trigger-wrapper-gap: 4px;\n    --mer-select-multiple-side-padding: 2px;\n    --mer-select-multiple-input-min-width: 33%;\n    --mer-select-multiple-input-height: 24px;\n    --mer-select-multiple-input-padding: 0 4px;\n    --mer-select-multiple-values-gap: 4px;\n    --mer-select-multiple-values-padding: 0;\n    --mer-select-chip-text-color: inherit;\n    --mer-select-chip-background-color: #e6e6e6;\n    --mer-select-chip-border-radius: 8px;\n    --mer-select-chip-border: none;\n    --mer-select-chip-padding-top: 2px;\n    --mer-select-chip-padding-right: 2px;\n    --mer-select-chip-padding-bottom: 2px;\n    --mer-select-chip-padding-left: 8px;\n    --mer-select-chip-font-size: 0.875rem;\n\n    --mer-select-chip-text-color-hover: var(--mer-select-chip-text-color, inherit);\n    --mer-select-chip-background-color-hover: var(--mer-select-chip-background-color,#e6e6e6);\n    --mer-select-chip-border-hover: var(--mer-select-chip-border, none);\n\n    --mer-select-chip-readonly-padding-right: 8px;\n\n    --mer-select-chip-remove-cursor: pointer;\n    --mer-select-chip-remove-margin-left: 4px;\n    --mer-select-chip-remove-font-size: 1rem;\n    --mer-select-chip-remove-line-height: 1rem;\n    --mer-select-chip-remove-font-weight: normal;\n    --mer-select-chip-remove-text-color: #000;\n    --mer-select-chip-remove-bg-color: #d1d1d1;\n    --mer-select-chip-remove-border-radius: 9999px;\n    --mer-select-chip-remove-padding: 0;\n    --mer-select-chip-remove-width: 12px;\n    --mer-select-chip-remove-height: 12px;\n    --mer-select-chip-remove-opacity: .5;\n    --mer-select-chip-remove-border: none;\n\n    --mer-select-chip-remove-text-color-hover: white;\n    --mer-select-chip-remove-bg-color-hover: #505050;\n    --mer-select-chip-remove-opacity-hover: 1;\n    --mer-select-chip-remove-border-hover: none;\n\n\n\n    // Colors and states\n    --mer-select-background-color: white;\n    --mer-select-color: black;\n    --mer-select-border: 1px solid #8c8a8a;\n\n    // Focus state\n    --mer-select-background-color--focused: white;\n    --mer-select-color--focused: black;\n    --mer-select-border--focused: 1px solid #8c8a8a;\n    --mer-select-outline--focused: solid #4e95e8 2px;\n    --mer-select-outline-offset--focused: -1px;\n\n    // Disabled state\n    --mer-select-background-color--disabled: #ececec;\n    --mer-select-color--disabled: #707070;\n    --mer-select-border--disabled: 1px solid #8c8a8a;\n\n    // Invalid state\n    --mer-select-background-color--invalid: white;\n    --mer-select-color--invalid: black;\n    --mer-select-border--invalid: 1px solid #c10909;\n    --mer-select-outline--invalid: solid #c10909 2px;\n    --mer-select-outline-offset--invalid: -1px;\n\n    // Icons\n    --mer-select-chevron-icon-color: #b3b3b3;\n    --mer-select-chevron-icon-color--hover: #353535;\n\n    // Loading indicator\n    --mer-select-loading-height: 2px;\n    --mer-select-loading-background-color: #d7e8fb;\n    --mer-select-loading-color: #0772CD;\n}\n```\n\n#### MerSelect Panel\n\n```scss\n.mer-select-panel {\n    --mer-select-panel-background-color: #ffffff;\n    --mer-select-panel-border-radius: 8px;\n    --mer-select-panel-box-shadow: rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px;\n}\n```\n\n#### MerOption\n\n```scss\n.mer-option {\n    // Base option appearance\n    --mer-option-font: system-ui, Roboto, sans-serif;\n    --mer-option-font-size: 1em;\n    --mer-option-font-weight: normal;\n    --mer-option-line-height: 1em;\n    --mer-option-letter-spacing: normal;\n    --mer-option-min-height: 48px;\n    --mer-option-side-padding: 8px;\n    --mer-option-material-side-padding: 16px;\n    --mer-option-group-indent: 20px;\n\n    // Colors and states\n    --mer-option-color: #121212;\n    --mer-option-hover-background-color: #f6f6f6;\n    --mer-option-active-background-color: #ececec;\n    --mer-option-selected-color: #0d67ca;\n    --mer-option-selected-background-color: #eef6ff;\n\n    --mer-option-selected-hover-color: #0d67ca;\n    --mer-option-selected-hover-background-color: #e1eef8;\n    --mer-option-selected-active-color: #0d67ca;\n    --mer-option-selected-active-background-color: #dcecfb;\n    --mer-option-selected-active-hover-color: #0d67ca;\n    --mer-option-selected-active-hover-background-color: #dceafa;\n}\n\n```\n\n#### MerProgressBar\n\n```scss\n.mer-progress-bar {\n    --mer-progress-bar-height: 4px;\n    --mer-progress-bar-background-color: rgba(5, 114, 206, 0.2);\n    --mer-progress-bar-color: rgb(5, 114, 206);\n}\n```\n\n---\n\n## Contributing\n\nContributions are welcome! Feel free to open issues or submit pull requests.\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/amazing-feature`)\n3. Commit your changes (`git commit -m 'Add some amazing feature'`)\n4. Push to the branch (`git push origin feature/amazing-feature`)\n5. Open a Pull Request\n\n## License\n\nThis project is licensed under the MIT License - see the LICENSE file for details.\n","funding_links":[],"categories":["Third Party Components"],"sub_categories":["UI Libraries"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjean-merelis%2Fangular-components","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjean-merelis%2Fangular-components","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjean-merelis%2Fangular-components/lists"}