.. currentmodule:: minihtml
.. _components:
Components
==========
A component is a re-usable snippet of HTML.
Defining components
-------------------
You create components with the :deco:`component` decorator on a function.
The function always receives a :class:`Slots` objects as its first positional
argument, but can have other arguments as well.
Here is a simple component that returns a div with a configurable "name" attribute:
>>> from minihtml import component
>>> from minihtml.tags import div
>>>
>>> @component()
... def my_component(slots, name):
... return div["my-component"](name=name)
You create an instance of the component by calling it. Like elements, a
component called inside an element context will add content to the parent
element:
>>> comp = my_component("my-name")
>>> print(comp)
>>> with div["container"] as elem:
... my_component("my-name")
<...>
>>> print(elem)
Component slots
---------------
In addition to arguments, components can have *slots* that can be filled by the
caller with arbitrary content. So, instead of passing elements as arguments to
a component, you would use a slot. If you do not configure slots explicitly,
your component has a *default slot*.
To refer to the contents of the slot within the component definition, use the
:meth:`Slots.slot` method inside an element context. Here is a component that
wraps it's content in a ``div`` with ``class="greeting"``:
>>> @component()
... def my_greeting(slots):
... with div["greeting"] as elem:
... slots.slot()
... return elem
To fill the slot of a component, use it as a context manager. Elements created
within the context are added to the default slot.
>>> from minihtml.tags import p
>>> with my_greeting() as comp:
... p("Hello, world!")
... p("More content.")
<...>
>>> print(comp)
Hello, world!
More content.
Named slots
-----------
A component can also be declared with one or more *named slots*:
>>> @component(slots=("header", "footer"))
... def two_slots(slots):
... with div["two-slots"] as elem:
... with div["header"]:
... slots.slot("header")
... with div["footer"]:
... slots.slot("footer")
... return elem
To interact with a named slot, pass the slot name to :meth:`Slots.slot`, as
shown above. To fill a slot by name, use the :meth:`Component.slot`
method of the component returned by the context manager:
>>> with two_slots() as comp:
... with comp.slot("header"):
... p("header content")
... with comp.slot("footer"):
... p("footer content")
<...>
>>> print(comp)
Specifying a default slot
-------------------------
If your component has multiple slots, it's a good idea to define one to be the
*default slot*:
>>> from minihtml.tags import h4
>>>
>>> @component(slots=("title", "content"), default="content")
... def my_card(slots):
... with div["my-card"] as elem:
... with h4:
... slots.slot("title")
... with div["content"]:
... slots.slot("content")
... return elem
The default slot can always be referred to (both inside and outside of the
component definition) via it's name (as shown above), or as the unnamed default
slot:
>>> from minihtml import text
>>> with my_card() as comp:
... with comp.slot("title"):
... text("card title")
... p("card content") # this goes into the default slot ("content")
<...>
>>> print(comp)
Checking if a slot is filled
----------------------------
Inside a component, you can find out whether or not a slot has been filled
using :meth:`Slots.is_filled`:
>>> from minihtml.tags import span, img
>>>
>>> @component(slots=("icon", "message"), default="message")
... def my_message(slots):
... with div["my-message"] as elem:
... if slots.is_filled("icon"):
... with span["icon"]:
... slots.slot("icon")
... slots.slot("message")
... return elem
As a result, the ``span`` element will only be present if the ``image`` slot
has been filled:
>>> with my_message() as comp:
... p("the message")
<...>
>>> print(comp)
>>> with my_message() as comp:
... with comp.slot("icon"):
... img(src="warning.png", alt="warning icon")
... p("the message")
<...>
>>> print(comp)
the message
Default content for slots
-------------------------
You can provide default content that will be inserted if a slot has not been
filled. To do hat, use :meth:`Slots.slot` as a context manager. Elements
created within the context will be used as the default content if the slot was
not filled. If the slot *was* filled, the elements will be ignored and the slot
content inserted in their place.
>>> @component(slots=("title", "icon"), default="title")
... def my_warning(slots):
... with div["my-warning"] as elem:
... with slots.slot("icon"):
... img(src="warning.png", alt="warning icon")
... with slots.slot("title"):
... h4("Warning")
... return elem
>>> comp = my_warning()
>>> print(comp)
Warning
>>> with my_warning() as comp:
... with comp.slot("icon"):
... img(src="error.png", alt="error icon")
... h4("Error")
<...>
>>> print(comp)
Error
.. _component_resources:
Component styles and scripts
----------------------------
A component can define style and/or script resources that should be included in
the page where a component is used. This only has an effect when the component
is used inside a :ref:`template `.
To associate a style with a component, pass an element or list of elements to
the ``style`` parameter of the :deco:`component` decorator. Typically, this
will be one or more ``style`` or ``link`` elements. For scripts, use the
``script`` parameter.
>>> from minihtml.tags import link, script, style
>>>
>>> @component(
... style=[
... style(".my-component { background #ccc }"),
... link(rel="stylesheet", href="/path/to/stylesheet.css"),
... ],
... script=script("alert('hello, world!);")
... )
... def my_component(slots):
... return div["my-component"]
See :ref:`collecting` for details on how to use component styles and scripts in
a template.