Хороший случай для Eval в JavaScript

Или: Как генерировать динамические html-теги внутри шаблонов lit-html? eval иногда путают с evil. Мы также иногда слышим, что есть случаи, когда это необходимо. Это один из таких случаев...

Проект lit — это фреймворк для работы с веб-компонентами. В ярких версиях 1.x и 2.x мы сильно полагались на lit. Пока мы усердно работаем над Vivid 3.x, мы по-прежнему поддерживаем и добавляем функции в 2.x.

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

Мы решили, что уровень H по умолчанию равен 3 (<h3>). Этого недостаточно, потому что панель расширения также может быть подзаголовком любого другого уровня заголовка:

<h1>Is there alien life out there?</h1>
<expansion-panel>
  <h2><button>What is life?</button></h2>
</expansion-panel>
<expansion-panel>
  <h2><button>What is alien?</button></h2>
</expansion-panel>

Вот видите — ставить h3 в этом случае было бы неправильно. То же самое относится и к панелям расширения под h3, h4 и h5.

Кто-то скажет использовать div с aria-level и role=header. Хотя это сработало бы, есть цитата из MDN (и других мест):

Принимая все это во внимание, мы хотели сохранить лучшие практики MDN, но при этом разрешить использование динамических тегов.

Попытка настроить динамический тег с помощью lit-html

Как работают освещенные шаблоны?

Давайте сначала объясним, как работают шаблоны в lit, не вдаваясь в подробности.

lit-html — это служебная функция, которая превращает наши строки в живые шаблоны. Затем метод рендеринга lit «волшебным образом» прослушивает изменения свойств в шаблоне и соответствующим образом обновляет представление.

Итак, у вас будет функция render, которая выглядит примерно так:

Заголовок панели будет контентом, который мы хотели бы обозначить как заголовок.

Как я уже упоминал, мы хотели обернуть его h3 по умолчанию, поэтому он может выглядеть примерно так:

Как не менять динамически имя тега в lit-html?

Второе требование состояло в том, чтобы иметь динамику h tag. Это означает, что мы хотели бы иметь редактируемое пользователем свойство, которое изменит уровень заголовка. Глядя на шаблон, это может показаться легкой задачей. Просто установите переменную внутри строки шаблона рядом с h:

Как чудесно это выглядит! И как эффектно это не удается :)

Видите ли — lit-html и система шаблонов плохо работает со статическими данными. Я избавлю вас от жутких подробностей, но ошибка, которую мы получаем, заключается в том, что он не может проанализировать шаблон - и это происходит во время выполнения! (ой - представьте, что было бы, если бы у нас не было тестов)

Пытаюсь использовать unsafeHTML

Знаете, как говорят: «Не можешь с ними бороться — используй их методы». Освещенный метод под названием unsafeHTML обещает следующее: Renders the result as HTML, rather than text. Так как документации не хватало, я обратился к официальной документации - модульным тестам:

Применение этого к нашему коду выглядит так:

Ок — сборка прошла. Тесты провалились (но кто смотрит на тесты, верно?). Почему они потерпели неудачу? Вот как это выглядит при использовании этого решения:

Я не буду вдаваться в подробности, почему это не сработало — достаточно сказать, что способ lit-html рендеринга своих шаблонов не допускает «свободных» концов HTML-тегов.

Мы вернулись к нулевой точке. Мы попробовали что-то, что не сработало… пора двигаться дальше!

Пытаюсь использовать unsafeStatic

Нашей третьей попыткой было использовать другую встроенную функцию lit под названием unsafeStatic. Опять же, документация не слишком полезна, так что... вы знаете... модульные тесты:

Кажется, наша проблема решена. Давайте просто реализуем это в нашем коде:

И тогда… мы получаем это:

По-видимому, хотя их тесты работали с рендером unit-tests, в «реальном мире» это не сработало. Запуск render самостоятельно показал мне то же самое.

Итак… unsafeStatic не работает. Что дальше?

Как использовать Eval для создания динамических тегов в шаблонах lit-html

В нашей предыдущей неудачной попытке мы попытались динамически добавить headerLevel следующим образом:

<h${this.headerLevel}>
	${this.renderPanelHeader()}
</h${this.headerLevel}>

Попробуем решить это по-другому. Поскольку мы можем вызывать функции рендеринга, которые возвращают объекты TemplateResult, мы можем создать функцию, которая возвращает evaled TemplateResult следующим образом:

Наше решение теперь возвращает динамический шаблон с именем динамического тега для нашего заголовка:

Давайте все это свяжем

Кажется, нам удалось то, что мы хотели. У нас внутри lit-html есть динамический тег, и мы даже использовали eval...

Мы нашли еще одну «маленькую» проблему. При запуске наших визуальных регрессионных тестов пользовательского интерфейса (которые поставляются в комплекте с веб-пакетом) мы получили следующую ошибку:

Та же ошибка обнаружилась в нашем сборнике рассказов (снова веб-пакет…).

О, нет! Теперь наши компоненты нельзя использовать в пакетных приложениях (а это примерно… 99% приложений, использующих Vivid).

Что делать? Что делать? Я знаю — давайте впадать в депрессию и сдаваться!

Или… мы могли бы придумать другое неприятное решение!

Если мы просто установим html в качестве локальной переменной и будем использовать локальную переменную, она должна быть доступна во время выполнения.

Таким образом, добавление const safeHTML = html; в начало файла, а затем использование safeHTML вместо html решает нашу проблему.

На самом деле переменная может быть проблематичной, так как углифификатор/минификатор может изменить ее. Мы могли бы использовать статический метод для самого класса, чтобы убедиться, что он более устойчив. Это зависит от того, насколько «агрессивен» ваш углификатор/минификатор…

Не моя проблема на данный момент :)

Краткое содержание

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

Использование правильной семантики в вашем HTML — большая часть этого. Вот почему команда vivid постоянно работает над добавлением специальных возможностей в компоненты нашей системы дизайна.

В нашем случае мы хотели убедиться, что expansion panels получает правильную семантику в качестве заголовков. Поскольку мы хотели, чтобы потребители могли определять уровень заголовка, мы хотели, чтобы тег h был динамическим.

lit-html было не очень хорошо с этим, поэтому, попробовав несколько способов сделать это, мы обнаружили, что с помощью eval мы можем создать динамический тег внутри lit-html.

Затем мы обнаружили еще одну небольшую проблему: при объединении нашего компонента с другими приложениями (в нашем случае с визуальными регрессионными тестами пользовательского интерфейса и сборником рассказов) мы обнаружили, что использование eval сложно. Он не найдет импортированный html, поскольку выражение eval оценивается во время выполнения, а упаковщики имеют тенденцию давать импорту свои собственные имена...

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

Операция прошла успешно.

Большое спасибо Rachel B. Tannenbaum и Yinon Oved за вдохновение и обзор этой статьи.