Наследование и композиция с Polymer

Вы, наверное, уже слышали про эту новую крутую штуковину, которая изменит всё представление о построении веб-приложений, верно? Да, я говорю о Web Components. Web Components — это новая технология, которая позволит нам создавать собственные компоненты для веб с помощью инструментов, которые используют разработчики браузеров.

На самом деле, Web Components представляет собой набор из четырех спецификаций: это HTML Imports, HTML Templates, Custom Elements и Shadow DOM. Я не буду подробно о них рассказывать, так как статья не об этом. Если вы хотите больше узнать о веб-компонентах, рекомендую обратить внимание на сообщество webcomponents.org, в нем есть много полезных ресурсов и информации об этих технологиях.

С развитием стандартов также происходит развитие инструментов, библиотек и фреймворков. Что касается веб-компонентов, тут на помощь приходят два фреймворка, которые пытаются облегчить вашу жизнь: X-Tag от Mozilla и Polymer от Google. Оба под капотом используют одни и те же полифиллы для совместимости со всеми основными версиями браузеров. Однако, X-Tag предоставляет только императивный способ создания компонентов, в то время как Polymer идет несколько дальше, предлагая также и декларативный стиль.

Например, определение собственных элементов с помощью Polymer выглядит так:

<polymer-element name="my-custom-element" noscript>
  <template>
    <!-- shadow dom -->
    <div>Мой элемент</div>
  </template>
</polymer-element>

Для использования созданного элемента достаточно его импортировать:

<!-- import custom element -->
<link rel="import" href="path/to/my-custom-element.html">

<my-custom-element></my-custom-element>

Это все, что нужно для создания собственных элементов с помощью Polymer. Заглянув немного глубже, мы сможем увидеть все четыре технологии в действии. Мы создали элемент (его регистрация в DOM происходит за кулисами, Polymer позаботится об этом), мы определили HTML-шаблон для нашего элемента, который впоследствии будет использован в Shadow DOM, и, в заключение, мы импортировали элемент, чтобы его использовать.

Конечно, в мире Polymer намного больше возможностей. Я рекомендую почитать документацию, чтобы узнать о них всё.

Расширение существующих элементов

Одним из главных достоинств Web Components является возможность расширять существующие элементы. И, так как созданные вами компоненты являются простыми HTML-элементами, вы можете расширять их точно также, как нативные элементы. По большому счёту, нет никакой разницы между пользовательскими и встроенными элементами.

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

Но, прежде чем приступить к делу, давайте создадим какой-нибудь более полезный элемент. Вот определение элемента basic-button:

<polymer-element name="basic-button" noscript>
  <template>
    <span><content></content></span>
  </template>
</polymer-element>

Новым здесь является элемент <content>, с его помощью определяются, так называемые, «точки вставки». Точки — это место в вашем Shadow DOM, где будет находится содержание используемого в текущий момент элемента. Вставляем наш basic-button:

<basic-button>Hello</basic-button>

В результате Shadow DOM будет выглядеть так:

<span>Hello</span>

Если вы знакомы с AngularJS, точка вставки — это что-то вроде ngTransclude. Точки вставки, на самом деле, более сложны, но я сейчас не буду вдаваться в подробности.

Хорошо, теперь давайте расширим элемент basic-button. Наша цель — получить кнопку с иконкой, так что новый элемент будет называться icon-button, а атрибут extends будет указывать на существующий элемент basic-button.

<polymer-element name="icon-button" extends="basic-button">
  <template>
    <span>
      <i class="icon"></i><shadow></shadow>
    </span>
  </template>
  <script>
    Polymer('icon-button');
  </script>
</polymer-element>

В этом фрагменте есть несколько новых приёмов, давайте рассмотрим их повнимательней. Во первых, это элемент <shadow>. <shadow> — очень мощный, так как он может расширить Shadow DOM вашего элемента вместе с содержимым родительского Shadow DOM.

Мы так же используем конструктор Polymer, чтобы явно зарегистрировать наш пользовательский элемент (через минуту мы узнаем зачем).

Осталось вызвать наш расширенный элемент:

<icon-button>Hello</icon-button>

В зависимости от ваших требований, вы можете использовать is="" синтаксис для расширения существующих элементов в коде приложения. Просто запомните, что is="" работает только для нативных элементов. Поэтому, вместо использования кастомного тега элемента, можно написать что-то вроде:

<button is="mega-button"></button>

Опять же, это зависит от ваших задач.

Если существует дополнительный функционал, который вы хотели бы реализовать, например, обратный вызов ready, вы можете определить его в конструкторе. А чтобы быть уверенным в том, что обратный вызов в родительском элементе сработал, можно вызвать this.super():

Polymer('icon-button', {
  ready: function () {
    // выполняется, когда компонент будет готов
    console.log('icon-button ready');

    // вызываем родительский `ready`
    this.super();
  }
});

Вы также можете переопределять существующие методы и получать доступ к свойствам с привязкой данных. Хотите узнать как? Об этом можно почитать здесь.

Расширение нескольких элементов

Теперь мы знаем как расширять элементы с помощью Polymer, при этом вы могли бы задаться вопросом: как можно расширить несколько элементов? Например, у нас есть компонент, расширяющий элементы таким образом, чтобы их можно было таскать мышкой. Мы могли бы его использовать, применяя к существующим элементам, таким как этот:

<!-- перетаскиваемый img -->
<img is="my-draggable">

<!-- перетаскиваемое что-то другое -->
<div is="my-draggable"></div>

Так как же расширить несколько элементов? Вы наверняка ждете решения в духе extends="foo bar", верно? К сожалению, решения нет. Чтобы сделать немного яснее:

Невозможно расширить несколько элементов

Вы ничего не найдете в официальной документации, ещё можете почитать это обсуждение на StackOverflow. Однако есть несколько способов получить несколько результатов, когда речь заходит о расширении множества элементов.

Композиция вместо наследования

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

Так мы могли взять элемент icon-button и разбить его на три более мелких элемента с помощью композиции. Например, мы могли бы получить элементы basic-icon, basic-button и icon-button. Они не расширяют basic-button, но используют его Shadow DOM. Затем мы могли бы определить элемент icon-link, использующий basic-icon, и какой-нибудь basic-link (или что вам придет в голову).

Чтобы получить представление о том, как сильно можно сломать суть вещей, просто взгляните на Polymer Paper Elements. Еще один проект, на который стоит обратить внимание, это Basic Web Components. Этот проект диктует основные принципы, по большей части, основанные на композиции.

Повторное использование функциональности с помощью миксинов

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

Можно ли сделать это с помощью Polymer? Да, можно. Polymer поддерживает миксины. С миксинами мы можем расширять существующие компоненты общим функционалом без явного расширения других элементов. Всё, что нам нужно — это изолировать общую функциональность и примешивать её в конструктор наших компонентов используя Platform.mixin(). Если Polymer подключен, объект Platform доступен глобально, поскольку он поставляется вместе с Polymer по умолчанию.

Например, скажем, что у нас есть миксин, обеспечивающий общую функциональность для нескольких объектов:

var sharedMixin = {
  // определяем общие функции и свойства
};

Просто чистый JavaScript. Теперь, для повторного использования функционала, всё, что потребуется сделать, это расширить конструктор нашего компонента, используя Platform.mixin() следующим образом:

Polymer('my-component', Platform.mixin({
  // логика компонента
}, sharedMixin));

Общие миксины для нескольких импортов

Сила Web Components позволяет сделать миксины доступными для нескольких импортированных элементов. Даже если термин «Web Components» звучит суперсложно, это всё ещё HTML, CSS и JavaScript. Те же инструменты, те же правила.

Так что, если нужно сделать миксин доступным глобально для нескольких импортированных элементов, всё, что нам нужно сделать, это положить общие миксины в отдельный документ, который ничего не делает, но определяет миксины в глобальном пространстве имен. Наш shared.html может выглядеть примерно так:

<script>
  window.sharedMixin = {
    // логика компонента
  };
</script>

И, благодаря HTML Imports, можно просто импортировать его в наши компоненты, как мы делали это с другими полноценными компонентами:

<link rel="import" href="path/to/shared.html">

<element name="foo-element">
  <script>
    Polymer('foo-element', Platform.mixin({
      // логика компонента
    }, sharedMixin));
  </script>
</element>

(Поскольку я изо всех сил боролся именно с этой проблемой, я включил сюда часть моего вопроса с StackOverflow.)