Интеграция с Angular

Интеграция Slim Sel ect в Angular-проекты опирается на классическую модель: библиотека работает с нативным <select>, а Angular управляет состоянием формы и жизненным циклом компонентов. Такой подход позволяет совместить декларативность Angular с лёгкостью и производительностью Slim Sel ect.

Установка Slim Sel ect выполняется через пакетный менеджер:

npm install slim-select

В Angular-проекте дополнительно подключается типовой стиль библиотеки:

// angular.json
"styles": [
  "node_modules/slim-select/dist/slimselect.css"
]

Импорт JavaScript-модуля выполняется внутри компонента:

import SlimSelect fr om 'slim-select';

Базовая интеграция через компонент Angular

Angular-компонент служит контейнером для инициализации Slim Sel ect после рендера DOM-элемента. Основная задача — получить ссылку на <select> через ViewChild.

import { Component, ElementRef, AfterViewInit, ViewChild, OnDestroy } fr om '@angular/core';
import SlimSelect fr om 'slim-select';

@Component({
  selector: 'app-select',
  template: `
    <sel ect #select>
      <option value="1">Первый</option>
      <option value="2">Второй</option>
      <option value="3">Третий</option>
    </select>
  `
})
export class SelectComponent implements AfterViewInit, OnDestroy {
  @ViewChild('sel ect', { static: true }) selectRef!: ElementRef<HTMLSelectElement>;

  private slim!: SlimSelect;

  ngAfterViewInit(): void {
    this.slim = new SlimSelect({
      select: this.selectRef.nativeElement
    });
  }

  ngOnDestroy(): void {
    this.slim?.destroy();
  }
}

Ключевой момент заключается в инициализации в ngAfterViewInit, поскольку DOM должен быть уже сформирован.


Связка Slim Select с Reactive Forms

Интеграция с реактивными формами требует синхронизации состояния Angular FormControl и внутреннего состояния Slim Select.

import { Component, AfterViewInit, OnDestroy, ViewChild, ElementRef } fr om '@angular/core';
import { FormControl, ReactiveFormsModule } fr om '@angular/forms';
import SlimSelect fr om 'slim-select';

@Component({
  selector: 'app-select-reactive',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <sel ect #select [formControl]="control">
      <option value="a">A</option>
      <option value="b">B</option>
      <option value="c">C</option>
    </select>
  `
})
export class SelectReactiveComponent implements AfterViewInit, OnDestroy {
  @ViewChild('select', { static: true }) selectRef!: ElementRef<HTMLSelectElement>;

  control = new FormControl('a');
  private slim!: SlimSelect;

  ngAfterViewInit(): void {
    this.slim = new SlimSelect({
      select: this.selectRef.nativeElement,
      value: this.control.value
    });

    this.control.valueChanges.subscribe(value => {
      this.slim.setSelected(value as string);
    });
  }

  ngOnDestroy(): void {
    this.slim?.destroy();
  }
}

Синхронизация работает в обе стороны: Angular обновляет Slim Select, а изменения выбора можно пробрасывать через события Slim Select в FormControl.


Обработка изменений выбора

Slim Select предоставляет callback onChange, который используется для синхронизации с Angular формами или локальным состоянием компонента.

this.slim = new SlimSelect({
  select: this.selectRef.nativeElement,
  onChange: (info) => {
    this.control.setValue(info.value);
  }
});

info.value содержит выбранное значение, что позволяет избежать ручного чтения DOM.


Динамическое обновление данных

Angular часто работает с данными, полученными из API. Slim Select требует ручного обновления списка опций при изменении массива.

options = [
  { text: 'Первый', value: '1' },
  { text: 'Второй', value: '2' }
];

updateOptions(): void {
  this.options = [
    { text: 'Обновлённый 1', value: '10' },
    { text: 'Обновлённый 2', value: '20' }
  ];

  const select = this.selectRef.nativeElement;

  select.innerHTML = this.options
    .map(o => `<option value="${o.value}">${o.text}</option>`)
    .join('');

  this.slim.setData(this.options);
}

Важный аспект — Slim Select не отслеживает изменения Angular автоматически, поэтому обновление данных требует явного вызова setData.


Работа с асинхронной загрузкой

Типовой сценарий — загрузка данных с сервера и последующая инициализация.

ngAfterViewInit(): void {
  fetch('/api/options')
    .then(res => res.json())
    .then(data => {
      const select = this.selectRef.nativeElement;

      select.innerHTML = data
        .map((d: any) => `<option value="${d.id}">${d.name}</option>`)
        .join('');

      this.slim = new SlimSelect({
        select
      });
    });
}

При таком подходе инициализация происходит только после получения данных, что исключает необходимость последующего пересоздания экземпляра.


Интеграция с Template-driven Forms

Template-driven подход требует минимальной логики в компоненте.

@Component({
  selector: 'app-template-select',
  template: `
    <select [(ngModel)]="value" #select="ngModel">
      <option value="x">X</option>
      <option value="y">Y</option>
    </select>
  `
})
export class TemplateSelectComponent implements AfterViewInit {
  @ViewChild('select', { static: true }) selectRef!: ElementRef<HTMLSelectElement>;

  value = 'x';

  private slim!: SlimSelect;

  ngAfterViewInit(): void {
    this.slim = new SlimSelect({
      select: this.selectRef.nativeElement,
      value: this.value,
      onChange: (info) => {
        this.value = info.value;
      }
    });
  }
}

Angular и Slim Select в этом случае работают через общий источник данных — переменную value.


Поддержка multiple select

Slim Select полностью поддерживает множественный выбор, но Angular требует массив значений.

<select #select multiple>
  <option value="1">A</option>
  <option value="2">B</option>
  <option value="3">C</option>
</select>
control = new FormControl(['1', '3']);

ngAfterViewInit(): void {
  this.slim = new SlimSelect({
    select: this.selectRef.nativeElement,
    value: this.control.value
  });

  this.control.valueChanges.subscribe(values => {
    this.slim.setSelected(values as string[]);
  });
}

Slim Select возвращает массив значений в onChange, что напрямую соответствует Angular модели формы.


Управление жизненным циклом и уничтожение экземпляра

Angular уничтожает компоненты при навигации, поэтому экземпляр Slim Select должен быть корректно освобождён.

ngOnDestroy(): void {
  if (this.slim) {
    this.slim.destroy();
  }
}

Отсутствие уничтожения приводит к утечкам памяти и сохранению слушателей событий.


Использование внутри структурных директив

При использовании *ngIf или динамического рендеринга важно учитывать момент создания DOM.

<div *ngIf="visible">
  <select #select>
    <option value="1">One</option>
  </select>
</div>
setTimeout(() => {
  this.slim = new SlimSelect({
    select: this.selectRef.nativeElement
  });
});

Более корректный подход — отслеживание visible через ngAfterViewChecked, но это требует защиты от повторной инициализации.


Обновление выбранного значения программно

Slim Select позволяет напрямую управлять выбранным значением без вмешательства DOM.

setValue(): void {
  this.slim.setSelected('2');
}

Для множественного выбора:

this.slim.setSelected(['1', '3']);

Angular при этом должен синхронизировать своё состояние через FormControl.


Типизация и работа с моделями данных

При использовании TypeScript удобно вводить строгую модель опций:

interface SelectOption {
  text: string;
  value: string;
}
options: SelectOption[] = [];

Slim Select принимает тот же формат, что позволяет избегать преобразований при обновлении данных:

this.slim.setData(this.options);

Особенности производительности в Angular-приложениях

При частых обновлениях данных важно минимизировать пересоздание экземпляра Slim Select. Оптимальная стратегия:

  • инициализация один раз в ngAfterViewInit
  • обновление через setData
  • синхронизация через FormControl
  • избегание прямого DOM-manipulation вне Angular lifecycle

При больших списках целесообразно использовать серверную фильтрацию, так как Slim Select не предназначен для виртуализации списка на уровне Angular CDK.


Взаимодействие с Angular Change Detection

Slim Select работает вне зоны Angular, поэтому изменения, происходящие в onChange, могут не инициировать детектирование автоматически.

import { ChangeDetectorRef } fr om '@angular/core';

constructor(private cdr: ChangeDetectorRef) {}

this.slim = new SlimSelect({
  select: this.selectRef.nativeElement,
  onChange: (info) => {
    this.control.setValue(info.value);
    this.cdr.detectChanges();
  }
});

Такой подход гарантирует актуализацию UI при внешних изменениях.


Комбинирование с кастомными компонентами

Slim Select часто используется как базовый слой для кастомных Angular компонентов селектов, где логика UI полностью переопределяется, а Slim Select отвечает за обработку выбора и управление состоянием списка.