Библиотека Slim Select не содержит полноценной официальной системы плагинов в стиле крупных UI-фреймворков, однако её внутренняя архитектура позволяет создавать собственные расширения, надстройки и интеграционные модули. Под «плагином» в контексте Slim Select обычно понимаются:
Плагин строится вокруг экземпляра SlimSelect и
использует:
После инициализации создаётся объект:
const slim = new SlimSelect({
select: '#users'
})
Экземпляр содержит:
slim.setData(...)
slim.getData()
slim.setSelected()
slim.getSelected()
slim.open()
slim.close()
slim.search()
Именно вокруг этих методов обычно строится плагин.
Самый простой вариант — функция, принимающая экземпляр Slim Select.
function createLoggerPlugin(instance) {
instance.select.onValueCha nge = (value) => {
console.log('Новое значение:', value)
}
}
Использование:
const slim = new SlimSelect({
select: '#categories'
})
createLoggerPlugin(slim)
Такой подход подходит для:
Более масштабные плагины оформляются в виде класса.
class AutoSavePlugin {
constructor(instance, options = {}) {
this.instance = instance
this.options = options
this.init()
}
init() {
this.instance.select.onValueCha nge = (value) => {
this.save(value)
}
}
save(value) {
localStorage.setItem(
this.options.key,
JSON.stringify(value)
)
}
}
Использование:
const slim = new SlimSelect({
select: '#tags'
})
new AutoSavePlugin(slim, {
key: 'selected-tags'
})
Преимущества:
Большинство расширений строятся по одинаковой схеме.
constructor(instance) {
this.instance = instance
}
if (!instance.setData) {
throw new Error('Slim Select API недоступен')
}
instance.select.onValueCha nge = this.handleChange.bind(this)
this.state = {
loading: false,
cache: []
}
destroy() {
clearTimeout(this.timer)
}
Главное событие выбора.
const slim = new SlimSelect({
select: '#countries',
events: {
afterChange(newVal) {
console.log(newVal)
}
}
})
Плагин может переопределять обработчики:
class ChangeTracker {
constructor(instance) {
this.instance = instance
this.bind()
}
bind() {
const original =
this.instance.settings.events.afterChange
this.instance.settings.events.afterChange =
(newVal) => {
console.log('Изменение:', newVal)
if (original) {
original(newVal)
}
}
}
}
Такой механизм позволяет:
Для крупных систем полезен промежуточный слой.
class MiddlewarePlugin {
constructor(instance) {
this.instance = instance
this.middlewares = []
this.init()
}
use(callback) {
this.middlewares.push(callback)
}
init() {
const original =
this.instance.settings.events.afterChange
this.instance.settings.events.afterChange =
(value) => {
let current = value
for (const middleware of this.middlewares) {
current = middleware(current)
}
if (original) {
original(current)
}
}
}
}
Использование:
const plugin = new MiddlewarePlugin(slim)
plugin.use((value) => {
console.log('Middleware 1')
return value
})
plugin.use((value) => {
console.log('Middleware 2')
return value
})
Одно из самых популярных расширений — кастомный поиск.
class SearchPlugin {
constructor(instance) {
this.instance = instance
this.overrideSearch()
}
overrideSearch() {
const originalSearch =
this.instance.search.bind(this.instance)
this.instance.search = (value) => {
console.log('Поиск:', value)
return originalSearch(value)
}
}
}
class RemoteDataPlugin {
constructor(instance, options) {
this.instance = instance
this.url = options.url
this.init()
}
init() {
this.instance.settings.events.search =
this.handleSearch.bind(this)
}
async handleSearch(search, currentData) {
const response = await fetch(
`${this.url}?q=${search}`
)
const items = await response.json()
this.instance.setData(
items.map(item => ({
text: item.name,
value: item.id
}))
)
return []
}
}
Использование:
new RemoteDataPlugin(slim, {
url: '/api/users'
})
Асинхронные плагины часто используют кэш.
class CachedSearchPlugin {
constructor(instance) {
this.instance = instance
this.cache = new Map()
}
async search(query) {
if (this.cache.has(query)) {
return this.cache.get(query)
}
const response = await fetch(
`/search?q=${query}`
)
const data = await response.json()
this.cache.set(query, data)
return data
}
}
Без debounce удалённый поиск создаёт слишком много запросов.
class DebouncePlugin {
constructor(instance, delay = 300) {
this.instance = instance
this.delay = delay
this.timer = null
this.wrapSearch()
}
wrapSearch() {
const original =
this.instance.settings.events.search
this.instance.settings.events.search =
(...args) => {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
original(...args)
}, this.delay)
}
}
}
Некоторые расширения работают напрямую с DOM.
class ClearButtonPlugin {
constructor(instance) {
this.instance = instance
this.render()
}
render() {
const button =
document.createElement('button')
button.textContent = 'Очистить'
button.addEventListener('click', () => {
this.instance.setSelected([])
})
this.instance.slim.container
.appendChild(button)
}
}
Slim Select генерирует собственный DOM.
Плагин может:
class BadgePlugin {
constructor(instance) {
this.instance = instance
this.decorate()
}
decorate() {
const items =
this.instance.slim.list.querySelectorAll(
'.ss-option'
)
items.forEach(item => {
const badge =
document.createElement('span')
badge.className = 'badge'
badge.textContent = 'NEW'
item.appendChild(badge)
})
}
}
Slim Select может перерисовывать DOM.
Поэтому прямые изменения иногда исчезают.
class ObserverPlugin {
constructor(instance) {
this.instance = instance
this.observe()
}
observe() {
const observer =
new MutationObserver(() => {
console.log('DOM изменён')
})
observer.observe(
this.instance.slim.list,
{
childList: true,
subtree: true
}
)
}
}
При больших объёмах данных полезна виртуальная прокрутка.
class VirtualScrollPlugin {
constructor(instance, limit = 50) {
this.instance = instance
this.limit = limit
}
render(data) {
return data.slice(0, this.limit)
}
}
В реальных проектах виртуализация включает:
class AnalyticsPlugin {
constructor(instance) {
this.instance = instance
this.track()
}
track() {
const original =
this.instance.settings.events.afterChange
this.instance.settings.events.afterChange =
(value) => {
this.send(value)
if (original) {
original(value)
}
}
}
send(value) {
fetch('/analytics', {
method: 'POST',
body: JSON.stringify({
value,
timestamp: Date.now()
})
})
}
}
Крупные плагины удобно строить через hooks API.
class HookSystem {
constructor() {
this.hooks = {}
}
on(name, callback) {
if (!this.hooks[name]) {
this.hooks[name] = []
}
this.hooks[name].push(callback)
}
emit(name, payload) {
if (!this.hooks[name]) {
return
}
for (const callback of this.hooks[name]) {
callback(payload)
}
}
}
Несколько плагинов могут работать одновременно.
class PluginManager {
constructor(instance) {
this.instance = instance
this.plugins = []
}
register(Plugin, options) {
const plugin =
new Plugin(this.instance, options)
this.plugins.push(plugin)
return plugin
}
destroy() {
this.plugins.forEach(plugin => {
if (plugin.destroy) {
plugin.destroy()
}
})
}
}
Использование:
const manager =
new PluginManager(slim)
manager.register(RemoteDataPlugin, {
url: '/api/tags'
})
manager.register(AnalyticsPlugin)
manager.register(DebouncePlugin)
Несколько плагинов могут:
Плохой вариант:
instance.search = customSearch
Лучший вариант:
const original = instance.search.bind(instance)
instance.search = (...args) => {
console.log('before')
const result = original(...args)
console.log('after')
return result
}
Плагин не должен загрязнять экземпляр.
Плохой пример:
instance.cache = {}
Лучший вариант:
instance.plugins = instance.plugins || {}
instance.plugins.remoteCache = {}
class ValidationPlugin {
constructor(instance) {
this.instance = instance
this.init()
}
init() {
const original =
this.instance.settings.events.beforeChange
this.instance.settings.events.beforeChange =
(newValue, oldValue) => {
const valid =
this.validate(newValue)
if (!valid) {
return false
}
if (original) {
return original(newValue, oldValue)
}
return true
}
}
validate(values) {
return values.length <= 3
}
}
Плагин может использоваться внутри адаптера.
useEffect(() => {
const slim = new SlimSelect({
select: ref.current
})
const plugin =
new AnalyticsPlugin(slim)
return () => {
plugin.destroy?.()
slim.destroy()
}
}, [])
onMounted(() => {
slim.value = new SlimSelect({
select: selectRef.value
})
new RemoteDataPlugin(slim.value, {
url: '/api/users'
})
})
Иногда плагину нужен собственный EventBus.
class EventEmitter {
constructor() {
this.events = {}
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
}
emit(event, payload) {
const listeners =
this.events[event] || []
listeners.forEach(listener => {
listener(payload)
})
}
}
class SortPlugin {
constructor(instance) {
this.instance = instance
this.enable()
}
enable() {
const container =
this.instance.slim.values
container.addEventListener(
'dragstart',
this.handleDrag.bind(this)
)
}
handleDrag(event) {
console.log(event)
}
}
class SyncPlugin {
constructor(master, slave) {
this.master = master
this.slave = slave
this.bind()
}
bind() {
this.master.settings.events.afterChange =
(values) => {
this.slave.setSelected(values)
}
}
}
Тяжёлые плагины можно загружать отложенно.
class LazyPlugin {
constructor(instance) {
this.instance = instance
this.initialized = false
this.bind()
}
bind() {
this.instance.slim.main
.addEventListener('click', () => {
if (!this.initialized) {
this.init()
}
})
}
init() {
this.initialized = true
console.log('Плагин загружен')
}
}
Корректная очистка особенно важна в SPA.
destroy() {
window.removeEventListener(
'resize',
this.handleResize
)
clearTimeout(this.timer)
this.observer?.disconnect()
this.instance = null
}
Без очистки возникают:
Типичный production-ready плагин содержит:
plugin/
├── core/
├── dom/
├── api/
├── events/
├── utils/
├── styles/
├── index.js
└── types.d.ts
interface SlimPlugin {
init(): void
destroy(): void
}
interface RemotePluginOptions {
url: string
debounce?: number
}
class RemotePlugin
implements SlimPlugin {
constructor(
private instance: any,
private options:
RemotePluginOptions
) {}
init(): void {}
destroy(): void {}
}
Распространённые форматы:
export default RemoteDataPlugin
Некоторые системы используют глобальный реестр.
window.SlimPlugins = {
RemoteDataPlugin,
AnalyticsPlugin
}
Плагин может зависеть от внутреннего API.
Поэтому важно проверять:
if (!instance.version) {
console.warn('Версия не определена')
}
Иногда требуется адаптер:
if (instance.version.startsWith('2')) {
this.initV2()
} else {
this.initLegacy()
}
Проверяются:
test('plugin saves values', () => {
const slim = createInstance()
const plugin =
new AutoSavePlugin(slim)
slim.setSelected('admin')
expect(
localStorage.getItem('selected')
).not.toBeNull()
})
Основные проблемы плагинов:
Методы оптимизации:
class SlimPlugin {
constructor(instance, options = {}) {
this.instance = instance
this.options = options
this.initialized = false
this.init()
}
init() {
if (this.initialized) {
return
}
this.initialized = true
this.bindEvents()
}
bindEvents() {}
destroy() {
this.initialized = false
}
}