Шаблоны, в основном, остались прежними по структуре, но поменялись детали:
- Все контрольные инструкции используют XML namespace
end
. Например, теперь надо писать<end:if>
вместо<t-if>
. - Сами шаблоны теперь используют строгий XML. А это значит, что все тэги нужно обязательно закрывать. А все пустые тэги можно писать в сокращённой форме, типа
<div />
. Однако можно писать атрибуты без кавычек и использовать<
в тексте без экранирования. - События привязываются через namespace
on
:<div on:click={foo()}>
. - Использование выражений стало более строгим: теперь везде, где используются выражения, надо использовать
{...}
. Например, если раньше условия в некоторых конструкциях можно было писать в виде строковых литералов (<t-if test="foo">
), теперь их строго нужно писать внутри{...}
:<end:if test={foo}
. В большинстве случаев компилятор ругнётся, если где-то забыли выражение. - Изменилась схема обращения к свойствам, состоянию и локальным переменным:
Было
@prop
— обращение к атрибутуprop
компонентаstate('foo')
— обращение к параметруfoo
локального состояния$item
— обращение к локальной переменной шаблонаitem
Стало
-
prop
— обращение к атрибутуprop
компонента -
#foo
— обращение к параметруfoo
локального состояния -
@item
— обращение к локальной переменной шаблонаitem
-
$data
— обращение к свойствуdata
стора компонента (пока не реализовано) -
Изменилась логика работы циклов: теперь контекст не переключается на итерируемый элемент, а остаётся глобальным для компонента. Это значит, что теперь для обращения к свойству родительского компонента внутри цикла не нужно создавать локальную переменную со значением атрибута. В самом цикле итерируемый элемент присваивается локальной переменной
$value
. Также создаются переменные$index
(позиция интегрируемого элемента в списке, аналогposition()
) и$key
(ключ элемента в коллекции, аналогname()
). /TODO
: придумать синтаксис для указания названий этих переменных./ -
Выражения стали более похожими на JavaScript: теперь
[...]
— это обращение к свойству объекта, как в JS, предикатов больше нет, как и выборок по.*
. Вместо них используются /фильтры/. Фильтр — это стрелочная функция, которая передаётся в качестве ключа для получения значения:arr[value => value > 10]
. На чистом JS эта запись означает «дай мне значение изarr
по ключу, который равен строгому представлению выражения, то есть"value => value > 10"
, что не имеет практического смысла в данном случае. Поэтому компилятор такое выражение превращает в аналогarr.filter(value => value > 10)
с той разницей, чтоarr
может быть любой итерируемой коллекцией, не только массивом. Фильтр всегда возвращает массив отфильтрованных значений, преобразования, когда массив из одного значения превращался в само значение больше нет, чтобы избежать путаницы. -
Изменился синтаксис добавления атрибутов и объявления переменных. Если раньше для указания атрибута или переменной нужно было использовать контрольную инструкцию с парой атрибутов
name="..."
иvalue="..."
, то теперь нужно писатьname=value
. То есть в одной инструкции можно передать несколько атрибутов/переменный:<end:attribute foo="bar" baz={a + b} />
-
Названия атрибутов также могу быть выражениями:
<div {a ? 'foo' : 'bar'}="10" />
. -
Любому элементу можно добавить атрибут
end:if={...}
, это будет то же самое, что обрамить элемент в<end:if test={...}>
:<div end:if={enabled}>foo</div>
при компиляции превратиться в<end:if test={enabled}><div>foo</div></end:if>
-
Выражения внутри текста теперь пишутся через ординарные
{...}
вместо{{...}}
. В{{...}}
теперь нужно передавать текст, который должен рендерится как HTML. -
Поддержка
refs
: теперь любому элементу можно указатьref="refname"
и этот элемент будет доступен в компоненте какcomponent.refs.refname
. -
В дополнение к
<end:add-class>
можно добавлять классы элементу через атрибут видаclass:name={cond}
:<div class:enabled class:first={$index === 0}>
более короткая версия кода<div><end:add-class>enabled</end:add-class><end:add-class if="$index === 0">first</end:add-class></div>
. -
Булевые атрибуты теперь можно передавать как в HTML, просто указанием названия атрибута:
<input type="radio" checked />
. Если используется выражение, то булевой атрибут выведется только в том случае, если его значение будет истинным и не выведется совсем, если ложным. -
Для простых случаев для обработки событий не обязательно создавать отдельную функцию в компоненте, которая будет, например, запускать событие или менять стэйт, можно воспользоваться встроенными функциями:
<div on:click={emit('clicked')} on:mouseenter={ setState({hovered: true}) } on:mouseleave={ setState({hovered: false}) }>
. -
При импорте дочерних компонентов теперь обязательно нужно указывать атрибут
as="tag-name"
с названием тэга, для которого этот импорт предназначен:<link rel="import" href="inner-component.html" as="inner-component" />
В противном случае тэг<inner-component>
будет создан как обычный DOM-элемент, а не компонент. С другой стороны, это позволит проще экспериментировать, заменяя реализации компонента на другую в рамках эксперимента.
Шаблоны теперь поддерживают partials: переиспользуемые и переопределяемые фрагменты кода:
<template>
<ul>
<end:for-each select={items}>
<partial:button item={$item} enabled={$index !== 1} />
</end:for-each>
</ul>
</template>
<template partial:button item enabled={true} pos={0}>
<li class:enabled={$enabled}>{ $item }</li>
</template>
Выведет
<ul>
<li class="enabled">item 1</li>
<li>item 2</li>
<li class="enabled">item 3</li>
</ul>
Partial объявляется как элемент <template>
верхнего уровня с указанием partial:name
, где name
— это название шаблона. В качестве атрибутов указываются аргументы шаблона и их значения по умолчанию. Чтобы вызвать его , нужно в месте использования написать <partial:name ...args />
. Также partials можно переопределять. Например, если внутри компонента <my-component>
используется partial с названием my-item
, то его можно переопределить, передав в качестве свойства имя локального partial: <my-component partial:my-item="my-local-item" />
. ВАЖНО: тело partial исполняется в контексте компонента, в котором он отрисовывается, а заучит имеет доступ к локальным переменным, стэйту и свойствам компонента, в котором отрисовывается, а не в котором определен.
Описание компонента теперь представляет из себя обычный ES-модуль, который экспортирует свои данные и обработчики (публичный контракт). Также описание компонента подразумевает функциональный стиль : все экспортируемые методы являются общими для всех экземпляров компонента. Каждому методу в качестве первого аргумента приходит экземпляр компонента, для которого он должен сработать.
Пример описания компонента:
// Вызывается при создании компонента, возвращает props со значениями по умолчанию
export function props() {
return { a: 1, b: 2 };
}
// Вызывается при создании компонента, возвращает state со значениями по умолчанию
export function state() {
return { foo: 'bar' };
}
// Список методов, которые нужно добавить DOM-элементу компонента.
// Это будет его публичный контракт: если получить DOM-элемент компонента,
// за эти методы его можно дёргать
export const methods = {
enable() {
this.setState({ enabled: true });
}
}
// Инициализация компонента
export function init(component) {
console.log('inited');
}
// Вместо компонента удобно использовать деструктуризауию,
// чтобы удобнее получать и обновлять данные
export function onClick({ props, setState }) {
setState({ enabled: props.enabled });
}
Список всех известных свойств и методов описания компонента доступен в интерфейсе ComponentDefinition
.
Также концептуально поменялся публичный контракт передаваемых свойств компонента. Теперь это не атрибуты, у которых была поддержка типов данных, а свойство props
. Атрибуты у компонента теперь используются в качестве наглядного представления соответствующих данных в props
. Чтобы получить свойства и стэйт компонента, нужно использовать .props
и .state
соответственно. А для обновления вызывать методы .setProps({ prop1: 1, prop2: 2 })
и .setState({ state1: 1, state2: 2 })
. Оба метода отвязано от контекста, то есть могут использоваться как самостоятельные функции после деструктуризации.
Также из-за того, что атрибуты не являются источником данных, отсутствует метод attributeChangedCallback()
: вместо него нужно использовать один из will*
методов, в который передаётся список изменившихся пропсов и стэйтов, которые привели к перерисовке компонента.
Методы жизненного цикла также были расширены. Теперь в компоненте чётко разделяются стадии монтирования: willMount()/didMount()
(первичная отрисовка компонента) и willUpdate()/didUpdate()
(все последующие обновления). Если нужно реагировать на все изменения, то нужно по-прежнему использовать willRender()/didRender()
. Также появились методы willUnmount()
(компонент буден удалён, но всё ещё находится в DOM и работает) и didUnmount()
(компонент полностью удалён).
Вспомогательные методы вроде диспетчеризации событий также были вынесены в отдельные функции и подключаются по запросу:
import { on, emit } from 'endorphin';
export function init(component) {
on(component, 'play', () => emit(component, 'play-init'));
}
В шаблона стили и скрипты можно подключать как внешними ресурсами:
<template>
<p>Hello world</p>
</template>
<script src="my-component.js" />
<link rel="stylesheet" href="my-component.scss" />
...так и писать инлайн:
<template>
<p>Hello world</p>
</template>
<script>
export function init() {
console.log('inited');
}
</script>
<style type="scss">
p {
font-size: 20px;
}
</style>
В случае, если скрипты и стили отличаются от JS и CSS, в тэгах <script>
и <style>
можно указать атрибут с MIME-типом кода, который там используется: этот тип будет учтён при компиляции.