Обратите внимание: исходный код в этом посте больше не компилируется, так как Elm обновлен до версии 0.18. Обратитесь к странице github за примерами работы.

В этом посте я рассмотрю, что вы можете сделать, если пишете приложение Elm и вам нужно выяснить, какие размеры браузер выбрал для ваших элементов DOM. Мы увидим, как считывать такие свойства, как .offsetHeight из модели DOM, и найти замену для getBoundingClientRect.

В Javascript, если я хочу узнать высоту отображаемого элемента, я могу сделать это:

var height = 
  document.getElementById(“my-important-element”).offsetHeight;

Но в стандартных библиотеках Elm нет функции offsetHeight. Почему нет?

Elm сталкивается с двумя серьезными препятствиями, которые необходимо преодолеть при работе с DOM:

  1. Функции Elm не имеют состояния, но модель DOM видоизменяется. Функция, считывающая «offsetHeight» элемента DOM, не может иметь состояния. Он вернет какое-то значение, а затем, после того, как пользователь изменит размер окна браузера, он вернет другое значение.
  2. Если вы используете VirtualDom, у вас все равно нет прямого доступа к DOM.

Разбор событий Javascript

Однако есть одно место в Elm, где вы можете преодолеть обе эти проблемы: обработчики событий. При обработке обработчиков событий DOM не мутирует (исправление 1), а события DOM содержат ссылки на узлы DOM (исправление 2).

В библиотеке Html.Events мы находим такой пример:

Обратите внимание на реализацию: функция on принимает Json.Decode.Decoder msg и возвращает (обработчик события) Attribute msg. Декодер будет применен к структуре Javascript Event, переданной обработчику события. (Если вас интересуют подробности, вот источник для on; он приведет к вызову этой функции во время рендеринга виртуального дома.)

Событие Javascript содержит свойство target, которое является ссылкой на фактический элемент DOM, из которого возникло событие. Декодер targetValue в приведенном выше примере просто извлекает значение из этого свойства:

Чтобы прочитать offsetHeight, мы можем проделать тот же трюк:

Вот как мы считываем свойства из DOM: мы предоставляем JSON-декодер для on, который извлекает свойства target узла DOM.

Прогулка по DOM

Но что, если элемент, вызвавший событие, не тот, свойства которого мы хотим прочитать?

Элементы DOM имеют свойства для своих родительских и дочерних, поэтому мы можем написать кодировщик, который обходит DOM. Поскольку декодеры тихо выходят из строя, написание таких прогулок довольно утомительно; Я собрал несколько полезных декодеров в библиотеке, чтобы не повторять глупых ошибок. Используя эту библиотеку, вы можете выполнять такие примеры, как этот, который находит внучатых потомков элемента button, являющегося источником события:

То есть мы можем «пройтись по DOM», начиная с target узла DOM, предоставленного нам on,

Помимо свойств

К сожалению, не все, что мы хотели бы знать об элементе DOM, представлено в виде свойств. В частности, getBoundingClientRect, канонический способ получить абсолютную позицию элемента DOM, - это метод для Element, а не свойство, поэтому мы не можем написать для этого Json.Decoder.

Мы могли бы использовать порты, но для писателей библиотеки это недопустимо. В настоящее время (Elm 0.17) порты требуют, чтобы пользователи добавляли различные шаблоны в свои приложения верхнего уровня, а также пользовательский Javascript, что резко увеличивает барьер для входа в вашу библиотеку.)

Было время, когда getBoundingClientRect поддерживался не всеми браузерами. Вместо этого люди делали бы такие вещи:

На самом деле это не так уж и плохо: мы идем вверх по DOM, собирая геометрию по ходу. Мы только что увидели, что можем обойти DOM из Elm, поэтому давайте повторно реализуем вышеуказанную функцию как декодер JSON. Вместо этого нам придется перефразировать цикл while как рекурсивную функцию, но это просто сделает реализацию более понятной.

Сначала мы декодируем scrollLeft, -Top, offsetLeft, и -Top в текущем элементе и добавляем это подходящим образом к заданным x и у; и затем мы передаем эти новые x и y в один и тот же декодер, выполняемый только на родительском узле. Декодер offsetParent возвращает свой первый аргумент, если вы уже находитесь в корне.

Что ты не можешь делать

В этом подходе отсутствует возможность считывать свойства элементов DOM без необходимости ждать события DOM.

За исключением портов, я не думаю, что Elm дает нам возможность сделать это.

Чего не следует делать

Но подождите!, - скажете вы. Если target является ссылкой на узел DOM, не можете ли вы просто сохранить эту ссылку как reference:« Json.Value в своей модели? Тогда вы можете запускать декодеры на этом, когда захотите - не нужно ждать события DOM! »

Ну да. По крайней мере, после первого раза я получаю событие DOM.

Но нет. Я нарушил бы основной принцип Elm, согласно которому ценности неизменны. Если я напишу функцию measure: Json.Value - ›Float, которая принимает эту ссылку и возвращает, скажем, ширину некоторого элемента, эта функция может возвращать разные результаты для одного и того же ввода. Вы вызываете measure model.reference, получаете 50, изменяете размер браузера, вызываете его снова с тем же аргументом measure model.reference, и теперь вы получаете 60. Это центральный точка Elm (и большинства функциональных языков) в том, что функции не имеют состояния, т. е. на одном и том же входе они дают один и тот же результат. Всегда.

Заключение

Итак, немного взломав, вы можете считывать свойства из модели DOM. У вас нет возможности вызывать методы, в частности getBoundingClientRect—, и вам нужно дождаться, пока сначала произойдет событие DOM. Даже в этом случае вы можете пройти немалую часть пути; в частности, этого достаточно для распространенного случая запуск геометрической анимации в ответ на щелчок.

Настоящая публикация предлагает две взаимосвязанные точки, в которых Элм мог бы улучшить:

  1. Это неудобно и немного удивительно, что у Elm нет более прямой поддержки для чтения свойств из визуализированной DOM, особенно геометрии. В качестве языка веб-платформы Elm должен предоставить мне удобный способ получения информации, предоставляемой свойством offsetWitdth и методом getBoundingClientRect. .
  2. Меня беспокоит то, что я могу использовать механизм анализа событий Html.Events.on для введения функций с отслеживанием состояния. Используя это, я могу в принципе подорвать все четкие гарантии, которые дает мне язык. Этого не должно быть.

Ни один из этих моментов не кажется особенно трудным для исправления, хотя, конечно, существует некоторая напряженность между (1) введением API для доступа к узлам DOM, с одной стороны, и (2) проверкой значений этого API, с другой стороны.