Writing HTML

Writing HTML with PH7 might be a bit weird at first since it does not follow the open/close bracket format. But once you get used to it, it can be ridiculously fast with autocomplete and type checking.

from ph7.html import body, div, h1, head, html, title

template = html(
    head(
        title(
            "H1 Tag Example",
        ),
    ),
    body(
        div(
            h1(
                "This is an example of H1 tag",
            )
        )
    ),
)

print(template)
<html>
  <head>
    <title>H1 Tag Example</title>
  </head>
  <body>
    <div>
      <h1>This is an example of H1 tag</h1>
    </div>
  </body>
</html>

Attributes

HTML attributes can be passed down as function arguments, all of the HTML tag functions are defined with arguments representing the attributes as keyword arguments so you that can also utilise your autocomplete for faster development.

from ph7.html import body, div, head, html, title

template = html(
    head(
        title("HTML Attributes"),
    ),
    body(
        div(
            div(
                "Example for different attributes",
                class_name=["text", "bold", "teal"],
                id="child",
            ),
            div(
                "Clickable div",
                on={
                    "click": "sayHello",
                },
            ),
            class_name="container",
            id="container",
        )
    ),
    lang="en",
)

print(template)
<html lang="en">
  <head>
    <title>HTML Attributes</title>
  </head>
  <body>
    <div id="container" class="container">
      <div id="child" class="text bold teal">Example for different attributes</div>
      <div onclick="sayHello()">Clickable div</div>
    </div>
  </body>
</html>

Placeholders

PH7 allows you to define placeholders for data which can be filled out at when rendering a view using context argument. A placeholder can be defined with a default value

Hello ${name|John Doe}

and without a default value

Hello ${name}
from ph7.html import body, div, html

template = html(
    body(
        div(
            "Hello, ${name|John Doe}, I'm ${age} years old and I like ${food|Coffee}",
        )
    )
)

print(template.render(context={"name": "Jane Doe", "age": 24}))
<html>
  <body>
    <div>Hello, Jane Doe, I'm 24 years old and I like Coffee</div>
  </body>
</html>

Not providing value for placeholder without default value will result in error

from ph7.html import body, div, html

template = html(
    body(
        div(
            "Hello, ${name|John Doe}, I'm ${age} years old and I like ${food|Coffee}",
        )
    )
)

print(template)
ValueError: Error rendering 'Hello, ${name|John Doe}, I'm ${age} years old and I like ${food|Coffee}'; Value for 'age' not provided

Reusable views

You can define empty views with specific attributes and hydrate them later with data. When hydrating or rehydrating a view, you can also override the attributes.

from ph7.html import body, div, html

text = div(
    style={
        "font_size": "16px",
        "font_weight": "600",
        "letter_spacing": "0.75px",
    }
)

template = html(
    body(
        div(
            text("Text 1"),
            text("Text 2"),
            text("Text 3", class_name=["active"]),
        )
    )
)

print(template)
<html>
  <body>
    <div>
      <div style="font-size:16px;font-weight:600;letter-spacing:0.75px">Text 1</div>
      <div style="font-size:16px;font-weight:600;letter-spacing:0.75px">Text 2</div>
      <div class="active" style="font-size:16px;font-weight:600;letter-spacing:0.75px">Text 3</div>
    </div>
  </body>
</html>

Overridable views

You can define a block as overridable by using overridable method, these blocks can be filled with content when hydrating/rehydrating the parent component.

from ph7.html import a, body, div, html, title

button = div(class_name=["btn", "btn-primary"])

base = html(
    title("Templates Example"),
    body(
        div(class_name="nav").overridable("navbar"),
        div(class_name="container").overridable("container"),
        div(
            "© Organisation 2024 | Made by ",
            a("angrybayblade", href="https://github.com/angrybayblade"),
            class_name="footer",
        ),
    ),
)

template = base(
    navbar=div(
        button("Home", class_name=["btn", "btn-primary", "active"]),
        button("About Us"),
        button("Contact Us"),
    ),
    container=div(
        div(
            "Hello ",
            "${name|John Doe}",
            class_name="greeting",
        ),
        class_name="flex-center",
    ),
)

print(template.render(context={"name": "Jane Doe"}))
<html>
  <title>Templates Example</title>
  <body>
    <div class="nav">
      <div>
        <div class="btn btn-primary active">Home</div>
        <div class="btn btn-primary">About Us</div>
        <div class="btn btn-primary">Contact Us</div>
      </div>
    </div>
    <div class="container">
      <div class="flex-center">
        <div class="greeting">Hello Jane Doe</div>
      </div>
    </div>
    <div class="footer">© Organisation 2024 | Made by <a href="https://github.com/angrybayblade">angrybayblade</a></div>
  </body>
</html>

Using overridable views and reusable views you can create a modular and reusable codebase and save a lot of time an effort in the process.

Function As View

You can define your view as a python function create views based on conditions, using for loops or any other kind of logical operation to build your view.

import typing as t

from ph7.html import HtmlNode, body, div, html

user = div(class_name="user")
users = div(class_name="user")
nousers = div("Error, Users not found", class_name="error")


def render_users(context: t.Dict) -> HtmlNode:
    if "number_of_users" not in context:
        return nousers
    return users(user(f"User {i}") for i in range(context["number_of_users"]))


template = html(
    body(
        render_users,
    )
)

print("<!-- With `number_of_users` parameter -->\n")
print(template.render(context={"number_of_users": 5}), end="\n\n")

print("<!-- Without `number_of_users` parameter -->\n")
print(template.render(context={}))
<!-- With `number_of_users` parameter -->
<html>
  <body>
    <div class="user">
      <div class="user">User 0</div>
      <div class="user">User 1</div>
      <div class="user">User 2</div>
      <div class="user">User 3</div>
      <div class="user">User 4</div>
    </div>
  </body>
</html>
<!-- Without `number_of_users` parameter -->
<html>
  <body>
    <div class="error">Error, Users not found</div>
  </body>
</html>

You can also use named arguments instead of context argument to make thing more simple.

import typing as t

from ph7.html import HtmlNode, body, div, html

user = div(class_name="user")
users = div(class_name="user")
nousers = div("Error, Users not found", class_name="error")


def render_users(number_of_users: t.Optional[int] = None) -> HtmlNode:
    if number_of_users is None:
        return nousers
    return users(user(f"User {i}") for i in range(number_of_users))


template = html(
    body(
        render_users,
    )
)

print("<!-- With `number_of_users` parameter -->\n")
print(template.render(context={"number_of_users": 5}), end="\n\n")

print("<!-- Without `number_of_users` parameter -->\n")
print(template.render(context={}))
<!-- With `number_of_users` parameter -->
<html>
  <body>
    <div class="user">
      <div class="user">User 0</div>
      <div class="user">User 1</div>
      <div class="user">User 2</div>
      <div class="user">User 3</div>
      <div class="user">User 4</div>
    </div>
  </body>
</html>
<!-- Without `number_of_users` parameter -->
<html>
  <body>
    <div class="error">Error, Users not found</div>
  </body>
</html>

Caching

Since you can define your views as python functions you can also use caching utilities like functools.lru_cache to reduce rendering time.

import time
import typing as t
from functools import lru_cache

from ph7.html import HtmlNode, body, div, html

user = div(class_name="user")
users = div(class_name="user")
nousers = div("Error, Users not found", class_name="error")


@lru_cache
def _render_users(n: int) -> HtmlNode:
    return users(user(f"User {i}") for i in range(n))


def render_users(context: t.Dict) -> HtmlNode:
    if "number_of_users" not in context:
        return nousers
    return _render_users(n=context["number_of_users"])


template = html(
    body(
        render_users,
    )
)

tick = time.perf_counter()
template.render(context={"number_of_users": 300000})
print(f"First render: {time.perf_counter() - tick}")

tick = time.perf_counter()
template.render(context={"number_of_users": 300000})
print(f"Second render: {time.perf_counter() - tick}")

tick = time.perf_counter()
template.render(context={"number_of_users": 300000})
print(f"Third render: {time.perf_counter() - tick}")
First render: 4.100849833999746
Second render: 0.1824485419992925
Third render: 0.1806802080000125

When making a view cacheable, make sure the view satisfies following constrains

  • View only depends on the data provided by the arguments and does not fetch any external data
  • View does not return very large objects, or you might run into out of memory error after a while. If a view returns very large objects and you still want to cache it, you can use a smaller cache stack size on functools.lru_cache.