Повторно используемые компоненты в Django с помощью Stimulus и Tailwind CSS — Часть 1

В этом руководстве мы рассмотрим, как создавать компоненты пользовательского интерфейса в вашем полнофункциональном приложении Django, которые помогут вам повторно использовать код (как внешний, так и внутренний) и поддерживать его в чистоте и ремонтопригодности.

В этой серии руководств рассматриваются как интерфейсные, так и серверные компоненты Django, а также следующие инструменты и технологии:

  1. python-веб-пакет-шаблон
  2. Stimulus
  3. Tailwind CSS
  4. django-viewкомпонент

Серия:

  1. Часть 1 (это руководство!) - посвящена настройке проекта, а также работе на стороне клиента
  2. Часть 2 - посвящена серверной части

Если интерфейсные инструменты и технологии вам не по душе, и вы хотите, чтобы все было просто, ознакомьтесь с Создание повторно используемых компонентов в Django, в котором речь идет только о создании серверных компонентов пользовательского интерфейса с помощью Django..

Настройка проекта

Проект Django

Создайте новый каталог проекта вместе с новым проектом Django:

$ mkdir django-component-example && cd django-component-example
$ python3.12 -m venv venv
$ source venv/bin/activate
(venv)$

(venv)$ pip install Django==5.0.6
(venv)$ django-admin startproject django_component_app .
(venv)$ python manage.py migrate
(venv)$ python manage.py runserver

Не стесняйтесь заменить virtualenv и Pip на Poetry или Pipenv. Для получения дополнительной информации ознакомьтесь с Современными средами Python.

Перейдите к http://127.0.0.1:8000/, чтобы просмотреть экран приветствия Django. После завершения работы отключите сервер.

Создайте requirements.txt файл и добавьте Django в качестве зависимости:

Django==5.0.6

python-webpack-шаблонный

Далее, чтобы работать с современными интерфейсными инструментами и технологиями, мы добавим python-webpack-boilerplate. После настройки мы сможем импортировать Tailwind CSS, Stimulus JS и другие современные интерфейсные библиотеки через npm install без добавления CDN-ссылки на ваши шаблоны.

Добавьте его в requirements.txt:

python-webpack-boilerplate==1.0.3

А затем установите пакет:

(venv)$ pip install -r requirements.txt

Добавьте его в INSTALLED_APPS в django_component_app/settings.py:

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    "webpack_boilerplate", # new
]

Чтобы запустить новый интерфейсный проект, выполните следующую команду управления:

(venv)$ python manage.py webpack_init

[1/2] project_slug (frontend): frontend
[2/2] run_npm_command_at_root (y): y
[SUCCESS]: Frontend app 'frontend' has been created.

Примечания:

  1. Был создан новый каталог "frontend", который содержит предопределенные файлы для нашего проекта frontend.
  2. файл package.json и некоторые другие конфигурационные файлы были помещены в корневой каталог.
  3. Установив для run_npm_command_at_root значение y, мы можем запускать npm команд непосредственно в корне проекта Django.

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

├── .babelrc
├── .browserslistrc
├── .eslintrc
├── .nvmrc
├── .stylelintrc.json
├── django_component_app
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── frontend
│   ├── .gitignore
│   ├── README.md
│   ├── src
│   │   ├── application
│   │   │   ├── README.md
│   │   │   └── app.js
│   │   ├── components
│   │   │   ├── README.md
│   │   │   └── jumbotron.js
│   │   └── styles
│   │       └── index.scss
│   ├── vendors
│   │   ├── .gitkeep
│   │   └── images
│   │       ├── .gitkeep
│   │       ├── sample.jpg
│   │       └── webpack.png
│   └── webpack
│       ├── webpack.common.js
│       ├── webpack.config.dev.js
│       ├── webpack.config.prod.js
│       └── webpack.config.watch.js
├── manage.py
├── package-lock.json
├── package.json
├── postcss.config.js
└── requirements.txt

Чтобы запустить webpack dev-сервер, начните с установки Node и npm если они у вас еще не установлены.

Вы можете либо загрузить их напрямую здесь, либо использовать nvm или fnm.

$ node -v
v20.10.0

$ npm -v
10.2.3

Затем выполните следующую команду для установки пакетов зависимостей внешнего интерфейса:

$ npm install

Чтобы убедиться, что все работает, запустите сервер разработки:

$ npm run start

До тех пор, пока вы не получите сообщение об ошибке, вы можете считать, что все в порядке. Завершите работу сервера после завершения. Теперь мы можем установить и использовать интерфейсные библиотеки, такие как Tailwind CSS и Stimulus JS.

Tailwind

В корневом каталоге выполните следующую команду:

$ npm install -D tailwindcss@3.4.3 postcss-import@16.1.0

Далее обновите postcss.config.js вот так:

// https://tailwindcss.com/docs/using-with-preprocessors

module.exports = {
  plugins: {
    "postcss-import": {},
    "tailwindcss/nesting": "postcss-nesting",
    tailwindcss: {},
    "postcss-preset-env": {
      features: { "nesting-rules": false },
    },
  }
}

Эта конфигурация позволяет нам использовать Tailwind CSS с PostCSS.

Затем сгенерируйте конфигурационный файл для вашего интерфейсного проекта с помощью утилиты Tailwind CLI:

$ npx tailwindcss init

Теперь вы должны увидеть tailwind.config.js файл в корне проекта:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [],
  theme: {
    extend: {},
  },
  plugins: [],
}

Мы можем обновить этот файл, чтобы настроить Tailwind CSS. А пока давайте оставим все как есть.

Затем обновите интерфейс/src/styles/index.scss:

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

.jumbotron {
  // should be relative path of the entry scss file
  background-image: url("../../vendors/images/sample.jpg");
  background-size: cover;
}

.btn-blue {
  @apply inline-flex items-center;
  @apply px-4 py-2;
  @apply font-semibold rounded-lg shadow-md;
  @apply text-white bg-blue-500;
  @apply hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75;
}

Здесь мы:

  1. Импортирован Tailwind CSS
  2. Использован классический синтаксис CSS для применения стилей к классу jumbotron
  3. Используется @apply для применения служебных классов Tailwind CSS к классу btn-blue

Давайте проверим еще раз:

$ npm run start

Вы должны увидеть, что Tailwind успешно скомпилирован. После этого давайте протестируем все в шаблоне Django.

Шаблон Django

Начните с добавления следующего к django_component_app/settings.py:

STATICFILES_DIRS = [
    str(BASE_DIR / "frontend/build"),
]

WEBPACK_LOADER = {
    "MANIFEST_FILE": str(BASE_DIR / "frontend/build/manifest.json"),
}

Примечания:

  1. Мы добавили каталог "frontend/build" в STATICFILES_DIRS, чтобы Django мог находить статические ресурсы, созданные webpack.
  2. Мы также определили местоположение MANIFEST_FILE как WEBPACK_LOADER, чтобы наш пользовательский загрузчик мог помочь нам загрузить JS и CSS.

Ознакомьтесь с Стандартными документами Python Webpack, чтобы узнать больше.

Обновить django_component_app/urls.py:

from django.contrib import admin
from django.urls import path
from django.views.generic import TemplateView  # new

urlpatterns = [
    path("", TemplateView.as_view(template_name="index.html")),  # new
    path("admin/", admin.site.urls),
]

Создайте новую папку под названием "templates" внутри "django_component_app". Затем обновите TEMPLATES в django_component_app/settings.py , чтобы Django знал, где найти шаблоны:

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": ["django_component_app/templates"],  # updated
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

Добавьте index.html в "django_component_app/templates":

{% load webpack_loader static %}

<!DOCTYPE html>
<html>
<head>
  <title>Index</title>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  {% stylesheet_pack 'app' %}
</head>
<body>

<div class="jumbotron py-5">
  <div class="w-full max-w-7xl mx-auto px-4">
    <h1 class="text-4xl mb-4">Hello, world!</h1>
    <p class="mb-4">This is a template for a simple marketing or informational website. It includes a large callout called a
        jumbotron and three supporting pieces of content. Use it as a starting point to create something more unique.</p>

    <p><a class="btn-blue mb-4" href="#" role="button">Learn more »</a></p>

    <div class="flex justify-center">
      <img src="{% static 'vendors/images/webpack.png' %}"/>
    </div>
  </div>
</div>

{% javascript_pack 'app' %}

</body>
</html>

Здесь мы:

  1. Установите load webpack_loader в верхней части шаблона, который взят из python-webpack-boilerplate и содержит теги stylesheet_pack и javascript_pack.
  2. По-прежнему использовал тег шаблона Django static для импорта изображений из проекта frontend.
  3. Использовал stylesheet_pack и javascript_pack для загрузки файлов CSS и JS bundle соответственно.

Ознакомьтесь с Стандартными документами Python Webpack, чтобы узнать больше.

Запустив сервер разработки webpack (через npm run start) в одном окне терминала, запустите сервер разработки Django в новом окне:

(venv)$ python manage.py migrate
(venv)$ python manage.py runserver

Перейдите к http://127.0.0.1:8000/. Вы должны увидеть страницу приветствия:

Webpack Tailwind Welcome

Обратите внимание, что класс btn-blue работает должным образом, в то время как классы w-full max-w-7xl mx-auto px-4 не работают. Чтобы заставить их работать, нам нужно сообщить Tailwind, какие CSS-классы используются в нашем проекте.

JIT

Tailwind работает, сканируя все ваши HTML-компоненты, JavaScript-компоненты и любые другие файлы шаблонов на предмет имен классов, а затем генерирует все соответствующие CSS-файлы для этих стилей. Для того чтобы Tailwind сгенерировал весь необходимый вам CSS, он должен знать о каждом отдельном файле в вашем проекте, который содержит какие-либо имена классов Tailwind.

Tailwind использует JIT (точно в срок) все время, начиная с версии 3.

Итак, мы должны настроить раздел content в разделе tailwind.config.js , чтобы Tailwind знал, какие классы CSS используются.

Обновить tailwind.config.js вот так:

const Path = require("path");
const pwd = process.env.PWD;

// We can add current project paths here
const projectPaths = [
  Path.join(pwd, "./django_component_app/templates/**/*.html"),
  // add js file paths if you need
];

const contentPaths = [...projectPaths];
console.log(`tailwindcss will scan ${contentPaths}`);

module.exports = {
  content: contentPaths,
  theme: {
    extend: {},
  },
  plugins: [],
}

Итак, мы добавили путь к шаблонам Django в projectPaths, а затем передали contentPaths в content. Конечный созданный CSS-файл будет содержать CSS-классы, используемые в шаблонах Django.

Перезапустите сервер разработки webpack:

$ npm run start

Теперь вы должны увидеть:

Webpack Tailwind Welcome

Оперативная перезагрузка

Добавьте следующее к devServer в frontend/webpack/webpack.config.dev.js , чтобы включить оперативную перезагрузку:

watchFiles: [
  Path.join(__dirname, "../../django_component_app/**/*.py"),
  Path.join(__dirname, "../../django_component_app/**/*.html"),
],

Теперь сервер разработки webpack будет отслеживать изменения в любых .py или .html файлах в каталоге "django_component_app". Если какой-либо код изменится, сервер разработки автоматически перезагрузится.

Перезапустите сервер разработки webpack:

$ npm run start

Наконец, вы можете удалить папку "frontend/src/components", поскольку в этом руководстве мы не будем использовать эти файлы. Также обновите frontend/src/application/app.js вот так:

// This is the scss entry file
import "../styles/index.scss";

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

├── .babelrc
├── .browserslistrc
├── .eslintrc
├── .nvmrc
├── .stylelintrc.json
├── django_component_app
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── templates
│   │   └── index.html
│   ├── urls.py
│   └── wsgi.py
├── frontend
│   ├── .gitignore
│   ├── README.md
│   ├── build
│   │   ├── css
│   │   │   └── app.css
│   │   ├── frontend
│   │   │   └── vendors
│   │   │       └── images
│   │   │           └── sample.jpg
│   │   ├── js
│   │   │   ├── app.js
│   │   │   └── runtime.js
│   │   ├── manifest.json
│   │   └── vendors
│   │       ├── .gitkeep
│   │       └── images
│   │           ├── .gitkeep
│   │           ├── sample.jpg
│   │           └── webpack.png
│   ├── src
│   │   ├── application
│   │   │   ├── README.md
│   │   │   └── app.js
│   │   └── styles
│   │       └── index.scss
│   ├── vendors
│   │   ├── .gitkeep
│   │   └── images
│   │       ├── .gitkeep
│   │       ├── sample.jpg
│   │       └── webpack.png
│   └── webpack
│       ├── webpack.common.js
│       ├── webpack.config.dev.js
│       ├── webpack.config.prod.js
│       └── webpack.config.watch.js
├── manage.py
├── package-lock.json
├── package.json
├── postcss.config.js
├── requirements.txt
└── tailwind.config.js

Поздравляем! Теперь у вас есть проект на Django, который использует современные технологии для компиляции ресурсов внешнего интерфейса. Теперь мы можем приступить к созданию компонентов!

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

HTML

Давайте сначала взглянем на приведенный ниже код, скопированный из Flowbite, современного инструментария пользовательского интерфейса, созданного с помощью Tailwind CSS:

<!-- Modal toggle -->
<button data-modal-target="default-modal" data-modal-toggle="default-modal" class="block text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" type="button">
  Toggle modal
</button>

<!-- Main modal -->
<div id="default-modal" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
  <div class="relative p-4 w-full max-w-2xl max-h-full">
    <!-- Modal content -->
    <div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
      <!-- Modal header -->
      <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
        <h3 class="text-xl font-semibold text-gray-900 dark:text-white">
          Terms of Service
        </h3>
        <button type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" data-modal-hide="default-modal">
          <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
            <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
          </svg>
          <span class="sr-only">Close modal</span>
        </button>
      </div>
      <!-- Modal body -->
      <div class="p-4 md:p-5 space-y-4">
        <p class="text-base leading-relaxed text-gray-500 dark:text-gray-400">
          With less than a month to go before the European Union enacts new consumer privacy laws for its citizens, companies around the world are updating their terms of service agreements to comply.
        </p>
      </div>
      <!-- Modal footer -->
      <div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
        <button data-modal-hide="default-modal" type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">I accept</button>
        <button data-modal-hide="default-modal" type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">Decline</button>
      </div>
    </div>
  </div>
</div>

Примечания:

  1. Верхняя кнопка служит для переключения режима отображения/скрытия.
  2. Кнопки в модальном нижнем колонтитуле также могут скрывать модальный элемент.
  3. Flowbits включает в себя некоторый JavaScript, который имеет специальные атрибуты данных (data-modal-toggle, data-modal-hide) для управления поведением модели. В этом руководстве мы не будем использовать Flowbits JavaScript; вместо этого мы напишем свой собственный.

JavaScript

Давайте напишем некоторый JavaScript-код, чтобы заставить модель работать.

Обновить django_component_app/templates/index.html:

{% load webpack_loader static %}

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Modal Example</title>
  {% stylesheet_pack 'app' %}
  {% javascript_pack 'app' %}
</head>
<body>

<!-- Modal toggle -->
<button id="open-modal-button"
        class="block text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
        type="button">
  Toggle modal
</button>

<!-- Main modal -->
<div tabindex="-1" aria-hidden="true"
     id="default-modal"
     class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
  <div class="relative p-4 w-full max-w-2xl max-h-full">

    <!-- Modal content -->
    <div class="relative bg-white rounded-lg shadow">
      <!-- Modal header -->
      <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t">
        <h3>
          Modal Title
        </h3>
      </div>

      <!-- Modal body -->
      <div class="p-4 md:p-5 space-y-4">
        <p class="text-base leading-relaxed text-gray-500">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer in lectus et ipsum eleifend consequat. Aenean
          pellentesque tortor velit, non molestie ex ultrices in. Nulla at neque eu nulla imperdiet mattis vel sit amet
          neque. In ac mollis augue, ac iaculis purus. Donec nisl massa, gravida pharetra euismod nec, ultrices ut quam.
          Vivamus efficitur bibendum hendrerit. In iaculis sagittis elementum. Sed sit amet dolor ultrices, mollis nisl
          sed, cursus eros. Suspendisse sollicitudin quam nulla, at dignissim ex scelerisque non. Mauris ac porta nisl.
        </p>
      </div>

      <!-- Modal footer -->
      <div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b">
        <button type="button"
                id="close-modal-button"
                class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100">
          Close
        </button>
      </div>
    </div>
  </div>
</div>

<script>
  document.addEventListener("DOMContentLoaded", function () {
    const openModalButton = document.getElementById('open-modal-button');
    const closeModalButton = document.getElementById('close-modal-button');
    const modal = document.getElementById('default-modal');

    openModalButton.addEventListener('click', function () {
      modal.classList.remove('hidden');
    });

    closeModalButton.addEventListener('click', function () {
      modal.classList.add('hidden');
    });

  });
</script>

</body>
</html>

Примечания:

  1. Мы добавили обработчик событий для события DOMContentLoaded, чтобы убедиться, что DOM загружен, прежде чем мы добавим наши прослушиватели событий.
  2. Мы добавили прослушиватели событий, чтобы показывать/скрывать модальный режим при нажатии соответствующей кнопки.
  3. Мы использовали getElementById для получения модального элемента, чтобы прослушиватели событий применялись к модальному элементу.

Если вы взглянете на HTML-код Flowbite, то увидите, что он также работает аналогичным образом.

При запущенных серверах разработки webpack и Django вы сможете протестировать модель на http://127.0.0.1:8000/.

Хотя наша модель работает, код не является надежным, поскольку настройка обработчиков событий с использованием DOMContentLoaded может вызвать некоторые проблемы, если HTML вставляется динамически. Например, что, если HTML-код модели загружается с помощью AJAX после загрузки страницы?

  1. Проблема с Github: помогите разобраться в поведении HTML и JS
  2. HTMX/Flowbite.js функция init Flow bite() приводит к тому, что открытый модальный объект отображается дважды

Как вы можете видеть, решение немного сложное и быстро усложняется. Поскольку событие DOMContentLoaded уже запущено до вставки компонента, вам необходимо вручную инициализировать компонент после динамической вставки HTML-кода. Это станет еще сложнее, если у вас будет несколько компонентов.

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

Вместо этого мы будем использовать интерфейс MutationObserver.

Наблюдатель мутаций

Мы можем использовать интерфейс MutationObserver, чтобы отслеживать изменения в дереве DOM и настраивать обработчики событий для динамически вставляемого HTML-кода.

В то время как MutationObserver предлагает низкоуровневый подход к отслеживанию изменений DOM, разработчики часто выбирают высокоуровневые абстракции, предоставляемые специализированными пакетами. Эти пакеты упрощают процесс добавления интерактивности и реактивности в веб-приложения.

Есть два хороших варианта для разработчиков:

  1. Alpine.js - Надежный, минимальный фреймворк для создания поведения JavaScript в вашей разметке; синтаксис аналогичен Vue.js.
  2. Stimulus - JavaScript-фреймворк со скромными амбициями. В отличие от других интерфейсных фреймворков, Stimulus предназначен для улучшения статического или отображаемого сервером HTML - "HTML, который у вас уже есть" - путем подключения объектов JavaScript к элементам страницы с помощью простых аннотаций.

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

Alpine.js уже очень популярен в сообществе Django, но Stimulus также является хорошим выбором. На самом деле, Интерфейс администратора Wagtail CMS создан с использованием Stimulus.

В этой статье мы сосредоточимся на стимулах, но вы можете использовать Alpine.js вместо этого для достижения той же цели.

Stimulus

Обновить django_component_app/templates/index.html вот так:

{% load webpack_loader static %}

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Modal Example</title>
  {% stylesheet_pack 'app' %}
  {% javascript_pack 'app' %}
</head>
<body>

<div data-controller="modal">

  <!-- Modal toggle -->
  <button data-action="click->modal#openModal" class="block text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" type="button">
    Toggle modal
  </button>

  <div data-modal-target="container" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
    <div class="relative p-4 w-full max-w-2xl max-h-full">

      <!-- Modal content -->

      <div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
        <!-- Modal header -->
        <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
          <h3>
            Modal Title
          </h3>
        </div>

        <!-- Modal body -->
        <div class="p-4 md:p-5 space-y-4">
          <p class="text-base leading-relaxed text-gray-500">
            Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer in lectus et ipsum eleifend consequat. Aenean
            pellentesque tortor velit, non molestie ex ultrices in. Nulla at neque eu nulla imperdiet mattis vel sit amet
            neque. In ac mollis augue, ac iaculis purus. Donec nisl massa, gravida pharetra euismod nec, ultrices ut quam.
            Vivamus efficitur bibendum hendrerit. In iaculis sagittis elementum. Sed sit amet dolor ultrices, mollis nisl
            sed, cursus eros. Suspendisse sollicitudin quam nulla, at dignissim ex scelerisque non. Mauris ac porta nisl.
          </p>
        </div>

        <!-- Modal footer -->
        <div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
          <button data-action="click->modal#closeModal" type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100">
            Close
          </button>
        </div>

      </div>
    </div>
  </div>

</div>

<script type="module">
  import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
  const application = Application.start();
  application.register("modal", class extends Controller {
    static targets = ["container"];

    openModal() {
      this.containerTarget.classList.remove("hidden");
    }

    closeModal() {
      this.containerTarget.classList.add("hidden");
    }
  });
</script>

</body>
</html>

Примечания:

  1. В теге script мы устанавливаем для type значение module, указывая, что скрипт следует рассматривать как модуль ECMAScript (ES) .
  2. Затем мы импортировали Stimulus из CDN и зарегистрировали новый контроллер Stimulus под названием modal.
  3. Контроллер Stimulus на самом деле является классом JavaScript, и экземпляр контроллера будет создан для каждого элемента, который имеет атрибут data-controller="modal".
  4. Элемент button имеет значение data-action="click->modal#openModal", что означает, что при нажатии на кнопку будет вызван метод openModal в контроллере modal для открытия модального элемента.
  5. Аналогично, кнопка закрытия имеет значение data-action="click->modal#closeModal", что означает, что при нажатии кнопки закрытия будет вызван метод closeModal в контроллере modal для закрытия модала.
  6. static targets = ["container"]; определяет целевой элемент в контроллере, который является модальным элементом контейнера. Мы использовали data-modal-target="container" для определения целевого элемента в HTML. Преимущество использования data-modal-target заключается в том, что мы можем легко получить доступ к целевому элементу (this.containerTarget) без выбора элемента.

Убедитесь, что модель по-прежнему работает в браузере.

Компонент JavaScript

Хотя это технически возможно, я не рекомендую помещать код JavaScript в ваши файлы HTML-шаблонов:

  1. Потенциальные проблемы с безопасностью - если вы поместите код JavaScript в HTML-файл, злоумышленникам будет легче внедрить вредоносный код.
  2. JavaScript-код нелегко проверить с помощью линтера; его также нелегко читать, тестировать или поддерживать.

Давайте поместим весь JavaScript-код в *.js файлы и посмотрим, какие преимущества мы получим.

Начните с установки Stimulus:

$ npm install --save-exact @hotwired/stimulus@3.2.2

Создайте новый файл с именем frontend/src/controllers/modal_controller.js:

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["container"];

  openModal() {
    this.containerTarget.classList.remove("hidden");
  }

  closeModal() {
    this.containerTarget.classList.add("hidden");
  }
}

Обновить frontend/src/application/app.js:

// This is the scss entry file
import "../styles/index.scss";

import { Application } from "@hotwired/stimulus";
import modalController from "../controllers/modal_controller";

window.Stimulus = Application.start();
window.Stimulus.register("modal", modalController);

Здесь мы импортировали modalController, а затем зарегистрировали его, используя метод Stimulus.register.

Обновить django_component_app/templates/index.html:

{% load webpack_loader static %}

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Modal Example</title>
  {% stylesheet_pack 'app' %}
  {% javascript_pack 'app' attrs='defer' %}
</head>
<body>

<div data-controller="modal">

  <!-- Modal toggle -->
  <button data-action="click->modal#openModal" class="block text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" type="button">
    Toggle modal
  </button>

  <div data-modal-target="container" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
    <div class="relative p-4 w-full max-w-2xl max-h-full">

      <!-- Modal content -->

      <div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
        <!-- Modal header -->
        <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
          <h3>
            Modal Title
          </h3>
        </div>

        <!-- Modal body -->
        <div class="p-4 md:p-5 space-y-4">
          <p class="text-base leading-relaxed text-gray-500">
            Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer in lectus et ipsum eleifend consequat. Aenean
            pellentesque tortor velit, non molestie ex ultrices in. Nulla at neque eu nulla imperdiet mattis vel sit amet
            neque. In ac mollis augue, ac iaculis purus. Donec nisl massa, gravida pharetra euismod nec, ultrices ut quam.
            Vivamus efficitur bibendum hendrerit. In iaculis sagittis elementum. Sed sit amet dolor ultrices, mollis nisl
            sed, cursus eros. Suspendisse sollicitudin quam nulla, at dignissim ex scelerisque non. Mauris ac porta nisl.
          </p>
        </div>

        <!-- Modal footer -->
        <div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
          <button data-action="click->modal#closeModal" type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100">
            Close
          </button>
        </div>

      </div>
    </div>
  </div>
</div>

</body>
</html>

Примечания:

  1. Поскольку контроллер Stimulus отвечает за модальные действия открытия и закрытия, мы смогли перенести весь JavaScript-код из файла HTML-шаблона.
  2. Мы также переместили JavaScript-ссылку app из конца body в элемент head и установили атрибут defer. Использование <script src="" defer> в разделе head позволяет выполнять асинхронную загрузку скриптов, не блокируя отображение страницы. Это необязательно, но рекомендуется. Вы можете проверить Производительность JavaScript – Как повысить скорость работы страницы с помощью асинхронности и отсрочки, чтобы узнать больше.

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

├── .gitignore
├── README.md
├── build
│   ├── css
│   │   └── app.css
│   ├── frontend
│   │   └── vendors
│   │       └── images
│   │           └── sample.jpg
│   ├── js
│   │   ├── app.js
│   │   └──  runtime.js
│   ├── manifest.json
│   └── vendors
│       ├── .gitkeep
│       └── images
│           ├── .gitkeep
│           ├── sample.jpg
│           └── webpack.png
├── src
│   ├── application
│   │   ├── README.md
│   │   └── app.js
│   ├── controllers
│   │   └── modal_controller.js
│   └── styles
│       └── index.scss
├── vendors
│   ├── .gitkeep
│   └── images
│       ├── .gitkeep
│       ├── sample.jpg
│       └── webpack.png
└── webpack
    ├── webpack.common.js
    ├── webpack.config.dev.js
    ├── webpack.config.prod.js
    └── webpack.config.watch.js

Перезапустите сервер разработки webpack и снова протестируйте модель в браузере.


С помощью Stimulus мы можем заставить контроллер modal работать с различными элементами HTML. Теперь код можно использовать повторно, и его намного проще тестировать.

Стиль компонента

Давайте извлекем CSS-классы Tailwind в отдельный SCSS-файл, чтобы сделать CSS в шаблоне Django более читабельным.

Создать файл frontend/src/styles/components/modal.scss:

@layer components {
  [data-controller="modal"] {
    & .modal-container {
      @apply overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full max-h-full;
    }

    & .modal-content {
      @apply relative bg-white rounded-lg shadow;
    }

    & .modal-header {
      @apply flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600;
    }

    & .modal-body {
      @apply p-4 md:p-5 space-y-4;
    }

    & .modal-footer {
      @apply flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600;
    }
  }
}

Импортируйте модальные стили в интерфейс/src/styles/index.scss:

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "components/modal";  // new

.jumbotron {
  // should be relative path of the entry scss file
  background-image: url("../../vendors/images/sample.jpg");
  background-size: cover;
}

.btn-blue {
  @apply inline-flex items-center;
  @apply px-4 py-2;
  @apply font-semibold rounded-lg shadow-md;
  @apply text-white bg-blue-500;
  @apply hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75;
}

Обновить django_component_app/templates/index.html:

{% load webpack_loader static %}

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Modal Example</title>
  {% stylesheet_pack 'app' %}
  {% javascript_pack 'app' attrs='defer' %}
</head>
<body>

<div data-controller="modal">

  <!-- Modal toggle -->
  <button data-action="click->modal#openModal" class="btn-blue" type="button">
    Toggle modal
  </button>

  <div data-modal-target="container" tabindex="-1" aria-hidden="true" class="hidden modal-container">
    <div class="relative p-4 w-full max-w-2xl max-h-full">

      <div class="modal-content">
        <!-- Modal header -->
        <div class="modal-header">
          <h3>
            Modal Title
          </h3>
        </div>

        <!-- Modal body -->
        <div class="modal-body">
          <p class="text-base leading-relaxed text-gray-500">
            Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer in lectus et ipsum eleifend consequat. Aenean
            pellentesque tortor velit, non molestie ex ultrices in. Nulla at neque eu nulla imperdiet mattis vel sit amet
            neque. In ac mollis augue, ac iaculis purus. Donec nisl massa, gravida pharetra euismod nec, ultrices ut quam.
            Vivamus efficitur bibendum hendrerit. In iaculis sagittis elementum. Sed sit amet dolor ultrices, mollis nisl
            sed, cursus eros. Suspendisse sollicitudin quam nulla, at dignissim ex scelerisque non. Mauris ac porta nisl.
          </p>
        </div>

        <!-- Modal footer -->
        <div class="modal-footer">
          <button data-action="click->modal#closeModal" type="button" class="btn-blue">
            Close
          </button>
        </div>

      </div>
    </div>
  </div>
</div>

</body>
</html>

Примечания:

  1. С классами CSS modal-header, modal-body, и modal-footer мы можем использовать их в нашем приложении для простого создания согласованных моделей.
  2. Если нам понадобится обновить модальный стиль в будущем, мы можем просто обновить файл modal.scss.
  3. Кроме того, наш компонент будет работать, даже если HTML-код динамически вставляется на страницу.

Вкладочный компонент

Далее давайте создадим компонент интерактивных вкладок.

frontend/src/controllers/tabs_controller.js:

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  connect() {
    this.showTab(1);
  }

  showTab(tabIndex) {
    const targetId = tabIndex.toString();
    const contentTargets = this.element.querySelectorAll("[data-panel]");
    const tabTargets = this.element.querySelectorAll("[data-tabs-target]");

    contentTargets.forEach(content => {
      content.classList.toggle("hidden", content.dataset.panel !== targetId);
    });

    tabTargets.forEach(tab => {
      const selected = tab.getAttribute("data-tabs-target") === targetId;
      tab.classList.toggle("text-blue-600", selected);
      tab.classList.toggle("border-blue-600", selected);
    });
  }

  showContent(event) {
    const targetId = event.currentTarget.getAttribute("data-tabs-target");
    this.showTab(parseInt(targetId));
  }
}

Обновить tailwind.config.js:

const Path = require("path");
const pwd = process.env.PWD;

// We can add current project paths here
const projectPaths = [
  Path.join(pwd, "./frontend/src/**/*.js"),          // new
  Path.join(pwd, "./django_component_app/templates/**/*.html"),
  // add js file paths if you need
];

const contentPaths = [...projectPaths];
console.log(`tailwindcss will scan ${contentPaths}`);

module.exports = {
  content: contentPaths,
  theme: {
    extend: {},
  },
  plugins: [],
}

Мы добавили Path.join(pwd, "./frontend/src/**/*.js"), чтобы имена классов Tailwind, используемые в вышеупомянутом контроллере, были обнаружены и добавлены в окончательный CSS-файл.

Обновить frontend/src/application/app.js чтобы зарегистрировать контроллер вкладок, выполните следующие действия:

// This is the scss entry file
import "../styles/index.scss";

import { Application } from "@hotwired/stimulus";
import modalController from "../controllers/modal_controller";
import tabsController from "../controllers/tabs_controller";  // new

window.Stimulus = Application.start();
window.Stimulus.register("modal", modalController);
window.Stimulus.register("tabs", tabsController);       // new

Обновить django_component_app/templates/index.html:

{% load webpack_loader static %}

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Tab Example</title>
  {% stylesheet_pack 'app' %}
  {% javascript_pack 'app' attrs='defer' %}
</head>
<body>

    <div data-controller="tabs">
      <div class="mb-4 border-b border-gray-200 dark:border-gray-700">
        <ul class="flex flex-wrap -mb-px text-sm font-medium text-center">
          <li class="me-2" role="presentation">
            <button class="inline-block p-4 border-b-2 rounded-t-lg hover:text-gray-600 hover:border-gray-300 "
                    data-action="click->tabs#showContent" data-tabs-target="1" type="button">
              Profile
            </button>

            <button class="inline-block p-4 border-b-2 rounded-t-lg hover:text-gray-600 hover:border-gray-300 "
                    data-action="click->tabs#showContent" data-tabs-target="2" type="button">
              Dashboard
            </button>

            <button class="inline-block p-4 border-b-2 rounded-t-lg hover:text-gray-600 hover:border-gray-300 "
                    data-action="click->tabs#showContent" data-tabs-target="3" type="button">
              Settings
            </button>
          </li>
        </ul>
      </div>
      <div>
        <div class="hidden p-4 rounded-lg bg-gray-50 dark:bg-gray-800" data-panel="1" role="tabpanel">
          <p class="text-sm text-gray-500 dark:text-gray-400">This is some placeholder content the <strong
                  class="font-medium text-gray-800 ">Profile tab's associated content</strong>. Clicking
            another tab will toggle the visibility of this one for the next. The tab JavaScript swaps classes to control the
            content visibility and styling.</p>
        </div>

        <div class="hidden p-4 rounded-lg bg-gray-50 dark:bg-gray-800" data-panel="2" role="tabpanel">
          <p class="text-sm text-gray-500 dark:text-gray-400">This is some placeholder content the <strong
                  class="font-medium text-gray-800 ">Dashboard tab's associated content</strong>. Clicking
            another tab will toggle the visibility of this one for the next. The tab JavaScript swaps classes to control the
            content visibility and styling.</p>
        </div>

        <div class="hidden p-4 rounded-lg bg-gray-50 dark:bg-gray-800" data-panel="3" role="tabpanel">
          <p class="text-sm text-gray-500 dark:text-gray-400">This is some placeholder content the <strong
                  class="font-medium text-gray-800 ">Settings tab's associated content</strong>. Clicking
            another tab will toggle the visibility of this one for the next. The tab JavaScript swaps classes to control the
            content visibility and styling.</p>
        </div>
      </div>
    </div>

</body>
</html>

http://127.0.0.1:8000 / теперь должно выглядеть так:

Tab Component

Поздравляем! Вы создали повторно используемый компонент tab с помощью Stimulus и Tailwind CSS.

Размышляем о интерфейсных инструментах

С помощью python-webpack-boilerplate мы можем напрямую работать с интерфейсными инструментами и технологиями, вместо того чтобы работать с ними косвенно в экосистеме Python/Django, например:

  1. Django-компрессор
  2. django-libsass
  3. django-tailwind
  4. Django-browser-reload
  5. ...

Иными словами, с помощью python-webpack-boilerplate вы можете установить интерфейсные инструменты напрямую через npm, а затем подключить их к файлам конфигурации. Требуется немного больше усилий, чтобы наладить работу, но теперь у вас гораздо больше гибкости, особенно по мере роста вашего проекта.

Заключение

В этом руководстве вы узнали, как использовать Stimulus и Tailwind CSS для создания повторно используемых интерфейсных компонентов.

В Стимуле отличная экосистема и сообщество. Полезные ресурсы:

  1. tailwindcss-stimulus-components - Набор компонентов стимула для попутного ветра.
  2. stimulus-webpack-helpers - С помощью этого инструмента вы можете просто создать свои компоненты, и он выполнит регистрацию контроллеров Stimulus за вас.

Теперь вы можете импортировать интерфейсные компоненты в свои шаблоны Django, и они должны хорошо работать. В следующем руководстве я покажу вам, как повторно использовать код компонента на стороне сервера, и мы создадим серверные компоненты.


Серия:

  1. Часть 1 (это руководство!) - посвящена настройке проекта, а также работе на стороне клиента
  2. Часть 2 - посвящена серверной части
Вернуться на верх