Skip to content

Repository files navigation

Component.js

Реактивный микро-фреймворк для создания и обновления 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

demo

NPM: npm i @alexgyver/component

Browser: 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

EL

Конфиг

Параметры для конфига cfg, с которым вызывается make/update:

ПараметрПринимаетОписание
parentNodeМонтировать созданный элемент в указанный
insertBeforeNumberМонтировать в parent перед индексом. Работает также для child
tagСтрокаHTML тег для child-объектов
svgBooleanФлаг 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
Строка|Функция|StateCSS стили вида 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) и т.д.

Версии

tiny

  • Не создаются методы на элемент
  • Нет lifecycle и обработчиков onMount, onUpdate...
  • Удаление и очистка элемента не рекурсивные
  • Нет отключения обработчиков событий в remove
  • Нет функций replace, makeShadow, setTemplate, useTemplate
  • Нет поддержки State и функций в значениях параметров

pico

  • Как tiny
  • Нет SVG
  • Нет функций addCSS, removeCSS

State

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 компонентов:

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'),]});

Shadow DOM

// элемент со своими изолированными стилямиmakeShadow('div',{parent: document.body,child: [{class: 'myclass',child: {text: 'I am shadow!',}}]},'.myclass{color:red;}');

SVG

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);

About

Simple HTML&SVG element builder

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages