Я уже знаю ваши мысли: «ээээ, 2017ый год на календаре, везде ангуляр да реакт, jsx там всякие, ты о чём вообще?». Но, вся проблема в том что, к сожалению, в мире существуют ещё очень и очень много проектов с frontend на ванильном js или на лапше из jquery, и зачастую есть ряд причин не переводить их на современные фреймворки. А поддерживать дальше их надо. Написав очередное уродство в стиле

'<spаn id="' + id + '">'

я задумался — да неужели нет варианта поярче?


А что не так с готовыми решениями?


Следует начать с того что со стандартом ES6 у нас появились замечательные string templates, которые, на самом деле, решают несколько проблем генерации html вручную. Они умеют в шаблонизацию, и можно забыть о всех этих мерзких '" + text + "'.

Но, при этом всём, бывают случаи когда и темплейты не спасают. Например, они не умеют в автоматическую экранизацию, даже с ними можно промахнуться с какой-нибудь скобкой атрибутов html, и.т.д. Поэтому у js есть свои методы для создания валидного html, но, как всегда, использовать их напрямую крайне неудобно. Чтобы по фен-шую создать один блок с текстом внутри нам надо сделать целую кучу действий:

var div = document.createElement('div').append(document.createTextNode('text'))

Многословненько, как для такого простого действия. Именно поэтому существуют библиотеки которые делают это более компактно. Что же с ними не так? Всё та же громоздкость. Я перебрал несколько библиотек, и все они делают более-менее одно и тоже. Вкратце, все выглядят примерно как-то вот так (скопировано с официальной документации github.com/insin/DOMBuilder):


with(DOMBuilder.dom) {
  var article =
    DIV({'class': 'article'}
    , H2('Article title')
    , P('Paragraph one')
    , P('Paragraph two')
    )
}

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

Idobi.js


Именно так я назвал свою, если её можно так назвать, библиотеку.
Почему так?
Что-то вроде Intuitive DOM Builder, если кому-то вообще интересно)
Целью было создать максимально простой и интуитивный интерфейс, освоить который можно за 3 строчки документации, и который будет иметь минимум визуального оверхеда. Благодаря новым фичам ES6, удалось достичь практически такой же плотности и читаемости как у обычного html. Сравните:



Библиотека предоставляет «текучий» интерфейс. Чтобы установить атрибут — нужно просто вызвать функцию с именем этого атрибута, и передать в неё значение. Чтобы добавить вложенный элемент — вызвать всю цепочку как функцию, и передать в неё вложенные элементы. В конце всего нужно вызвать get(), чтобы получить dom элемент, или же getString(), чтобы получить html-строку. Вот и все правила! Впрочем, лучше один раз показать


// underscore-нотация преобразовывается в кебаб-нотацию
_.div.data_test('42').getString() // <div data-test="42"></div>

// вообще любые теги, так как они нигде не захардкожены, и подтягиваются динамически
_.custom("content").getString() // <custom>content</custom>

// вложенные теги
_.div.class('row')(
    _.div.class('col-md-6')("one"),
    _.div.class('col-md-6')(_.h1.id("header")("two")),
).getString() 
/* 
<div class="row">
    <div class="col-md-6">one</div>
    <div class="col-md-6">
        <h1 id="header">two</h1>
    </div>
</div
*/

Исходный код на Github

Комментарии (7)


  1. Aingis
    02.11.2017 20:23
    +1

    Если уж говорить за ES6, то проще взять Template literals и hyper(HTML), который заодно предоставляет виртуальный DOM — то есть обновляет только те части HTML, в которых изменились данные.

    // this is hyperHTML
    function tick(render) {
      render`
        <div>
          <h1>Hello, world!</h1>
          <h2>It is ${new Date().toLocaleTimeString()}.</h2>
        </div>
      `;
    }


  1. vlreshet Автор
    02.11.2017 21:04

    Aingis А что с экранированием? Насколько я знаю, еs6 шаблоны ничего сами не экранируют, и xss вполне себе пройдёт, в случае чего.


    1. kahi4
      02.11.2017 21:23

      А документацию открыть? Функция (в данном случае render) получит исходную разобранную на составляющие строку и массив аргументов, которые нужно в нее подставить. Берите и экранируйте.


  1. VovanZ
    02.11.2017 21:08

    Вы переизобрели JSX?


  1. kahi4
    02.11.2017 21:20

    1. class, get являются зарезервированными словами, крайне не стоит их использовать не по назначению (особенно как метод)


    2. continue в конце тела цикла без условия? Оригинально, мягко говоря.


    3. Тут либо должна быть проверка на массив перед этой конструкцией и использование for-of, либо hasOwnProperty


    4. Proxy не работает в <= IE11 и не полифилится.


    5. _ давно зарезервировано underscore (и братом-плагиатом lodash), будут конфликты имен.


    6. 2017 год, говорите? А где umd? Установка через npm и подобные странные никому не нужные вещи.


    7. Для getString есть toString, почему не переопределить его?


    8. class BaseElement extends Function { оригинально. Зачем?


    9. Aingis абсолютно прав про Template literals (ну и hyper).


  1. bgnx
    02.11.2017 21:41

    Возьмите плагин babel-plugin-transform-react-jsx замените настройку вызова создания реакт-элементов на свою функцию
    { "plugins": [ ["transform-react-jsx", { "pragma": "h" }] ] }
    дальше заимпортируйте функцию h, которая будет создавать дом-элементы


    function h(tag, attrs, ...children){
     const el = document.createElement(tag);
     for(const key in attrs){
       el.setAttribute(key, attrs[key]);
     }
     for(const child of children){
       el.appendChild(typeof child === 'string' ? document.createTextNode(child) : child)
     }
     return el;
    }

    И у вас получится еще более удобный интерфейс для создания html как например


    const TodoApp = (state)=>(
      <div>
         <div style={state.style}>{state.title}</div>
         {state.todos.map(todo=>Todo(todo)}
     </div>
    )
    const Todo  = (todo)=>(
        <div>{todo.text}</div>
    )
    const app = <TodoApp state={{title: 'todo app', style:'background: aqua', todos: [{text: 'todo1'}, {text: 'todo2'}]}}/>;
    
    document.body.appendChild(app)

    где вообще не нужен ни какой-то текучий интерфейс ни болерплейта вызова функций и сразу видно где верстка а где код. При этом получаем широкую поддержку редакторов, различных линтеров и статической типизации.
    А дальше если захотите добавить оптимизацию чтобы при обновлении интерфейса не пересоздавались лишние дом-элементы (не было лишних вызовов createElement и setAttribute) можно в функции h() создавать и возвращать только временные js-объектов описывающие верстку {tag: 'div', attrs : {style: '...'} , children: [..]} а потом отдельная функция рекурсивно пройдется по новым объектам и сравнив их с объектами из предыдущего прохода и применит только нужные изменения в реальном думе. И вуаля — вы придумаете виртуальный дум. А если добавите еще 20 тысяч строк кода — у вас получится реакт ;)


  1. vintage
    02.11.2017 21:46

    Смотрите как это можно выразить на JSX с кастомным обработчиком:


    // создали дом
    const dom =
    <div className="row">
        <div className="col-md-6">one</div>
        <div className="col-md-6">
            <h1 id="header">two</h1>
        </div>
    </div>
    
    // сериализовали в строку
    dom.outerHTML 
    
    // нашли заголовок по селектору
    dom.queryselector( '#header' )