npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

endorphin

v0.11.0

Published

Component-based UI rendering library

Downloads

122

Readme

EndorphinJS

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

Disclaimer: документация ещё очень поверхностная и сырая и предназначена скорее для энтузиастов, разделяющих ценности и проблемы, которые пытается решить Endorphin. Позже появится полноценный сайт с примерами и лучшими практиками.

Основные возможности

  • Декларативный шаблонизатор, имеющий JavaScript-подобный синтаксис выражений для удобного обращения к данным. Синтаксис выражений намеренно ограничен по сравнению с JavaScript, но также имеет расширенную семантику для работы с данными.
  • В отличие от React/Vue.js/Svelte, каждый Endorphin-компонент имеет реальное представление в виде DOM-элемента. Поэтому Endorphin-компоненты больше похожи на веб-компоненты, но ими не являются (об этом ниже). Такой подход радикально упрощает отладку и стилизацию компонентов: все они доступны прямо в инструментах разработчика любого браузера, в том числе через протокол удалённой отладки.
  • Отсутствие стороннего runtime для работы приложения на EndorphinJS: код, необходимый для работы приложения, определяется на этапе компиляции и внедряется непосредственно в приложение. Сам runtime достаточно компактный: вес всего кода — около 6 КБ (gzip).
  • Изоляция CSS: на этапе сборки весь CSS компонента полностью изолируется и применяется только к своему компоненту.
  • Очень быстрое обновление UI: никаких Virtual DOM, шаблон компонента анализируется на этапе сборки и для него генерируется код, который обновляет только изменяемые части шаблона. Сгенерированный код оптимизирован под особенности JIT-компиляторов современных JS-движков для большей производительности.
  • Endorphin-компоненты не скрывают, а наоборот, пропагандируют использование Web API: вы можете обращаться к содержимому компонента как к любому содержимому DOM-элемента, а также манипулировать содержимым. Сам Endorphin работает только с теми данными, которые сам создал. Это значит, что вы можете манипулировать DOM-элементами компонента (в разумных пределах, конечно) и не боятся, что следующий цикл перерисовки компонента всё отменит.
  • Встроенная поддержка анимаций появления и удаления элемента на основе CSS Animations.
  • Endorphin-приложение можно безопасно вставлять на любой сайт: за счёт полной изоляции и отсутствия стороннего runtime можно быть уверенным, что приложение никак не повлияет на остальные части сайта.

Первое знакомство

Давайте создадим наш первый компонент на Endorphin:

<!-- my-component.html -->
<style>
button {
    appearance: none;
    display: inline-block;
    background: none;
    border: 3px solid blue;
    padding: 5px;
}
</style>
<template>
    <button on:click={ #count++ }>Click me</button>
    <e:if test={#count}>
        <p>Clicked { #count } { #count !== 1 ? 'times' : 'time' }</p>
    </e:if>
</template>
<script>
export function state() {
    return { count: 0 }
}

export function didMount(component) {
    console.log('Mounted component', component.nodeName);
}
</script>

И создадим приложение, которое вставляет этот компонент на страницу:

// app.js
import endorphin from 'endorphin';
import * as MyComponent from './my-component.html';

endorphin('my-component', MyComponent, {
    target: document.body
});

После сборки мы получим приложение с кнопкой Click me, клик на которую будет менять надпись с количеством кликов. Если посмотреть через DevTools, то вы увидите на странице элемент <my-component> и его содержимое. Из приведённого примера можно узнать следующее:

  • Компонент описывается стандартными HTML-тэгами: стили описываются в тэге <style> или подключаются через <link rel="stylesheet" />, шаблон описывается в тэге <template>, а поведение — в тэге <script> либо подключается из стороннего файла через <script src="...">.
  • Имя файла используется как имя DOM-компонента. Так как Endorphin-компоненты идейно похожи на веб-компоненты, имя файла должно содержать дефис, однако это поведение можно переопределить (см. раздел Вложенные компоненты).
  • В стилях можно безопасно использовать в том числе и тэги для стилизации: за счёт изоляции можно быть уверенным, что стили для button из my-component.html никак не повлияют на кнопку из other-component.html. Стандартный CSS содержит несколько расширений, позволяющих управлять изоляцией (см. раздел CSS).
  • Шаблон (как и всё описание компонента) использует XML-подобный синтаксис. Это означает, что все тэги должны быть закрыты (<p></p>) либо иметь закрывающий слэш в конце (<br />). При этом, в отличие от XML, можно не экранировать спецсимволы вроде < и >, а также не обязательно использовать кавычки для значений атрибутов.
  • Контрольные инструкции для описания динамических частей шаблона также описываются XML-тэгами, как правило, с префиксом e:. Динамические выражения указываются внутри фигурных скобок: { #count }. Динамические значения атрибутов пишутся как name={...}, однако если ваш редактор/IDE не понимает такой синтаксис, можно писать name="{...}".
  • Поведение компонента описывается в виде ES-модуля: вы экспортируете объекты и функции, которые известны Endorphin runtime. В экспортируемые функции первым аргументом всегда (кроме некоторых случаев с обработчиками событий) передаётся экземпляр компонента (DOM-элемент), которым можно управлять. Таким образом, Endorphin продвигает функциональный подход к описанию поведения компонента и избавляет от множества проблем с this.
  • У компонента есть несколько источников данных: props (внешний контракт, передаётся в компонент снаружи), state (внутренний контракт, управляется самим компонентом) и store (данные приложения). Для удобства внутри шаблона используется используется специальный префикс для каждого источника данных:
    • Для обращения к значению из props достаточно написать propName, то есть обращаться как к глобальной переменной.
    • Для обращения к state используется префикс # (по аналогии с приватными свойствами классов в JS): #stateName.
    • Для обращения к store используется префикс $: $storeName.

Шаблон

Как было отмечено выше, шаблон представляет из себя XML-подобный со специальными тэгами, которые управляют содержимым компонента.

Элемент

Элементы в шаблоне описываются так же, как и в HTML: с помощью тэгов и атрибутов. Тэги обязательно должны быть закрыты либо с помощью закрывающего тэга, либо с помощью закрывающего слэша:

<h1>My header</h1>
<p class="intro">First<br />paragraph</p>

Атрибуты могут иметь следующий вид:

  • name="value" — значение в кавычках, можно указывать либо ординарные, либо двойные кавычки;
  • name=value — короткая запись значений, состоящих из одного слова, кавычки можно не использовать;
  • name={expr} или name="{expr}" — значением атрибута является результат работы выражения expr`.
  • name="foo {expr} bar" – интерполяция строк и выражений: в указанной строке значения в фигурных скобках заменяются на результат выражения. Аналогичный результат можно получить с помощью Template Strings: name={`foo ${expr} bar`}.
  • name — булевое значение атрибута, аналог name={true}.
  • {name} — сокращённая запись вида name={name}, такая же запись доступна и для state и store: {#enabled}enabled={#enabled}, {$data}data={$data}.

Endorphin различает два типа тэгов: обычные HTML-элементы и DOM-компоненты. Последние имеют дефис в названии (по аналогии с веб-компонентами):

<h1>My header</h1>
<my-component enabled data={items} />

У обычных элементов и DOM-компонентов немного отличается поведение атрибутов:

  • Для обычных элементов атрибуты выводятся как обычные HTML-атрибуты, но для компонентов атрибуты — это props. Для удобства разработки props также отображаются как HTML-атрибуты у сгенерированного элемента. А для того, чтобы соответствовать семантике HTML, названия атрибутов конвертируются из camelCase в kebab-case: <my-component someItems={items}> в DevTools отобразится как <my-component some-items="{}">. HTML-атрибуты у DOM-компонентов носят чисто информативный характер и используются в CSS-стилизации и отладки кода, а сами данные доступны в свойстве .props элемента. У обычных элементов атрибуты являются источником данных, то есть влияют на работу элемента.
  • Для обоих типов элементов выполняется приведение значений атрибутов для отображения в HTML:
    • строки и числа отображаются как есть;
    • функция отображается как 𝑓;
    • массив отображается как [];
    • прочие непустые значения отображаются как {};
    • для булевых значений true выводит атрибут с пустым значением (<input disabled={true} /><input disabled />), false не выводит атрибут совсем (<input disabled={false} /><input />).
    • значения null и undefined не выводят атрибут совсем;
    • у DOM-компонентов для значений атрибутов без кавычек выполняется простое приведение типов для чисел, true, false, null и undefined: <my-item foo=1 bar=true type=null /> равнозначно <my-item foo={1} bar />.

Директивы

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

class:

Директива class: добавляет элементу указанный класс, если условие, указанное в значении директивы, истинно. Если значение отсутствует, класс добавляется всегда.

<div class="foo" class:bar class:baz={enabled != null}></div>

Следует помнить, что директива class: именно добавляет класс к имеющимся, в то время как атрибут class полностью его заменяет:

<div class:foo class:bar class:baz={enabled != null}>
    <!-- Всегда будет выводить <div class="abc"> -->
    <e:attribute class="abc"/>
</div>

ref:

Добавляет в текущий DOM-компонент ссылку на указанный элемент:

<template>
    <div ref:container></div>
</template>
<script>
export function didRender(component) {
    console.log(component.refs.container); // <div>
}
</script>

Также может быть указан в виде атрибута: ref="container"ref:container. Значением атрибута может быть выражение, которое должно вернуть либо имя рефа, либо null, если ссылку надо удалить:

<div ref={enabled ? 'container' : null}></div>

on:

Добавляет событие с указанным названием элементу. Значением директивы всегда должно быть выражение. В качестве обработчика события указывается функция, экспортируемая из поведения компонента:

<template>
    <button on:click={handleClick}>Click me</button>
</template>
<script>
export function handleClick(component, event, target) {
    console.log('Clicked on', target.nodeName); // Clicked on BUTTON
}
</script>

В обработчик всегда передаются следующие аргументы:

  • component — текущий экземпляр компонента;
  • event — событие;
  • target — элемент, к которому было привязано событие.

Дополнительно в обработчике можно передавать произвольные аргументы, они будут добавлены в начало списка аргументов обработчика:

<template>
    <button on:click={handleClick('show', 1)}>Click me</button>
</template>
<script>
export function handleClick(action, count, component, event, target) {
    event.preventDefault();
    console.log('Run %s action %d time(s) on %s', action, count, target.nodeName);
    // Run show action 1 time(s) on BUTTON
}
</script>

В качестве обработчика события можно указать arrow function, в которую первым аргументом передаётся объект события:

<template>
    <button on:click={evt => moveTo(evt.pageX, evt.pageY)}>Click me</button>
</template>
<script>
export function moveTo(x, y) {
    console.log('Move to %d, %d', x, y);
}
</script>

Обработчики событий — это единственное место в шаблоне, где разрешено присвоение в state и store:

<template>
    <button on:click={#enabled = !#enabled}>Click me</button>
    <e:if test={#enabled}>
        Block is enabled
    </e:if>
</template>

При объявлении события можно дополнительно указывать модификаторы stop (вызовет event.stopPropagation()) и prevent (вызовет event.preventDefault()):

<button
    on:click:stop
    on:mousemove:prevent={handleMouseMove}>
    Click me
</button>

animate:

Указывает CSS-анимацию на добавление (in) или удаление (out) элемента. Если указана анимация удаления, сам элемент очистится и удалиться только после завершения указанной анимации.

В качестве значения директива принимает значение CSS-свойства animation: название анимации, длительность, задержка, функция изинга и т.д.

<template>
    <section
        animate:in="show-block 0.3s ease-out"
        animate:out="hide-block 0.2s ease-in"
        e:if={#enabled}>
        Lorem ipsum dolor sit amet.
    </section>
</template>
<style>
@keyframes show-block {
    from {
        transform: scale(0.5);
        opacity: 0;
    }
}

@keyframes hide-block {
    to {
        transform: scale(0.5) translateY(30%);
        opacity: 0;
    }
}
</style>

Так как весь CSS (в том числе анимации) изолируются, возможна ситуация, что вы дублируете описания одних и тех же анимаций между компонентами. Чтобы избежать этого, при указании названия анимации можно использовать префикс global: — в этом случае будет использована анимация без изоляции, определённая где-нибудь в другом месте. В разделе про CSS вы узнаете, что для отмены изоляции CSS нужно использовать @media global:

<template>
    <section
        animate:in="global:show-block 0.3s ease-out"
        animate:out="global:hide-block 0.2s ease-in"
        e:if={#enabled}>
        Lorem ipsum dolor sit amet.
    </section>
</template>
<style>
@media global {
    @keyframes show-block {
        from {
            transform: scale(0.5);
            opacity: 0;
        }
    }

    @keyframes hide-block {
        to {
            transform: scale(0.5) translateY(30%);
            opacity: 0;
        }
    }
}
</style>

В текущей реализации нет проверки, определена ли CSS-анимация с указанным названием. Это означает, что если на animate:out вы укажете название анимации, которая не была объявлена, элемент и его содержимое никогда не удалится, так как рантайм будет ожидать событие animationend для выполнения очистки и это событие никогда не произойдёт. В будущих версиях эта проблема будет исправлена.

use:

Директива use:action выполняет функцию action в момент создания элемента. В качестве первого аргумента action передаётся элемент, у которого указана директива. Функция может вернуть объект с методом destroy(), который вызовется в момент удаления элемента:

<template>
    <img src="image.png" use:checkLoad e:if={visible} />
</template>
<script>
    export function checkLoad(elem) {
        const onLoad = () => console.log('image loaded);
        elem.addEventListener('load', onLoad);

        return {
            destroy() {
                elem.removeEventListener('load', onLoad);
            }
        }
    }
</script>

Дополнительно в качестве значения директивы можно передать произвольное значение и вернуть из action объект с методом update: этот метод будет вызываться каждый раз, когда указанное значение поменяется:

<template>
    <img src="image.png" use:checkLoad={#visible} e:if={visible} />
</template>
<script>
    export function checkLoad(elem, param) {
        const onLoad = () => console.log('image loaded);
        elem.addEventListener('load', onLoad);

        return {
            update(param) {
                console.log('param updated', param);
            },
            destroy() {
                elem.removeEventListener('load', onLoad);
            }
        }
    }
</script>

Текст

Текстовые значения описываются так же, как и в HTML. Для отображения результатов выражений используются фигурные скобки:

Hello { greeting }!

Если фигурные скобки надо вывести в качестве текста, достаточно заменить их соответствующими HTML entity:

Hello &#123; greeting &#125;!

<e:variable> (or alias <e:var>)

Создаёт локальные переменные шаблона. Именем переменной является название атрибута в <e:variable>. Для обращения к локальной переменной в шаблоне используется префикс @:

<e:variable sum={a + b} enabled={isEnabled != null} />

<e:if test={@enabled}>
    Sum is { @sum }
</e:if>

<e:if>

Выводит содержимое, если условие истинно.

  • test — выражение для проверки.
<e:if test={a > 1}>
    <p><code>a</code> is greater than 1</p>
</e:if>

Для удобства, если выводить нужно только один элемент, условие можно записать как директиву e:if у элемента:

<p e:if={a > 1}><code>a</code> is greater than 1</p>

<e:choose>/<e:when>/<e:otherwise> (or alias <e:switch>/<e:case>/<e:default>)

Аналог if/else if/else: внутри элемента <e:choose> перечисляются секции <e:when test={...}>, из которых выполнится первая, в которой условие атрибута test истинной. Если ни одно из условий не было истинным, сработает секция <e:otherwise>:

<e:choose>
    <e:when test={#color === 'red'}>Color is red</e:when>
    <e:when test={#color === 'blue' || #color === 'green'}>Color is blue or green</e:when>
    <e:otherwise>Unknown color</e:otherwise>
</e:choose>

<e:for-each>

Выводит содержимое для каждого элемента из полученной коллекции.

  • select — выражение, которое должно вернуть коллекцию для интерации. Коллекция определяется по наличию метода .forEach у результата, то есть это может быть массив, Map, Set или любой другой объект, поддерживающий семантику .forEach коллекций. Если результат выражения не содержит этот метод, цикл выполнится один раз для этого значения.
  • [key] — выражение, которое должно возвращать строковый ключ для текущего элемента. При наличии этого ключа сгенерированный результат «привязывается» к элементу с этим ключом. В этом случае при пересортировке данных в коллекции гарантируется, что именно эти DOM-элементы, сгенерированные на прошлом шаге отрисовки, будут использоваться для отрисовки этого же элемента. В основном это используется вместе с анимациями, когда нужно гарантировать идентичность элементов при перерисовке, а также в некоторых случаях может повысить производительность.

Для каждого элемента коллекции создаётся три локальные переменные:

  • @value — значение элемента коллекции
  • @key — ключ элемента в коллекции
  • @index — порядковый номер элемента в коллекции, начиная с 0 (для массива это значение равно @key).
<ul>
    <e:for-each select={items}>
        <li value={@key}>Value is { @value }<li>
    </e:for-each>
</ul>

Использование key:

<ul>
    <e:for-each select={items} key={@value.id}>
        <li value={@key}>{ @value.id }: { @value.name }<li>
    </e:for-each>
</ul>

<e:attribute> (or alias <e:attr>)

Выводит либо заменяет указанные атрибуты у родительского элемента:

<div title="Section">
    <e:attribute class="block" title="Block" />
</div>

Эту инструкцию удобно использовать, когда некоторые атрибуты нужно вывести или удалить в зависимости от условий, а также для удобной организации кода в <e:choose> блоках:

<my-component data={items} enabled>
    <!-- Меняем занчение `enabled` на `false` если `data != 'foo'` -->
    <e:attribute enabled=false e:if={data != 'foo'} />

    <e:choose>
        <e:when test={type === 'block'}">
            <!--
            Организуем значение `data` родительского элемента и его содержимое
            в единый логический блок
            -->
            <e:attribute data={blockItems} />
            <p>This is block</p>
        </e:when>
        <e:when test={type === 'hidden'}">
            <!--
            Удаляем значение атрибута `data` у родительского элемента,
            также в едином логическом блоке
            -->
            <e:attribute data=null />
            <div>This block is hidden</div>
        </e:when>
    </e:choose>
</my-component>

В блоке <e:attribute>, помимо самих атрибутов, можно использовать директивы class: и on:.

<e:add-class>

Добавляет указанный класс (или несколько классов) родительскому элементу. Как правило, используется для добавления динамических классов, для которых нужен результат выражения:

<div>
    <e:add-class>foo bar-{#bar + 1}</e:add-class>
</div>

Для статических классов удобнее использовать директиву class: у элемента.

Выражения

Выражения в шаблонах представляют собой обычные JavaScript-выражения но со следующими важными изменениями.

Возможности выражений намеренно ограничены подмножеством, необходимым для получения данных. То есть вы можете обращаться к свойствам объектов, выполнять над ними логические и математические операции, но не сможете, например, создать класс или генератор (а оно вам надо в шаблонах?). Это сделано для того, чтобы ту же самую семантику выражений можно было повторить на любом языке программирования, например, Java, Python, Go и т.д.

Вызов методов внутри объектов допустим, но это не рекомендуется, так как правильно это реализовать для SSR на любом языке программирования будет достаточно проблематично. Например, вот такое выражение будет работать в браузере и SSR на JS, но не будет работать, скажем, на Go SSR, так как для этого нужно будет реализовать целый JS-интерпретатор, чтобы определять тип объекта и его методы:

<e:for-each select={items.slice().sort((a, b) => a.pos > b.pos)}>
    ...
</e:for-each>

Поэтому задача синтаксиса выражений в Endorphin — это покрыть 90% нужд разработчика, а остальные 10% — с помощью хэлперов. Хэлпер — это функция, которая имеет реализацию и на JS (для браузера), и на языке для SSR. Подробности про хэлперы появятся позже.

Обработчики событий — единственное место, где можно пользоваться JS без указанных выше ограничений, так как этот код будет работать только в браузере.

На данный момент выражения в Endorphin обладают следующими возможностями:

  • Все «глобальные» переменные считаются свойствами (props) компонента. То есть выражение {enabled}, по сути, обращается к component.props.enabled. Для обращения к state и store компонента используются префиксы # и $ соответственно: #enabled, $config.user.admin и т.д.
  • В названиях переменных и свойствах допустимо использование дефиса для лучшей интеграцией с HTML: $config.current-user.active, my-prop.list. Для операции вычитания дефис (знак минуса) нужно отделять пробелами: prop1 - prop2.
  • Все обращения к свойствам и методам абсолютно безопасны. Вы можете написать так и не переживать, что какого-то объекта (например, my или nested) не будет существовать: такое выражение просто вернёт undefined.
<e:if test={my.deeply.nested.prop}>
    ...
</e:if>
  • Для поиска элемента в коллекции можно использовать синтаксис arr[item => item.enabled], что является аналогом arr.find(item => item.enabled) для массива, но работает в том числе и для Map, Set или любого другого объекта, у которого есть метод .forEach(). Это рекомендуемый синтаксис для поиска элемента, так как в AST шаблона для него выделяется специальный узел, благодаря чему будет легче реализовать поддержку SSR для всех языков.
  • Аналогично, для фильтрации коллекции рекомендуется использовать синтаксис arr[[item => item.enabled]] (аналог arr.filter(item => item.enabled)), то есть обрамить стрелочную функцию массивом.

CSS

Для стилизации содержимого компонента используется обычный CSS с добавлением селекторов веб-компонентов, таких как :host() и ::slotted. Весь CSS компонента автоматически изолируется и применяется только к текущему компоненту: теперь вы можете безопасно стилизовать обычные тэги и не переживать, что CSS-правила пересекутся с другим компонентом. Например, вот такой CSS:

ul {
    padding: 10px;
}

ul li {

}

после компиляции превратится примерно в такой код:

ul[endo4tueq] {
    padding: 10px;
}

ul[endo4tueq] li[endo4tueq] {

}

Для каждого компонента высчитывается уникальный хэш на этапе компиляции, который добавляется к селекторам и к элементам шаблона.

В дополнение к стандартному CSS, компилятор понимает следующие селекторы и правила:

:host spec

Используется для стилизации самого DOM-компонента. Можно использовать как :host (стили для DOM-компонента), так и :host(sel) (стили для DOM-компонента, если к нему применён селектор sel).

:host {
    display: block;
    padding: 10px;
}

:host(.selected) {
    background: red;
}

:host-context() spec

Свойства внутри :host-context(sel) применяются к DOM-компоненту только в том случае, если он находится внутри элемента, к которому применим селектор sel.

:host {
    background: red;
}

:host-context(main article) {
    background: blue;
}
<my-component /> <!-- bg: red -->

<main>
    <article>
        <my-component /> <!-- bg: blue -->
    </article>
</main>

::slotted(sel) spec

Применяется к элементам sel, которые были переданы в текущий компонент снаружи через слот. К данным, указанным в слоте по умолчанию, правила не применяются.

::slotted(p) {
    color: red;
}

@media local

Внутри правила @media local указываются правила, которые должны применятся каскадом от текущего компонента. Для этих правил не выполняется изоляция, им только добавляется селектор текущего компонента:

@media local {
    p {
        margin: 1em;
    }

    blockquote {
        padding: 10px;
    }
}

...сгенерирует примерно такой код:

[endo4tueq-host] p {
    margin: 1em;
}

[endo4tueq-host] blockquote {
    padding: 10px;
}

Это правило удобно применять, когда нужно, например, указать базовые стили для всего приложения или когда вы вставляете в компонент стороннюю библиотеку, которая сама генерирует HTML и CSS и вы хотите поменять стиль для этого кода. Например, если ваш компонент вставляет редактор CodeMirror, для его стилизации вам нужно использовать @media local, чтобы стили не изолировались:

@media local {
    .CodeMirror {
        font-size: 20px;
    }

    .CodeMirror-gutters {
        border-right: 2px solid red;
    }
}

@media global

Внутри @media global указываются правила, которым вообще не применяется никакая изоляция, то есть они применимы к всему сайту и выводятся как есть. Самый частый пример применения @media global – это создание библиотеки CSS-анимаций появления и удаления элементов.

@media global {
    @keyframes show-item {
        from: {
            transform: scale(0);
            opacity: 0;
        }
    }

    @keyframes hide-item {
        to: {
            transform: scale(0);
            opacity: 0;
        }
    }
}

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

Другой пример применения @media global — это стилизация элементов за пределами вашего приложения. Например, вы разрабатываете приложение, которое должно вставляться на существующий сайт и вы знаете, как обратиться к элементу, в который вставляется ваше приложение, чтобы применить ему стандартные стили.

Препроцессоры

Так как все Endorphin-специфичные дополнения полностью совместимы с базовым CSS, для стилизации компонентов можно использовать популярные CSS-препроцессоры вроде SCSS и Less. В репозитрии с примерами есть шаблон настройки сборки с использованием SCSS для стилизации.

Поведение компонента

Endorphin-компонент — это обычный DOM-элемент, которому добавляется несколько свойств и методов:

  • props (Object) — свойства компонента, переданные снаружи. Это внешний контракт, по которому внешний мир общается с компонентом.
  • setProps(obj) — обновляет свойства компонента, указанные в obj. Данные должны быть иммутабельными: если меняете свойство какого-то объекта в props, сам объект нужно пересоздавать.
  • state (Object) — внутренние свойства компонента (внутренний контракт), которые компонент сам у тебя меняет.
  • setState(obj) — обновляет внутренние свойства компонента, указанные в obj. Как и в setProps(), данные должны быть иммутабельными.
  • root (Element) — указатель на основной компонент приложения.
  • refs (Object) — указатели на элементы шаблона.
  • store (Store) — указатель на store приложения, автоматически наследуется от родителя.

Поведение компонента описывается в виде ES-модуля: вы описываете всю логику в модуле и экспортируете функции жизненного цикла, за которые рантайм будет дёргать при наступлении изменений:

/** Начальные свойства компонента */
export function props() {
    return { items: null, enabled: false };
}

/** Начальные внутренние свойства компонента */
export function state() {

}

/** Создан экземпляр компонента */
export function init(component) {

}

/** Вызывается при изменении props */
export function didChange(component, { enabled }) {
    if (enabled) {
        console.log('Enabled changed from', enabled.prev, ' to ', enabled.current);
    }
}

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

Методы setProps() и setState() являются bound-методами, то есть они не используют this и вы можете деструктурировать их в методах:

export function didChange({ setState }, { enabled }) {
    if (enabled) {
        setState({ show: true });
    }
}

Доступны следующие методы жизненного цикла:

  • props() — возвращает объект с начальными публичными свойствами компонента. Значения из этого объекта являются значениями по умолчанию, то есть если вызов setProp() выставит какое-то свойство в null или undefined, значение свойства будет взято из этого объекта.
  • state() — возвращает объект с начальными приватными свойствами компонента.
  • store() — возвращает стор компонента. Если не указан, стор будет унаследован от родителя.
  • init(component) — создан экземпляр компонента. Он ещё пустой, не содержит начальных свойств.
  • willMount(component) — сформированы входные данные для компонента (слоты, props) и он собирается отрисоваться.
  • didMount(component) — компонент отрисовался в первый раз.
  • willUpdate(component, changes) — пришло обновление и компонент собирается перерисоваться. В changes перечислены props, которые поменялись после предыдущей отрисовки. Ключом является название свойства, а значением — объект {prev, current} (предыдущее и текущее значение свойства). Объект changes может быть пустым, если перерисовка была вызвана изменением стэйта.
  • didUpdate(component, changes) — компонент перерисовался после обновления.
  • willRender(component, changes) — вызывается перед любой отрисовкой компонента. Фактически, willMount() — это самый первый willRender(), willUpdate() — все последующие.
  • didRender(component, changes) — вызывается после любой отрисовки компонента. Фактически, didMount() — это самый первый didRender(), didUpdate() — все последующие.
  • didChange(component, changes) — вызывается после изменения props. Все предыдущие will*/did* методы могут быть вызваны при изменении стэйта и стора.
  • willUnmount(component) — компонент будет удалён. Он всё ещё присутствует в дереве и активен.
  • didUnmount(component) — компонент удалён. Его больше нет в дереве, все события отвязаны, компонент более не активен.
  • didSlotUpdate(component, slotName, slotContainer) — поменялось содержимое слота slotName.

Также доступны следующие свойства модуля:

  • events — список DOM-событий, на которые нужно подписать компонента. Подписки будут автоматически удалены при удалении компонента.
  • extend — свойства и методы, которые нужно добавить DOM-компоненту. За эти свойства и методы можно дёргать компонент напрямую из DOM.
  • plugins — список плагинов (описание добавится позже).
export const events = {
    click(component, event) {
        event.stopPropagation();
        console.log('Clicked on component at %d, %d', event.pageX, event.pageY);
        component.toggle();
    }
}

export function state() {
    return { enabled: false };
}

export const extend = {
    // Так как extend добавляет свойства и методы непосредственно компоненту,
    // для обращения к нему нужно использовать `this`
    get enabled() {
        return this.state.enabled;
    },

    set enabled(enabled) {
        this.setState({ enabled });
    },

    toggle() {
        this.enabled = !this.enabled;
    }
}

Вложенные компоненты

Как и веб-компоненты, Endorphin-компоненты можно вкладывать друг в друга с помощью слотов.

По спецификации, <slot> — это «дырка», через которую можно передавать HTML-элементы в текущий компонент. В этом Endorphin полностью повторяет поведение веб-компонентов: в компоненте можно объявить несколько слотов (один слот по умолчанию + именованные слоты), в них можно указать значение по умолчанию. Если в слот пришли данные снаружи, у него появится атрибут slotted.

Чтобы добавить вложенный компонент, его нужно сначала подключить через <link rel="import" href="..." />:

<link rel="import" href="./my-component.html" />

<template>
    <my-component size=10 />
</template>

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

<link rel="import" href="./my-component.html" as="something-different" />

<template>
    <something-different size=10 />
</template>

Всё содержимое компонента попадает в слот по умолчанию:

<link rel="import" href="./my-component.html" />

<template>
    <my-component>Hello <strong>world!</strong></my-component>
    <!-- Выведет Greeting is <slot>Hello <strong>world!</strong></slot> -->
</template>

<!-- my-component.html -->
<template>
    Greeting it <slot>default</slot>
</template>

У компонента может быть несколько слотов, у всех у них должны быть свои названия. Чтобы передать элемент в конкретный слот, нужно указать ему slot="...":

<link rel="import" href="./my-component.html" />

<template>
    <my-component>
        <h2 slot="header">Main header</h2>
        Hello <strong>world!</strong>
        <p slot="footer">Outer footer</p>
        <!-- Порядок и количество элементов для передачи в слот не важен -->
        <h3 slot="header">Sub header</h3>
        <h4 slot="header">Small header</h4>
    </my-component>
</template>

<!-- my-component.html -->
<style>
slot[name=header] {
    border: 2px solid red;
    padding: 5px;
}

/* Не выводим элемент слота, если он пустой */
slot[name=header]:empty {
    display: none;
}
</style>
<template>
    <slot name="header"></slot>
    Greeting it <slot>default</slot>
    <footer>
        <slot name="footer"></slot>
    </footer>
</template>