Реактивный микро-фреймворк для создания и обновления HTML элементов как JS объектов
- Ванильный JS, прямая работа из браузера без компиляции, поддержка рантайм eval-плагинов
- 2.5 кБ gzip против 6 кБ Preact (минимальный идентичный пример), возможна компиляция в пико-версию 1.5 кБ
- Без Virtual DOM - в 1.5 раза быстрее Preact
- Установка и удаление свойств, атрибутов, датасетов, классов, стилей, анимаций, обработчиков событий
- Обновление параметров
- Поддержка Shadow DOM
- Нормализация текстовых и численных полей
- Поддержка реактивного State
- Жизненный цикл: коллбэки на монтирование, рендер, обновление и удаление элемента
- Автоотключение обработчиков и биндов при удалении элемента
- Поддержка SVG элементов + набор готовых инструментов
- Создание шаблонов компонентов
- Проекты на базе: UI.js, SVPlot.js, Settings, Bitmaper2, ota-projects
NPM:
npm i @alexgyver/componentBrowser: https://gyverlibs.github.io/Component.js/Component.min.js
Browser (tiny): https://gyverlibs.github.io/Component.js/Component.tiny.min.js
Browser (pico): https://gyverlibs.github.io/Component.js/Component.pico.min.js
Параметры для конфига cfg, с которым вызывается make/update:
| Параметр | Принимает | Описание |
|---|---|---|
parent | Node | Монтировать созданный элемент в указанный |
insertBefore | Number | Монтировать в parent перед индексом. Работает также для child |
tag | Строка | HTML тег для child-объектов |
svg | Boolean | Флаг SVG для child-объектов |
children | Конфиг|Массив [Конфиг|HTML|Node|Null|Массив|State] | Создать детей и добавить к элементу. Без указания tag будет добавлен div. Null игнорируются |
children_r | Как у children | Заменить детей |
props | Объект {name: Any|Функция|State} | Добавить свойства как el[name] = val |
attrs | Объект {name: Any|Функция|State} | Добавить аттрибуты как setAttribute(name, val). null - удалить аттрибут |
data | Объект {name: Any|Функция|State} | Добавить датасеты как data-name = val. null - удалить датасет |
style | Добавить стили в style | |
Строка|Функция|State | CSS стили вида color: red; padding: 0 | |
Объект {styleName: Строка|Функция|State} | При styleName==_raw прибавляет стили в виде строки. Поддерживает '--property' | |
style_r | Как у style | Заменить все стили |
class | Добавить классы в classList | |
Строка|Функция|State | Строка вида newClass active foo bar | |
Объект {className: Bool|Функция|State} | true значение добавляет класс, false убирает. При className==_raw прибавляет классы в виде строки | |
Массив [Строка|Null] | Массив строк классов, null игнорируются, например ['foo', false && 'bar'] | |
class_r | Как у class | Заменить все классы |
transition | Объект {styleName: Any, ... } | Добавить CSS переход duration: 300, easing: 'ease', delay: 0, onEnd: func, время в мс, можно задать обработчик однократного окончания |
events | Объект {eventName: func} | Подключить события, где eventName вида click, change... |
Функция | Функция-обработчик | |
Объект {handler: Функция, opts...} | Можно добавить опции для addEventListener, например once: true, passive: false | |
events_r | Как у events | Заменить все обработчики событий |
on<> | Функция|Объект | Если параметр начинается с on - добавить обработчик события (перехват стандартных onclick/onpress + lowercase). Вид объекта как у events |
onMount | Функция | Вызовется при присоединении к DOM |
onRender | Функция | Вызовется при отрисовке (момент появления размеров) |
onUpdate | Функция | Вызовется при обновлении (через update или State) |
onResize | Функция | Вызовется при изменении размера |
onDestroy | Функция | Вызовется при удалении через EL.remove(el), EL.clear(el) или el.remove() |
register | Функция | Должна вернуть функцию отписки или массив функций отписки, они будут вызваны при удалении элемента |
context | Объект | Привязать контекст, он сам пробрасывается в child. При обновлении контекста все вызовы будут происходить с ним, а не со старым |
$ | Строка | Добавить созданный элемент в context с именем $значение |
ref | Строка|Функция(el) | Если строка - добавить созданный элемент в context с именем значение, если функция - вызвать с элементом |
push | Массив | Добавить созданный элемент в указанный массив |
Другие | Строка|Функция|State | Будут добавлены как props |
Tip
Короткие имена параметров: text == textContent, html == innerHTML, ctx == context, child == children
Note
- При обновлении
updateвсё что указано как добавить - добавится, а заменить - заменит старое State-бинды на тот же параметр (+ под-параметр) и имя заменяют старые- Обработчики событий элементов на те же события заменяют старые. С параметром
once: true- не заменяют - В обработчики событий элементов (click, input...) и onEnd анимаций добавляется сам
Event, т.е. свойстваe.targetи прочие - Во все обработчики передаётся объект
{el, ctx}, где el - сам элемент, ctx - его контекст. Также во все обработчики прокидывается контекст вthis(если обработчик - function()) - Обработчики жизненного цикла (onMount, onRender...) подключаются только при создании элемента, в update их обновить нельзя
Статические методы EL:
// Создать элементEL.make(tag,cfg={},svg=false)// Обновить элементEL.update(el,cfg);// Подключить к родителю, null - отключить. beforeIndex - индекс child у родителя, перед которым вставитьEL.mount(el,parent,beforeIndex=null);// Заменить ребёнка old на нового el, old удалить, у el запустить монтаж с вызовом обработчиков. Вернёт elEL.replace(old,el,keepContext=true);// Удалить всех детейEL.clear(el,recursive=true);// Удалить элементEL.remove(el,recursive=true);// Добавить функцию отписки или массив функций, будут вызваны при удалении элементаEL.register(el,unsub);// двигать по DOM дереву (1, -1)EL.move(el,dir);Note
Методы update, mount, clear, remove добавляются к созданному элементу и передают его первым аргументом, то есть можно вызывать el.update(cfg), el.mount(parent) и т.д.
- Не создаются методы на элемент
- Нет lifecycle и обработчиков onMount, onUpdate...
- Удаление и очистка элемента не рекурсивные
- Нет отключения обработчиков событий в remove
- Нет функций replace, makeShadow, setTemplate, useTemplate
- Нет поддержки State и функций в значениях параметров
- Как
tiny - Нет SVG
- Нет функций addCSS, removeCSS
constructor(init={});// добавить состоянияaddStates(obj);// имеет состояниеhasState(name);// подключить состояниеbind(name,map=(e)=>e.value);// подписаться, функция вида fn(name, val)subscribe(name,fn);Для реактивного поведения нужно создать State с параметрами как:
letstate=newState({foo: 1});letstate2=useState({foo: 1});// React-стильWarning
Нельзя создавать параметры с именами subscribe, addStates, hasState и bind! Они не будут работать
У объекта можно менять и читать созданные поля как
state.foo=123;console.log(state.foo);Для подключения стейта к элементу используется метод bind(имя) или bind(имя, map), например:
letstate=useState({count: 0});EL.make('button',{parent: document.body,text: state.bind('count'),onclick: ()=>{state.count+=1;// будет менять text},});Note
Подключать State и массив с ними можно только в параметры, где это поддерживается
Кастомизированный вывод:
EL.make('button',{parent: document.body,text: state.bind('count',e=>'Count: '+e.value),onclick: ()=>{state.count+=1;// будет менять text},});Также можно передать массив стейтов. Начальное значение будет взято из первого, реакция будет на все:
letstate=useState({count: 0,name: 'test'});EL.make('button',{parent: document.body,class: 'btn',text: [state.bind('count',e=>'Count: '+e.value),state.bind('name',e=>'Name: '+e.value),]});state.count=10;// изменит текст на 'Count: 10'state.name='hello';// изменит текст на 'Name: hello'Стейты поддерживаются везде, где это указано в списке параметров выше, то есть у классов и стилей тоже:
letstate=useState({foo: false,bar: true,display: 'block'});EL.make('button',{parent: document.body,// class: state.bind('foo'),class: {// Для удобного задания дефолтных значений в объекте можно прописать их в параметр _raw_raw: 'my-button my-class',foo: state.bind('foo'),// boolbar: state.bind('bar'),// bool},style: {display: state.bind('display'),// string}});Стейт поддерживает любые типы данных, то есть можно прибиндиться к child_r и реактивно заменять детей объектами конфига:
letstate=useState({children: []});EL.make('div',{parent: document.body,class: 'card',child_r: state.bind('children'),});state.children=[{tag: 'span',text: 'hello',},{tag: 'span',text: 'world',}];Повторный стейт (на один и тот же параметр) будет заменён, т.е. при обновлении останется последний:
letbtn=EL.make('button',{parent: document.body,text: state.bind('name'),});EL.update(btn,{text: state.bind('name'),});На стейт можно подписаться самому как:
state.subscribe('foo',(key,val)=>console.log(key,val));Набор инструментов для создания SVG компонентов:
SVG.make(tag,cfg);SVG.update(el,cfg);// вернёт конфигSVG.svg(attrs,cfg);SVG.rect(x,y,w,h,rx,ry,attrs,cfg);SVG.circle(x,y,r,attrs,cfg);SVG.line(x1,y1,x2,y2,attrs,cfg);SVG.polyline(points,attrs,cfg);SVG.polygon(points,attrs,cfg);SVG.path(d,attrs,cfg);SVG.text(text,x,y,attrs,cfg);// создаст и вернёт элементSVG.make_svg(attrs,cfg);SVG.make_rect(x,y,w,h,rx,ry,attrs,cfg);SVG.make_circle(x,y,r,attrs,cfg);SVG.make_line(x1,y1,x2,y2,attrs,cfg);SVG.make_polyline(points,attrs,cfg);SVG.make_polygon(points,attrs,cfg);SVG.make_path(d,attrs,cfg);SVG.make_text(text,x,y,attrs,cfg);// Добавить стили уникально. Без ID будет вычислен хэшfunctionaddCSS(css,id=null);// Удалить стили. Без ID будет вычислен хэшfunctionremoveCSS(css,id=null);// Создать теневой элемент от указанного тега/Node host, дети подключатся к нему в shadowRoot, стили запишутся в $stylefunctionmakeShadow(host,cfg={},css='');// Определить глобальный шаблон, fn - функция, возвращающая cfg-конфигfunctionsetTemplate(name,tag,fn);// Вызвать шаблонfunctionuseTemplate(name, ...args);letdiv1=EL.make('div',{// создать divparent: document.body,// прикрепить к bodytext: 'hello 1',// вывести текстclass: 'card bordered',// класс строкойstyle: 'color: red',// стиль строкой});// стили и классы можно задавать объектомletdiv2=EL.make('div',{// создать divtext: 'hello 2',// вывести текстclass: {// класс объектом + условноcard: true,bordered: false,},style: {// стиль объектомcolor: 'green','font-size': '20px',}});// можно прикрепить вручнуюdiv2.mount(document.body);// (переменные div1 и div2 из прошлого примера)// через 1 сек поменять текст, стиль и убрать класс borderedsetTimeout(()=>{div1.update({text: 'hello world!',class: {bordered: false,},style: {color: 'unset',}});},1000);// можно очистить/перезаписать стили и классы через _rsetTimeout(()=>{div2.update({class_r: '',style_r: '',});},2000);EL.make('div',{parent: document.body,class: 'card',child: [// может быть массивом{tag: 'span',text: 'hello 1',},{// без указания тега будет divclass: 'card',child: {// может быть объектом (1 элемент)tag: 'span',text: 'hello 2',}}]});// трюкиEL.make('div',{parent: document.body,class: 'card',child: [{},// валидно, пустой divnull,// валидно, ничего не добавитсяundefined,// валидно, ничего не добавитсяtrue&&{// можно добавлять детей по условиюtag: 'span',text: 'hello 1',}]});EL.make('button',{parent: document.body,text: 'press me',class: 'btn',// обработчик клика. Можно писать onclick и onClickonClick: (e)=>{console.log('click!',e,e.el,e.ctx);// e - Event// e.el - сам элемент (кнопка)// e.ctx - контекст (о нём ниже)},// можно добавлять options для событий, обработчик указывается в handleronmousedown: {handler: ()=>console.log('press'),once: true},// можно подключить ещё вот такevents: {mousemove: ()=>{},input: {handler: ()=>{},passive: false}}});// кнопка меняет счётчик, после 5 кликов кнопка удаляетсяletcount=0;EL.make('button',{parent: document.body,class: 'btn',text: 'press',onclick: e=>{e.el.update({text: 'update '+count});if(++count==5)e.el.remove();},onMount: ()=>{console.log('div mount');// вызовется после добавления в body},onRender: ()=>{console.log('div render');// вызовется после фактического рендера},onUpdate: ()=>{console.log('div update');// вызовется после обновления параметров},onDestroy: ()=>{console.log('div destroy');// вызовется после удаления},});// создаём стейтletstate=newState({count: 0,name: 'Alex'});// пример 1 (без стейта)EL.make('button',{parent: document.body,class: 'btn',text: 0,onclick: (e)=>e.el.update({text: Number(e.el.textContent)+1}),});// пример 2EL.make('button',{parent: document.body,class: 'btn',text: state.bind('count'),// привязываем к countonclick: ()=>{state.count+=1;// будет менять text},});// пример 3EL.make('button',{parent: document.body,class: 'btn',text: state.bind('count',e=>'Count: '+e.value),// кастомный выводonclick: ()=>{state.count+=1;// будет менять text},});// пример 4EL.make('div',{parent: document.body,class: 'card',child: [{tag: 'input',type: 'text',size: 10,value: state.name,// значение по умолчаниюoninput: e=>state.name=e.el.value,// меняем},{tag: 'span',text: state.bind('name'),// и меняется тут}]});letobj={};// контекстletarr=[];// массивletbutton;// переменнаяEL.make('div',{// создать divpush: arr,// добавить div в массив (переменная arr выше)ctx: obj,// контекст для $ и обработчиков (переменная obj выше)$: 'myDiv',// создать $myDiv в контекстеparent: document.body,// прикрепить к bodychild: [// добавить вложенные{tag: 'span',// элемент span$: 'mySpan',// контекст прокидывается в детей, создать $mySpantext: 'text',// с текстом 'text'},{tag: 'button',class: 'btn',text: 'say hello',ref: el=>button=el,// положить в переменнуюonclick: (e)=>{// e.ctx - контекст// обновим текст и цвет mySpane.ctx.$mySpan.update({text: 'hello!',style: 'color: red',});}},{tag: 'button',class: 'btn',text: 'remove',onclick: (e)=>{e.ctx.$myDiv.remove();// удалить весь контейнер div},},],});console.log(obj);// {$myDiv: div.card, $mySpan: span, $counter: span}console.log(arr);// [div.card]console.log(button);// [button]// добавим поле для счётчика. Используется контекст родителяobj.$myDiv.update({child: {tag: 'span',$: 'counter',// создать $counter в objtext: 0,}});// будем менять счётчик по таймеруletcount=0;setInterval(()=>{obj.$counter.update({text: count++,});},1000);// через 3 сек заменим mySpan на новую кнопку и сохраним в контекстеsetTimeout(()=>{obj.$mySpan=EL.replace(obj.$mySpan,EL.make('button',{class: 'btn',text: obj.$mySpan.textContent,}));},3000);EL.make('div',{// анимируем и удаляем после завершенияparent: document.body,style: {width: '50px',height: '50px',backgroundColor: 'orange'},transition: {width: '150px',height: '150px',duration: 1500,onEnd: (e)=>e.el.remove()},});// через глобальный шаблон ELsetTemplate('userCard','div',(name,lastname,birthdate)=>({class: 'card',child: [{tag: 'h3',text: `${name}${lastname}`},{tag: 'p',text: `Birthdate: ${birthdate}`}]}));useTemplate('userCard','Alice','Smith','1995-06-12').mount(document.body);useTemplate('userCard','Bob','Johnson','1990-01-01').mount(document.body);// вручную + родительconstmyTemplate=(name,lastname,birthdate,parent)=>(EL.make('div',{class: 'card',
parent,child: [{tag: 'h3',text: `${name}${lastname}`},{tag: 'p',text: `Birthdate: ${birthdate}`}]}));myTemplate('Alice','Smith','1995-06-12',document.body);myTemplate('Bob','Johnson','1990-01-01',document.body);// фабрика конфигурацииconstmyButton=(text,color)=>({tag: 'button',text: text,style: {_raw: 'padding: 5px 10px; color: white; border: none; border-radius: 4px; margin: 0 5px;',backgroundColor: color,}});EL.make('div',{class: 'card',parent: document.body,child: [myButton('hello','red'),myButton('world','blue'),myButton('kek','green'),]});// элемент со своими изолированными стилямиmakeShadow('div',{parent: document.body,child: [{class: 'myclass',child: {text: 'I am shadow!',}}]},'.myclass{color:red;}');letcirc=SVG.make_circle(100,100,30,{fill: 'red'});SVG.make_svg({width: 200,height: 200},{parent: document.body,style: 'border: 1px solid #ccc',child: [// вручную{tag: 'rect',attrs: {x: 10,y: 130,width: 50,height: 50,fill: 'green',}},// внешнийcirc,// билдерыSVG.rect(10,10,50,50,5,5,{fill: 'blue'}),SVG.line(0,0,200,200,{stroke: 'black','stroke-width': 2})],});EL.make('hr',{parent: document.body});// двигаем кружокsetInterval(()=>{circ.update({attrs: {cx: Math.random()*200,cy: Math.random()*200}})},300);