Polymer 3: Intro

Polymer 3: Intro

Front-end 17.03.2019

Polymer – это библиотека от Google, предназначена для создания и использования пользовательских элементов (WebComponents).

Что такое WebComponents

Веб-компоненты – это элементы (теги), которые Вы можете многократно использовать где-угодно – на веб-страницах или в веб-приложениях и даже с другими библиотеками JavaScript.

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

Рассмотрим на примере. Предположим, вы создали пользовательский элемент без Shadow DOM с хорошим CSS, а какой-то другой разработчик хочет использовать ваш элемент в своих веб-приложениях. Если класс стилей его приложений совпадает с классом в вашем веб-компоненте, тогда он переопределит стилизацию для Вашего элемента, что на практике может принести много неудобств, так как разработчику придется тратить время, на то, что бы поправить конфликты стилей.

При необходимости компоненты могут принимать внешние данные, но об этом будет немного далее на примере Polymer. Хотелось бы отметить, что Polymer имеет достаточно не маленькую библиотеку WebComponents, а так же позволяет создавать свои.

Установка

Polymer CLI предоставляет достаточно широкий выбор команд, которые Вы можете использовать для своего проекта. Это и init, и build, и lint, полный список и их описание Вы сможете найти по ссылке Polymer CLI Commands. В своем проекте я использовала gulp для создание билда, так как для него можно создать более гибкую конфигурацию, в отличии от возможностей, предоставленных Polymer CLI, но и polymer build является довольно интересным и может быть использован в Вашем проекте.

Для уставноки polymer cli глобально, polymer и webcomponents используйте команды:

npm i -g polymer-cli
npm i @polymer/polymer @webcomponents/webcomponentsjs --save

Настройку Polymer CLI можно произвести с помощью флагов при запуске команды polymer serve:

polymer serve --npm --module-resolution=node options

Или создать файл polymer.json в корне Вашего проекта с нужными настройками,которые будут подтягиваться при запуске команды polymer serve. Пример моего файла polymer.json:

{
  "entrypoint": "index.html",
  "shell": "pm-lib/pm-app.js",
  "extraDependencies": [
    "node_modules/@webcomponents/webcomponentsjs/*.js",
    "!node_modules/@webcomponents/webcomponentsjs/gulpfile.js",
    "node_modules/@webcomponents/webcomponentsjs/bundles/*.js"
  ],
  "moduleResolution": "node",
  "npm": true
}

Настройка gulp и babel

Установка gulp v4 и дополнительных пакетов для работы:

npm i gulp gulp-babel gulp-concat gulp-uglify-es gulp-watch --save-dev

Далее создаем файл Gulpfile.js в корне Вашего проекта:

const del = require("del");
const gulp = require('gulp');
const watch = require('gulp-watch');
const babel = require('gulp-babel');
const concat = require('gulp-concat');
const uglify = require('gulp-uglify-es').default;

const dir = {
    src         : 'pm-lib/',
    build       : 'build/'
  };


var clean = () => {
  return del([ dir.build ]);
}

var jsTask = () => {
  return gulp.src(`${dir.src}/*.js`)
    .pipe(babel({
          presets: [
            "@babel/preset-env",
          ]
      }))
    .pipe(concat('index.min.js'))
    .pipe(uglify())
    .pipe(gulp.dest(`${dir.build}`));
}

var watchFiles = () => {
  gulp.watch(`${dir.src}/**/*.js`, jsTask);
}

exports.watch = gulp.parallel(watchFiles);
exports.default = gulp.series(clean, gulp.parallel(jsTask));

При подобной настройке команда gulp позволит создать билд проекта, а gulp watch следит за изменениями и билдит при необходимости.
Установка babel v7:

npm i @babel/cli @babel/core @babel/preset-env --save-dev

В моем случае понадобилось только указать presets в файле .babelrc:

{
  "presets": [
    "@babel/preset-env",
  ]
}

Создание первых компонентов

Создадим простой компонент в файле pm-demo.js.
Первое, что нам нужно сделать – импортировать PolymerElement и html из установленной библиотеки, дальше создаем класс PmDemo с его стилями и шаблоном, и объявляем компонент через customElements.define.

import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';

class PmDemo extends PolymerElement {
  static get template() {
    return html `
      <style>
        :host {
          display: block;
        }

        .navbar {
          display: flex;
          background-color: #343a40;
          padding: .3125rem 0;
        }

        .navbar-collapse {
          display: flex;
        }

        .navbar-brand {
          color: #fff;
          text-decoration: none;
          padding: .3125rem .625rem;
          margin-right: 1rem;
        }

        .navbar-nav {
          display: flex;
          align-items: stretch;
          margin: 0;
          padding: 0;
        }

        .navbar-nav .nav-item {
          display: flex;
          align-items: center;
          padding: 0 .3125rem;
        }


        .navbar-nav .nav-link {
          color: rgba(255,255,255,.5);
        }
      </style>

      <nav class="navbar">
        <a class="navbar-brand" href="#">[[title]]</a>

        <div class="navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav">
            <li class="nav-item">
              <a class="nav-link" href="#" >Link</a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="#" >Link</a>
            </li>
          </ul>
        </div>
      </nav>
    `;
  }

  constructor() {
    super();
    this.title = 'Polymer';
  }
}

customElements.define('pm-demo', PmDemo);

Переменную title я задала в конструкторе и использую в коде как [[title]], аналогично зададим ссылки для меню, после чего их можно вывести циклом и избавиться повторяющегося кода:

<li class="nav-item">
  <a class="nav-link" href="#" >Link</a>
</li>
<li class="nav-item">
  <a class="nav-link" href="#" >Link</a>
</li>

Для того, что бы вывести часть шаблона циклом, нужно импортировать в компонент библиотеку “dom-repeat”:

import '@polymer/polymer/lib/elements/dom-repeat.js';

Так же зададим массив ссылок в блоке constructor()

constructor() {
    super();
    this.title = 'Polymer';
    this.links = [
      {name: 'Home', url: '/'},
      {name: 'Sources', family: '/sources'},
    ];
  }

Поправим шаблон, согласно новым настройкам:

import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';

import '@polymer/polymer/lib/elements/dom-repeat.js';

class PmDemo extends PolymerElement {
  static get template() {
    return html `
      <style>
        :host {
          display: block;
        }
      </style>

      <nav class="navbar">
        <a class="navbar-brand" href="#">[[title]]</a>

        <div class="navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav">
            <template is="dom-repeat" items="{{links}}">
              <li class="nav-item">
                <a class="nav-link" href="[[item.url]]" >[[item.name]]</a>
              </li>
            </template>
          </ul>
        </div>
      </nav>
    `;
  }

  constructor() {
    super();
    this.title = 'Polymer';
    this.links = [
      {name: 'Home', url: '/'},
      {name: 'Sources', family: '/sources'},
    ];
  }
}

customElements.define('pm-demo', PmDemo);

Наш базовый компонент готов к использованию. Создадим файл index.html в корне нашего проекта и подключим туда наш созданный модуль pm-demo.js, так же необходимо подключить библиотеку webcomponents-loader.js. Вызов нашего компонента на странице html будет производиться таким образом

<pm-demo></pm-demo>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Polymer demo</title>

    <script>
      window.MyAppGlobals = { rootPath: '/' };
    </script>

    <link rel="stylesheet" href="./general.css">

    <script src="./node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
    <script type="module" src="./pm-lib/base/header/pm-demo.js"></script>
  </head>
  <body>
    <pm-demo></pm-demo>
  </body>
</html>

Обратите внимание, что стили, заданные для страницы index.html, не будут влиять на стилизацию компонента <pm-demo></pm-demo>, а компонент будет использовать стили, которые находятся в его шаблоне.

При объявлении компонента на странице или в другом шаблоне мы можем передавать ему параметры, в качестве атрибутов тега:

<pm-demo  title="Demo Title"></pm-demo>

Для того, что бы их использовать на странице самого шаблона, нужно прописать функцию get properties() для атрибутов тега:

import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';

import '@polymer/polymer/lib/elements/dom-repeat.js';

class PmDemo extends PolymerElement {
  static get template() {
    return html `
      <nav class="navbar">
        <a class="navbar-brand" href="#">[[title]]</a>
      </nav>
    `;
  }

  static get properties() {
    return {
      title: {
        type: String
      }
    }
  }
}

customElements.define('pm-demo', PmDemo);

Поздравляю, Ваш первый компонент готов!
Для того, что бы увидеть результат, необходимо выполнить 2 команды с консоли:

gulp
polymer serve

Так как мы уже научились передавать переменные в компоненты, мы можем отображать или прятать элементы при помощи dom-if. К примеру, это может выглядеть так – при вызове компонента мы передаем ему параметр:

<pm-header-search enable-searching></pm-header-search>

В конструкторе самого компонента значение преобразуется в true (Boolean) или в false, если этот параметр не задан. Для использования dom-if нужно подключить 'dom-if.js' через import как показано в примере:

import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';
import { PmComponent } from '../pm-component.js';

import '@polymer/polymer/lib/elements/dom-if.js';

class PmHeaderSearch extends PolymerElement {
  static get template() {
    return html `
      
      <template is="dom-if" if="{{enableSearching}}">
        <form class="form-inline my-2 my-lg-0" on-submit="search">
          <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" />
          <button class="btn btn-outline-pm my-2 my-sm-0" type="submit">submit</button>
        </form>
      </template>
    `;
  }

  static get properties() {
    return {
      enableSearching: {
        type: Boolean
      },
    }
  }

}

customElements.define('pm-header-search', PmHeaderSearch);

Data binding

В Polymer существует 2 типа data binding:

  1. One-way binding – отобразит значение name в дочернем элементе и изменится, если было изменено в родительском элементе [[name]]
  2. Two-way binding – отобразит значение name в дочернем элементе, а при изменении его в поле ввода – значение изменится в родительском элементе {{name}}
<p>Name: [[name]]</p>
<my-input value="{{name}}" label="Name"></my-input>

Events

Элементы Polymer используют события для изменения своего состояния, рассмотрим основные из них. Для добавления обработчика используйте конструкцию on-event в Вашем шаблоне, передавая ей функцию обработчик.
В примере реализован элемент accordion с функциями обработчиками on-click и функцией _getText, которая принимает в качестве аргумента статус аккордиона и заменяет текст кнопки по открытию.

import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';
import { PmComponent } from './pm-component.js';

import '@polymer/iron-collapse/iron-collapse.js';

class PmAccordion extends PmComponent {
  static get template() {
    return html `
      ${super.template}
      <div class="accordion">
        <div class="card">
          <div class="card-header">
            <h2 class="mb-0">
              <button id="trigger"
                on-click="toggle"
                class="btn btn-link"
                aria-expanded\$="[[opened]]"
                aria-controls="collapse">
                [[_getText(opened)]]
              </button>
            </h2>
          </div>
          <iron-collapse id="collapse" opened="{{opened}}" class="collapse show" horizontal="[[horizontal]]" no-animation="[[noAnimation]]" tabindex="0">
            <div class="card-body">
              [[data]]
            </div>
          </iron-collapse>
        </div>
      </div>
    `;
  }
  static get properties() {
    return {
      data: {
        type: String
      }
    }
  }

  constructor() {
    super();
  }

  toggle() {
    this.$.collapse.toggle();
  }

  _getText(opened) {
    return opened ? 'Collapse' : 'Expand';
  }

}

customElements.define('pm-accordion', PmAccordion);

Больше информации Вы сможете найти в документации Polymer Events.

Наследование от пользовательского класса

Порой Вам нужно создать несколько компонентов, структура которых может быть похожа и не всегда хочется при этом дублировать код в каждом файле. В таком случае можно создать пользовательский класс, в нашем случае он будет называться PmComponent, который будет наследоваться от PolymerElement, а от него уже будут наследоваться Ваши пользовательские компоненты.

Создадим файл pm-component.js:

import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';
import { setPassiveTouchGestures, setRootPath } from '@polymer/polymer/lib/utils/settings.js';

setPassiveTouchGestures(true);
setRootPath(MyAppGlobals.rootPath);

export class PmComponent extends PolymerElement {

  static get template () {
    return html`
        <link rel="stylesheet" href="./global.css">
        <style>
          :host {
            display: block;
          }
        </style>
      `;
  }
}

customElements.define('pm-component', PmComponent);

Теперь создадим файл pm-card.js, но наследоваться он будет от PmComponent, а в шаблоне добавим ${super.template}, таким образом элемент pm-card унаследует стили с "./global.css" файла, не смотря на то, что он не прописан в pmCard.

import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';
import { PmComponent } from './pm-component.js';

class PmCard extends PmComponent {
  static get template() {
    return html `
      ${super.template}

      <div class="card">
         ...
      </div>
    `;
  }

  static get properties() {
    return {
      card: {
        type: Object
      },
    }
  }
}

customElements.define('pm-card', PmCard);

Дальнейшее подключение и использование pm-component происходит так же как в примере выше.

Пользовательские CSS переменные

Пользовательские свойства CSS позволяют вам определять переменную CSS и использовать ее в стилях своих компонентов.
Для удобства подключим в index.html файл "./general.css" с таким содержимым:

html {
  --pm-primary-color: #ffde3b;
  --pm-hover-color: #e0a800;
}

Здесь я задала 2 переменные, которые смогу использовать в компонентах, подключеных в этом файле подобным образом:

import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';
import '@polymer/polymer/lib/elements/dom-if.js';

class PmHeaderSearch extends PmElement {
  static get template() {
    return html `
      <style>
        .pm-form .btn-outline-pm {
          color: var(--pm-primary-color);
          border-color: var(--pm-primary-color);
        }

        .pm-form .btn-outline-pm:hover {
          color: var(--pm-hover-color);
          border-color: var(--pm-hover-color);
          background: transparent;
        }
      </style>

      <form class="form-inline my-2 my-lg-0 pm-form" on-submit="search">
       <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" />
       <button class="btn btn-outline-pm my-2 my-sm-0" type="submit">Search</button>
      </form>
    `;
  }

  static get properties() {
    return {
      enableSearching: {
        type: Boolean
      },
    }
  }

  constructor() {
    super();
    this.button = 'Search';
  }

  search() {
    ...
  }
}

customElements.define('pm-header-search', PmHeaderSearch);

Ссылки

Полную информацию Вы сможете найти в документации библиотеки Polymer library, а полный демо проект по ссылке Polymer Intro.

Поделиться

Отправить ответ

avatar
  Subscribe  
Notify of