viewdom: View Layer for Python VDOMs

viewdom brings modern frontend templating patterns to Python:

  • tagged to have language-centered templating (like JS tagged templates)

  • htm to generate virtual DOM structures from a template run (like the htm JS package)

  • viewdom to render a VDOM to a markup string, along with other modern machinery

Installation

Installation follows the normal Python packaging approach:

  $ pip install viewdom

Note

viewdom depends on htm (which depends on tagged which depends on nothing) and markupsafe.

Quick Examples

In the htm.py approach, you generate a VDOM and then convert that to a string. viewdom does the latter, htm.py does the former, via a bound html function in viewdom. Let’s first generate a VDOM:

"""
Simplest example of generating a static vdom, no variables.
"""

from viewdom import VDOMNode, html

# Make a VDOM
result = html('<div>Hello World</div>')

# Here's what the result will look like
expected = VDOMNode(tag='div', props={}, children=['Hello World'])


This time we’ll do both in one line: use html to generate a VDOM, then render to convert to a string:


result = render(html('<div>Hello World</div>'))
expected = '<div>Hello World</div>'


If you’d like, you can split those into two steps:


vdom = html('<div>Hello World</div>')
result = render(vdom)
expected = '<div>Hello World</div>'


Insert variables from the local or global scope:


name = 'viewdom'
result = render(html('<div>Hello {name}</div>'))
expected = '<div>Hello viewdom</div>'


Use HTML attributes as “props” which can have values or even expressions:


name = 'viewdom'
this_id = 42

result = render(
    html('<div id={f"id-{this_id}"} title="{name}">Hello {name}</div>')
)
expected = '<div id="id-42" title="viewdom">Hello viewdom</div>'


Expressions aren’t some special language, it’s just Python in inside curly braces:


name = 'viewdom'

result = render(html('<div>Hello {name.upper()}</div>'))
expected = '<div>Hello VIEWDOM</div>'


Strings with markup get escaped by markupsafe:


body = '<span>Escaping</span>'
result = render(html('<div>{body}</div>'))
expected = '<div>&lt;span&gt;Escaping&lt;/span&gt;</div>'


But you can flag a string as safe using markupsafe.Markup:


body = Markup('<span>No Escaping</span>')
result = render(html('<div>{body}</div>'))
expected = '<div><span>No Escaping</span></div>'


Rendering something conditionally is also “just Python”:


message = 'Say Howdy'
not_message = 'So Sad'
show_message = True
result = render(
    html(
        '''
    <h1>Show?</h1>
    {message if show_message else not_message}
'''
    )
)
expected = '<h1>Show?</h1>Say Howdy'


Looping? Yes, “just Python”:


message = 'Hello'
names = ['World', 'Universe']

result = render(
    html(
        '''
  <ul title="{message}">
    {[
        html('<li>{name}</li>')
        for name in names
     ] }
  </li>
'''
    )
)
expected = '<ul title="Hello"><li>World</li><li>Universe</li></ul>'


Reusable components and subcomponents, passing in props and children as arguments:


title = 'My Todos'
todos = ['first']


def Todo(label):
    return html('<li>{label}</li>')


def TodoList(todos):
    return html('<ul>{[Todo(label) for label in todos]}</ul>')


result = render(
    html(
        '''
  <h1>{title}</h1>
  <{TodoList} todos={todos} />
'''
    )
)
expected = '<h1>My Todos</h1><ul><li>first</li></ul>'


If your component is a callable instance, viewdom can detect that and call it:



@dataclass
class Greeting:
    name: str

    def __call__(self):
        return f'Hello {self.name}'


greeting = Greeting(name='viewdom')
result = render(html('<div><{greeting} /></div>'))
expected = '<div>Hello viewdom</div>'


Tired of passing props down a deep tree and want something like React context/hooks?


title = 'My Todos'
todos = ['first']


def Todo(label):
    prefix = use_context('prefix')
    return html('<li>{prefix}{label}</li>')


def TodoList(these_todos):
    return html('<ul>{[Todo(label) for label in these_todos]}</ul>')


result = render(
    html(
        '''
  <{Context} prefix="Item: ">
      <h1>{title}</h1>
      <{TodoList} these_todos={todos} />
  <//>
'''
    )
)
expected = '<h1>My Todos</h1><ul><li>Item: first</li></ul>'


Acknowledgments

The idea and code for viewdom – the rendering, the idea of a theadlocal context, obviously tagged and htm — essentially everything – come from Joachim Viide.