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

Я решил создать свой собственный движок шаблонов, который я решил назвать DOMinator просто для удовольствия. Я рассмотрю основные шаги, которые я сделал, и код, который я использовал для его создания.

Сначала я хотел создать простой формат для шаблонов, поэтому я создал новый файл с именем templates.js, в котором у меня есть единственный объект Templates, заполненный каждым из используемых мной шаблонов. Я создал шаблоны как функции, чтобы передавать им переменные. это шаблон для моих значков документов.

const Templates = {
  docIcon: function(obj) {
    return {
      tag: 'div',
      id: obj.id,
      classes: ['doc-icon'],
      properties: {
        title: obj.name,
        tabIndex: 0
      },
      children: [
        {
          tag: 'div',
          classes: ['doc-icon-header'],
          children: [
            { tag: 'h3', content: obj.name }
          ]
        },
        {
          tag: 'p',
          content: obj.exerp
        },
        {
          tag: 'div',
          id: 'docControls',
          classes: ['doc-controls'],
          children: [
            {
              tag: 'button',
              classes: ['doc-control'],
              id: 'moveDoc',
              children: [
                {
                  tag: 'i',
                  classes: ['fa', 'fa-folder']
                }
              ]
            },
            {
              tag: 'button',
              classes: ['doc-control'],
              id: 'deleteDoc',
              children: [
                {
                  tag: 'i',
                  classes: ['fa', 'fa-trash']
                }
              ]
            }
          ]
        }
      ]
    }
  }

Как видите, тег, идентификатор и классы явно определены как ключи для наших объектов, но все остальное передается в ключе свойств. У нас также есть массив дочерних элементов, каждый из которых имеет тот же формат, что и родительский. Теоретически мы могли бы бесконечно вкладывать элементы DOM с этим форматом.

Когда у нас есть этот простой формат объекта Javascript для наших шаблонов, можно легко создать множество из них довольно быстро, но как нам преобразовать в HTML и добавить в DOM?

Я создал класс под названием Dominator с экземплярами, представляющими элемент DOM как объект. Экземпляр будет очень похож на наш шаблон, за исключением того, что будут применяться динамические значения. Я просто использую Object.assign() для репликации шаблона.

class Dominator {
  constructor(object) {
    Object.assign(this, object)
  }
}

Далее нам нужно выяснить, как создать детей. Мы можем использовать цикл for для рекурсивного создания новых экземпляров класса для потомков. Затем мы помещаем дочерние элементы в пустой инициализированный массив и после прохождения цикла назначаем this.children равным нашему массиву.

----
  constructor(object) {
    Object.assign(this, object)
    let childObjects = []
    if (object.children){
      for (const child of object.children) {
        let childObject = new Dominator(child)
        childObjects.push(childObject)
      }
    }
    this.children = childObjects
    this.element = this.domElement
  }
----

Пока у нас есть объект, который является копией шаблона с примененным к нему нашим контентом. Пока бесполезно. Далее нам нужно превратить их в элементы DOM. Я создал функцию получения с именем domElement(), чтобы вернуть сгенерированный элемент DOM. Я использовал get, чтобы мы могли обновлять наши свойства и регенерировать элемент, когда захотим.

Первая часть функции создает элемент и устанавливает основные свойства.

get domElement() {
    const domElement = document.createElement(this.tag)
    if (this.id) domElement.id = this.id
    if (this.content) domElement.innerText = this.content
    
  -----
    return domElement
}

Следующие три вещи, которые должны произойти, - это итерация и присвоение свойств, итерация и установка классов, а также итерация и добавление дочерних элементов к нашему domElement.

get domElement() {
    const domElement = document.createElement(this.tag)
    if (this.id) domElement.id = this.id
    if (this.content) domElement.innerText = this.content
    if (this.properties) for (const prop in this.properties) {
      domElement[prop] = this.properties[prop]
    }
    if (this.classes) for (const cssClass of this.classes) {
      domElement.classList.add(cssClass)
    }
    if (this.children) for (const child of this.children) {
      domElement.append(child.domElement)
      if (child.id) this[child.id] = child.domElement
    }
    this.element = domElement
    return domElement
  }

Как видите, мы условно проверяем все, чтобы убедиться, что оно существует в нашем шаблоне, затем применяем эти вещи к нашему domElement и затем возвращаем его.

Наш полный класс пока выглядит так:

class Dominator {
  constructor(object) {
    Object.assign(this, object)
    let childObjects = []
    if (object.children){
      for (const child of object.children) {
        let childObject = new Dominator(child)
        childObjects.push(childObject)
      }
    }
    this.children = childObjects
    this.element = this.domElement
  }
  get domElement() {
    const domElement = document.createElement(this.tag)
    if (this.id) domElement.id = this.id
    if (this.content) domElement.innerText = this.content
    if (this.properties) for (const prop in this.properties) {
      domElement[prop] = this.properties[prop]
    }
    if (this.classes) for (const cssClass of this.classes) {
      domElement.classList.add(cssClass)
    }
    if (this.children) for (const child of this.children) {
      domElement.append(child.domElement)
      if (child.id) this[child.id] = child.domElement
    }
    this.element = domElement
    return domElement
  }
}

Давайте посмотрим на это в действии!

get docIcon() {
  const dominator = new Dominator(Templates.docIcon(this))
  const div = dominator.domElement
  div.addEventListener('click', this.openDoc.bind(this))
  div.querySelector('#deleteDoc').addEventListener('click', this.deleteDoc.bind(this))
  div.querySelector('#moveDoc').addEventListener('click', this.moveDoc.bind(this))
  this.icon = div
  return div
}

Как мы можем упростить процесс прослушивания событий? Давайте добавим новую функцию findChildById().

findChildById(id) {
  if (this.children) {
    for (const child of this.children) {
      if (child.id === id) {
        return child
      } else if (child.children) {
        let found = child.findChildById(id)
        if (found) return found
      }
    }
  }
  return false
}

Используя небольшую рекурсию, мы можем найти дочерний экземпляр Dominator, чтобы внести в него изменения. Теперь мы можем создать массив с именем this.eventListeners = [] в нашем конструкторе, который мы добавим наши прослушиватели событий, которые позже будут назначены элементу DOM.

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

event(action, id, type = 'click') {
    if (id) {
      const node = this.findChildById(id)
      if (node) {
        node.eventListeners.push({type: type, action: action})
      }
    } else {
      this.eventListeners.push({type: type, action: action})
    }
  }

Наконец, в нашей функции get domElement() нам понадобится следующее:

if (this.eventListeners) for (const eventListener of this.eventListeners) {
   domElement.addEventListener(eventListener.type, eventListener.action)
}

Теперь мы можем провести рефакторинг нашего кода, использованного ранее:

get docIcon() {
  const dominator = new Dominator(Templates.docIcon(this))
  dominator.event(this.openDoc.bind(this))
  dominator.event(this.deleteDoc.bind(this), 'deleteDoc')
  dominator.event(this.moveDoc.bind(this), 'moveDoc')
  const div = dominator.domElement
  this.icon = div
  return div
}

Альт! Намного чище.

Вот весь код нашего Доминатора:

class Dominator {
  constructor(object) {
    Object.assign(this, object)
    let childObjects = []
    if (object.children){
      for (const child of object.children) {
        let childObject = new Dominator(child)
        childObjects.push(childObject)
      }
    }
    this.eventListeners = []
    this.children = childObjects
    this.element = this.domElement
  }
  get domElement() {
    const domElement = document.createElement(this.tag)
    if (this.id) domElement.id = this.id
    if (this.content) domElement.innerText = this.content
    if (this.properties) for (const prop in this.properties) {
      domElement[prop] = this.properties[prop]
    }
    if (this.classes) for (const cssClass of this.classes) {
      domElement.classList.add(cssClass)
    }
    if (this.children) for (const child of this.children) {
      domElement.append(child.domElement)
      if (child.id) this[child.id] = child.domElement
    }
    if (this.eventListeners) for (const eventListener of this.eventListeners) {
      domElement.addEventListener(eventListener.type, eventListener.action)
    }
    this.element = domElement
    return domElement
  }
  findChildById(id) {
    if (this.children) {
      for (const child of this.children) {
        if (child.id === id) {
          return child
        } else if (child.children) {
          let found = child.findChildById(id)
          if (found) return found
        }
      }
    }
    return false
  }
  event(action, id, type = 'click') {
    if (id) {
      const node = this.findChildById(id)
      if (node) {
        node.eventListeners.push({type: type, action: action})
      }
    } else {
      this.eventListeners.push({type: type, action: action})
    }
  }
}