Declarative layouts

This notebook demonstrates how to use an experimental display type called application/vdom.v1+json or vdom for short.

Instead of sending HTML, you send a declarative JSON format that lists the nodes for HTML like so:

{
  'tagName': 'h1',
  'attributes': {
    'style': {
        'color': 'DeepPink'
    },
  },
  'children': []
}

This is a bit low level, so you'll have to bear with us. The goal for an end user would be to be able to write something like this in Python:

layout = (
    Div([
        H1('Hello there!'),
        P('''
            Living the dream
        '''),
    ])
)

(which is exactly the API that dash provides)

We'll start out a little raw and show off some of the chief benefits as we go.


In [1]:
display(
    {
        'application/vdom.v1+json': {
            'tagName': 'h1',
            'attributes': {
            },
            'children': 'Welcome to VDOM',
        }
    },
    raw=True
)


We'll wrap that boilerplate up in a VDOM class, similar to the IPython.display.HTML class:


In [2]:
class VDOM():
    def __init__(self, obj):
        self.obj = obj
        
    def _repr_mimebundle_(self, include, exclude, **kwargs):
        return {
                'application/vdom.v1+json': self.obj
        }

VDOM({
    'tagName': 'h1',
    'attributes': {
        'style': {
            'textAlign': 'center'
        }
    },
    'children': "Now you're cooking with VDOM",
})


Out[2]:
<__main__.VDOM at 0x10f2e03c8>

If we really want, we could also create individual HTML element helpers


In [3]:
def h1(children=None, **kwargs):
    return {
        'tagName': 'h1',
        'attributes': {

            # Fold everything else in as props
            # Note that we'd _really_ want to do some validation here
            **kwargs
        },
        'children': children,

    }

In [4]:
h1('hey', style={ 'fontSize': '5em'})


Out[4]:
{'attributes': {'style': {'fontSize': '5em'}},
 'children': 'hey',
 'tagName': 'h1'}

In [5]:
VDOM(h1('This is great',
        style={ 'fontSize': '5em', 'color': 'DeepPink' }))


Out[5]:
<__main__.VDOM at 0x10f2e0c18>

Why not just use HTML?

When we send updates using display(obj, display_id='x', update=True), the HTML gets wiped out losing any state in the frontend. This especially matters with elements like <details>:


In [6]:
%%html
<details>
    <summary>Click me to expand</summary>
    <p>I am some hidden text</p>
</details>


Click me to expand

I am some hidden text

An easier element to demonstrate in a tutorial notebook is the infamous (and deprecated) <marquee> tag. Fun fact: It's GPU accelerated on Chrome.


In [7]:
from IPython.display import HTML
import time

handle = display(HTML("""<marquee>Here I am scrolling</marquee>"""), display_id='html_marquee')
time.sleep(2)
handle.display(HTML("""<marquee>RESET MUAHAHAHAHAHAH</marquee>"""), update=True)
time.sleep(2)
handle.display(HTML("""<marquee>😔</marquee>"""), update=True)


😔

Whereas, if you do it with the VDOM, you get nice clean updates that keep state.


In [8]:
import time

def marquee(children=None, **kwargs):
    return {
        'tagName': 'marquee',
        'attributes': {
            # Fold everything else in as props
            # Note that we'd _really_ want to do some validation here
            **kwargs
        },
        'children': children,

    }

h = display(VDOM(marquee('HERE WE GO VDOM')), display_id='vdom_marquee')
time.sleep(1.5)

for ii in range(12):
    h.display(VDOM(marquee('😁')), update=True)
    time.sleep(0.5)
    h.display(VDOM(marquee('😁✌🏻')), update=True)
    time.sleep(0.5)

h.display(VDOM(h1('❤️ VDOM ❤️')), update=True)


<__main__.VDOM at 0x10f2f23c8>

Game time

We can use this to make a silly little game where you and your friends pick an emoji and watch as it shuffles through them.


In [9]:
import secrets
import time

winner = ""

# Each player should pick an emoji that you put into this array
choices = ["🥑", "🐰", "🤷", "🚁", "🐰", "🐱"]
  #  "🍄", "🐱", "🚁", "☃", "🌀", "🏇", "🐼", "🦆", "🚀", "🎡"]


game = display(VDOM(h1('GAMETIME')), display_id="game")

for ii in range(40):
    winner = secrets.choice(choices)
    game.display(
        VDOM(
            marquee(winner, style={ "fontSize" : '4em' })
        ),
        update=True
    )
    time.sleep(0.1)

game.display(VDOM(h1('WINNER ' + winner, style={ "fontSize" : '4em' })), update=True)


<__main__.VDOM at 0x10f2f2470>

Collapsible job progress views

That was good and fun, let's try something that would be useful for spark and other background jobs. We've also been making a lot of boilerplate to declare marquee and h1. Let's create a little wrapper to make new elements simply.


In [10]:
def element(elementType):
    def elemental(children=None, **kwargs):
        return {
            'tagName': elementType,
            'attributes': {
                # Fold everything else in as props
                # Note that we'd _really_ want to do some validation here
                # Likely using http://bit.ly/domprops
                **kwargs
            },
            'children': children,

        }
    return elemental

In [11]:
bold = element('b')
div = element('div')
p = element('p')
img = element('img')


VDOM(
    div([
        h1('Now Incredibly Declarative'),
        p(['Can you believe we wrote ', bold('all this from scratch'), '?']),
        img(src="https://media.giphy.com/media/xUPGcguWZHRC2HyBRS/giphy.gif"),
        p('SO COOL!'),
    ])
)


Out[11]:
<__main__.VDOM at 0x10f303a20>

In [12]:
details = element('details')
summary = element('summary')
progress = element('progress')

In [13]:
progress_style= dict(width="100%", appearance="none")

job_progress = display(
    VDOM(
        details([
            summary("Job Progress"),
            progress(value=0, max=100, style=progress_style)
        ], open=True)
    ),
    display_id="job_progression"
)

for value in range(10, 105, 5):
    time.sleep(0.2)

    job_progress.display(VDOM(
            details([
                summary("Job Progress - toggle me"),
                progress(value=value, max=100, style=progress_style),
                
            ], open=True),
        
        ),
        update=True)


<__main__.VDOM at 0x10f2bfb70>

You can continue to refine these basic building blocks, building even richer UIs, all in declarative Python structures.

The beauty of all this is that you can update these displays without forcing weird scrolling behavior on the users or changing the state of interactive controls on them.

🎉 Here's to building cool things! 🎉