Multi page app documentation

Hi @micdonato

Welcome to the community. The answer to your question depends on your use case and requirements.

Option 1 (preferred): panel serve multiple files

The simplest is to serve a multi page app using multiple files.

panel serve page1.py page2.ipynb page3.py ...

Add the --autoreload flag while developing. This will also give you the index page out of the box.

For example for panel-chemistry I just panel serve examples/reference/*.ipynb ( See Link). You can explore the multi-page application on binder here.

Option 2: pn.serve multiple functions or panels

For some use case you would need to use pn.serve instead and serve the application using python app.py. For example if you want to use other urls than the file names.

Run the script via python app.py

app.py

import panel as pn

def page1():
    return "page 1"

def page2():
    return pn.Column(
        "# Page 2", "Welcome to the second page"
    )
ROUTES = {
    "1": page1, "2": page2
}
pn.serve(ROUTES, port=5006)

Please note that if you don’t pn.serve functions but instances, then they will be shared across users/ sessions. The latter can be confusing. But it also makes it really simple interact in real time with the same app instance for example for demo/ testing purposes. Another use case for instances is a live updating dashboard that should look the same to all users.

Option 3 Use a Widget to navigate between pages in a Single Page Application.

panel serve or pn.serve one page and use a widget and/ or url arguments to navigate between “pages”.

import panel as pn

pn.extension(sizing_mode="stretch_width")

pages = {
    "Page 1": pn.Column("# Page 1", "...bla bla bla"),
    "Page 2": pn.Column("# Page 2", "...more bla"),
}


def show(page):
    return pages[page]


starting_page = pn.state.session_args.get("page", [b"Page 1"])[0].decode()
page = pn.widgets.RadioButtonGroup(
    value=starting_page,
    options=list(pages.keys()),
    name="Page",
    sizing_mode="fixed",
    button_type="success",
)
ishow = pn.bind(show, page=page)
pn.state.location.sync(page, {"value": "page"})

ACCENT_COLOR = "#0072B5"
DEFAULT_PARAMS = {
    "site": "Panel Multi Page App",
    "accent_base_color": ACCENT_COLOR,
    "header_background": ACCENT_COLOR,
}
pn.template.FastListTemplate(
    title="As Single Page App",
    sidebar=[page],
    main=[ishow],
    **DEFAULT_PARAMS,
).servable()

Please note the downside of this approach is that when Panel/ Bokeh replaces one page with another it can be slow if your layout is complex. This should hopefully be fixed when bokeh 3.0 is released.

If you want to have multiple pages in one “single page app” consider using Tabs with argument dynamic=True or a custom template. That will make you app more snappy.

Share State Across Pages

If you need to share state across your pages you can use pn.state.cache for that. It’s just normal python dictionary. You can share Parameterized Classes, DataFrames, Widgets etc if you need to.

Lets take an example show casing some of the possibilities

You can run the below via python name_of_script.py. Alternatively you can refactor out into seperate files and panel serve them.

import panel as pn
import param
import datetime
from threading import Thread
import time

pn.extension(sizing_mode="stretch_width")
class StreamClass(param.Parameterized):
    value = param.Integer()
class MessageQueue(param.Parameterized):
    value = param.List()

    def append(self, asof, user, message):
        if message:
            self.value = [*self.value, (asof, user, message)]

ACCENT_COLOR = "#0072B5"
DEFAULT_PARAMS = {
    "site": "Panel Multi Page App",
    "accent_base_color": ACCENT_COLOR,
    "header_background": ACCENT_COLOR,
}

def fastlisttemplate(title, *objects):
    """Returns a Panel-AI version of the FastListTemplate

    Returns:
        [FastListTemplate]: A FastListTemplate
    """
    return pn.template.FastListTemplate(**DEFAULT_PARAMS, title=title, main=[pn.Column(*objects)])


def get_shared_state():
    if not "stream" in pn.state.cache:
        state=pn.state.cache["stream"]=StreamClass()
        pn.state.cache["messages"]=MessageQueue()

        def update_state():
            while True:
                if state.value==100:
                    state.value=0
                else:
                    state.value+=1
                time.sleep(1)

        Thread(target=update_state).start()

    return pn.state.cache["stream"], pn.state.cache["messages"]

def show_messages(messages):
    result = ""
    for message in messages:
        result = f"- {message[0]} | {message[1]}: {message[2]}\n" + result
    if not result:
        result = "No Messages yet!"
    return result

def page1():
    _, messages = get_shared_state()

    user_input = pn.widgets.TextInput(value="Guest", name="User")
    message_input = pn.widgets.TextInput(value="Hello", name="Message")
    add_message_button = pn.widgets.Button(name="Add")

    def add_message(event):
        messages.append(datetime.datetime.utcnow(), user_input.value, message_input.value)
    add_message_button.on_click(add_message)

    return fastlisttemplate("Add Message", user_input, message_input, add_message_button)

def page2():
    _, messages = get_shared_state()

    ishow_messages = pn.bind(show_messages, messages=messages.param.value)
    return fastlisttemplate("Show Messages",pn.panel(ishow_messages, height=600),)

def page3():
    stream, _ = get_shared_state()

    return fastlisttemplate("Show Streaming Value",stream.param.value,)

ROUTES = {
    "1": page1, "2": page2, "3": page3
}
pn.serve(ROUTES, port=5006, autoreload=True)
12 Likes