Download an html version of the app

Here is a reworked version of my initial code with following optimizations:

  • Handover of file content with DataModel subclass
  • Deleting the file content in DataModel subclass after downloading to release memory space
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import Button, CustomJS, Div, Paragraph
from bokeh.resources import CDN
from bokeh.embed import file_html
from bokeh.core.properties import String, expr
from bokeh.model import DataModel

js_code = """
// Handed over from Python:
// file_content as DataModel with .string attribute
// cb_obj as the model that triggered the Javascript callback, here a paragraph with text attribute.

console.log("Javascript download code triggered.")

const filetext = file_content.text
const trigger_text = cb_obj.text

function download(filetext) {
    const filename = "HTML Export.html"
    const blob = new Blob([filetext], { type: 'application/xhtml+xml;charset=utf-8;' })

    const link = document.createElement('a') // Creates an <a> type element (anchor = hyperlink)
    link.href = URL.createObjectURL(blob) // Sets the blob as the hyperlink the anchor element points to
    link.download = filename // Tells that the file has to be downloaded and sets the file name.
    link.target = '_blank' // For some browsers, may influence the way the file is downloaded
    link.style.visibility = 'hidden' // Actually not necessary, sinse object not in DOM
    link.dispatchEvent(new MouseEvent('click')) // Simulates clicking of the link.
    URL.revokeObjectURL(link.href) // Release the object URL
    link.remove() // Remove anchor element
}

if (trigger_text == "file_ready") {
    console.log("cb_obj.text == \'file_ready\', initiating download...")
    download(filetext)
    console.log("Download completed.")
    // Empty the text of the DataModel, which can be quite big depending on file size:
    console.log("Setting file_content.text to \'\'...")
    file_content.text = ""
    // Reset the text of the triggering Paragraph model:
    console.log("Setting cb_obj.text (triggerJS.text in Python) to \'file_not_ready\'...")
    cb_obj.text = "file_not_ready"
} else { // The Javascript code will be restarted after download because cb_obj.text (triggerJS.text in Python) changed. Handle this case:
    console.log("cb_obj.text != \'file_ready\'. Javascript was most probably triggered because cb_obj.text was reset to \'file_not_ready\' after the last download. Quitting Javascript without doing anything.")
}
"""

my_div = Div(text="Some display element (could be a chart)")
my_div2 = Div(text="Another display element (could be a second chart)")
html_create_button = Button(label="Create HTML")

# Create a DataModel subclass to pass the up-to-date file content to Javascript.
class string_buffer(DataModel):
    text = String()
# Create an instance of it:
file_content = string_buffer(text="")

# Create a model to trigger the Javascript code executing the download.
# Since it only works if added to the layout but does not have to be seen, make it invisible.
triggerJS = Paragraph(text="file_not_ready", visible=False)

def html_create_callback():
    # Preparation: hide button which would be dead in the standalone html:
    html_create_button.visible = False
    # Create html file:
    html_doc = file_html(models=my_layout, resources=CDN, title="HTML Export", suppress_callback_warning=True)
    html_create_button.visible = True
    file_content.text = str(html_doc)
    triggerJS.text = "file_ready"

# Python callback:
html_create_button.on_click(html_create_callback)

# Javascript callback:
triggerJS.js_on_change("text", CustomJS(args=dict(file_content=file_content), code=js_code))

my_layout = column(my_div, my_div2, html_create_button, triggerJS)

curdoc().add_root(my_layout)