Forward HTML attributes
A component does not copy arbitrary inputs onto its first HTML element. That would be ambiguous for a component with several roots, and it could place an accessibility or browser attribute on the wrong element.
Give a reusable component an explicit attribute mapping, then choose the element that receives it.
Accept and apply an attribute mapping
This button combines its own required attributes with values supplied by the template using it:
from dataclasses import field
from typing import Any
from citry import Component, merge_attrs
class ActionButton(Component):
class Kwargs:
label: str
attrs: dict[str, Any] = field(default_factory=dict)
def template_data(
self,
kwargs: Kwargs,
slots,
) -> dict[str, object]:
return {
"button_attrs": merge_attrs(
{"class": "action-button", "type": "button"},
kwargs.attrs,
),
"label": kwargs.label,
}
template = """
<button c-bind="button_attrs">
{{ label }}
</button>
"""
Pass the mapping as a Python expression:
<c-ActionButton
label="Save"
c-attrs="{
'aria-label': accessible_name,
'class': {'action-button--quiet': quiet},
'disabled': unavailable,
}"
/>
c-bind applies the final mapping to the <button>. The component could apply separate mappings to separate roots, or place the public attributes on a nested input instead.
The order in merge_attrs() is deliberate. Values supplied by the template come last, so they may replace type. The class values combine, preserving the component's own class. Reverse the two mappings when an attribute must stay under the component's control:
button_attrs = merge_attrs(
kwargs.attrs,
{"class": "action-button", "type": "button"},
)
Here the caller can add classes, but cannot change type.
Merge mappings from left to right
merge_attrs() keeps the last ordinary value for each name. class and style are different: every contribution is collected and normalized.
from citry import merge_attrs
attrs = merge_attrs(
{"class": "button", "id": "old"},
{"class": {"is-active": True}, "id": "save"},
)
assert attrs == {
"class": "button is-active",
"id": "save",
}
A name keeps the position where it first appeared, even when a later mapping replaces its value. This makes the resulting attribute order predictable.
Build class and style values
normalize_class() accepts a string, a mapping, or a nested list or tuple of those forms. A truthy mapping value keeps its name. A later falsy value removes a class that appeared earlier:
from citry import normalize_class
classes = normalize_class([
"button button-large",
{"is-active": True, "button-large": False},
])
assert classes == "button is-active"
normalize_style() accepts CSS text, a mapping, or a nested sequence of either. Later values replace earlier properties. None leaves an earlier value in place, while False removes the property:
from citry import normalize_style
styles = normalize_style([
"color: red; width: 10rem",
{"color": "green", "width": False},
])
assert styles == "color: green;"
Passing another kind of value, such as an integer, raises TypeError from the matching normalizer.
parse_string_style() turns inline CSS into a property mapping. It removes CSS comments, keeps semicolons inside functions such as url(...), and ignores a declaration without a colon.
Turn a mapping into HTML
format_attrs() formats a mapping as an escaped HTML attribute string:
from citry import format_attrs
attrs = format_attrs({
"class": ["button", {"is-active": True}],
"data-id": 42,
"disabled": True,
"hidden": False,
})
assert attrs == (
'class="button is-active" data-id="42" disabled'
)
The formatting rules are:
Trueproduces a bare attribute;FalseandNoneleave the attribute out;- an empty
classorstyleis left out; - names and values are HTML-escaped;
- a value with
__html__()is treated as trusted HTML.
Attribute names must be strings. A non-string key raises TypeError. An empty name, whitespace, =, /, >, <, or the template-comment opener {# in a name raises ValueError. The same validation applies when c-bind produces an HTML attribute at render time.
Treat __html__() values as an escape hatch. Only pass one when the producing code is trusted and is responsible for its own escaping.
Keep browser attributes explicit too
Alpine directives and browser event handlers are ordinary HTML attributes when a component deliberately spreads them onto an HTML element. They do not fall through a <c-Component> tag automatically.
Read Client interactivity for the component-boundary rules, and Attributes for static, dynamic, and spread values in templates.