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

Добро пожаловать в часть 2. В первой части этой серии руководств мы настроили Django, настроили python-webpack-boilerplate и использовали Stimulus и Tailwind CSS для создания повторно используемых модальных компонентов и компонентов табуляции. В этой второй части мы рассмотрим, как использовать django-viewcomponent для создания серверных компонентов, которые могут помочь нам более эффективно использовать код повторно.


Серия:

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

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

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

<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>

Можем ли мы повторно использовать часть кода шаблона и передавать в шаблон только содержимое верхнего, основного и нижнего колонтитулов?

Мы можем использовать django-viewcomponent, чтобы помочь с этим.

django-viewcomponent

компонент django-view - это библиотека Django, которая предоставляет способ создания повторно используемых компонентов для вашего проекта Django.

Впервые используете компонент django-view? Ознакомьтесь с Созданием повторно используемых компонентов в Django.

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

django-viewcomponent==1.0.5

Установка:

(venv)$ pip install -r requirements.txt

Затем добавьте приложение в INSTALLED_APPS в settings.py:

INSTALLED_APPS = [

    ...

    "django_viewcomponent",
]

Измените раздел TEMPLATES в разделе settings.py следующим образом:

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": ["django_component_app/templates"],
        "APP_DIRS": False,  # updated
        "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",
            ],
            "loaders":[(  # new
                "django.template.loaders.cached.Loader", [
                    "django.template.loaders.filesystem.Loader",
                    "django.template.loaders.app_directories.Loader",
                    "django_viewcomponent.loaders.ComponentLoader",
                ]
            )],
        },
    },
]

Обновить 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"),
  Path.join(pwd, "./django_component_app/templates/**/*.html"),
  // django-viewcomponent
  Path.join(pwd, "./components/**/*.py"),          // new
  Path.join(pwd, "./components/**/*.html"),        // new
  // 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, "./components/**/*.py"), и Path.join(pwd, "./components/**/*.html"),, чтобы CSS-классы Tailwind были обнаружены и добавлены в окончательный CSS-файл.

Создайте новый файл с именем components/modal/modal.py:

from django_viewcomponent import component
from django_viewcomponent.fields import RendersOneField


@component.register("modal")
class ModalComponent(component.Component):
    modal_trigger = RendersOneField(required=True)
    modal_header = RendersOneField(required=True)
    modal_body = RendersOneField(required=True)
    modal_footer = RendersOneField(required=True)

    template_name = "modal/modal.html"

Примечания:

  1. ModalComponent это класс Python, который расширяет класс component.Component.
  2. Мы использовали RendersOneField для определения поля slot для модального компонента.

Создайте новый файл с именем components/modal/modal.html , который является файлом шаблона для компонента:

<div data-controller="modal">

  {{ self.modal_trigger.value }}

  <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">
        <div class="modal-header">
          {{ self.modal_header.value }}
        </div>
        <div class="modal-body">
          {{ self.modal_body.value }}
        </div>
        <div class="modal-footer">
          {{ self.modal_footer.value }}
        </div>
      </div>
    </div>
  </div>
</div>

{{ self.modal_trigger.value }} отображает значение интервала в поле modal_trigger.

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

{% load webpack_loader static %}
{% load viewcomponent_tags %}

<!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>
  {% component 'modal' as modal_comp %}
    {% call modal_comp.modal_trigger %}
      <button data-action="click->modal#openModal" class="btn-blue" type="button">
        Toggle modal
      </button>
    {% endcall %}

    {% call modal_comp.modal_header %}
      <h3>
        Modal Title
      </h3>
    {% endcall %}

    {% call modal_comp.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>
    {% endcall %}

    {% call modal_comp.modal_footer %}
      <button data-action="click->modal#closeModal" type="button" class="btn-blue">
        Close
      </button>
    {% endcall %}
  {% endcomponent %}
</body>
</html>

Примечания:

  1. Мы устанавливаем {% load viewcomponent_tags %} вверху, чтобы мы могли использовать теги {% component %}.
  2. {% component 'modal' as modal_comp %} создает модальный компонент и присваивает его переменной modal_comp.
  3. {% call modal_comp.modal_trigger %} передает content в modal_trigger слот модального компонента.

Для тестирования запустите сервер разработки Django в одном окне терминала:

(venv)$ python manage.py runserver

И запустите сервер разработки webpack в другом окне:

$ npm run start

Перейдите к http://127.0.0.1:8000/ и убедитесь, что режим по-прежнему работает должным образом.

Пока ваш недавно созданный серверный модальный компонент работает отлично, допустим, ваш дизайнер приходит к вам и говорит: "Мне нужен модал побольше". Чтобы справиться с этим, начните с обновления components/modal/modal.py вот так:

from django_viewcomponent import component
from django_viewcomponent.fields import RendersOneField


@component.register("modal")
class ModalComponent(component.Component):
    modal_trigger = RendersOneField(required=True)
    modal_header = RendersOneField(required=True)
    modal_body = RendersOneField(required=True)
    modal_footer = RendersOneField(required=True)

    template_name = "modal/modal.html"

    size_map = {
        "md": "max-w-2xl",
        "lg": "max-w-4xl",
    }

    def __init__(self, size=None, **kwargs):
        self.size = size

    @property
    def size_css(self):
        # return the class based on the size, if not, return the default size
        return self.size_map.get(self.size, self.size_map["md"])

Примечания:

  1. Теперь компонент может принимать параметр size.
  2. Мы определили size_map для сопоставления размера классу CSS вместе со свойством size_css, возвращающим класс CSS на основе параметра size.

Затем обновите components/modal/modal.html:

<div data-controller="modal">

  {{ self.modal_trigger.value }}

  <div data-modal-target="container" tabindex="-1" aria-hidden="true" class="hidden modal-container">
    <div class="relative p-4 w-full {{ self.size_css }} max-h-full m-auto">

      <div class="modal-content">
        <div class="modal-header">
          {{ self.modal_header.value }}
        </div>
        <div class="modal-body">
          {{ self.modal_body.value }}
        </div>
        <div class="modal-footer">
          {{ self.modal_footer.value }}
        </div>
      </div>
    </div>
  </div>
</div>

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

Затем в файле django_component_app/templates/index.html мы можем изменить модальный размер следующим образом:

{% component 'modal' size='lg' as modal_comp %}

...

{% endcomponent %}

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

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

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

components/tab/tab.py:

from django_viewcomponent import component
from django_viewcomponent.fields import RendersManyField


@component.register("tab")
class TabComponent(component.Component):
    tabs = RendersManyField(required=True)
    panels = RendersManyField(required=True)

    template_name = "tab/tab.html"

Здесь мы использовали RendersManyField, чтобы поле "слот" могло принимать набор элементов.

Добавьте шаблон components/tab/tab.html:

<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">
      {% for tab in self.tabs.value %}
      <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="{{ forloop.counter }}" type="button">
          {{ tab }}
        </button>
      </li>
      {% endfor %}
    </ul>
  </div>
  <div>
    {% for panel in self.panels.value %}
    <div class="hidden p-4 rounded-lg bg-gray-50 dark:bg-gray-800" data-panel="{{ forloop.counter }}" role="tabpanel">
      {{ panel }}
    </div>
    {% endfor %}
  </div>
</div>

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

{% load webpack_loader static %}
{% load viewcomponent_tags %}

<!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>

  {% component 'tab' as component %}

    {% call component.tabs %}
      Profile
    {% endcall %}
    {% call component.panels %}
      <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>
    {% endcall %}

    {% call component.tabs %}
      Dashboard
    {% endcall %}
    {% call component.panels %}
      <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>
    {% endcall %}

    {% call component.tabs %}
      Settings
    {% endcall %}
    {% call component.panels %}
      <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>
    {% endcall %}

  {% endcomponent %}

</body>
</html>

Примечания:

  1. Мы объединили содержимое вкладки и соответствующей панели, чтобы сделать код более читабельным.
  2. RendersManyField это мощная функция. Это позволяет нам определять поле слота и передавать содержимое в слот компонента несколько раз.

http://127.0.0.1:8000/:

Tab Component

Заключение

В этой статье мы перенесли HTML-код для наших компонентов на серверную сторону. Серверные компоненты делают код более удобным для обслуживания, повторного использования и тестирования. Я надеюсь, что это поможет вам создать лучший проект на Django.

Если вы хотите узнать больше о django-viewcomponent, ознакомьтесь с официальной документацией. Я также рекомендую вам ознакомиться со следующими разделами статьи Создание повторно используемых компонентов в Django:

  1. Предварительный просмотр компонентов
  2. Ресурсы внешнего интерфейса
  3. Библиотека компонентов
  4. Другие компонентные решения
  5. Ресурсы

Серия:

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