Skip to content

Instantly share code, notes, and snippets.

@unbracketed
Last active August 3, 2024 08:13
Show Gist options
  • Save unbracketed/db78c1b58427a6abefa2508b745eb25c to your computer and use it in GitHub Desktop.
Save unbracketed/db78c1b58427a6abefa2508b745eb25c to your computer and use it in GitHub Desktop.
Docs about how to make a Repeater FastTag for easily rendering lists or tables of data

Repeater FastTag

This intended as a utility component for rendering lists, iterables, or sequence-type objects, with a goal of providing useful results with minimal configuration, while allowing all aspects of rendering to be easily overridden.

Source

class Repeater:
    """
    A component for rendering iterables or sequences.

    :param data: The iterable or sequence to render
    :param item: The component to use for each item (default: Div)
    :param container: The component to use as a container (default: Div)
    :param container_kwargs: Additional kwargs for the container component (can be callable)
    :param item_kwargs: Additional kwargs for the item component (can be callable)
    :param kwargs: Additional kwargs for the DataList itself
    """

    def __init__(self, data, item=Div, container=Div, **kwargs):
        self.data = data
        self.Container = container
        self.container_kwargs = kwargs.pop("container_kwargs", lambda _data: {})
        self.Item = item
        self.item_kwargs = kwargs.pop("item_kwargs", lambda _itm, _idx: {})
        self.kwargs = kwargs

    def __ft__(self):
        items = []
        for index, item_data in enumerate(self.data):
            item_kwargs = self.item_kwargs
            if callable(item_kwargs):
                item_kwargs = item_kwargs(item_data, index)
            items.append(self.Item(item_data, **item_kwargs))

        container_kwargs = self.container_kwargs
        if callable(container_kwargs):
            container_kwargs = container_kwargs(self.data)

        return self.Container(*items, **container_kwargs)

General Use

Repeater(iterable, ItemFT, ContainerFT)

Examples

Simplest Case

Repeater(["a", "b", "c"])

<div>
  <div>a</div>
  <div>b</div>
  <div>c</div>
</div>

Override Item Component

Repeater(["inside", "a", "paragraph"], P)

<div>
  <p>inside</p>
  <p>a</p>
  <p>paragraph</p>
</div>

Override Item and Container Components

Repeater(["using", "unordered", "list"], Li, Ul)

<ul>
  <li>using</li>
  <li>unordered</li>
  <li>list</li>
</ul>

This is a shortcut for writing:

Repeater(["using", "unordered", "list"], item=Li, container=Ul)

Overriding Keyword Arguments / Styles

Keyword arguments can be passed to either of the Item components or Container component:

 Repeater(
    ["cat", "dog", "bird"],
    item=Li,
    item_kwargs={"cls": "bg-green-200 p-4"},
    container=Ul,
    container_kwargs={"cls": "text-center text-white"},
)
<ul class="text-center text-white">
  <li class="bg-green-200 p-4">cat</li>
  <li class="bg-green-200 p-4">dog</li>
  <li class="bg-green-200 p-4">bird</li>
</ul>

Using More Complex Item Components

This example shows composing a couple of custom FT componnents together to use as the Item FT for each item in the list:

def BlueDataItem(item, **kwargs): return Div(item, cls="bg-blue-200 p-4")
def BorderListItem(item, **kwargs): return Li(BlueDataItem(item), cls='border-solid border-2', **kwargs)
Repeater(["one", "blue", "three"], BorderListItem, Ul)
<ul>
  <li class="border-solid border-2">
    <div class="bg-blue-200 p-4">one</div>
  </li>
  <li class="border-solid border-2">
    <div class="bg-blue-200 p-4">blue</div>
  </li>
  <li class="border-solid border-2">
    <div class="bg-blue-200 p-4">three</div>
  </li>
</ul>

Advanced Example

Here is an example illustrating using callables to change how the components render dynamically based on data available in the component and in the current loop context of the iterable. Using lambda syntax offers a chance to keep the logic "inline" with the component layout, though some may find it harder to read and prefer to define functions elswhere.

@rt("/")
def get():
    products = [
        {"name": "Laptop", "price": 999, "stock": 50},
        {"name": "Smartphone", "price": 699, "stock": 100},
        {"name": "Tablet", "price": 399, "stock": 30},
        {"name": "Smartwatch", "price": 199, "stock": 75},
    ]
    
    return Titled(
        "Product Catalog",
        Container(
            H1("Our Products", cls="text-3xl font-bold mb-6"),
            Repeater(
                products,
                item=lambda p, **ikw: Div(
                    H2(p["name"], cls="text-xl font-semibold"),
                    P(f"${p['price']}", cls="text-gray-600"),
                    P(f"In stock: {p['stock']}", cls="text-sm"),
                    **ikw,
                ),
                container=Grid,
                container_kwargs=lambda data: {
                    "cols": {"base": 1, "md": 2, "lg": 3},
                    "gap": 6,
                    "cls": f"{'bg-green-50 p-4 rounded' if sum(p['stock'] for p in data) > 200 else ''}",
                },
                item_kwargs=lambda p, i: {
                    "cls": f"{'bg-white shadow-md rounded-lg p-4 border-t-4 border-green-500' if p['stock'] > 50 else 'bg-white shadow-md rounded-lg p-4'}"
                },
            ),
            cls="my-8",
        ),
    )

We're passing a callable to item to handle how each item will be rendered. This is effectively a custom FT component and could be re-written like:

def ProductCard(item, **kwargs):
    return Div(
        H2(item["name"], cls="text-xl font-semibold"),
        P(f"${item['price']}", cls="text-gray-600"),
        P(f"In stock: {item['stock']}", cls="text-sm"),
        **kwargs
    )

...and update the Repeater like:

return Repeater(
    products,
    item=ProductCard,
    ...

Notes:

  • To override the component for the items, pass a callable that accepts two arguments:
    • the current item in the loop
    • the item kwarg arguments, which be passed or derived from item_kwargs
  • To override the component for the container, pass a callable that accepts a single argument:
    • the data sequence / iterable passed in. This allows the container to set properties based on aggregations of the input data
  • There's no support for easily merging css class properties together, for example starting from a set of base properties and then adding additional properties based on a condition. You'll have to explicitly set the full class strings.

Here is what the output would be:

<main class="container">
  <h1>Product Catalog</h1>
  <div class="container mx-auto px-4 sm:px-6 lg:px-8 my-8">
    <h1 class="text-3xl font-bold mb-6">Our Products</h1>
    <div class="grid base:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 bg-green-50 p-4 rounded">
      <div class="bg-white shadow-md rounded-lg p-4">
        <h2 class="text-xl font-semibold">Laptop</h2>
        <p class="text-gray-600">$999</p>
        <p class="text-sm">In stock: 50</p>
      </div>
      <div class="bg-white shadow-md rounded-lg p-4 border-t-4 border-green-500">
        <h2 class="text-xl font-semibold">Smartphone</h2>
        <p class="text-gray-600">$699</p>
        <p class="text-sm">In stock: 100</p>
      </div>
      <div class="bg-white shadow-md rounded-lg p-4">
        <h2 class="text-xl font-semibold">Tablet</h2>
        <p class="text-gray-600">$399</p>
        <p class="text-sm">In stock: 30</p>
      </div>
      <div class="bg-white shadow-md rounded-lg p-4 border-t-4 border-green-500">
        <h2 class="text-xl font-semibold">Smartwatch</h2>
        <p class="text-gray-600">$199</p>
        <p class="text-sm">In stock: 75</p>
      </div>
    </div>
  </div>
</main>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment