npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@merelis/angular

v2.0.0

Published

A library of reusable Angular components and utilities that provides high-quality UI elements for your applications.

Readme

Merelis Angular Components

A library of reusable Angular components and utilities that provides high-quality UI elements for your applications.

License: MIT

Showcase

https://jean-merelis.github.io/angular-components/

Installation

npm install @merelis/angular --save

Available Components

Currently, the library provides the following components:

MerSelect

An advanced select component with filtering and typeahead capabilities. Supports single or multiple selection, full customization, reactive forms integration, and conditional rendering.

MerProgressBar

A progress bar component that can be used independently or integrated with other components.

Installation and Usage

Initial Setup

After installing the package, you need to import the necessary styles in your application's styles.scss file:

@use '@angular/cdk/overlay-prebuilt.css';
@use '@merelis/angular/select/styles';

This will import both the CDK overlay styles (required for the dropdown functionality) and the component-specific styles.

Since these are standalone components, you can import them directly in your components:

Direct import in a component

import { Component } from '@angular/core';
import { MerSelect } from '@merelis/angular/select';
import { MerProgressBar } from '@merelis/angular/progress-bar';

@Component({
    selector: 'app-example',
    standalone: true,
    imports: [
        MerSelect,
        MerProgressBar
    ],
    template: `
    <mer-select [dataSource]="items" [(value)]="selectedItem"></mer-select>
    <mer-progress-bar [value]="0.5"></mer-progress-bar>
  `
})
export class ExampleComponent {
    // ...
}

MerSelect

The MerSelect offers a robust alternative to the native HTML select, with additional features like filtering and typeahead.

Basic HTML

<mer-select 
  [dataSource]="optionsList" 
  [(value)]="selectedValue"
  [placeholder]="'Select an option'">
</mer-select>

Input Properties

| Name | Type | Default | Description | |------|------|---------|-------------| | dataSource | Array<T> | SelectDataSource<T> | undefined | List of available options for selection or data source for the component | | value | T | T[] | null | undefined | Currently selected value | | loading | boolean | false | Displays loading indicator using MerProgressBar | | disabled | boolean | false | Disables the component | | readOnly | boolean | false | Sets the component as read-only | | disableSearch | boolean | false | Disables text search functionality | | disableOpeningWhenFocusedByKeyboard | boolean | false | Prevents the panel from opening automatically when focused via keyboard | | multiple | boolean | false | Allows multiple selection | | canClear | boolean | true | Allows clearing the selection | | 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. | | autoActiveFirstOption | boolean | true | Automatically activates the first option when the panel is opened | | debounceTime | number | 100 | Debounce time for text input (in ms) | | panelOffsetY | number | 0 | Vertical offset of the options panel | | compareWith | Comparable<T> | undefined | Function to compare values | | displayWith | DisplayWith<T> | undefined | Function to display values as text | | filterPredicate | FilterPredicate<T> | undefined | Function to filter options based on typed text. Note: Only effective when using an array as dataSource, not when using a custom SelectDataSource. | | disableOptionPredicate | OptionPredicate<T> | () => false | Function to determine which options should be disabled | | disabledOptions | T[] | [] | List of options that should be disabled | | connectedTo | MerSelectPanelOrigin | undefined | Element to which the panel should connect | | panelClass | string | string[] | undefined | CSS class(es) applied to the options panel | | panelWidth | string | number | undefined | Width of the options panel | | position | 'auto' | 'above' | 'below' | 'auto' | Position of the panel relative to the input | | placeholder | string | undefined | Text to display when no item is selected |

Output Events

| Name | Description | |------|-------------| | opened | Emitted when the options panel is opened | | closed | Emitted when the options panel is closed | | focus | Emitted when the component receives focus | | blur | Emitted when the component loses focus | | inputChanges | Emitted when the text input value changes |

Complete Example

import { Component } from '@angular/core';

interface User {
  id: number;
  name: string;
}

@Component({
  selector: 'app-example',
  template: `
    <mer-select
      [dataSource]="users"
      [(value)]="selectedUser"
      [displayWith]="displayUserName"
      [compareWith]="compareUsers"
      [placeholder]="'Select a user'"
      [loading]="isLoading"
      (opened)="onPanelOpened()"
      (closed)="onPanelClosed()"
      (inputChanges)="onInputChanged($event)">
    </mer-select>
  `
})
export class ExampleComponent {
  users: User[] = [
    { id: 1, name: 'John Smith' },
    { id: 2, name: 'Mary Johnson' },
    { id: 3, name: 'Peter Williams' }
  ];
  selectedUser: User | null = null;
  isLoading = false;

  displayUserName(user: User): string {
    return user.name;
  }

  compareUsers(user1: User, user2: User): boolean {
    return user1?.id === user2?.id;
  }

  onPanelOpened(): void {
    console.log('Options panel opened');
  }

  onPanelClosed(): void {
    console.log('Options panel closed');
  }

  onInputChanged(text: string): void {
    console.log('Search text:', text);
  }
}

Filtering Behavior and DataSource

The MerSelect supports two operation modes for data filtering:

1. Automatic Filtering (Array as dataSource)

When 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:

| Name | Type | Description | |------|------|-------------| | filterPredicate | FilterPredicate<T> | Custom function to filter options based on typed text. Only applied when the dataSource is an array, not a custom SelectDataSource. | | 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. |

2. Custom Filtering (SelectDataSource)

When you implement and provide a custom SelectDataSource, the filtering behavior is determined by the implementation of the dataSource's applyFilter method. In this case:

  • The component invokes the applyFilter method when the user types
  • The filterPredicate and alwaysIncludesSelected inputs are ignored
  • The filtering logic is entirely controlled by the dataSource
export class CustomDataSource<T> implements SelectDataSource<T> {
    // ...
    
    async applyFilter(criteria: FilterCriteria<T>): void | Promise<void> {
        // Here you implement your own filtering logic
        // The criteria parameter contains:
        // - searchText: the text typed by the user
        // - selected: the currently selected item(s)
        
        // You can decide to include selected items even if they don't match the filter
        // (equivalent to the alwaysIncludesSelected behavior)
        
        // You can also implement your own filtering logic
        // (equivalent to the filterPredicate behavior)
    }
}

Choosing Between Array and SelectDataSource

  • Use a simple array when you have a small set of static data that doesn't require server-side filtering.
  • Implement a SelectDataSource when you need complete control over filtering, especially for:
    • Fetching data from the server based on typed text (typeahead)
    • Handling large datasets
    • Implementing complex filtering logic
    • Showing loading indicators during asynchronous operations

Typeahead Functionality with TypeaheadDataSource

The 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.

TypeaheadSearchFn Type

A simple function type that can be used to perform typeahead searches:

export type TypeaheadSearchFn<T> = (query: string) => Observable<T[]>;

TypeaheadSearchService Interface

Alternatively, you can implement the TypeaheadSearchService interface to define how search operations will be performed:

export interface TypeaheadSearchService<T> {
    /**
     * Search method that takes a query string and returns an Observable of results
     * @param query The search query string
     * @returns Observable of search results
     */
    search(query: string): Observable<T[]>;
}

TypeaheadDataSourceOptions Interface

The TypeaheadDataSource accepts a configuration options object:

export interface TypeaheadDataSourceOptions<T> {
  /**
   * Whether to always include selected items in the results. Default false.
   */
  alwaysIncludeSelected?: boolean;

  /**
   * Whether to suppress loading events. Default false.
   */
  suppressLoadingEvents?: boolean;

  /**
   * Custom function to compare items for equality (defaults to comparing by reference)
   * @param a First item to compare
   * @param b Second item to compare
   */
  compareWith?: (a: T, b: T) => boolean;
}

Using the TypeaheadDataSource

The TypeaheadDataSource provides a robust solution for typeahead functionality with automatic cancellation of previous requests, which is essential for a smooth user experience.

Implementation

You can implement typeahead functionality in two ways:

1. Using a Simple Search Function
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { TypeaheadDataSource, TypeaheadDataSourceOptions } from '@merelis/angular/select';

// Define your data model
interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  selector: 'app-user-search',
  standalone: true,
  imports: [MerSelect],
  template: `
    <mer-select
      [(value)]="selectedUser"
      [dataSource]="userDataSource"
      [displayWith]="displayUserName"
      [placeholder]="'Search for users...'">
    </mer-select>
  `
})
export class UserSearchComponent implements OnDestroy {
  selectedUser: User | null = null;
  userDataSource: TypeaheadDataSource<User>;
  
  constructor(private http: HttpClient) {
    // Define a search function that returns an Observable
    const searchFn = (query: string): Observable<User[]> => {
      return this.http.get<User[]>(`/api/users?q=${query}`);
    };
    
    // Define options for the data source
    const options: TypeaheadDataSourceOptions<User> = {
      compareWith: (a, b) => a.id === b.id
    };
    
    // Create the data source with the search function and options
    this.userDataSource = new TypeaheadDataSource<User>(searchFn, options);
  }

  // Display function for the select component
  displayUserName(user: User): string {
    return user?.name || '';
  }
}
2. Using a TypeaheadSearchService
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { TypeaheadSearchService, TypeaheadDataSource, TypeaheadDataSourceOptions } from '@merelis/angular/select';

// Define your data model
interface User {
  id: number;
  name: string;
  email: string;
}

// Implement TypeaheadSearchService for your data type
@Injectable({ providedIn: 'root' })
export class UserSearchService implements TypeaheadSearchService<User> {
  constructor(private http: HttpClient) {}
  
  search(query: string): Observable<User[]> {
    // Real implementation would use HttpClient
    return this.http.get<User[]>(`/api/users?q=${query}`);
  }
}

@Component({
  selector: 'app-user-search',
  standalone: true,
  imports: [MerSelect],
  template: `
    <mer-select
      [(value)]="selectedUser"
      [dataSource]="userDataSource"
      [displayWith]="displayUserName"
      [placeholder]="'Search for users...'">
    </mer-select>
  `
})
export class UserSearchComponent implements OnDestroy {
  selectedUser: User | null = null;
  userDataSource: TypeaheadDataSource<User>;
  
  constructor(private userSearchService: UserSearchService) {
    // Create the data source with the service and options
    this.userDataSource = new TypeaheadDataSource<User>(
      userSearchService,
      {
        compareWith: (a, b) => a.id === b.id
      }
    );
  }
  
  // Rest of the component...
}

TypeaheadDataSource API

The TypeaheadDataSource constructor accepts the following parameters:

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | searchService | TypeaheadSearchFn | TypeaheadSearchService | Yes | A function or service that implements the search functionality | | options | TypeaheadDataSourceOptions | No | Configuration options object |

TypeaheadDataSourceOptions Properties

| Option | Type | Default | Description | |--------|------|---------|-------------| | alwaysIncludeSelected | boolean | false | Whether to always include selected items in the results even if they don't match the search criteria | | suppressLoadingEvents | boolean | false | Whether to suppress loading event emissions | | compareWith | (a: T, b: T) => boolean | (a, b) => a === b | Custom function to determine equality between items |

How It Works

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Benefits of Using TypeaheadDataSource

  1. Flexibility: Supports two ways to implement search - through a simple function or a full service
  2. Performance: Efficiently handles rapid typing by cancelling outdated requests
  3. User Experience: Shows loading indicators at appropriate times
  4. Resilience: Provides graceful error handling
  5. Adaptability: Works with any data type and search implementation
  6. Integration: Seamlessly works with MerSelect's search capabilities

The TypeaheadDataSource implementation follows best practices for reactive programming with RxJS and works with both simple and complex typeahead scenarios.


Custom Templates

The MerSelect allows customization of the trigger (clickable area) and options through templates.

Custom Trigger Template

<mer-select [dataSource]="users" [(value)]="selectedUser">
  <ng-template merSelectTriggerDef>
    <div class="custom-trigger">
      <img *ngIf="selectedUser?.avatar" [src]="selectedUser.avatar" class="avatar">
      <span>{{ selectedUser?.name }}</span>
    </div>
  </ng-template>
</mer-select>

Custom Option Template

<mer-select [dataSource]="users" [(value)]="selectedUser">
  <ng-template merSelectOptionDef let-option>
    <div class="custom-option">
      <img *ngIf="option.avatar" [src]="option.avatar" class="avatar">
      <div class="user-info">
        <div class="name">{{ option.name }}</div>
        <div class="email">{{ option.email }}</div>
      </div>
    </div>
  </ng-template>
</mer-select>

Testing with Component Harnesses

The 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.

Installation

The testing harnesses are included in the package and can be imported from:

import { MerSelectHarness } from '@merelis/angular/select/testing';
import { MerSelectOptionHarness } from '@merelis/angular/select/testing';

Setting Up Component Test Harnesses

To use the harnesses in your tests, you'll need to set up the Angular test environment with the harness environment:

import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MerSelectHarness } from '@merelis/angular/select/testing';

describe('YourComponent', () => {
  let fixture: ComponentFixture<YourComponent>;
  let component: YourComponent;
  let loader: HarnessLoader;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [YourComponent],
      // Include other necessary imports here
    }).compileComponents();

    fixture = TestBed.createComponent(YourComponent);
    component = fixture.componentInstance;
    loader = TestbedHarnessEnvironment.loader(fixture);
  });

  // Tests go here
});

MerSelectHarness API

The MerSelectHarness provides methods to interact with and query the state of a MerSelect:

| Method | Description | |--------|-------------| | static with(filters: MerSelectHarnessFilters) | Gets a HarnessPredicate that can be used to find a select with specific attributes | | click() | Clicks on the select trigger to open/close the panel | | clickOnClearIcon() | Clicks on the clear icon to clear the selection | | focus() | Focuses the select input | | blur() | Removes focus from the select input | | isFocused() | Gets whether the select is focused | | getValue() | Gets the text value displayed in the select trigger | | isDisabled() | Gets whether the select is disabled | | getSearchText() | Gets the current text in the search input | | setTextSearch(value: string) | Sets the text in the search input | | isOpen() | Gets whether the options panel is open | | getOptions(filters?: Omit<SelectOptionHarnessFilters, 'ancestor'>) | Gets the options inside the panel | | clickOptions(filters: SelectOptionHarnessFilters) | Clicks the option(s) matching the given filters |

MerSelectOptionHarness API

The MerSelectOptionHarness provides methods to interact with and query the state of a select option:

| Method | Description | |--------|-------------| | static with(filters: SelectOptionHarnessFilters) | Gets a HarnessPredicate that can be used to find an option with specific attributes | | click() | Clicks the option | | getText() | Gets the text of the option | | isDisabled() | Gets whether the option is disabled | | isSelected() | Gets whether the option is selected | | isActive() | Gets whether the option is active | | isMultiple() | Gets whether the option is in multiple selection mode |

Example Test

Here's an example of testing a component that uses MerSelect:

import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { HarnessLoader } from '@angular/cdk/testing';
import { MerSelectHarness, MerSelectOptionHarness } from '@merelis/angular/select/testing';
import { MerSelect } from '@merelis/angular/select';

@Component({
  template: `
    <mer-select
      [dataSource]="fruits"
      [(value)]="selectedFruit"
      [placeholder]="'Select a fruit'">
    </mer-select>
  `,
  standalone: true,
  imports: [MerSelect]
})
class TestComponent {
  fruits = ['Apple', 'Banana', 'Orange', 'Strawberry'];
  selectedFruit: string | null = null;
}

describe('TestComponent', () => {
  let fixture: ComponentFixture<TestComponent>;
  let component: TestComponent;
  let loader: HarnessLoader;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [TestComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(TestComponent);
    component = fixture.componentInstance;
    loader = TestbedHarnessEnvironment.loader(fixture);
    fixture.detectChanges();
  });

  it('should open the select and select an option', async () => {
    // Get the select harness
    const select = await loader.getHarness(MerSelectHarness);
    
    // Check initial state
    expect(await select.getValue()).toBe('');
    expect(await select.isOpen()).toBe(false);
    
    // Open the select
    await select.click();
    expect(await select.isOpen()).toBe(true);
    
    // Get all options
    const options = await select.getOptions();
    expect(options.length).toBe(4);
    
    // Click the "Banana" option
    await select.clickOptions({ text: 'Banana' });
    
    // Check that the panel is closed after selection
    expect(await select.isOpen()).toBe(false);
    
    // Check that the value is updated
    expect(await select.getValue()).toBe('Banana');
    expect(component.selectedFruit).toBe('Banana');
  });

  it('should filter options based on search text', async () => {
    const select = await loader.getHarness(MerSelectHarness);
    
    // Open the select
    await select.click();
    
    // Enter search text
    await select.setTextSearch('ber');
    
    // Get filtered options
    const options = await select.getOptions();
    expect(options.length).toBe(1);
    expect(await options[0].getText()).toBe('Strawberry');
    
    // Select the filtered option
    await options[0].click();
    expect(await select.getValue()).toBe('Strawberry');
  });
});

Testing with Complex Data Structures

When using objects as options, you can leverage the harness methods to test more complex scenarios:

import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { HarnessLoader } from '@angular/cdk/testing';
import { MerSelectHarness } from '@merelis/angular/select/testing';
import { MerSelect } from '@merelis/angular/select';

interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  template: `
    <mer-select
      [dataSource]="users"
      [(value)]="selectedUser"
      [displayWith]="displayUser"
      [compareWith]="compareUsers"
      [placeholder]="'Select a user'">
    </mer-select>
  `,
  standalone: true,
  imports: [MerSelect]
})
class UserSelectComponent {
  users: User[] = [
    { id: 1, name: 'John Doe', email: '[email protected]' },
    { id: 2, name: 'Jane Smith', email: '[email protected]' },
    { id: 3, name: 'Bob Johnson', email: '[email protected]' }
  ];
  selectedUser: User | null = null;
  
  displayUser(user: User): string {
    return user?.name || '';
  }
  
  compareUsers(user1: User, user2: User): boolean {
    return user1?.id === user2?.id;
  }
}

describe('UserSelectComponent', () => {
  let fixture: ComponentFixture<UserSelectComponent>;
  let component: UserSelectComponent;
  let loader: HarnessLoader;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [UserSelectComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(UserSelectComponent);
    component = fixture.componentInstance;
    loader = TestbedHarnessEnvironment.loader(fixture);
    fixture.detectChanges();
  });

  it('should select a user by name and update the component model', async () => {
    const select = await loader.getHarness(MerSelectHarness);
    
    // Open the select
    await select.click();
    
    // Click the option with Jane's name
    await select.clickOptions({ text: 'Jane Smith' });
    
    // Check that the select shows the correct text
    expect(await select.getValue()).toBe('Jane Smith');
    
    // Check that the component model is updated with the correct object
    expect(component.selectedUser).toEqual(component.users[1]);
    expect(component.selectedUser?.id).toBe(2);
  });
});

Testing Multiple Selection

You can also test the multiple selection mode of the MerSelect:

@Component({
  template: `
    <mer-select
      [dataSource]="colors"
      [(value)]="selectedColors"
      [multiple]="true"
      [placeholder]="'Select colors'">
    </mer-select>
  `,
  standalone: true,
  imports: [MerSelect]
})
class ColorSelectComponent {
  colors = ['Red', 'Green', 'Blue', 'Yellow', 'Purple'];
  selectedColors: string[] = [];
}

describe('ColorSelectComponent', () => {
  // Test setup...

  it('should support multiple selection', async () => {
    const select = await loader.getHarness(MerSelectHarness);
    
    // Open the select
    await select.click();
    
    // Select multiple options
    await select.clickOptions({ text: 'Red' });
    await select.clickOptions({ text: 'Blue' });
    await select.clickOptions({ text: 'Yellow' });
    
    // Check component model
    expect(component.selectedColors).toEqual(['Red', 'Blue', 'Yellow']);
    
    // Verify that the selected options are marked as selected
    const options = await select.getOptions();
    for (const option of options) {
      const text = await option.getText();
      const isSelected = await option.isSelected();
      
      if (['Red', 'Blue', 'Yellow'].includes(text)) {
        expect(isSelected).toBe(true);
      } else {
        expect(isSelected).toBe(false);
      }
    }
  });
});

Integration with Angular Material

The 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.

Installation

npm install @merelis/angular-material --save

Usage with mat-form-field

import { Component } from '@angular/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MerSelect } from '@merelis/angular/select';
import { MerSelectFormFieldControl } from "@merelis/angular-material/select";

@Component({
  selector: 'app-material-example',
  standalone: true,
  imports: [
    MatFormFieldModule,
    MatInputModule,
    MerSelect,
    MerSelectFormFieldControl
  ],
  providers: [
    provideMerMaterialIntegration() // Enable integration with Angular Material
  ],
  template: `
    <mat-form-field appearance="outline">
      <mat-label>Select a user</mat-label>
      <mer-select merSelectFormField
        [dataSource]="users"
        [(value)]="selectedUser"
        [displayWith]="displayUserName"
        [compareWith]="compareUsers">
      </mer-select>
      <mat-hint>Select a user from the list</mat-hint>
      <mat-error>Please select a valid user</mat-error>
    </mat-form-field>
  `
})
export class MaterialExampleComponent {
  users = [
    { id: 1, name: 'John Smith' },
    { id: 2, name: 'Mary Johnson' },
    { id: 3, name: 'Peter Williams' }
  ];
  selectedUser = null;
  
  displayUserName(user: any): string {
    return user?.name || '';
  }

  compareUsers(user1: any, user2: any): boolean {
    return user1?.id === user2?.id;
  }
}

Component Integration

The MerSelect internally uses the MerProgressBar to display a loading indicator when the loading property is set to true.

<mer-select
  [dataSource]="dataItems"
  [(value)]="selectedItem"
  [loading]="isLoadingData">
</mer-select>

CSS Customization

The components can be customized using CSS variables. Below are the available variables for each component.

MerSelect

.mer-select {
  // Base select appearance
  --mer-select-font: system-ui, Roboto, sans-serif;
  --mer-select-font-size: 1em;
  --mer-select-font-weight: normal;
  --mer-select-line-height: 1em;
  --mer-select-letter-spacing: normal;
  --mer-select-min-height: 32px;
  --mer-select-side-padding: 8px;
  --mer-select-input-height: 100%;
  --mer-select-input-width: 100%;
  --mer-select-trigger-wrapper-gap: 4px;
    
    
  // multiple select  
  --mer-select-multiple-trigger-wrapper-gap: 4px;
  --mer-select-multiple-side-padding: 2px;
  --mer-select-multiple-input-min-width: 33%;
  --mer-select-multiple-input-height: 24px;
  --mer-select-multiple-input-padding: 0 4px;
  --mer-select-multiple-values-gap: 4px;
  --mer-select-multiple-values-padding: 0;
  --mer-select-chip-text-color: inherit;
  --mer-select-chip-background-color: #e6e6e6;
  --mer-select-chip-border-radius: 8px;
  --mer-select-chip-border: none;
  --mer-select-chip-padding-top: 2px;
  --mer-select-chip-padding-right: 2px;
  --mer-select-chip-padding-bottom: 2px;
  --mer-select-chip-padding-left: 8px;
  --mer-select-chip-font-size: 0.875rem;

  --mer-select-chip-text-color-hover: var(--mer-select-chip-text-color, inherit);
  --mer-select-chip-background-color-hover: var(--mer-select-chip-background-color,#e6e6e6);
  --mer-select-chip-border-hover: var(--mer-select-chip-border, none);

  --mer-select-chip-readonly-padding-right: 8px;

  --mer-select-chip-remove-cursor: pointer;
  --mer-select-chip-remove-margin-left: 4px;
  --mer-select-chip-remove-font-size: 1rem;
  --mer-select-chip-remove-line-height: 1rem;
  --mer-select-chip-remove-font-weight: normal;
  --mer-select-chip-remove-text-color: #000;
  --mer-select-chip-remove-bg-color: #d1d1d1;
  --mer-select-chip-remove-border-radius: 9999px;
  --mer-select-chip-remove-padding: 0;
  --mer-select-chip-remove-width: 12px;
  --mer-select-chip-remove-height: 12px;
  --mer-select-chip-remove-opacity: .5;
  --mer-select-chip-remove-border: none;

  --mer-select-chip-remove-text-color-hover: white;
  --mer-select-chip-remove-bg-color-hover: #505050;
  --mer-select-chip-remove-opacity-hover: 1;
  --mer-select-chip-remove-border-hover: none;

 
  
  // Colors and states
  --mer-select-background-color: white;
  --mer-select-color: black;
  --mer-select-border: 1px solid #8c8a8a;
  
  // Focus state
  --mer-select-background-color--focused: white;
  --mer-select-color--focused: black;
  --mer-select-border--focused: 1px solid #8c8a8a;
  --mer-select-outline--focused: solid #4e95e8 2px;
  --mer-select-outline-offset--focused: -1px;
  
  // Disabled state
  --mer-select-background-color--disabled: #ececec;
  --mer-select-color--disabled: #707070;
  --mer-select-border--disabled: 1px solid #8c8a8a;
  
  // Invalid state
  --mer-select-background-color--invalid: white;
  --mer-select-color--invalid: black;
  --mer-select-border--invalid: 1px solid #c10909;
  --mer-select-outline--invalid: solid #c10909 2px;
  --mer-select-outline-offset--invalid: -1px;
  
  // Icons
  --mer-select-chevron-icon-color: #b3b3b3;
  --mer-select-chevron-icon-color--hover: #353535;
  
  // Loading indicator
  --mer-select-loading-height: 2px;
  --mer-select-loading-background-color: #d7e8fb;
  --mer-select-loading-color: #0772CD;
}

MerSelect Panel

.mer-select-panel {
  --mer-select-panel-background-color: #ffffff;
  --mer-select-panel-border-radius: 8px;
  --mer-select-panel-box-shadow: rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px;
}

MerOption

.mer-option {
  // Base option appearance
  --mer-option-font: system-ui, Roboto, sans-serif;
  --mer-option-font-size: 1em;
  --mer-option-font-weight: normal;
  --mer-option-line-height: 1em;
  --mer-option-letter-spacing: normal;
  --mer-option-min-height: 48px;
  --mer-option-side-padding: 8px;
  --mer-option-material-side-padding: 16px;
  --mer-option-group-indent: 20px;
  
  // Colors and states
  --mer-option-color: #121212;
  --mer-option-hover-background-color: #f6f6f6;
  --mer-option-active-background-color: #ececec;
  --mer-option-selected-color: #0d67ca;
  --mer-option-selected-background-color: #eef6ff;
  
  --mer-option-selected-hover-color: #0d67ca;
  --mer-option-selected-hover-background-color: #e1eef8;
  --mer-option-selected-active-color: #0d67ca;
  --mer-option-selected-active-background-color: #dcecfb;
  --mer-option-selected-active-hover-color: #0d67ca;
  --mer-option-selected-active-hover-background-color: #dceafa;
}

MerProgressBar

.mer-progress-bar {
  --mer-progress-bar-height: 4px;
  --mer-progress-bar-background-color: rgba(5, 114, 206, 0.2);
  --mer-progress-bar-color: rgb(5, 114, 206);
}

Contributing

Contributions are welcome! Feel free to open issues or submit pull requests.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.