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.
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)
Repeater(iterable, ItemFT, ContainerFT)
Repeater(["a", "b", "c"])
<div>
<div>a</div>
<div>b</div>
<div>c</div>
</div>
Repeater(["inside", "a", "paragraph"], P)
<div>
<p>inside</p>
<p>a</p>
<p>paragraph</p>
</div>
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)
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>
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>
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>