Multi-Page App: how to use the content of the whole page_registry odict from a subpage?

Hi there,

Consider a Multi-Page app such as the one below:

- app.py 
- pages
    - chapter1
       |-- chapter1title.py       
       |-- page1.py
       |-- page2.py
    - chapter2       
       |-- chapter2title.py   
       |-- page1.py
       |-- page2.py
    - coverPage.py
    - all.py

Then, say app.py contains the below code, (dash.plotly.com/urls)

from dash import Dash, html, dcc
import dash

app = Dash(name, use_pages=True)

app.layout = html.Div([
html.H1(‘Multi-page app with Dash Pages’),

html.Div(
    [
        html.Div(
            dcc.Link(
                f"{page['name']} - {page['path']}", href=page["relative_path"]
            )
        )
        for page in dash.page_registry.values()
    ]
),

dash.page_container
])

if name == ‘main’:
app.run_server(debug=True)

The goal is that a click on “chapter1title” not only displays the layout of “chapter1title”, but also all the pages of this section the one after the other, below.

Since the pages are added to the page_registry the one after the other, it is necessary to use the “order” argument in dash.register_page(“…”, layout=… order=) or to rename “chapter1title” as “titleChapter1”, so that the “chapter1title” (or titleChapter1" is added to the page_registry after all the other pages of the relevant section; in that case, when i click on “titleChapte1” in the navbar and assuming that this page contains the below code:

from dash import html, dcc
import dash

dash.register_page(__name__)

listOfPages=[]
for page in dash.page_registry.values():
    try:
        listOfPages.append(html.Div(page["layout"]))
    except:
        pass


def listAll():
    return html.Div(listOfPages)

layout=html.Div(children=listAll())

I will get what I want.

Problem is that doing so in each section might not be very convenient. Also, i would like that the “All” page displays all the other page, but there is an issue:
If the “all” page is at the same level as the “chapter1”, “chapter2” folders, then it is added to the registry first. And then the content of the “chapter1”. Therefore, the “all” page should be in a folder which should itself have a name starting with a “Z”, for instance, so that the “all” page is loaded at the end (…)

Is there no other way to do it? For instance by calling the whole dash.page_registry as it is in app.py, “from a subpage”?
The “circular imports” section let me think that there might be a way, but I did not find it.

KR

S.

Hello @David22,

If I understand correctly, you are trying to create a navbar for all the pages on the same tier?

Could you instead use something like an accordion to develop this?

Hi,
Nope, I am not trying to create a navbar. The goal is to display all the pages of one section “at the top” of this section when there is a click on the titre of that section, and all the page of the app when we click on “all”.
See what happens when you click on “full version”, onthis app:
https://dash.gallery/financial-report/

I built such an app without Dash Page; question is, is there a way to replicate the above behaviour with Dash Page

Ok…

So, if in chapter1, you would have tabs for chater1title, page1, page2.
Chapter2 and so on,.

Coverpage offers navigation and then all generates the chapters and tabs stacked one on top of the other?

Nope; you would get a better representation if you think of PowerPoint.

On the left i already have my navbar. In the middle I have my slides/pages. On the right, I have my toolbars, to modify the content/central part of each “slide” of my app, ie the pages. Each page comes with its own toolbar.

But, i need to have the possibility to only display the 4-5 pages of one specific section, keeping the correct pagination, when i click on the slide (or link) representing the chapter.

Hmm, ok.

Here, check this out:

image

image

app.py:

import dash
from dash import Dash, Output, Input, dash_table, dcc, html, State

app = Dash(__name__, use_pages=True, pages_folder='pages_testing')

regPages = dash.page_registry

app.layout = html.Div([
    html.Div([dcc.Link(children=regPages[pg]['name'], href=regPages[pg]['path']) for pg in regPages if len(regPages[pg]['path'].split('/')) < 3],
             style={'height':'100%', 'display':'flex', 'flexDirection':'column', 'width':'150px'}),
    dash.page_container
], style={'display':'flex'})

app.run(port=12345, debug=True)

utils/generate_layout.py:

import dash
from dash import html, dcc

def getLayouts(url):
    regPages = dash.page_registry
    layout = []
    for pg in regPages:
        if url in regPages[pg]['path'] and len(regPages[pg]['path'].split('/'))>2:
            layout.append(regPages[pg]['layout'])
    return layout

def makeTempLayout(name):
    layout = html.Div(name)
    return layout

chapter1/all.py:

import dash
from utils.generate_layout import getLayouts

def layout():
    return getLayouts('/chapter1')

dash.register_page('chapter1', path='/chapter1', layout=layout)

chapterx/pagesy:

import dash
from utils.generate_layout import makeTempLayout

dash.register_page(__name__)

layout = makeTempLayout(__name__)

What are your thoughts on this?

1 Like

I had missed your last reply, sorry for the delay,
It’s an interesting idea, I’ll experiment and update this post after, thank you!

1 Like

Hi @jinnyzor

Thanks for your time, the hints and the provided example., which helped a lot. The meaning of “To access dash.page_registry from within a file in the pages directory, you’ll need to use it within a function.”, in https://dash.plotly.com/urls#dash-page-registry , is now clearer.

From your example, I improved the function generating the layouts of my pages, by adding two arguments:

  1. “showAll”, whose default value is False. If True, then it extends the layout= (as in your example) with all the layouts of the pages in the same chapter, based on “callerName”, and
  2. “callerName”, whose value is the name of the script from which the function is called. From the name, the function retrieves the path of the top folder. This way, a page (say, a Title + the page of this chapter) can be built by extending the title page layout with all the other page of the same chapter.

Note that the function must first generate the layout of the “title” page, and then extend it with the layout of the other pages, which are themselves generated by a function, if showAll is True,
Otherwise, the first page would be defined by a function calling itself, when the functions goes through the dash.registry.

def gen_wrapper_of_wrapper_page_and_toolbar(type=None, … , showAll=False, callerName=None):
“”"
# Docstring here
“”"
wrapper_of_wrapper_page_and_toolbar = [
gen_wrapper_page_and_toolbar(type=type, …)
]

if showAll:
    pageName=callerName
    path_top_folder = os.path.dirname(dash.page_registry[str(pageName)]["path"])
    regPages = dash.page_registry
    for pg in regPages:
        if path_top_folder in regPages[pg]['path'] and regPages[pg]['module']!=pageName:
            # excludes the "caller page" to not return 2x the first page
            wrapper_of_wrapper_page_and_toolbar.extend(regPages[pg]['layout']().children)
            # retrieve only the children, each individual page being eventually wrapped into
            # a "main-wrapper-content". We dont want to include this wrapper again,
            # otherwise we would get a list like :
            # html.Div([LayoutDivOfPage1, wrappedDivOfPage2, wrappedDivOfPage3, ...], className=main-wrapper-content)
    
return html.Div(children=wrapper_of_wrapper_page_and_toolbar, className="wrapper-of-wrapper-page-and-toolbar")

Edit on 07 Feb 2023: above code could leverage the possibilities of dash.register_page. There are currently duplications. I’m working on it and will edit this post as soon as I get satisfying results.

My workaround for the issue I think you are facing was to have an all page, and its layout was to load all the other ones from the registry.

Therefore, I didnt need to exclude it because I had the all page registered as the main branch, everything else followed after that. :slight_smile:

I found a better way, but there is still room for improvement. It currently perfectly works to display the whole content of my app if I click on the “coverPage” link, or only the content of a specific chapter if I click on this chapter/subchapter.

At this stage the automatic page numbering works like a charm; but I will still add a “type” key to the dash_registry so that I can display the title numbering inside this kind of page (chapter, subchapter, etc)

I’m also currently working on a func returning the table of content nicely.

def gen_wrapper_of_wrapper_page_and_toolbar(type=None, sectionTitle=None, coverPageDict=None, footerContentLeft=None,
                                            footerContentRight=None, pageNumber=None, chapterDict=None,
                                            subChapterDict=None, bioDict=None, quoteDict=None, innerTitle=None,
                                            innerContent_spg="", innerContent_stb="", callerName=None):

    print("*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*_*")
    print("1 - callerName is: {}".format(callerName))
    wrapper_of_wrapper_page_and_toolbar = [
        gen_wrapper_page_and_toolbar(
            type=type, sectionTitle=sectionTitle, coverPageDict=coverPageDict, chapterDict=chapterDict,
            subChapterDict=subChapterDict, bioDict=bioDict, quoteDict=quoteDict,
            innerTitle=innerTitle, # in the inBetween (top)
            innerContent_spg=innerContent_spg, #in the inBetween (middle)
            pageNumber=list(dash.page_registry.keys()).index(callerName),
            footerContentLeft=footerContentLeft, footerContentRight=footerContentRight,
            innerContent_stb=innerContent_stb
        )
    ]
    ########################################################
    pg_reg = dash.page_registry
    # reminder: callerName is the name of the module from which the func is called. It is a str like "pages.0_coverPage"
    print("0 - pg_reg[callerName][show_childrens] is: {}".format(pg_reg[callerName]["show_childrens"]))
    if pg_reg[callerName]["show_childrens"]:
        # if the flag "show_childrens" is True, then the layout of the page from which the function is called must
        # contain 1) the typical layout of that kind of page, 2) the layouts of the other pages at the same level
        # and 3) its relevant direct childrens.
        # For instance, a page type "coverPage" shows its sisters (table of contents) and childrens (chapter1_title)
        for pg in pg_reg:
            # for each page in the page_registry - Note: pg is a key (in the dict pg_reg), and the key
            # are inferred from the module name. "pg" is therefore a string like "pages.0_coverPage"
            if pg != callerName:
                print("pg ({}) was not ignored because it is different from the caller page ({})".format(pg, callerName))
                # if the key - which is inferred from the module name, for each page - is different from
                # the callerName, then we *might* have to add the page.
                # rationale: the layout of the page calling the function has already been built, above,
                # by calling "wrapper_of_wrapper_page_and_toolbar". Therefore, we don't include it again
                print("pg path is: {}".format(os.path.dirname(pg_reg[pg]["path"])))
                print("caller page path is: {}".format(os.path.dirname(pg_reg[callerName]["path"])))
                if os.path.dirname(pg_reg[callerName]["path"]) in os.path.dirname(pg_reg[pg]["path"]):
                    print("{} is in {}".format(os.path.dirname(pg_reg[callerName]["path"]), os.path.dirname(pg_reg[pg]["path"])))
                    # check that the path of the calling page is in the path of "pg""
                    # Obviously, if pg_reg[callerName]["path"] is '/' because the "page" from which the function is
                    # called is "pages.0_coverPage" (i.e, in the root folder), then '/' will be in the path of each pg.
                    # But, if the function is called from a chapter title, such as "pages.chapter_4.title", then it
                    # enables us to exclude a page such as "0_coverPage", whose path would be '/', while the path of the
                    # page from which the function is called would be '/chapter4', which is not in '/'.
                    # Therefore, coverPage would not be included in the returned layout, if the function is called from
                    # a chapter. In short: if "pg" is not in the chapter from which the page calling the function is,
                    # then it wont be included into the returned layout.
                    print("re.findall on pg path is: {}".format(re.findall("/.+?(?=/|$)",os.path.dirname(pg_reg[pg]["path"]))))
                    print("re.findall on caller page is: {}".format(re.findall("/.+?(?=/|$)",os.path.dirname(pg_reg[callerName]["path"]))))
                    if len(re.findall("/.+?(?=/|$)",os.path.dirname(pg_reg[pg]["path"])))-len(re.findall("/.+?(?=/|$)",os.path.dirname(pg_reg[callerName]["path"])))==0:
                        # Warning: a simple .split("/") would returns ["",""] if we splitted a string like "/",
                        # therefore "/" and "/4-sectionContent" would both return a list of length 2 if we splitted them
                        # Rather, we use re.findall to look for all the matches "starting with a /, followed by several
                        # characters until another / or an end of string". This way, re.findall on a "/" returns an
                        # empty list, re.findall on "/section4" returns a list with 1 match, and re.findall on
                        # "/section4/subsection4" returns a list of 2 matches

                        # This checks that the "pg" page is at the same level as the calling page. If it is, then "pg"
                        # page is a sister of the calling page. E.g: "table of contents" would typically be a sister
                        # of the "coverPage". Both path would be "/" and both would have the same length".
                        # Therefore, the "tableOfContents" page layout is added to the returned layout
                        wrapper_of_wrapper_page_and_toolbar.extend(pg_reg[pg]['layout']().children)
                    elif len(re.findall("/.+?(?=/|$)",os.path.dirname(pg_reg[pg]["path"])))-len(re.findall("/.+?(?=/|$)",os.path.dirname(pg_reg[callerName]["path"])))==1:
                        # If the path of the considered "pg" page has exactly one level more than the path of the
                        # calling page, (i.e is one level lower in the hierarchy/folder tree),
                        # then we *might* have to add the pg
                        if pg_reg[pg]["show_childrens"]:
                            # if that pg has the "show_childrens" flag, then it means it is itself a page showing its
                            # sisters and childrens. Therefore, we add it to the returned layout.

                            # That way, when a page must return its childrens, it always shows 1) itself, then 2) adds
                            # its sister, and 3) adds its *direct* children if and only if this children also shows its
                            # children.
                            # E.g: "coverPage" calls the function,
                            # which adds the layout of the "table of contents" (sister, in the same folder)
                            # and then the layout of "chapter1_title" which is in "/chapter1" folder, because
                            # "chapter1_title" must itself shows its childrens
                            wrapper_of_wrapper_page_and_toolbar.extend(pg_reg[pg]['layout']().children)
                        else:
                            pass
                    else:
                        # If the path of the considered "pg" page has more than one level more than the path of the
                        # calling page, then this page is too far away in the hierarchy (and will be called by another
                        # "chapter" or "subchapter" title page.
                        pass
                else:
                    print("path of caller page is not in the current g. Current pg path is:".format(os.path.dirname(pg_reg[pg]["path"])))
                    pass
            else:
                print("pg ({}) was ignored because it is the caller page ({})".format(pg, callerName))
                # if the key is equal to the module name from which the function is called, then the layout of
                # this key must be ignored. (Otherwise the page would appear twice)
                pass
        else:
            pass

    return html.Div(children=wrapper_of_wrapper_page_and_toolbar, className="wrapper-of-wrapper-page-and-toolbar")

Assuming an app structure like this:

The func returning the layout of coverPage, when called from that module, returns the layout of coverPage + tableOfContent+chapter_1_title.py (because the show_childrens flag is True), and also chapter_2_title.py (for the same reason).
The function itself does not return the subchapter page because there are too “deep” in the hierarchy. However, when the function generating the layout of chapter_1_title is called, it will include chapter_1_intro and the subchapter_1_title.py.

Which, in turns, will return the subchapter page.

Therefore, everything is returned by the function called from coverPage, and I dont need to add any “all.py” as a workaround :wink:

Note that your solution helped me in understanding how to use the dash_registry in a func, I’m thankful for that :wink:

1 Like