Showing Components in Notebooks

## Understand the Problem

I currently have trouble following the SolveIt process while building FastHTML apps. I am able to build up an app's pieces as strings of HTML and think about if the strings are correct, but showing those pieces as rendered components in a notebook always trips me up.

Devise Plan

Carry Out Plan

Setup

from fasthtml.common import *
from fasthtml.jupyter import *
from IPython.display import display, HTML, IFrame
app, rt = fast_app()
server = JupyUvi(app)
if 'server' in globals(): server.stop()

Sample Component: P With a Little Style

def RectP(c):
    return P(c, style="border: 1px gray solid")
RectP("Hi")

Studying In-Notebook Component Rendering

Starting with the canonical docs:

Using render_ft

After importing fasthtml.jupyter and calling render_ft(), FT components render directly in the notebook.

# render_ft()
RectP("Hi")

That worked! Though how do I view the string version of it now?

to_xml(RectP("Hello"))

That worked too!

Rendering Without render_ft

It feels a bit weird to change what RectP does. Ideally I can render the component only when I want to, not always. For now I am commenting out render_ft above, so that I can restart this notebook's kernel and continue without it.

Let's explore what render_ft does.

def render_ft():
    @patch
    def _repr_markdown_(self:FT): return to_xml(Div(self, Script('if (window.htmx) htmx.process(document.body)')))

I asked AI: Wait so it patches a FT component's repr_markdown method?

Yes - it patches the _repr_markdown_ method of the FT class, which is what Jupyter uses to determine how to display objects. The patch wraps the component in a Div with an HTMX processing script, and converts it to XML. This is why components automatically render as HTML after calling render_ft().

That's also why to_xml() still works to see the string version - it's a different method that isn't affected by the patch.

htmx.process() tells HTMX to scan the given element (in this case document.body) and initialize any HTMX attributes it finds. This is necessary when HTML is dynamically added to the page, as HTMX only automatically processes elements when the page first loads. Without this call, any HTMX attributes (like hx-get, hx-post, etc.) in dynamically rendered components wouldn't be activated.

Since Jupyter dynamically inserts our component's HTML into the notebook after the page has loaded, we need htmx.process() to ensure any HTMX functionality in the component gets initialized. Even if our current simple RectP component doesn't use HTMX features, the script is included by default since FastHTML components often do use HTMX attributes and we want them to work properly when rendered in the notebook.

def show_ft(c):
    html = to_xml(Div(c, Script('if (window.htmx) htmx.process(document.body)')))
    display(HTML(html))
show_ft(RectP("Hi"))

IPython Display and HTML

Let's take a step back to understand IPython display, not worrying about JS for now.

display(HTML('<p style="border: 1px red solid">Hi</p>'))
RectP("Uma")
uma = RectP("Uma")
uma
display(HTML(to_xml(uma)))
display(HTML(to_xml(RectP("Uma"))))

Defining a show Function

def show(c): return display(HTML(to_xml(c)))
show(RectP("Uma is a girl who loves crafts, science, and magic. "*20))

Complex show Output: MonsterUI

from monsterui.all import Card
c = Card("I'm a card. "*20, header="Prepare yourself, it's coming", footer="Thank you for your attention")
c
show(c)

I guess it would be nice to grab the card CSS without all of MonsterUI here, and put it into Style(). I don't think MonsterUI has that feature, but Pygments does. I'll try that.

Complex show Output: Pygments

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import HtmlFormatter
fm = HtmlFormatter(style='paraiso-light')
c = highlight('print("Hey")', lexer=PythonLexer(), formatter=fm)
c
NotStr(c)
Div(c)
Div(NotStr(c))

Working examples directly below

display(HTML(to_xml(Div(NotStr(c),Style(fm.get_style_defs())))))

Note: the following cell only works when the previous cell works:

display(HTML(c))
def show_highlight(c): return display(HTML(to_xml(Div(NotStr(c),Style(fm.get_style_defs())))))
c = highlight('styles = L(pygments.styles.STYLES.items()).itemgot(1).itemgot(1)', lexer=PythonLexer(), formatter=fm)
show_highlight(c)
def show_highlight(c): return display(HTML(to_xml(Div(NotStr(c),Style(fm.get_style_defs())))))

To be continued with scoped CSS...

Complex show Output: My Own

def RectP(c):
    return P(c, style="border:1px lightgray solid;padding:6px;margin:0;")
show(RectP("Hi, I'm Audrey. Danny and Uma are sleeping. "*20))

To be continued...