Удобные JS-контролы без зависимостей

Ну что - в современном WEB наконец-то можно более-менее по-человечьи делать кастомные контролы в HTML.

Собственно, этим и поделюсь вместе с их дизайном.

Мне часто приходится делать утилитарные одностраничные приложения, использовать для которых VUE или какой-либо другой фреймворк с реактивностью, сборкой и т.д. смысла не имеет.
Хочется иметь только один файл с каким-то небольшим функционалом. Кто бы что не говорил про PHP, но именно сюда он идеально подходит в силу встроенной шаблонизации и жизненного цикла.
Но это бэкэнд, а как сделать достаточно просто фронтэнд и при этом получить удобный и приятный глазу интерфейс?

Вот про это и поговорим. Я, если честно, не понимаю, почему нормально шаблонизируемые, базовые контролы до сих пор не запилили в базовом виде в бразуерах?

Да, я понимаю, что универсальность == сложность, понятно, что желательно иметь две версии - для мобильных устройств и для полноценных устройств.
Понятно, что с первого раза такое прям максимально удобно не удастся сделать, но с чего-то же нужно начинать? - тем более базовых контролов не так уж и много.

Ну и я не стал бы заморачаиваться с нативными контролами под каждую операционную систему это, ИМХО, лишнее - достаточно их стандартного вида и одинакового поведения внутри каждого браузера.

Собственно, что хотелось сделать мне для создания своих одностраничников?
Простой, одинаково выглядящий и одинаково управляемый набор базовых контролов.

Нам тут недавно дали в руки и shadow dom и customElements.define - это прямо отличный, и, лично мной, давно ожидаемый функционал.
Но, вообще, чтобы было удобно работать на фронте нам нужен какой-то минимальный, базовый JS-функционал.

Собственно, у меня получился некий микрофреймворк, существенно облегчающий работу, и который можно положить непосредственно в файл ввиду его крохотного размера.


Микрофреймворк


На всякий случай про именование старался сделать максимальном коротким, но при этом понятным. d вначале названия, если есть, обычно означает DOM

dElCls( el, classes )добавить классы к DOM-элементу

el указатель на DOM-элемент
classes может массивом строк названий классов, или строкой с их названиями, разделенных пробелами

возвращает переданный DOM-элемент

dElC( tagNameOrHTML, classes )создание элемента

tagNameOrHTML названием тэга для создания или строка с HTML для создания documentFragment c HTML внутри
classes опционально, классы, назначаемые созданному элементу (не работают при documentFragment)

возвращает созданный объект

dElWrapAllInside
( el, wrapBy, classes, wrapOnly )
обернуть DOM-элемент el элементом wrapBy

el указатель на DOM-элемент, внутренние элементы которого будут обернуты.
wrapBy элемент, которым будут обернуто содержание el, может быть указателем на DOM-элемент, или названием тэга (тогда такой элемент сначала создастся)
classes опционально, классы, назначаемые wrapBy
wrapOnly опционально, true - wrapBy не будет добавлен, как новое содержание el, по-умолчанию false - wrapBy будет иметь родителем el

возвращает wrapBy

dElQ( s, c ) запрос элемента по селектору (сокращенный querySelector)

s строка с селектором
c опционально, контейнер, внутри которого будет выполняться запрос, по-умолчанию это document

возвращает найденный DOM-элемент или null

dElsQ( s, c ) запрос элементов по селектору (сокращенный querySelectorAll)

возвращает NodeList

dElV( el ) видимость DOM-элемента

el указатель на DOM-элемент

возвращает состояние видимости true/false
видимость определяется по отсуствию runtime стиля display не равного "none"

dEvA( els, ev, hr, misc ) назначение обработчика события

els может быть массивом или NodeList с указателями DOM-элементов, а также строкой, по которой элементы запрашиваются через document.querySelectorAll
ev название события
hr функция-обработчик события
misc дополнительные настройки обработки событий для addEventListener

возвращает hr

dEvR( els, ev, hr, misc ) снятие обработчика события
параметры аналогичны dEvA

dEvCnc( e ) отмена события
e указатель на событие

возвращает false

dEvT( el, ev, payload, evOpts ) принудительное формирование события

el указатель на DOM-элемент
ev название события
payload пользовательские данные в сообщении (попадают в event.detail)
evOpts опции события, по-умолчанию в них стоит bubbles: true для всплытия события

возвращает true/false - результат dispatchEvent

dElSC( el, s ) проверяет элемент на соответствие селектору

el указатель на DOM-элемент
s селектор в формате querySelector

возвращает true, если элемент соответствует селектору или false

dElDef( tagName, hr ) создание пользовательского элемента

tagName название пользовательского элемента, должно обязательно включать "-"
hr функция обработки событий создания hr( event, el )
event событие:
constructed элемент создан, но не присоединен к DOM-дереву
created элемент присоединен к DOM-дереву, все его атрибуты и дочерние элементы (если есть) - доступны
attached элемент присоединен к DOM-дереву
detached элемент присоединен от DOM-дерева

el указатель на DOM-элемент пользовательского элемента

dElSCUp( el, s ) проверить переданный элемент на соответствие селектору и двигаться вверх, пока есть элементы, пока не будет найден соответствующий

el указатель на DOM-элемент
s селектор

возвращает указатель на найденный DOM-элемент или null

debounce( hr, delayMs, misc ) отложенный вызов функции

hr( misc, data ) функция, выполнение которой требуется отложить
delayMs задержка в миллисекундах, опционально, по-умолчанию 250мс
misc доп. параметры, передаваемые в отложенную функцию, опционально

возвращает функцию, при вызове которой откладывается выполнении функции hr, ее можно вызывать с аргументом, который передастся в hr в параметр data, также можно вызывать эту функцию.abort() чтобы вообще отменить отложенный запуск

URLisRel( url ) определение, является ли URL-адрес относительным

url строка с URL-адресом

возвращает true, если url относительный, или false

emIs( v ) проверить, что объект пуст

v array/object/string/number

возвращает true - если пуст, иначе false
пуст означает === null / undefined / "", для массива его length = 0, для объекта нет ни одного собственного свойства

pad( s, lenMax, fillBy ) дополнить строку слева символами до заданной длины

s строка
lenMax максимальная длина строки, 2 по-умолчанию
fillBy символ для дополнения, "0" по-умолчанию

возвращает дополненную строку

clipboard работа с буфером обмена

.set( text ) установить текст из переменной text в буфер обмена
.get() получить текст и буфера обмена

fadeInOut( el, timeout, display ) плавное появление/скрытие элемента через прозрачность

el DOM элемент для проявления/скрытия
timeout время эффекта в формате CSS, по-умолчанию 0.2s
display какое значение display элемент должен иметь при видимости, по-умолчанию block

возвращает объект-апи с двумя функциями
visibleIs возвращает true если элемент видимый, тначе false
show( state, immidately, hr ) показать/скрыть элемент
state true для показа, false для скрытия
immediately применить эффект мгновенно
hr( el, visible ) функция, вызываемая по окончании появления/скрытия, где el элемент, visible true/false видимости



scrollIntoViewIfNeeded
( el, smooth, margin )
проскроллировать до элемента, если это необходимо

el DOM элемент
smooth true для плавного скролла, по-умолчанию true
margin rootMargin для IntersectionObserver, по-умолчанию -20px

rq( url, data, opts, cache, method
accept, ret, urlPreprocess )
кастомизированный fetch

url адрес для запроса
data данные запроса, могут быть объектом вида { идентификатор: данные }, либо строкой, также данные могут быть DOM элементом с файлами, тогда данные всего запроса автоматически преобразуются в FormData
opts опции fetch + дополнительная опция notify - если true и произошла ошибка, будет показано окно с этой ошибкой
cache если false - будут сформированы no-cache заголовки
method метод запроса, по-умолчанию POST
accept тип данных, по-умолчанию "application/json, text/plain, */*"
ret формат возвращаемых данных json или text, по-умолчанию json
urlPreprocess функция препроцессинга URL, опционально, возвращает новый url

возвращает данные после запроса

кидает исключения в случае ошибок, исключения дополняются двумя свойствами:
rStatus - статус запроса
rData - сырые данные запроса

по-умолчанию, если не было указано credentials в opts, они будут установлены в include



Контролы


Благодаря customElements.define можно задать свой тэг, при вставке которого будет вызываться свой собственный обработчик.
Это избавляет от необходимости делать контейнер, а потом через JS в этот контейнер рендерить какой-либо контрол.
Кроме того, это дает возможность работать с заданными атрибутами, передавать внутрь текст или какой-то HTML (аналог слота) и тд.

Я сделал так, что начальная кастомизация контрола может быть сделана с помощью атрибутов основного элемента контрола,
какие-то внутренние изменения состояний эмитятся с помощью стандартных событий DOM (например, change/input),
а управление осуществляется через свойство "api", которое появляется у основного элемента контрола после его создания.
К этому свойству можно обращаться, дождавшись события "created" (которое порождается dElDef).

Это всего-лишь один из вариантов взаимодействия, и я не уверен в его эффективности, но тем не менее оно имеет право на жизнь.

Вобщем, имя весь перечисленный выше микрофреймворк и все базовые контролы одностраничники получились у меня удобными, красивыми и легковесными, без каких-либо сборок или чего-то еще.

Реализованные контролы:

modalмодальное окно (имеет свое апи и события)
msgboxокно информационного сообщения с одной кнопкой (например OK, прождается из modal, работает на хуках событий )
msgbox2btnокно сообщения с двумя кнопками (например OK и CANCEL, порождается из msgbox, работает на хуках событий)
msgboxInputокно сообщения с двумя кнопками и полем для ввода (порождается из msgbox2btn, работает на хуках событий)
tooltipвсплывающая подсказка
loaderглобальный блокировщий ввода и отображения загрузки (синглтон, апи)
paginationпагинация (апи)
calкалендарь (апи, дата, дата + время)
autocompleterавтокомплитер/селект бокс (апи, стандартный select не используется, так как он нормально не кастомизируется и добиться одинакового внешнего вида и поведения не возможно).
listсписок (апи)
fileвыбор файла
scrollbarполоса прокрутки (апи)
buttonкнопка
progressполоса прогресса выполнения (апи)
input[type=checkbox]чекбокс, только стилизация
input[type=radio]радиокнопка, только стилизация
datepickerвыбор даты (даты со временем)

Автокомплитер можно вызвать так:
<d-autocompleter class="control" value='{ "id": 1, "name": "Item 1" }'></d-autocompleter/>

Ну и, чтобы задать ему функцию поиска элементов:
dEvA(
  ".control",
  "created",
  e => {
    const el = e.target;
 
    el.api.itemsRequestSet(
      async query => {
        return [ ... ];
      }
    )
  }
);
И, собственно, все - контрол будет жить, работать и сообщать о своих изменениях.

Все это можно посмотреть непосредственно в исходнике, JSDOC в наличии.

Ну и все контролы + микрофреймворк + все стили и встроенные svg-картинки без сжатия весят около 120кб без минимизации вообще.


Резюме

Не пугайтесь использовать современный чистый JS/CSS/HTML - он дает очень большие возможности при достаточно удобном синтаксисе.

Базовая реактивность, если хочется, также может быть очень просто реализована через прокси.

Ну а современный CSS с has/not и тд делает возможность достаточно лаконично придать этому весьма приятный вид.

При этом все это уже есть "из коробки", без каких-либо тяжелых библиотек.

Скачать ui.php, версия 1.0

скачать файл с контролами, микрофреймворком и примерами
скачать


Демонстрация всех реализованных контролов


TooltipTooltip Tooltip tempMessage BoxMessage Box 2ConfirmConfirm 2ModalLoaderButtonDisabledPressedDanger
link
checkbox 1checkbox 2
v1_1v1_2v1_3
v2_1v2_2
файл(ы)...файл(ы)...

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Suscipit veniam quisquam, rem illum dicta cumque, voluptate fugiat impedit beatae rerum ratione, voluptates nisi magni delectus ab, eaque atque iste. Molestias incidunt nemo veniam alias nam nisi distinctio optio error architecto odit! Illo dicta nulla fugiat distinctio laudantium, corrupti eum unde. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Suscipit veniam quisquam, rem illum dicta cumque, voluptate fugiat impedit beatae rerum ratione, voluptates nisi magni delectus ab, eaque atque iste. Molestias incidunt nemo veniam alias nam nisi distinctio optio error architecto odit! Illo dicta nulla fugiat distinctio laudantium, corrupti eum unde.


13.01.2026, Protocoder