javascript

Реактивность без React или как обойтись без id в html элементах

  • вторник, 6 января 2026 г. в 00:00:06
https://habr.com/ru/articles/982886/

Всем Привет! Зовут меня Майковский Вадим. Я программист-любитель и хочу поделиться с Вами своей находкой.

Странный заголовок, не правда ли?

А зачем вообще обходиться без id? Даже не знаю, но это вполне возможно, и приложение будет живым и вполне себе "реактивным". А всю "магию" при этом творит функция, которую я назвал tokenize.

Конечно же я, ни в коем случае, не настаиваю на отказе от id. "Элементарные" id никому не мешают и tokenize`у тоже. Но если обходиться без id, то как же получать ссылки на DOM элементы, для обращения к ним? Вот для этого и нужна функция tokenize, которая собирает референсы в удобную структуру с ветками, подветками и листьями (ссылками на DOM элементы). А вот как она это делает, мы с Вами сейчас и разберём.

Функция tokenize принимает следующие аргументы:

  • корневой DOM элемент, начало рекурсии (при старте приложения, обычно это document.body), обязательный аргумент;

  • Ваш объект-контейнер или экземпляр Вашего объекта/класса, в который будут собираться найденные ветки и листья (что это за ветки и листья будет объяснено в следующем абзаце). Если не указан или логическое ЛОЖЬ – объект-контейнер будет создан;

  • булевой аргумент. Если логическая ИСТИНА – найденные атрибуты удаляться не будут (про атрибуты читайте в следующем абзаце);

  • целочисленный аргумент – уровень вложенности для рекурсии. Если не указан или логическое ЛОЖЬ – без ограничений;

  • строковой аргумент – имя родительского объекта (имя токена).

Рекурсивно вызываемая, на каждом DOM элементе, функция tokenize, путём простого перебора, ищет атрибуты, имена которых маркированы спецсимволами:

* "звёздочка" – токен-ветка (объект-контейнер или экземпляр объекта);

^ "карет" – токен-лист (ссылка на DOM элемент).

Я выбрал именно эти спецсимволы, так-как только эти хорошо заметны (зрительно) в гипертексте.

Имена атрибутов без "*" и без "^" (а в случае "звёздного" атрибута, то так же и без имени пользовательского объекта/класса, при наличии), используется как имя токена. И в родительском объекте создаётся параметр с этим именем, который ассоциируется с объектом или с ссылкой на DOM элемент. Перед ассоциированием имена токенов проверяются на конечную последовательность двух символов "[]" (квадратные скобки). При нахождении оных, они удаляются из имени токена, и в родительском объекте создаётся параметр-массив (если он ещё не создан), в который и добавляется (методом push) новый объект или ссылка на DOM элемент.

В объекте, созданном при нахождении атрибута со звёздочкой, будь то объект-контейнер или экземпляр пользовательского объекта/класса, создается параметр с именем "_" (нижнее подчёркивание), который ассоциируется с DOM элементом. Этот же объект и имя его токена будет передаваться аргументами при рекурсивном вызове tokenize`а.

Предварительно имена "звёздных" атрибутов проверяются на наличие в них имени пользовательского объекта/класса, указанного через знак "-" (минус). При нахождении подобного, tokenize попытается создать экземпляр объекта (если этот объект присутствует в глобальном контексте �� доступен через window[искомое]), передав в его конструктор следующие аргументы (по порядку):

  • ссылка на элемент, которому принадлежит найденный атрибут;

  • значение "звёздного" атрибута (value);

  • родительский объект-ветка;

  • имя родительского объекта (имя токена).

Предупреждаю заранее. Так-как браузеры lowercase`ят атрибуты html элементов, использовать заглавные буквы в именах пользовательских объектов/классов не получится.

Крошечный пример использования 

<html>
	<head>
		<meta charset="UTF-8">
		<style>
.menu > * > INPUT {display: none}
.menu > * {background: none; border: none; padding: 5px 10px}
.menu > BUTTON:hover {background-color: rgba(0,0,0,.05)}
.menu > *:has(input:checked) {border-bottom: solid 2px black; border-top: solid 2px transparent}

.tab {padding: 50px; border-top: solid 1px grey}
.tab > DIV {width: 100%}
.tab:has(> input:not(:checked)) {display: none}
		</style>
		<script>

function tokenize(a, b, c, d, e){
	let f = a

	if(!b) b = {}

	loop:{
		for(const g of a.attributes){
			let h = g.name, i

			switch(h[0]){
				case '*':
					if(!c) a.removeAttribute(h)

					if((i = h.indexOf('-')) > 1){
						const j = h.slice(i + 1)

						if(f = window[j])
							f = new f(a, g.value, b, e)
						else
							console.error('Объект "' + j + '" (из атрибута "' + h + '") отсутствует в глобальном контексте!')

						h = h.slice(0, i)
					}

					if(f && f != a) f._ = a
					else f = {_: a}
				case '^':
					if(!c) a.removeAttribute(g.name)

					let k = h.slice(1)

					if(k.slice(-2) == '[]'){
						k = k.slice(0, -2)

						if(!b[k] || !Array.isArray(b[k])) b[k] = []

						b[k].push(f)
					}else
						b[k] = f

					if(f != a) e = k

					break loop
			}
		}
	}

	if(d >= 0) d--

	if(!(d < 0)){
		if(f == a) f = b

		for(const g of a.children) tokenize(g, f, c, d, e)
	}

	return b
}

function menubtn(a, b, c, d){
	const r = document.createElement('input')
	a.appendChild(r)
	r.type = 'radio'
	r.name = d

	this.click = a.onclick = e => {
		app.tab[b].checked = r.checked = true
	}
}

		</script>
	</head>
	<body full style="padding: 50px">
		<div *menu class="menu">
			<button *a[]-menubtn="0">Вкладка 1</button>
			<button *a[]-menubtn="1">Вкладка 2</button>
		</div>
		<div class="tab">
			<input ^tab[] type="radio" name="tabs" hidden>
			<div *tab0>
				<p ^p style="color:blue">Содержимое вкладки 1</p>
			</div>
		</div>
		<div class="tab">
			<input ^tab[] type="radio" name="tabs" hidden>
			<div *tab1>
				<p ^p style="color:red">Содержимое вкладки 2</p>
			</div>
		</div>

		<script>

const app = tokenize(document.body)

app.menu.a[0].click()

		</script>
	</body>
</html>

В приведённом примере, tokenize "проплывая" "проходя" по DOM дереву поместит в константу "app" главный объект-контейнер, в который, в свою очередь, поместит три объекта-контейнера с именами параметров-токенов: "menu", "tab0", "tab1" и один массив под именем "tab" в который поместит ссылки на два DOM элемента "input". В контейнере "menu" tokenize создаст массив под именем "a", в который поместит два экземпляра объекта "menubtn" ассоциированных с двумя кнопками в DOM дереве.

Переключать вкладки, в приведённом примере, можно программно, путём вызова метода "click", в одном из двух экземпляров объектов "menubtn". Что, собственно, и видно на приведённом примере. Строчка app.menu.a[0].click() активирует первую вкладку. А что бы, к примеру, поменять текст во второй вкладке, можно сделать так app.tab1.p.innerHTML = "и без длиннющего document.getElementById".

Более расширенный пример, с попами (ой, прошу прощения), с popmenu, с модальными окнами и с календариком смотрите здесь https://github.com/Mickommic/tokenize/tree/main

Возможности tokenize не ограничены только лишь его начальным вызовом, при старте приложения. К примеру, если во время работы приложения, каким‑либо способом было получен новый контент и вставлен где‑либо в DOM дереве через «innerHTML», то этот новый контент может быть «токенизирован» отдельно, путём вызова tokenize на том элементе, куда был вставлен новый контент.

Что ж, на этом, пожалуй, всё. Позвольте откланяться. Всех с Новым 2026-м годом. И да прибудет с Вами сила (алгоритмов).

Интересное наблюдение, в этой статье 26 раз встречается слово «объект».