Как компоновать фрагменты шаблонов Django с Alpine.js для создания многократно используемых серверно-рендерных компонентов?


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

Пример с кодом, который работает не так, как задумано - Карусель

У меня такая структура шаблона: cities_page.html (шаблон) / city.html (сниппет) / carousel.html (сниппет + Alpine) / carousel_item.html (сниппет).

cities_page рендерит множество cities, а каждый carousel рендерит множество carousel_items. Весь рендеринг происходит на стороне сервера.

cities_page.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Cities Page</title>
  <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.7/dist/cdn.min.js"></script>
</head>

<body>
{% for city in cities %}
  {% include "myapp/snippets/city.html" with city=city %}
{% endfor %}
</body>
</html>

city.html

<div>
  <h2>{{ city.name }}</h2>
  {% include "myapp/snippets/carousel.html" with images=city.images %}
</div>

carousel.html

<div x-data="carousel">
  <template x-for="item in items" :key="item">
    <div x-html="item.itemHtml"></div>
  </template>
</div>

<script>
  document.addEventListener('alpine:init', () => {
      Alpine.data(`carousel`, () => ({
        items: [
          {% for image in images %}
            {
              itemHtml: `{% include "myapp/snippets/carousel_item.html" with image=image %}`,
            },
          {% endfor %}
        ]
      }));
    });
</script>

carousel_item.html

<p>
  Item: {{ image }}
</p>

views.py

def cities_page(request):
    context = {
        "cities": [
            {
                "name": "London",
                "images": ["london1.jpg", "london2.jpg", "london3.jpg"]
            },
            {
                "name": "Paris",
                "images": ["paris1.jpg", "paris2.jpg", "paris3.jpg"]
            },
        ]
    }
    return render(request, "myapp/cities_page.html", context=context)

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

Результатом этого является:

Лондон

Item: paris1.jpg

Item: paris2.jpg

Item: paris3.jpg

Париж

Item: paris1.jpg

Item: paris2.jpg

Item: paris3.jpg

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

Моя лучшая попытка исправить это - переписать карусель следующим образом

<div>
  <template x-for="item in items" :key="item">
    <div x-html="item.itemHtml"></div>
  </template>
</div>

<script>
  (() => {
    const genRanHex = size => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');

    const id = genRanHex(5)
    const currentScript = document.currentScript;
    const previousElement = currentScript.previousElementSibling;
    previousElement.setAttribute("x-data", `carousel${id}`)

    document.addEventListener('alpine:init', () => {
      Alpine.data(`carousel${id}`, () => ({
        items: [
          {% for image in images %}
            {
              itemHtml: `{% include "myapp/snippets/carousel_item.html" with image=image %}`,
            },
          {% endfor %}
        ]
      }));
    });
  })();
</script>

Обратите внимание, что я

  • Удалено x-data
  • Сгенерировали случайный идентификатор и присвоили его имени компонента Alpine, чтобы сделать его глобально уникальным
  • .
  • Установил x-data с помощью скрипта
  • .
  • Убедились, что все, что определено в скрипте, не загрязняет глобальную область видимости (и другие версии этого скрипта, включенные в каждый экземпляр карусели), обернув его анонимной функцией
  • .

Это похоже на действительно халтурный способ создания многократно используемых компонентов.

Поэтому мой вопрос заключается в следующем: Какова лучшая практика в написании таких компонентов? Не слишком ли далеко я зашел с Alpine и не стоит ли мне рассмотреть возможность использования чего-то вроде Vue/React для достижения такой функциональности?

Благодарю вас за любую помощь или за то, что поделились соответствующими ресурсами.

У меня есть нечто похожее. Это может помочь, а может и не помочь.

https://github.com/tochy97/djangoAPI

Я использую загрузчик для вызова каждого содержимого Js по мере необходимости по их пути.

https://github.com/tochy97/djangoAPI/blob/main/static/components/common/js/loader.js

Индекс.js работает как инициализатор для всего.

https://github.com/tochy97/djangoAPI/blob/main/static/components/common/js/index.js

Вернуться на верх