Skip to content

Utils

These are an assortment of tools in the same vein of django-extensions:

Theme helpers

Instead of placing the javascript file in its proper place within the base, I opt to place it at the top before the html tag even loads. This allows me to insert a themeHTML() command to get the user preference for theme and place it in localStorage, or use an existing theme, if localStorage is already populated.

doSelect(id-of-container-node)
1
2
3
4
5
6
7
<!DOCTYPE html>
<script src="{% static 'doTheme.js' %}"></script>
<script>themeHTML()</script> {# before <html>: prevent flicker #}
<html lang="en"> {# class populated / changed  #}
  <button onclick=toggleTheme()>Theme</button> {# will toggle #}
  ...
</html>

So even before the <html> loads the following script gets executed:

base.html
<script>
  // document.documentElement = <html> tag
  // if local storage set, use it; check user pref; if still unset: light mode
  if (localStorage.getItem("theme") === "dark") {
    document.documentElement.classList.add("dark");
  } else if (localStorage.getItem("theme") === "light") {
    document.documentElement.classList.add("light");
  } else if (window.matchMedia("(prefers-color-scheme: dark)")) {
    document.documentElement.classList.add("dark");
    localStorage.setItem("theme", "dark");
  } else {
    document.documentElement.classList.add("light");
    localStorage.setItem("theme", "light");
  }
</script>

themeHTML()

Will populate <html> with class=light or class=dark depending on localStorage and/or media preference.

toggleTheme()

Will toggle the existing <html class=?> with light or dark.

{% toggle_icons %}

Usually placed in body
1
2
3
4
5
<script src="{% static 'doTheme.js' %}"></script>
<html>
  {% toggle_icons icon1_css='test-light' icon2_css='test-dark hello-darkness' btn_kls='btn'  %}
  ...
</html>
Usually placed in body
<script src="{% static 'doTheme.js' %}"></script>
<html>
  <button onclick="toggleTheme()" type="button" class="btn" aria-label="Toggle dark mode">
    <span class="icon1_svg">
      <span class="sr-only">Light mode</span>
      <svg class="test-light" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" stroke-linecap="round" stroke-linejoin="round"></path></svg>
    </span>
    <span class="icon2_svg">
      <span class="sr-only">Dark mode</span>
      <svg class="test-dark hello-darkness" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" stroke-linecap="round" stroke-linejoin="round"></path></svg>
    </span>
  </button>
  ...
</html>

Implements two sibling icon fragments, surrounded by a <button> that, when clicked, calls toggleTheme().

Toggle icons. Returns an HTML fragment implementing two {% icon %}'s surrounded by a single button which, when clicked, implements the toggleTheme() functionality from doTheme.js

Parameters:

Name Type Description Default
btn_kls str | None

description. Defaults to "theme-toggler".

'theme-toggler'
aria_label str | None

description. Defaults to "Toggle mode".

'Toggle mode'

Returns:

Name Type Description
SafeText SafeText

HTML fragment button

Source code in django_fragments/templatetags/fragments.py
Python
@register.simple_tag
def toggle_icons(
    btn_kls: str | None = "theme-toggler",
    aria_label: str | None = "Toggle mode",
    **kwargs,
) -> SafeText:
    """Toggle icons. Returns an HTML fragment implementing two `{% icon %}`'s surrounded by a single button which,
    when clicked, implements the toggleTheme() functionality from `doTheme.js`

    Args:
        btn_kls (str | None, optional): _description_. Defaults to "theme-toggler".
        aria_label (str | None, optional): _description_. Defaults to "Toggle mode".

    Returns:
        SafeText: HTML fragment button
    """
    return mark_safe(
        Template("""
            {% load fragments %}
            {% whitespaceless %}
            <button onclick=toggleTheme() type="button" class="{{ btn_kls }}" aria-label="{{aria_label}}">
                {{ icon1 }}
                {{ icon2 }}
            </button>
            {% endwhitespaceless %}
            """)  # noqa: E501
        .render(
            context=Context(
                {
                    "btn_kls": btn_kls,
                    "aria_label": aria_label,
                    "icon1": icon(**filter_attrs(key="icon1", d=kwargs, unprefix=True)),
                    "icon2": icon(**filter_attrs(key="icon2", d=kwargs, unprefix=True)),
                },
            )
        )
        .strip()
    )

htmx

These are just convenience fragments for oft-repeated idioms of + htmx. For a more comprehensive library, see django-htmx.

{% htmx_csrf %}

Usually placed in body
1
2
3
4
5
6
  <html>
    <head>
      ...
    </head>
    <body {% htmx_csrf %} class="container">
  </html>
Usually placed in body
1
2
3
4
5
6
7
  <html>
    <head>
      ...
    </head>
    <body hx-headers='{"X-CSRFToken": "the-token-itself"}' class="container">

  </html>

Just a tiny fragment to signify htmx-compatible requests that will include the csrf_token.

Source code in django_fragments/templatetags/helpers.py
Python
@register.simple_tag(takes_context=True)
def htmx_csrf(context) -> SafeText:
    """Just a tiny fragment to signify htmx-compatible requests
    that will include the csrf_token."""
    return mark_safe(
        Template("""hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'""").render(
            context=Context(context)
        )
    )

is_htmx

Checks if a request contains the HTTP_HX_REQUEST Header:

Usable in a view
@require_POST
def send_msg(request: HttpRequest):
    if is_htmx(request):
        ...

Determines whether or not the request should be handled differently because of the presence of the HTTP_HX_REQUEST header.

Parameters:

Name Type Description Default
request HttpRequest

The Django request object received from the view

required

Returns:

Name Type Description
bool bool

Whether or not HTTP_HX_REQUEST exists.

Source code in django_fragments/utils.py
Python
def is_htmx(request: HttpRequest) -> bool:
    """Determines whether or not the request should be handled differently
    because of the presence of the `HTTP_HX_REQUEST` header.

    Args:
        request (HttpRequest): The Django request object received from the view

    Returns:
        bool: Whether or not `HTTP_HX_REQUEST` exists.
    """
    return True if request.META.get("HTTP_HX_REQUEST") else False

Whitespaceless

Remove whitespace from template tag via a Stackover flow answer from one Will Gordon. See answer:

Invocation via Django Template Language
  {% whitespaceless %}
    <p class="  test
                test2
                test3  ">
        <a href="foo/">Foo</a>
    </p>
  {% endwhitespaceless %}
Output HTML after the Template is populated with the Context.
<p class="test test2 test3"><a href="foo/">Foo</a></p>

Filter Attributes

Filter k, v from d based on keys prefixed with <key>_. Based on this result, rename or replace the key, depending on the unprefix flag.

This enables a shortcut for gathering all <key>-* attributes found in the dict d and parse them properly before inserting them into html tags.

Examples:

Python Console Session
>>> res = filter_attrs(key="aria", d={"aria_hidden":"true"})
>>> res['aria-hidden'] == "true"
True
>>> res_hx = filter_attrs(key="hx", d={"hx_get":"https://test.html",  "hx_target":"body"})
>>> res_hx['hx-get'] == "https://test.html"
True
>>> res_hx['hx-target'] == "body"
True
>>> res_dt = filter_attrs(key="data", d={"data_site_good":"https://test.html"})
>>> res_dt['data-site-good'] == "https://test.html"
True
>>> parent_res = filter_attrs(key="parent", d={"non-a-parent": "test", "parent_class":"flex items-center", "parent_title": "I should be centered"}, unprefix=True)
>>> "parent_class" in parent_res
False
>>> "class" in parent_res
True
>>> "title" in parent_res
True
>>> "non-a-parent" in parent_res
False
>>> pre_res = filter_attrs(key="pre", d={"pre_class":"sr-only"}, unprefix=True)
>>> "class" in pre_res
True
>>> res_btn = filter_attrs(key="btn", d={"btn_name":"i-am-button", "btn_id": "btn-1"}, unprefix=True)
>>> res_btn["name"] == "i-am-button"
True
>>> res_btn["id"] == "btn-1"
True
>>> res_a= filter_attrs(key="a", d={"a_href":"#", "a_target": "_self"}, unprefix=True)
>>> res_a["href"] == "#"
True
>>> res_a["target"] == "_self"
True

Parameters:

Name Type Description Default
d dict

Values from a template tag.

required

Returns:

Type Description
dict[str, str]

dict[str, str]: dict to be used for a html tag's aria-* attributes.

Source code in django_fragments/templatetags/utils/filter_attrs.py
Python
def filter_attrs(key: str, d: dict, unprefix: bool = False) -> dict[str, str]:
    """Filter `k`, `v` from `d` based on keys prefixed with `<key>_`. Based on this result, rename or replace the key,
    depending on the `unprefix` flag.

    This enables a shortcut for gathering all `<key>-`* attributes found in the dict `d` and parse them properly
    before inserting them into html tags.

    Examples:
        >>> res = filter_attrs(key="aria", d={"aria_hidden":"true"})
        >>> res['aria-hidden'] == "true"
        True
        >>> res_hx = filter_attrs(key="hx", d={"hx_get":"https://test.html",  "hx_target":"body"})
        >>> res_hx['hx-get'] == "https://test.html"
        True
        >>> res_hx['hx-target'] == "body"
        True
        >>> res_dt = filter_attrs(key="data", d={"data_site_good":"https://test.html"})
        >>> res_dt['data-site-good'] == "https://test.html"
        True
        >>> parent_res = filter_attrs(key="parent", d={"non-a-parent": "test", "parent_class":"flex items-center", "parent_title": "I should be centered"}, unprefix=True)
        >>> "parent_class" in parent_res
        False
        >>> "class" in parent_res
        True
        >>> "title" in parent_res
        True
        >>> "non-a-parent" in parent_res
        False
        >>> pre_res = filter_attrs(key="pre", d={"pre_class":"sr-only"}, unprefix=True)
        >>> "class" in pre_res
        True
        >>> res_btn = filter_attrs(key="btn", d={"btn_name":"i-am-button", "btn_id": "btn-1"}, unprefix=True)
        >>> res_btn["name"] == "i-am-button"
        True
        >>> res_btn["id"] == "btn-1"
        True
        >>> res_a= filter_attrs(key="a", d={"a_href":"#", "a_target": "_self"}, unprefix=True)
        >>> res_a["href"] == "#"
        True
        >>> res_a["target"] == "_self"
        True

    Args:
        d (dict): Values from a template tag.

    Returns:
        dict[str, str]: dict to be used for a html tag's aria-* attributes.
    """  # noqa: E501
    return {
        (k.removeprefix(f"{key}_") if unprefix else k.replace("_", "-")): v
        for k, v in d.items()
        if k.startswith(f"{key}_")
    }

Wrap Icon

raw x_mark_mini from heroicons copy/pasted
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
  <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
</svg>
Output HTML after the Template is populated with the Context.
<span class="sr-only">Close menu</span>
<svg aria-hidden="true" class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="1.5" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
    <path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>

Supplement html fragment of <svg> icon with css classes and attributes, include parent/sibling <span>s when parameters dictate.

The following kwargs: pre_, post_, and parent_ args are respected.

So pre_text + pre_class will add:

HTML
 <!-- pre_ implies before the icon, with special rule for pre_text -->
<span class='the-value-of-pre_class'>the-value-of-pre_text</span><svg></svg>

post_text + post_class will add:

HTML
<!-- post_ implies after the icon, with special rule for post_text -->
<svg></svg><span class='the-value-of-post_class'>the-value-of-post_text</span>

parent_class + parent_title will add:

HTML
 <!-- parent_ implies a wrapper over the icon,
 'parent_text' will not have same effect.
 -->
<span class='the-value-of-parent_class' title='the-value-of-parent_title'><svg></svg></span>

Examples:

Python Console Session
>>> markup = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"><path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" /></svg>'
>>> res = wrap_svg(html_markup=markup, pre_text="Close menu", pre_class="sr-only", aria_hidden="true")
>>> len(res.contents) == 2
True
>>> res.contents[0]
<span class="sr-only">Close menu</span>
>>> res.contents[1].attrs == {'xmlns': 'http://www.w3.org/2000/svg', 'viewbox': '0 0 20 20', 'fill': 'currentColor', 'class': ['w-5', 'h-5'], 'aria-hidden': 'true'}
True
>>> parented = wrap_svg(html_markup=markup, parent_tag="button", pre_text="Close menu", pre_class="sr-only", aria_hidden="true")
>>> elements = list(parented.children)
>>> elements[0].name == 'button'
True
>>> list(elements[0].children)
[<span class="sr-only">Close menu</span>, <svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewbox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"></path></svg>]

Parameters:

Name Type Description Default
html_markup str

The template that contains the <svg> tag converted into its html string format.

required
css str

Previously defined CSS to add to the <svg> icon. Defaults to None.

None

Returns:

Name Type Description
SafeString BeautifulSoup

Small HTML fragment visually representing an svg icon.

Source code in django_fragments/templatetags/utils/wrap_svg.py
Python
def wrap_svg(html_markup: str, css: str | None = None, **kwargs) -> BeautifulSoup:
    """Supplement html fragment of `<svg>` icon with css classes and attributes, include parent/sibling `<span>`s when parameters dictate.

    The following kwargs: `pre_`, `post_`, and `parent_` args are respected.

    So `pre_text` + `pre_class` will add:

    ```html
     <!-- pre_ implies before the icon, with special rule for pre_text -->
    <span class='the-value-of-pre_class'>the-value-of-pre_text</span><svg></svg>
    ```

    `post_text` + `post_class` will add:

    ```html
    <!-- post_ implies after the icon, with special rule for post_text -->
    <svg></svg><span class='the-value-of-post_class'>the-value-of-post_text</span>
    ```

    `parent_class`  + `parent_title` will add:

    ```html
     <!-- parent_ implies a wrapper over the icon,
     'parent_text' will not have same effect.
     -->
    <span class='the-value-of-parent_class' title='the-value-of-parent_title'><svg></svg></span>
    ```

    Examples:
        >>> markup = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"><path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" /></svg>'
        >>> res = wrap_svg(html_markup=markup, pre_text="Close menu", pre_class="sr-only", aria_hidden="true")
        >>> len(res.contents) == 2
        True
        >>> res.contents[0]
        <span class="sr-only">Close menu</span>
        >>> res.contents[1].attrs == {'xmlns': 'http://www.w3.org/2000/svg', 'viewbox': '0 0 20 20', 'fill': 'currentColor', 'class': ['w-5', 'h-5'], 'aria-hidden': 'true'}
        True
        >>> parented = wrap_svg(html_markup=markup, parent_tag="button", pre_text="Close menu", pre_class="sr-only", aria_hidden="true")
        >>> elements = list(parented.children)
        >>> elements[0].name == 'button'
        True
        >>> list(elements[0].children)
        [<span class="sr-only">Close menu</span>, <svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewbox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"></path></svg>]

    Args:
        html_markup (str): The template that contains the `<svg>` tag converted into its html string format.
        css (str, optional): Previously defined CSS to add to the `<svg>` icon. Defaults to None.

    Returns:
        SafeString: Small HTML fragment visually representing an svg icon.
    """  # noqa: E501

    soup = BeautifulSoup(html_markup, "html.parser")
    icon = soup("svg")[0]
    if css:
        icon["class"] = css
    if aria_attrs := filter_attrs("aria", kwargs):
        for k, v in aria_attrs.items():
            icon[k] = v

    parent_tag = None
    if tagname := kwargs.pop("parent_tag", None):
        if tagname in ["button", "a", "span", "div"]:
            parent_tag = soup.new_tag(tagname)

    if parent_attrs := filter_attrs("parent", kwargs, unprefix=True):
        if not parent_tag:
            parent_tag = soup.new_tag("span")
        for k, v in parent_attrs.items():
            parent_tag[k] = v
    if parent_tag:
        icon.wrap(parent_tag)

    if left := kwargs.pop("pre_text", None):  # <span>, left of <svg>
        pre_span = soup.new_tag("span")
        pre_span.string = left
        if pre_attrs := filter_attrs("pre", kwargs, unprefix=True):
            for k, v in pre_attrs.items():
                pre_span[k] = v
        icon.insert_before(pre_span)
    elif right := kwargs.pop("post_text", None):  # <span>, right of <svg>
        post_span = soup.new_tag("span")
        post_span.string = right
        if post_attrs := filter_attrs("post", kwargs, unprefix=True):
            for k, v in post_attrs.items():
                post_span[k] = v
        icon.insert_after(post_span)
    return soup