Пользовательские теги

Пример

Пользовательские теги в Riot - основные строительные блоки для интерфейсов. Они берут на себя часть “представление” в приложении. Давайте начнём с TODO-приложения, чтобы осветить различный функционал Riot:

<todo>

  <h3>{ opts.title }</h3>

  <ul>
    <li each={ items }>
      <label class={ completed: done }>
        <input type="checkbox" checked={ done } onclick={ parent.toggle }> { title }
      </label>
    </li>
  </ul>

  <form onsubmit={ add }>
    <input name="input" onkeyup={ edit }>
    <button disabled={ !text }>Add #{ items.length + 1 }</button>
  </form>

  <script>
    this.disabled = true

    this.items = opts.items

    edit(e) {
      this.text = e.target.value
    }

    add(e) {
      if (this.text) {
        this.items.push({ title: this.text })
        this.text = this.input.value = ''
      }
    }

    toggle(e) {
      var item = e.item
      item.done = !item.done
      return true
    }
  </script>

</todo>

Пользовательские теги компилируются в JavaScript.

Вы увидеть пример, изучить исходный код, или скачать [zip-архив]]](https://github.com/riot/examples/archive/gh-pages.zip).

Синтаксис пользовательских тегов

Теги Riot - это сочетание шаблона (HTML) и логики (JavaScript). Вот основные правила:

Определение тега всегда должно быть в начале файла.

<!-- правильно -->
<my-tag>

</my-tag>

<!-- правильно -->
<my-tag></my-tag>

  <!-- вызовет ошибку из-за отступа -->
  <my-tag>

  </my-tag>

Без тега <script>

Не обязательно всегда писать тег <script>:

<todo>

  <!-- шаблон -->
  <h3>{ opts.title }</h3>

  // логика
  this.items = [1, 2, 3]

</todo>

В этом случае логика начинается после последнего HTML тега. Этот «открытый синтаксис”, он часто используется в примерах на этом сайте.

Пре-процессинг

Вы можете задать тип препроцессора через атрибут type. Например:

<my-tag>
  <script type="coffee">
    # тут ваш coffeescript
  </script>
</my-tag>

Сейчас доступны “coffee”, “typescript”, “es6” и “none”.

Подробности можно посмотреть здесь.

Стили тегов

Вы можете положить style внутрь пользовательcго тега. Riot.js автоматически вынесет содержимое в <head>.

<todo>

  <!-- шаблон -->
  <h3>{ opts.title }</h3>

  <style>
    todo { display: block }
    todo h3 { font-size: 120% }
    /** стили **/
  </style>

</todo>

Локальные CSS

Так же доступны локальный CSS. Пример ниже равносилен первому.

<todo>

  <!-- шаблон -->
  <h3>{ opts.title }</h3>

  <style scoped>
    :scope { display: block }
    h3 { font-size: 120% }
    /** стили **/
  </style>

</todo>

Стили обрабатываются только один раз, вне зависимости от того, сколько раз был инициирован пользовательский тег.

Для того, чтобы проще было переопределять стили, и использовать темы, вы можете указать где в <head> Riot должен поместить стили из пользовательских тегов:

<style type="riot"></style>

Стили вставятся после normalize.css, но перед стилями сайта и тем, что позволит вам переопределить дефолтные CSS на те, которые будут в теме.

Монтирование

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

<body>

  <!-- вы можете разместить тег в любой части страницы -->
  <todo></todo>

  <!-- подключаем riot.js -->
  <script src="riot.min.js"></script>

  <!-- подключаем тег -->
  <script src="todo.js" type="riot/tag"></script>

  <!-- монтируем тег -->
  <script>riot.mount('todo')</script>

</body>

Пользовательские теги внутри body должны закрываться, используя такой синтаксис: <todo></todo> самозакрытие (<todo/>) не поддерживается.

Немного наглядных примеров:

// монтируем все пользовательские теги на странице
riot.mount('*')

// монтируем элемент с определённым id
riot.mount('#my-element')

// монтируем выбранные элементы
riot.mount('todo, forum, comments')

Один и тот же тег можно монтировать на странице множество раз.

Доступ к элементам DOM

Riot дает вам доступ к элементам, имеющим атрибут name непосредственно из переменной this.

Как использовать jQuery, Zepto, querySelector, и т.д.

Если вам нужно получить доступ к DOM внутри Riot, взгляните на жизненный цикл тегов и обратите внимание, что элементы DOM не будут созданы до вызова метода update(). Учтите это при обращении к DOM-элементам из сторонних библиотек, так как это может быть причиной ошибок.

<example-tag>
  <p id="findMe">Я существую?</p>

  <script>
  var test1 = document.getElementById('findMe')
  console.log('test1', test1)  // ошибка

  this.on('update', function(){
    var test2 = document.getElementById('findMe')
    console.log('test2', test2) // всё верно!
  })
  </script>
</example-tag>

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

<example-tag>
  <p id="findMe">Я существую?</p>

  <script>
  var test1 = document.getElementById('findMe')
  console.log('test1', test1)  // ошибка

  this.on('update', function(){
    var test2 = document.getElementById('findMe')
    console.log('test2', test2) // сработает. Будет вызываться при каждом обновлении
  })

  this.on('mount', function(){
    var test3 = document.getElementById('findMe')
    console.log('test3', test3) // сработает. Сработает лишь однажды (при монтировании)
  })
  </script>
</example-tag>

Контекстные запросы к DOM

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

<example-tag>
  <p id="findMe">Я существую?</p>
  <p>Это - что, жизнь?</p>
  <p>Или сон?</p>

  <script>
  this.on('mount', function(){
    // контекстный jQuery
    $('p', this.root)

    // контекстный Query Selector
    this.root.querySelectorAll('p')
  })
  </script>
</example-tag>

Параметры

Вы можете передать параметры для тегов во втором аргументе

<script>
riot.mount('todo', { title: 'My TODO app', items: [ ... ] })
</script>

Передаваемые данные могут быть чем угодно, начиная от простого объекта до полномасштабного API приложения. Или это может быть хранилище Flus. Это зависит от архитектуры приложения.

Внутри тега можно получить параметры через opts:

<my-tag>

  <!-- параметры в HTML -->
  <h3>{ opts.title }</h3>

  // параметры в JavaScript
  var title = opts.title

</my-tag>

Примеси (Mixins)

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

var OptsMixin = {
  init: function() {
    this.on('updated', function() { console.log('Updated!') })
  },

  getOpts: function() {
    return this.opts
  },

  setOpts: function(opts, update) {
    this.opts = opts
    if (!update) this.update()
    return this
  }
}

<my-tag>
  <h1>{ opts.title }</h1>

  this.mixin(OptsMixin)
</my-tag>

В этом примере любой экземпляр тега my-tag получает примесь OptsMixin которая позволяет использовать методы getOpts и setOpts. Специальный метод init вызывается, когда примесь загружается в тег (init не доступен из тега).

var my_tag_instance = riot.mount('my-tag')[0]

console.log(my_tag_instance.getOpts()) // выведет список всех параметров, которые доступны в теге

Теги могут принимать любой объект – {'key': 'val'} var mix = new function(...) – и выдают ошибку, когда получают любой другой тип.

Тег my-tag теперь имеет метод getId.

function IdMixin() {
  this.getId = function() {
    return this._id
  }
}

var id_mixin_instance = new IdMixin()

<my-tag>
  <h1>{ opts.title }</h1>

  this.mixin(OptsMixin, id_mixin_instance)
</my-tag>

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

Разделяемые примеси

Для того, чтобы делить примеси между тегами или проектами, существует riot.mixin. Вы можете зарегистрировать вашу примесь глобально:

riot.mixin('mixinName', mixinObject)

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

<my-tag>
  <h1>{ opts.title }</h1>

  this.mixin('mixinName')
</my-tag>

Жизненный цикл тегов

Тег создаётся в такой последовательности:

  1. Тег инициaлизируется
  2. Выполняется JavaScript-логика тега
  3. Вычисляются HTML выражения и вызывается метод “update”
  4. Тег монтируется в приложение и вызывается метод “mount”

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

  1. Автоматически в момент, когда вызывается trigger(). (если вы не установите e.preventUpdate в значение true в обработчике событий) Например, вызов метода toggle в примере выше.
  2. Когда вызывается this.update() в текущей сущности тега
  3. Когда вызывается this.update() в каком-нибудь из родительских тегов. Обновления происходят сверху вниз, от родительских к дочерним тегам.
  4. Когда вызывается riot.update(), который глобально обновляет все выражения на странице.

Метод “update” автоматически вызывается каждый раз, когда тег обновляется.

Так как значения вычисляются перед монтированием, то не возникает сюрпризов, вроде http-запросов на несуществующий ресурс: <img src={ src }>.

Прослушивание событий тега

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

<todo>
  this.on('before-mount', function() {
    // перед тем, как тег будет примонтирован
  })

  this.on('mount', function() {
    // сразу после того, как тег будет примонтирован
  })

  this.on('update', function() {
    // позволяет изменять данные тега перед тем, как выражения пересчитаются
  })

  this.on('updated', function() {
    // сразу после того, как те обновился
  })

  this.on('before-unmount', function() {
    // перед тем, как тег отмонтируется
  })

  this.on('unmount', function() {
    // когда тег открепляется от страницы
  })

  // нужны сразу все события?
  this.on('all', function(eventName) {
    console.info(eventName)
  })

</todo>

Вы можете использовать множество обработчиков для одного и того же события. Смотри API наблюдателя для более подробной информации.

Выражения

В HTML-шаблоне можно использовать выражения, заключённые в фигурные скобки:

{ /* здесь размещается выражение */ }

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

<h3 id={ /* выражение для атрибута */ }>
  { /* выражение, результат которого увидит пользователь */ }
</h3>

Выражения на все 100% - JavaScript. Вот несколько примеров:

{ title || 'Untitled' }
{ results ? 'ready' : 'loading' }
{ new Date() }
{ message.length > 140 && 'Message is too long' }
{ Math.round(rating) }

Выражения нужны для того, чтобы сохранять HTML как можно более чистым и очевидным. Если ваши выражения слишком громоздки, попробуйте вынести часть логики в обработчик события “update”. Например:

<my-tag>

  <!-- `val` будет вычислено ниже .. -->
  <p>{ val }</p>

  // при каждом обновлении
  this.on('update', function() {
    this.val = some / complex * expression ^ here
  })
</my-tag>

Булевые атрибуты

Булевые атрибуты (checked, selected etc..) игнорируются, если выражение не равно true:

<input checked={ null }> becomes <input>.

Стандарт W3C гласит, что булевый атрибут считается установленным если присутствует среди атрибутов тега, каким бы ни было его значение, даже false.

Нижеследующие выражение работать не будет:

<input type="checkbox" { true ? 'checked' : ''}>

так как оно не является сеттером булевого атрибута и не находится внутри html-тега. Riot распознаёт 44 булевых атрибута.

Сокращённая запись CSS-классов

В Riot есть специальный синтаксис для имён CSS-классов:

<p class={ foo: true, bar: 0, baz: new Date(), zorro: 'a value' }></p>

это равно “foo baz zorro”. Если значение свойства верно, то название свойства отображается в списке классов.

Экранирование выражений

Вы можете отобразить выражение как текст, если заэкранируете открывающие и закрывающие символы:

\\{ this is not evaluated \\} выведет { this is not evaluated }

Символы для выражений

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

riot.settings.brackets = '${ }'
riot.settings.brackets = '\{\{ }}'

Они должны разделяться пробелом.

При использовании пре-компилятора вам стоит настроить brackets.

Остальное

Выражения внутри style игнорируются.

Вывод HTML в выражениях

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

<raw>
  <span></span>

  this.root.innerHTML = opts.content
</raw>

Этот тег можно использовать в других тегах:

<my-tag>
  <p>Here is some raw content: <raw content="{ html }"/> </p>

  this.html = 'Hello, <strong>world!</strong>'
</my-tag>

demo на jsfiddle

warning this could expose the user to XSS attacks so make sure you never load data from an untrusted source.

Вложенные теги

Давайте создадим родительский тег <account> с вложенным тегом <subscription>:

<account>
  <subscription  plan={ opts.plan } show_details="true" />
</account>


<subscription>
  <h3>{ opts.plan.name }</h3>

  var plan = opts.plan,
      show_details = opts.show_details

  // доступ к родителю
  var parent = this.parent

</subscription>

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

Затем мы монтируем account на страницу и передаём ему объект с параметром plan:

<body>
  <account></account>
</body>

<script>
riot.mount('account', { plan: { name: 'small', term: 'monthly' } })
</script>

Параметры из родительского тега можно прочитать в методе riot.mount.

важно Вложенные теги всегда объявляются в родительском теге. Они не инициализируются, если будут определены на странице.

Внутренний HTML (тег <yield>)

“Включение HTML” - это способ обработки внутреннего HTML на странице. Это достигается путём использования встроенного тега <yield>. Пример:

Определение тега

<my-tag>
  <p>Hello <yield/></p>
  this.text = 'world'
</my-tag>

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

Пользовательский тег размещается на странице с внутренним HTML

<my-tag>
  <b>{ text }</b>
</my-tag>

Результат

<my-tag>
  <p>Hello <b>world</b><p>
</my-tag>

Подробности есть в API.

Именованные элементы

Элементы с атрибутами name или id автоматически привязываются к соответствующему параметру переменной this, вы можете с лёгкостью обращаться к ним через JavaScript:

<login>
  <form id="login" onsubmit={ submit }>
    <input name="username">
    <input name="password">
    <button name="submit">
  </form>

  var form = this.login,
    username = this.username.value,
    password = this.password.value,
    button = this.submit

</login>

К именованным элементам так же можно обращаться через HTML: <div>{ username.value }</div>

Обработчики DOM-событий

Обработчики DOM-событий определяются следующим образом:

<login>
  <form onsubmit={ submit }>

  </form>

  // Этот метод вызывается перед отправкой вышеописанной формы формы
  submit(e) {

  }
</login>

Атрибуты, которые начинаются с “on” (onclick, onsubmit, oninput и т.д.) принимают в качестве параметра функцию, которая будет обрабатывать действие. Этот параметр может быть так же определён динамически, в виде выражения:

<form onsubmit={ condition ? method_a : method_b }>

Внутри этого обработчика this относится к текущему экземпляру тега.

Поведение дефолтного поведения автоматически отменяется если элемент не checkbox или radio-кнопка. Это означает, что e.preventDefault() уже вызвано за вас, потому что это как правило, нужно сделать (или забывают сделать). Вы можете позволить браузеру выполнить поведение по-умолчанию, вернув true в обработчике.

Например, этот обработчик будет на самом деле отправлять форму на сервер:

submit() {
  return true
}

Объект события

Обработчик DOM-события принимает в качестве первого аргумента стандартный объект события. Ниже приведён список его свойств, которые работают во всех браузерах:

Условия

Условия помогают вам показывать/скрывать элементы в зависимости от ситуации:

<div if={ is_premium }>
  <p>This is for premium users only</p>
</div>

Ещё раз, выражения могут быть обычным свойством или полноценным JavaScript выражением. Вот список доступных условных атрибутов:

Используется оператор сравнения ==, не ===. То есть : 'a string' == true.

important Использование условных аттрибутов не предотвращает выполенение выражений в скрытых выражениях. Мы работает над этим, прогресс можно увидеть в задаче на Github

Циклы

Циклы реализованы благодаря атрибуту each:

<todo>
  <ul>
    <li each={ items } class={ completed: done }>
      <input type="checkbox" checked={ done }> { title }
    </li>
  </ul>

  this.items = [
    { title: 'First item', done: true },
    { title: 'Second item' },
    { title: 'Third item' }
  ]
</todo>

Элемент с аттрибутом each будет повторён для всех значений массива. Новый элемент будет автоматически добавлен/создан если в массив добавится новое значение через такие методы, как push(), slice() или splice.

Контекст

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

Родитель доступен через переменную parent. Например:

<todo>
  <div each={ items }>
    <h3>{ title }</h3>
    <a onclick={ parent.remove }>Remove</a>
  </div>

  this.items = [ { title: 'First' }, { title: 'Second' } ]

  remove(event) {

  }
</todo>

Всё, что внутри элемента с атрибутом each принадлежит дочернему контексту. То есть, к title можно обращаться напрямую, но remove должен вызываться с префиксом parent. так как метод не является атрибутом элемента в цикле.

Перебираемые элементы - экземпляры пользовательских тегов. Riot не изменяет исходные элементы массива, новые свойства просто добавляются к исходному элементу.

Обработчики DOM-событий в циклах

Обработчики DOM-событий имеют доступ к конкретному элементу, вызывавшему событие через event.item. Вот пример функции remove:

<todo>
  <div each={ items }>
    <h3>{ title }</h3>
    <a onclick={ parent.remove }>Remove</a>
  </div>

  this.items = [ { title: 'First' }, { title: 'Second' } ]

  remove(event) {

    // looped item
    var item = event.item

    // index on the collection
    var index = this.items.indexOf(item)

    // remove from collection
    this.items.splice(index, 1)
  }
</todo>

После того, как обработчик DOM-события выполнится, текущий экземпляр пользовательского тега обновится, используя this.update() (если только в обработчике не указано e.preventUpdate = true). Родитель следит за состоянием первоначального массива. Если какой-либо элемент был удалён из массива, родитель удаляет его из DOM.

Перебор пользовательских тегов

Пользовательские теги так же могут быть использованы в циклах:

<todo-item each="{ items }" data="{ this }"></todo-item>

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

Массивы из не-объектов

Элементы массива не обязательно должны быть объектами. Они так же могут быть строками или числами. В этом случае, вы должны использовать конструкцию { name, i in items }, как показано ниже:

<my-tag>
  <p each="{ name, i in arr }">{ i }: { name }</p>

  this.arr = [ true, 110, Math.random(), 'fourth']
</my-tag>

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

Перебор объектов

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

<my-tag>
  <p each="{ name, value in obj }">{ name } = { value }</p>

  this.obj = {
    key1: 'value1',
    key2: 1110.8900,
    key3: Math.random()
  }
</my-tag>

Не рекомендуется использовать циклы по объектам, так как Riot определяет, изменился ли объект с помощью JSON.stringify. Изучается весь объект целиком, и если в нём что-то меняется, то весь цикл рендерится заново. Обычные массивы гораздо быстрее и изменения отдельного элемента затрагивают конкретный элемент на странице.

О циклах более подробно

Производительность

В Riot v2.3 для большей надёжности узлы DOM удаляются, создаются и перемещаются синхронно с данными в вашей коллекции: эта стратегия более медленная в сравнении с предыдущими релизами v2.2. Чтобы включить быстрый режим рендеринга, вам нужно добавить аттрибут no-reorder в DOM-узлы. Например:

<loop>
  <div each="{ item in items }" no-reorder>{ item }</div>
</loop>

Тег virtual

экспериментальный

В некоторых случаях вам может потребоваться перебирать теги без родительского тега-обёртки. В этом случае вы можете использовать тег <virtual>, который удаляется, будто html-теги ни во что не обёртывались. Например:

<dl>
  <virtual each={item in items}>
    <dt>{item.key}</dt>
    <dd>{item.value}</dd>
  </virtual>
</dl>

HTML-элементы и пользовательские теги

Стандартные HTML-элементы могут быть использованы как пользовательские теги путём добавления атрибута riot-tag.

<ul riot-tag="my-tag"></ul>

Такой тег рассматривается как любой другой пользовательский тег.

riot.mount('my-tag')

примонтирует ul-элемент если определён <my-tag></my-tag>

Рендеринг на стороне сервера

Riot поддерживает рендеринг на стороне сервера с помощью Node/io.js. Вы можете использовать require и рендерить теги:

var riot = require('riot')
var timer = require('timer.tag')

var html = riot.render(timer, { start: 42 })

console.log(html) // <timer><p>Seconds Elapsed: 42</p></timer>

Циклы и условия поддерживаются.