Skip to content
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ a([tab]
* ⚡ Live Reload server for rapid development
Run your Python webserver (i.e. Flask, FastAPI, anything!) with live-reload superpowers powered by [livereload-js](https://www.npmjs.com/package/livereload-js). See browser updates in real-time!

Note: This feature requires optional dependencies. `pip install html-compose[live-reload]` or `pip install html-compose[full]`
Note: This feature requires optional dependencies. `pip install html-compose[live-reload]` or `pip install html-compose[full]`. The feature also fetches livereload-js from a CDN.

`livereload.py`
```python
Expand Down
6 changes: 4 additions & 2 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# 0.9.1
* Class and style params now correctly type hint str | list | dict.
# 0.10.0
* onclick/onaction attrs are now generated as kwargs for elements.
* Improve typing: class and style params now correctly type hint, as do other
attributes.

# 0.9.0
This is primarly a documentation/automation patch.
Expand Down
117 changes: 93 additions & 24 deletions doc/ideas/04_attrs.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
# Attributes
There are multiple ways to define attributes for an html element i.e.
The goal of the library is to enable the user to make design choices
about how they generate their HTML.

Therefore, the library proposes several ways to define attributes for an
HTML element.

The theory is that creating the least resistance to a successful pattern makes way
for its adoption.

```python
from html_compose import div
is_error = False

# keyword arg syntax (preferred)
# note that attrs that conflict with Python keywords
# note that attributes that conflict with Python keywords
# have an underscore_ appended. This was chosen so autocomplete still works.

div(class_="flex")
div(class_=["flex", "items-center"])
div(class_={
"flex": True,
"error": is_error == True
"error": is_error
})
div(class_=div.hint.class_("flex"))
div([div.hint.class_("flex")])

div(class_=div._.class_("flex"))
# div._ is a syntax shorthand for div.attrhint
div([div._.class_("flex")])
# div._ is a syntax shorthand for div.hint

# attrs dict syntax
div(attrs={"class": "flex"})
Expand All @@ -39,18 +46,19 @@ div(attrs=[div.hint.class_({
"error": is_error == True
})])


# Combining the two:
div(attrs=[div.hint.class_("flex")], tabindex=1)
```

## BaseAttribute
All attributes inherit BaseAttribute which defines a key and a value and resolves at render time.
All attributes inherit from `BaseAttribute`, which defines a key and a value and resolves at render time.

The class attribute and style attribute have rules to split by their correct delimeter.
The `class` and `style` attributes have special rules to join values with their correct delimiter.

```python
from html_compose import div
is_red = False
# dict of dicts str:bool
# dict of str:bool - if the value is true, the key is rendered as part of the class list
# truthy = rendered
# falsey = ignored

Expand All @@ -69,20 +77,26 @@ div._.class_("red")
# "red"
```

## attrs= parameter syntax
## `attrs=` parameter syntax

In the constructor for any element you can specify the attrs paramter.
In the constructor for any element, you can specify the `attrs` parameter.

It can be either a list or a dictionary.

### Positional argument caveat
### Implicit/positional `attrs` argument
Although the documentation is explicit in using the `attrs` kwarg, `attrs` is
actually the first argument of the constructor and can be excluded i.e.
actually the first argument of the constructor and its name can be omitted.
```python
div({"class": "flex"})
```
Instead of
```python
div(attrs={"class": "flex"})
```

### list
It supports a list of BaseAttributes but also you can mix a dictionary in as well.

```python
from html_compose.elements import a, div

Expand All @@ -92,8 +106,15 @@ a(attrs=[
a.hint.href("https://google.com"),
a.hint.tabindex(1),
a.hint.class_(["flex", "flex-col"])
]
)
])


a(attrs=[
{"@custom": "value"},
a.hint.href("https://google.com"),
a.hint.tabindex(1),
a.hint.class_(["flex", "flex-col"])
])

# string / list of string is explicitly NOT supported
# it requires disabling sanitization and is therefore quietly prone to XSS
Expand All @@ -104,8 +125,8 @@ div(attrs=['class="red"']) # ❌

```python
a(attrs={
"href": a.hint.href("https://google.com")
"tabindex': 1
"href": "https://google.com"),
"tabindex": 1
})

div(attrs={
Expand All @@ -120,24 +141,31 @@ div(attrs={
```

## Keyword argument extension
An extention of the attr syntax was generated for all built-in HTML elements. It would be time-consuming to do this for custom element types, but code generation leans well into this case.
An extension of the `attrs` syntax was generated for all built-in HTML elements. It would be time-consuming to do this for custom element types, but code generation lends itself well to this case.

Traditionally, kwargs would be too non-descript to provide helpful editor hints.

To aid with fluent document writing, each element was generated with its attributes and a paired docstring
To aid with fluent document writing, each element was generated with its attributes as parameters and a paired docstring.
i.e.
`:param href: Address of the hyperlink`

```python
a(href="https://google.com", tabindex=1)
```

Under the hood, it's all translated to the BaseAttribute class and the value is
Under the hood, it's all translated to the `BaseAttribute` class, and the value is
escaped before rendering.

# Breakdown

There's a number of options for declaring an attribute value defined below. These are to aid in very common operations such as building a `class` string.
There are a number of options for declaring an attribute value, which are shown above.
The basic idea is


`attrs`, the first parameter, is a key,value attribute set, or a list
containing one or more of
* a `dict` that translates `key="{safe_text(value)}"`, as if attrs were a dict
* `BaseAttribute` which may be from a hint class for an element or library

## Attribute definitions
Care was put into generating attribute definitions for each class.
Expand All @@ -146,8 +174,49 @@ Anything found in the HTML specification document is available in an element's c

i.e. the `img` class has a cousin class `ImgAttrs`.

We can access the definition of an attribute for that element via `ImgAttrs.$attr` i.e. `ImgAttrs.alt(value="demo")`. Each element, like `img`, has a child class which is an inheritor of its sibling attrs class - `img.hint` inherits `ImgAttrs` so you can access the same definition via `img.hint.alt("...")`.
We can access the definition of an attribute for that element via `ImgAttrs.$attr` i.e. `ImgAttrs.alt(value="demo")`. Each element, like `img`, has a child class `hint` which inherits from its sibling attrs class (`ImgAttrs`), so you can access the same definition via `img.hint.alt("...")`.

Additionally, there's a `_` shorthand for `img.hint`. `img._` is just a reference to `img.hint`.

The purpose of this system is to provide full type hints.
The purpose of this system is to provide full type hints.

It also serves as an example for extensions to add attribute sets under their
own namespaces/classes.

## Extensions

Quality extensions are recommended to work with your chosen tech stack.
The idea is to give you guardrails and documentation directly in your IDE.

```python
from html_compose.base_attribute import BaseAttribute
from html_compose import button
class htmx:
'''
Attributes for the HTMX framework.
'''

@staticmethod
def hx_get(value: str) -> BaseAttribute:
'''
htmx attribute: hx-get
The hx-get attribute will cause an element to issue a
GET to the specified URL and swap the HTML into the DOM
using a swap strategy

:param value: URI to GET when the element is activated
:return: An hx-get attribute to be added to your element
'''

return BaseAttribute("hx-get", value)

```

Where we can write

```python
button(
[htmx.hx_get("/api/data")],
class_="btn primary"
)["Click me!"]
```
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "html-compose"
version = "0.9.1"
version = "0.10.0"
description = "Composable HTML generation in python"
authors = [
{ name = "jealouscloud", email = "github@noaha.org" }
Expand Down
7 changes: 4 additions & 3 deletions src/html_compose/attributes/a_attrs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import BaseAttribute
from ..base_types import Resolvable, StrLike


class AnchorAttrs:
Expand All @@ -8,7 +9,7 @@ class AnchorAttrs:
"""

@staticmethod
def download(value: str) -> BaseAttribute:
def download(value: StrLike) -> BaseAttribute:
"""
"a" attribute: download
Whether to download the resource instead of navigating to it, and its filename if so
Expand Down Expand Up @@ -44,7 +45,7 @@ def hreflang(value) -> BaseAttribute:
return BaseAttribute("hreflang", value)

@staticmethod
def ping(value: list) -> BaseAttribute:
def ping(value: Resolvable) -> BaseAttribute:
"""
"a" attribute: ping
URLs to ping
Expand All @@ -68,7 +69,7 @@ def referrerpolicy(value) -> BaseAttribute:
return BaseAttribute("referrerpolicy", value)

@staticmethod
def rel(value: list) -> BaseAttribute:
def rel(value: Resolvable) -> BaseAttribute:
"""
"a" attribute: rel
Relationship between the location in the document containing the hyperlink and the destination resource
Expand Down
3 changes: 2 additions & 1 deletion src/html_compose/attributes/abbr_attrs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import BaseAttribute
from ..base_types import StrLike


class AbbrAttrs:
Expand All @@ -8,7 +9,7 @@ class AbbrAttrs:
"""

@staticmethod
def title(value: str) -> BaseAttribute:
def title(value: StrLike) -> BaseAttribute:
"""
"abbr" attribute: title
Full term or expansion of abbreviation
Expand Down
9 changes: 5 additions & 4 deletions src/html_compose/attributes/area_attrs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from . import BaseAttribute
from typing import Literal
from ..base_types import Resolvable, StrLike


class AreaAttrs:
Expand All @@ -9,7 +10,7 @@ class AreaAttrs:
"""

@staticmethod
def alt(value: str) -> BaseAttribute:
def alt(value: StrLike) -> BaseAttribute:
"""
"area" attribute: alt
Replacement text for use when images are not available
Expand All @@ -33,7 +34,7 @@ def coords(value) -> BaseAttribute:
return BaseAttribute("coords", value)

@staticmethod
def download(value: str) -> BaseAttribute:
def download(value: StrLike) -> BaseAttribute:
"""
"area" attribute: download
Whether to download the resource instead of navigating to it, and its filename if so
Expand All @@ -57,7 +58,7 @@ def href(value) -> BaseAttribute:
return BaseAttribute("href", value)

@staticmethod
def ping(value: list) -> BaseAttribute:
def ping(value: Resolvable) -> BaseAttribute:
"""
"area" attribute: ping
URLs to ping
Expand All @@ -81,7 +82,7 @@ def referrerpolicy(value) -> BaseAttribute:
return BaseAttribute("referrerpolicy", value)

@staticmethod
def rel(value: list) -> BaseAttribute:
def rel(value: Resolvable) -> BaseAttribute:
"""
"area" attribute: rel
Relationship between the location in the document containing the hyperlink and the destination resource
Expand Down
5 changes: 3 additions & 2 deletions src/html_compose/attributes/button_attrs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from . import BaseAttribute
from typing import Literal
from ..base_types import StrLike


class ButtonAttrs:
Expand Down Expand Up @@ -99,7 +100,7 @@ def formtarget(value) -> BaseAttribute:
return BaseAttribute("formtarget", value)

@staticmethod
def name(value: str) -> BaseAttribute:
def name(value: StrLike) -> BaseAttribute:
"""
"button" attribute: name
Name of the element to use for form submission and in the form.elements API
Expand Down Expand Up @@ -149,7 +150,7 @@ def type(value: Literal["submit", "reset", "button"]) -> BaseAttribute:
return BaseAttribute("type", value)

@staticmethod
def value(value: str) -> BaseAttribute:
def value(value: StrLike) -> BaseAttribute:
"""
"button" attribute: value
Value to be used for form submission
Expand Down
3 changes: 2 additions & 1 deletion src/html_compose/attributes/data_attrs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import BaseAttribute
from ..base_types import StrLike


class DataAttrs:
Expand All @@ -8,7 +9,7 @@ class DataAttrs:
"""

@staticmethod
def value(value: str) -> BaseAttribute:
def value(value: StrLike) -> BaseAttribute:
"""
"data" attribute: value
Machine-readable value
Expand Down
3 changes: 2 additions & 1 deletion src/html_compose/attributes/details_attrs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import BaseAttribute
from ..base_types import StrLike


class DetailsAttrs:
Expand All @@ -8,7 +9,7 @@ class DetailsAttrs:
"""

@staticmethod
def name(value: str) -> BaseAttribute:
def name(value: StrLike) -> BaseAttribute:
"""
"details" attribute: name
Name of group of mutually-exclusive details elements
Expand Down
3 changes: 2 additions & 1 deletion src/html_compose/attributes/dfn_attrs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import BaseAttribute
from ..base_types import StrLike


class DfnAttrs:
Expand All @@ -8,7 +9,7 @@ class DfnAttrs:
"""

@staticmethod
def title(value: str) -> BaseAttribute:
def title(value: StrLike) -> BaseAttribute:
"""
"dfn" attribute: title
Full term or expansion of abbreviation
Expand Down
Loading