image

Всем доброго здравия.
Хотел предложить концепцию JavaScript(JS) библиотеки для удобного использования Custom Elements(по-русски назову «собственные элементы»). Перед тем как предлагать Polymer, X-Tag, Bosonic и другие, прошу прочитать текст до конца.

Введение


Главная идея состоит в том, чтобы сделать использование собственных элементов более похожим на нативный подход стилизации встроенных HTML-элементов. Например, я определил элемент вертикальной гистограммы(ui-vboxhistogramm). Добавил несколько гистограмм на страницу. В случае использования Polymer мне необходимо описывать визуальный стиль для каждой гистограммы по отдельности. И в случае повторения визуальных свойств(не CSS-свойств, а определённых вручную для собственного элемента) для каждой гистограммы описание становится избыточным. Поясню на примере:
<ui-vboxhistogramm id="histo1" column-count="5" column-width="10px"></ui-vboxhistogramm>
<ui-vboxhistogramm id="histo2" column-count="5" column-width="10px"></ui-vboxhistogramm>
<ui-vboxhistogramm id="histo3" column-count="5" column-width="10px"></ui-vboxhistogramm>

Тут сразу напрашивается система классов из HTML. Но классы работают только для нативных CSS-свойств. В итоге возникла задача сделать похожую систему классов на JS.

Концепция «Родные Элементы»(Native Elements)


В первую очередь хотелось бы сделать доступ к свойствам визуализации по аналогии с нативным стилем изменения этих свойств:
divElement.style.background = "color";

Собственные элементы позволяют воплотить в жизнь эту дерзкую идею:
//Это у нас базовый класс, который отвечает за создание ссылки на корневой элемент и метод для обработки декларационных объектов. Этот метод обходит свойства рекурсивно и вызывает соответствующие сеттеры для элемента.
class ExtendedStyleDeclaration
{
  constructor(parentNode)
  {
    this._root = parentNode;
  }
  declare(declarationObject)
  {
    //тут мы получаем свойства для описания собственного элемента и применяем их в соответствии с названием сеттера
  }
}

//Это класс стилизатора(element.xstyle.column), вложенного в корневой
class VBoxHistogrammCulumnStyle extends ExtendedStyleDeclaration
{
  constructor(parentNode)
  {
    super(parentNode);
  }
  set count(value)
  {
    //тут по аналогии с сеттером первого уровня, объекта xstyle
    saveProperty(this, "count", value);
    //далее работаем со встроенными в собственный элемент компонентами(canvas или svg, в случае с гистограммами)
  }
  get count()
  {
    return getLastProperty(this, "count");
  }
  set width(value){}
  get width()     {}
}

//Это класс корневого стилизатора(element.xstyle) для гистограммы
class VBoxHistogrammStyle extends ExtendedStyleDeclaration
{
  constructor(parentNode)
  {
    super(parentNode);
    this.column = new VBoxHistogrammCulumnStyle(this);
  }

  set width(value)
  {
    //Перед тем как изменить внешний вид элемента сначала сохраняем значение в специальный контейнер,
    //так как значение может задаваться из разных мест(напрямую или через группу(класс)), поэтому надо иметь возможность получать оба значения, а точнее последнее.
    saveProperty(this, "width", value);

    //Не совсем удачный пример, так как я просто продублировал CSS-свойство.
    //Но предположим, что здесь более сложная логика, чем просто установка width напрямую, например ширина высчитывается на основе каких-то условий
    if(condition)
    {
      this._root.style.width = this.width;
    }
    else
    {
      this._root.style.width = "расчёт по сложному алгоритму";
    }
  }
  get width()
  {
    return getLastProperty(this, "width");
  }
}

//Это наш будущий виджет
class VBoxHistogrammProto extends HTMLElement
{
  createdCallback()
  {
    setGroupsToQueue(this); //этой функцией мы проверяем наличие групп в атрибуте group, чтобы добавить их в очередь на установку в функции declareGroup
    this.xstyle = new VBoxHistogrammStyle(this); //передаём указатель на узел нашего элемента для дальнейших манипуляций с ним
  }
}

Теперь мы можем задавать стиль для наших элементов по аналогии с нативным подходом и/или даже более продвинутым способом:
let histo1 = document.getElementById("histo1");

histo1.xstyle.width = "100px";
//or
histo1.xstyle.declare(
{
  width : "100px",
  column:
  {
    count: 5,
    width: "10px"
  }
});

histo1.xstyle.column.count = 15; //тут произойдёт обновление гистограммы и на ней появится 15 столбцов. Свойство из группы теряет свою силу, так как свойство было изменено напрямую. Теперь свойство count не может быть изменено через группу(аналогия с CSS-классами).
//or
histo1.xstyle.column.declare(
{
  count: 5,
  width: "10px"
});


Группы — они же классы


Возвращаемся к вопросу стилизации через классы. Так как ключевое слово class уже зарезервировано для этого атрибута, то наиболее близким по значению названием для него по моему мнению является ключевое слово group. Группировка элементов происходит по аналогии с заданием класса для встроенных элементов:
<ui-vboxhistogramm id="histo1" group="histogroup1"></ui-vboxhistogramm>
<ui-vboxhistogramm id="histo2" group="histogroup1"></ui-vboxhistogramm>
<ui-vboxhistogramm id="histo3" group="histogroup1 histogroup2"></ui-vboxhistogramm>

Теперь наши элементы сгруппированы по общим сходным свойствам, определенным вручную в собственном элементе. Декларация групп происходит в JS-коде:
//функция declareGroup возвращает ссылку на массив groups, содержащий описание групп
let groups = declareGroup(
{
  histogroup1:
  {
    width: "100px",
    column:
    {
      count: 5,
      width: "10px"
    }
  },
  histogroup2:{}
});

//теперь можно добавить объявленные группы к элементам разными способами
//добавим группу histogroup1 к элементу histo5
let histo5 = document.getElementById("histo5");
histo5.groupList.add(groups.histogroup1);

//добавим все группы из массива groups к элементу histo6
let histo6 = document.getElementById("histo6");
histo6.xstyle.declare(
{
  group: groups
});

Алгоритм декларации группы реализуется следующим образом. В момент создания собственного элемента(функция обратного вызова createdCallback) необходимо организовать список групп(например в глобальном объекте window) с очередью из группируемых элементов на применение с помощью специального объекта groupList.

groupList — это аналог нативного classList только для собственного элемента. Он также имеет методы add и remove. Метод add(по аналогии с методом declare для отельного элемента) принимает декларационный объект, содержащий в себе описание визуальных свойств элемента. Таким образом функция declareGroup обходит все группы из своего декларационного объекта и добавляет их для всех элементов из очереди.

Приоритет задаваемых свойств можно реализовать по аналогии с классами. Если свойство задаётся напрямую через сеттер или метод declare, то свойства из групп теряют свою силу, то есть перекрываются.

Заключение


Подытожим. Суть концепции заключается в том, чтобы предоставить набор классов(JS) и функций для реализации поведения, показанного в примерах выше:
  • динамические сеттеры для задания стиля собственного элемента, близкие по механике к нативной стилизации через element.style.property
  • группировка элементов(аналог классов для CSS-свойств) схожих по стилю, для исключения избыточности при описании одних и тех же пользовательских свойств(например число и ширина столбцов гистограммы) для нескольких элементов

В итоге мы получаем близкий к нативному метод описания визуальных свойств для собственных элементов, содержащий в себе ряд преимуществ и недостатков. Хотелось бы послушать мнение знающих front end разработчиков, чтобы определиться с целесообразностью реализации этого подхода.
Применяете ли вы Веб Компоненты в ваших рабочих проектах?

Проголосовало 145 человек. Воздержалось 40 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

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


  1. nazarpc
    17.12.2015 17:02
    -1

    Прочитал и ничего не понял из того, что вы предлагаете. Polymer использую года полтора как + являюсь активным контрибутором, и всё равно не понял что здесь происходит.
    К стати, если вам нужны классы — веб-компоненты это просто HTML элементы, и, о чудо, к ним тоже можно добавлять классы. Да и вообще суть веб-компонентов далеко не в одном внешнем виде если на то пошло.


    1. Voronar
      17.12.2015 17:09

      Вы для меня сделали открытие.
      Через классы можно задать только CSS-свойства, но нельзя задать значение для кастомных сеттеров собственного элемента. Как в CSS через классы задать значение для сеттера xstyle.column.count элемента <ui-vboxhistogramm>? Никак. Вот о чём разговор.


      1. nazarpc
        17.12.2015 19:11

        А что есть xstyle.column.count на самом деле? Классы для CSS свойств, это верно, а с помощью CSS variables и CSS mixins если весьма широкое, хотя и не идеальное поле для настройки внешнего вида элемента.
        А ui-vboxhistogramm из чего состоит, это canvas какой-то, или набор обычных элементов внутри, или, может, там SVG?


        1. Voronar
          17.12.2015 21:10

          Хочу прояснить один момент. Пример гистограммы приведён в грубой форме(сеттеры для визуализации могут быть другие), но от этого суть концепции не меняется.
          Не важно из чего состоит ui-vboxhistogramm, так как реализация не влияет на интерфейс.

          xstyle.column.count это пользовательский сеттер(определенный разработчиком), который задаёт количество столбцов гистограммы.
          Я привожу пример, когда я хочу объединить несколько гистограмм в одну группу, схожую по визуальным свойствам, в данном случае по количеству и ширине столбцов гистограммы. И встроенные средства тут не помогут, нужно всё делать вручную.

          CSS variables и CSS mixins это всё вокруг да около встроенных свойств, типа element.style.color, element.style.display и так далее. У нас же пользовательские сеттеры с реализацией на JS c использованием встроенных CSS-свойств или методов взаимодействия с canvas или SVG, как в случае с нашей гистограммой.

          Суть концепции в том, чтобы предоставить набор классов(JS) и функций для реализации поведения, показанного в примерах выше:

          • сеттеры для задания стиля собственного элемента, близкие по механике к нативной стилизации через element.style.property
          • группировка элементов, схожих по стилю для исключения избыточности при описании одних и тех же пользовательских свойств(например число и ширина столбцов гистограммы) для нескольких элементов


          1. wheercool
            17.12.2015 22:23
            +2

            Не нужно городить фреймворк для такой простой задачи.
            Решение лежит на поверхности. В вашем примере нужно просто создать новый компонентв скажем ui-vboxhistogramm5Columns (не очень удачное название) который внутри использует ui-vboxhistogramm с нужными аттрибутами и использовать его :)
            С технической стороны это примерно тоже что и выносить повторяющийся код в новый метод.


            1. Voronar
              17.12.2015 22:32

              нужно просто создать новый компонентв скажем ui-vboxhistogramm5Columns...

              Можете показать пример на псевдокоде? А то на словах мне сложно понять.


              1. wheercool
                17.12.2015 22:44

                что-то близкое к реакту

                var Histogram5 = component({
                      render() {
                         return <Histogram columns="5" />;
                      }
                });
                



                1. Voronar
                  17.12.2015 22:57

                  Компонент — это уже готовый стилизованный элемент или это шаблон для создания? В Реакте не силен, но по-моему вы показали создание класса в Реакт.
                  Какую задачу решает этот кусок кода? Покажите, пожалуйста, пример создания 5 элементов, сгруппированных по свойствам columns count=5 и column width=10, чтобы не было повторения в указании свойств для каждого элемента.


                  1. wheercool
                    17.12.2015 23:16
                    +1

                    var StyledHistogram5 = component({
                          render() {
                             return <Histogram5 columnWidth="100px" />;
                          }
                    });
                    
                    //App - корневой компонент. (Можно воспринимать как body в html)
                    var App = component({
                        render() {
                           return (<StyledHistogram5/><StyledHistogram5/><StyledHistogram5/><StyledHistogram5/><StyledHistogram5/>)
                        }
                    });
                    


                    Можете это воспринимать так:
                    component — создает «шаблон» внешний вид которого определяется функцией render.
                    для создания «элемента» мы просто объявляем компонент в виде html-элемента вот так:
                     <myComponent>
                    
                    .
                    Все аттрибуты могут быть доступны внутри render через this.props и предполагается что они используются в компоненте Histogram
                    По факту это просто повторное использование кода.
                    В ООП для этого часто прибегают к наследованию
                    В функциональном программировании — частичное применение


                    1. Voronar
                      17.12.2015 23:31

                      Как в таком подходе динамически, во время выполнения менять значения атрибутов(columnWidth например). Как вообще получить доступ к элементу из вне?
                      И допустим мне захотелось убрать из первой гистограммы сгруппированные атрибуты(по аналогии с element.classList.remove(«StyledHistogram5»)) и вернуть их к изначальным(по умолчанию) значениям?
                      Да, и почему Histogram5 и StyledHistogram5 описываются отдельно? Слишком много лишнего кода получается для группировки свойств.


                      1. wheercool
                        17.12.2015 23:42

                        В двух предложениях тут не ответить. Лучше не поленитесь, изучите react.
                        Моя задача была показать как эта проблема решается, а не научить Реакту )
                        Вкратце метод render может содержать логику отображения и компоненты в идеале должны не иметь состояния.
                        Получать доступ к другим элементам из вне очень плохое архитектурное решение. Каждый компонент должен отвечать только за свои дочерние компоненты.
                        Histogram5 и StyledHistogram5 это два полноценных отдельных компонента. Касательно многословности, в последних версиях Реакта такого рода компоненты можно создавать используя лямбды


                        1. Voronar
                          17.12.2015 23:48

                          Спасибо. Возьму на заметку.

                          А так зачем мне два отельных элемента, если я только один описываю?

                          P.S. Ушёл спать. Отвечать буду завтра.


                        1. Voronar
                          18.12.2015 10:09

                          В двух предложениях тут не ответить.

                          Просто напишите пару строк кода.

                          Да, и зачем мне отдельные компоненты(элементы) для изменения вида другого элемента, если всю его визуализацию можно инкапсулировать в нём, а потом с помощью группировки свойств(просто отдельный декларативный объект или несколько объектов, которые могут применяться к любым подобным элементам) получать требуемый внешний вид.


                        1. Voronar
                          18.12.2015 12:58

                          Сейчас я начинаю понимать, что Реакт мне тут ничем помочь не может.
                          Насколько я понимаю, Реакт — это просто композиция встроенных элементов и Реакт-компонентов, ещё конечно есть конвертация Реакт-компонентов в custom elements.

                          Я же говорю именно о новых встроенных элементах(custom elements), со своей логикой и поведением. Таких элементах, которые я могу включать через HTML-файл, стилизация и поведение которых делается близким по подходу как для встроенных элементов.


          1. nazarpc
            17.12.2015 23:20

            Хм… в Polymer оно будет выглядеть так:

            Polymer({
              is: 'x-element',
              properties: {
                column: {
                  count: 1
                }
              }
            });
            
            var element = document.createElement('x-element');
            element.set('column.count', 5);
            

            Если вам нужно группировать в том смысле что вы описали в статье — то здесь нужно просто наследовать элементы, жаль, что в Polymer 1.x этого ещё нет, хотя в принципе достигается выносом всего JS в то, что там называется behaviors:

            var behavior = {
              properties: {
                column: {
                  count: 1
                }
              }
            };
            
            Polymer({
              is: 'x-element',
              behaviors: [behavior]
            });
            
            Polymer({
              is: 'x-element-column-5',
              behaviors: [behavior],
              created: function () {
                this.set('column.count', 5);
              }
            });
            

            Ну и никто не запрещает вам создать такой behavior, который будет подгружать параметры по умолчанию извне на основании значения атрибута group.

            Могу быть не объективен поскольку давно работаю с Polymer, но по-моему это гораздо проще чем то, что предлагаете вы. Разве что менять свойства нужно через метод set() из соображений производительности и универсальности (к примеру, когда у вас некоторое свойство не было объявлено изначально, то есть не будет и сеттера, что без ES2015 Proxy будет слишком накладно реализовать).

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


            1. Voronar
              17.12.2015 23:45
              -1

              Сейчас уже не могу в полном осмыслении проанализировать код потому что собираюсь спать.
              Первое, что бросилось в глаза — это создание двух элементов «x-element» и «x-element-column-5», как показали на примере Реакта(«Histogram» и «Histogram5») Зачем мне два элемента для одной сущности(элемент гистограммы ведь один)?

              И какую вторую половину не решает моя концепция?

              P.S. Ушёл спать. Отвечать буду завтра.


  1. Ohar
    17.12.2015 20:43
    +5

    image


    1. Voronar
      17.12.2015 21:20

      Намёк понял.
      Однако моя цель не фреймворк ради фреймворка. Просто хочется сделать веб-компоненты более удобными, для себя по крайней мере.


      1. PerlPower
        19.12.2015 23:03
        +1

        Вы не понимаете сути создания фреймворков на Javascript — если ты человек из Google или Facebook, который создал scalabale optimized robust crossbrowser framework, то вам везде почет и дороги все открыты, а если вы программист Вася, то вы создали велосипед на фоне других замечательных решений. Даже если эти решения не такие замечательные. Если ваше решение хорошее, и вы представите его на конференции, то скорее всего наберете сотню звездочек на гитхабе в первые полгода, а дальше… все. Конечно, если вы делаете что-то, у чего нет конкурентов даже близко, то ситуация иная.

        Тут не работает подход «я просто оставлю это здесь», даже если то что вы оставите хорошая штука. Вот тут автор пиарит «Матрешку», вроде бы неплохой фреймворк, простой и с документацией. Только он никому не нужен, на том же upwork ноль вакансий с его участием, а ведь фреймворку уже 2 года. Тоже самое будет и с вашей концепцией, я не беру судить хороша она или плоха, но во-первых она близка к уже имеющимся, а во-вторых вы не представляете серьезную компанию.


        1. rboots
          20.12.2015 03:03
          +2

          Я пользуюсь десятками библиотек, каждая из которых создана одним человеком. Да что библиотек, тот же Nginx сделан одиночкой. Вот за что я люблю наших сограждан, так за то, как отлично они могут «вдохновить» человека на созидательную деятельность. Сделал человек продукт — честь ему и хвала, хороший-плохой — тут уже можно обсуждать.


          1. PerlPower
            20.12.2015 03:19
            +3

            Есть огромная разница между конкретной библиотекой выполняющей конкретную задачу, и «еще одним MVVM фреймворком», «еще одним шаблонизатором», «еще одним подходом к созданию веб-компонентов». Каждый из этих «еще один что-там» отличается от других в основном тем, что решает задачу так, как удобно автору, соответствует его воркфлоу и представлениям. И для того чтобы навязать другим свое представление удобства нужно иметь за спиной авторитет.

            Упомянутый вами nginx имеет очень сильно отличается от apache, и эти отличия легко проверить замерами скорости, потребления памяти, одновременных соединений. А вот кодла фреймворков на JS отличается в основном только субъективными вещами, которые при раскрытии темы оказываются мифическими удобством работы, скоростью разработки и красотой. И все эти вещи понимают как хотят.


        1. Voronar
          20.12.2015 15:19

          Да, я не понимал сути создания фреймворков на JavaScript и вы мне её объяснили. Спасибо.
          Но я и не фреймворк собираюсь делать, а небольшую библиотеку(как видно из первого обзаца статьи) сугубо для личных целей. Но если кого-то заинтересует концепция этой библиотеки, то я рад был бы выслушать их предложения по совершенствованию этой концепции.


  1. rboots
    20.12.2015 02:57

    Отличная идея, с CSS давно пора что-то решать. Я бы всё же сделал это патчем к уже имеющимся библиотекам для создания компонентов, во первых потому, что так всем проще, а во вторых потому, что у вас тоже нет какого-то функционала из популярных библиотек, а так вы объедините плюсы.


    1. Voronar
      20.12.2015 19:00

      По поводу CSS. На самом первом этапе я вообще искал возможность создать новое CSS-свойство, чтобы интегрироваться в систему классов и не придумывать группы. Но такой возможности не нашёл и тогда решил делать дополнение в виде element.xstyle.
      С другой стороны появляется возможность делать вложенные подкатегории стилей типа element.xstyle.styleCategory:

      element.xstyle.declare(
      {
        prop1: value,
        styleCategory:
        {
          prop1: value
        }
      });
      


  1. Voronar
    20.12.2015 15:10

    Тут вы попали в точку!
    Сначала хотелось сделать что-то своё, но после анализа уже имеющихся библиотек захотелось предложить именно дополнение к ним. Но дополнение независимое от этих библиотек, так как многие пишут на чистых Веб Компонентах. То есть просто подключил модуль(import * as lib from «lib»;) и реализуешь непосредственно свои собственные элементы.