Steps

So far we’ve been using streamfields, which are great for dynamic content generation where the order of the content doesn’t matter.

An Orderable is a way to add custom models that can be ordered within another model. Think of it like a list of items that can be re-ordered by dragging and dropping them into a new position. This is useful for things like team members, portfolio items, or other content where the order matters.

We’re going to create an Orderable where somebody could upload 1-5 images for a carousel on our homepage.

Let’s go to models.py in our home app and create a new class for our Orderable!

from wagtail.models import Page, Orderable
from modelcluster.fields import ParentalKey

class HomePageCarouselImages(Orderable):
    """Between 1 and 5 images for the home page carousel."""

    page = ParentalKey("home.HomePage", related_name="carousel_images")

    carousel_image = models.ForeignKey(
        "wagtailimages.Image",
        null=True,  # we don't know what the default will be
        blank=False,  # should not be deleted
        on_delete=models.SET_NULL,  # we don't want anything else to delete
        related_name="+"
    )

    panels = [
        FieldPanel("carousel_images")
    ]

ParentalKey is saying “What is this inline model related to?” And we are telling it to belong to the Home app, and the HomePage class.

With this new class set up, you need to make migrations, and then add it to the HomePage’s content_panels!

from django.db import models
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import FieldPanel, PageChooserPanel, MultipleChooserPanel, InlinePanel
from wagtail.fields import RichTextField, StreamField
from wagtail.models import Page, Orderable
from wagtail.images.edit_handlers import ImageFieldComparison

from streams import blocks

class HomePageCarouselImages(Orderable):
    """Between 1 and 5 images for the home page carousel."""

    page = ParentalKey("home.HomePage", related_name="carousel_images")

    carousel_image = models.ForeignKey(
        "wagtailimages.Image",
        null=True,  # we don't know what the default will be
        blank=False,  # should not be deleted
        on_delete=models.SET_NULL,  # we don't want anything else to delete
        related_name="+"
    )

    panels = [
        FieldPanel("carousel_images")
    ]

class HomePage(Page):
    """Home Page Model"""

    templates = "home/home_page.html"
    max_count = 1

    banner_title = models.CharField(max_length=100, blank=False, null=True)
    banner_subtitle = RichTextField(
        features=["bold", "italic"]
    )
    banner_image = models.ForeignKey(
        "wagtailimages.Image",
        null=True,  # we don't know what the default will be
        blank=False,  # should not be deleted
        on_delete=models.SET_NULL,  # we don't want anything else to delete
        related_name="+"
    )
    banner_cta = models.ForeignKey(
        "wagtailcore.Page",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+"
    )

    content = StreamField(
        [
            ("cta", blocks.CTABlock()),
        ],
        null=True,
        blank=True,
        use_json_field=True
    )

    content_panels = Page.content_panels + [
        FieldPanel("banner_title"),
        FieldPanel("banner_subtitle"),
        FieldPanel('banner_image'),
        PageChooserPanel("banner_cta"),
        FieldPanel("content"),
        InlinePanel("carousel_images"),
    ]

    class Meta:
        verbose_name = "Home Page"
        verbose_name_plural = "Home Pages"

Untitled

You can now add images to this new Class inside the admin page, but there’s nothing labeling or separating it from the rest of the page! We must edit the content_panels again.

content_panels = Page.content_panels + [
        MultiFieldPanel([
            FieldPanel("banner_title"),
            FieldPanel("banner_subtitle"),
            FieldPanel('banner_image'),
            PageChooserPanel("banner_cta"),
        ], heading="Banner Options"),
        MultiFieldPanel([
            InlinePanel("carousel_images", max_num=5, min_num=1, label="Image"),
        ], heading="Carousel Images"),
        FieldPanel("content"),
    ]

Multifield panels allow us to group similar items together for displaying on the admin page.

Untitled

So when we add 3 images, nothing will happen yet because we need to set up the template to tell it how to handle these images.

I got a component of a Tailwind Carousel online and heavily editted it:

{% load wagtailcore_tags wagtailimages_tags %}

<div
  id="carouselExampleCaptions"
  class="relative border-2 border-slate-800 dark:border-gray-300"
  data-te-carousel-init
  data-te-carousel-slide>
  <!--Carousel indicators-->
  <div
    class="carousel-indicators"
    data-te-carousel-indicators>
    {% for loop_cycle in self.carousel_images.all %}
    <button
      type="button"
      data-te-target="#carouselExampleCaptions"
      data-te-slide-to="{{ forloop.counter0 }}"
      data-te-carousel-active
      class="mx-[3px] box-content h-[3px] w-[30px] flex-initial cursor-pointer border-0 border-y-[10px] border-solid border-transparent 
      bg-black dark:bg-white
      bg-clip-padding p-0 -indent-[999px] opacity-50 transition-opacity duration-[600ms] ease-[cubic-bezier(0.25,0.1,0.25,1.0)] motion-reduce:transition-none"
      aria-current="true"
      aria-label="Slide {{ forloop.counter }}"></button>
      {% endfor %}
  </div>

  <!--Carousel items-->
  <div
    class="relative w-full overflow-hidden after:clear-both after:block after:content-['']">
    {% for loop_cycle in self.carousel_images.all %}
        {% image loop_cycle.carousel_image fill-900x400 as img %}
        <!--Carousel item-->
        <div
        class="relative float-left -mr-[100%] w-full transition-transform duration-[1000ms] ease-in-out motion-reduce:transition-none
        {% if forloop.counter != 1%}
        hidden
        {% endif %}"
        {% if forloop.counter == 1 %}
        data-te-carousel-active
        {% endif %}
        data-te-carousel-item
        style="backface-visibility: hidden">
        <img
            src="{{ img.url }}"
            class="block w-full"
            alt="{{ img.alt }}" />
            <a href="
            {% if loop_cycle.button_page %}
            {{ card.button_page.url }}
            {% endif %}
            ">
                <div
                    class="absolute inset-x-[15%] bottom-5 py-5 text-center text-black dark:text-white bg-white dark:bg-black rounded-lg hover:bg-slate-300 dark:hover:bg-slate-700
                    transition-all duration-200 ease-linear my-6 mx-10
                    " style="--tw-bg-opacity:0.3;">
                    <h5 class="text-xl -mt-5">
                        {{loop_cycle.image_title}}
                    </h5>
                    <p class="text-xs">
                        {{loop_cycle.image_subtitle|safe}}
                    </p>
                    <div class="-mt-5"></div>
                </div>
            </a>
        </div>
    {% endfor %}
</div>

  <!--Carousel controls - prev item-->
  <button
    class="carousel-controls left-0 rounded-r-xl"
    type="button"
    data-te-target="#carouselExampleCaptions"
    data-te-slide="prev">
    <span class="inline-block h-8 w-8">
      <svg
        xmlns="<http://www.w3.org/2000/svg>"
        fill="none"
        viewBox="0 0 24 24"
        stroke-width="1.5"
        stroke="currentColor"
        class="h-6 w-6">
        <path
          stroke-linecap="round"
          stroke-linejoin="round"
          d="M15.75 19.5L8.25 12l7.5-7.5" />
      </svg>
    </span>
    <span
      class="!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]"
      >Previous</span
    >
  </button>
  <!--Carousel controls - next item-->
  <button
    class="carousel-controls right-0 rounded-l-xl"
    type="button"
    data-te-target="#carouselExampleCaptions"
    data-te-slide="next">
    <span class="inline-block h-8 w-8">
      <svg
        xmlns="<http://www.w3.org/2000/svg>"
        fill="none"
        viewBox="0 0 24 24"
        stroke-width="1.5"
        stroke="currentColor"
        class="h-6 w-6">
        <path
          stroke-linecap="round"
          stroke-linejoin="round"
          d="M8.25 4.5l7.5 7.5-7.5 7.5" />
      </svg>
    </span>
    <span
      class="!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]"
      >Next</span
    >
  </button>
</div>

<script>
    // Initialization for ES Users
    import {
    Carousel,
    initTE,
    } from "tw-elements";

    initTE({ Carousel });
</script>

Untitled