I’ve tried a number of different approaches, I’m not sure any of them are good ideas but I have the same basic principle which seems to be working for me:
- Have all IDs be class variables and swap them out dynamically
I’m not doing this just for pages, I’m doing this for user defined widgets in multiple pages, unfortunately I can’t share that code though.
Here is a very rough non-complete approach to how I think I would tackle this problem for multi-pages, for each Page have a class that looks like this:
from page import DashPage, ID, page_callback
class MyPage(DashPage):
button: ID = 'button'
output: ID = 'output'
def get_layout(self):
return html.Div([html.Div(id=self.output),
html.Button('Hello', id=self.button)])
@page_callback(
output=dash.dependencies.Output(output, 'children'),
inputs=[dash.dependencies.Input(button, 'n_clicks')]
)
def click_count(self, n_clicks):
if n_clicks is None:
return dash.exceptions.PreventUpdate
return n_clicks
Then in the page module have a metaclasses and callback class that register everything that is the MyPage class does and dynamically swap out the IDs for namespaced values:
# ID Type
class ID(str):
pass
# Helper Classes
class DashPageMeta(abc.ABCMeta):
def __new__(mcs, name, bases, dct):
new_class = abc.ABCMeta.__new__(mcs, name, bases, dct)
types = typing.get_type_hints(new_class)
for item_name, item_type in types.items():
if issubclass(item_type, ID):
item_value = getattr(new_class, item_name)
attribute_value = f'page-{dct["__qualname__"]}-{item_value}'
setattr(new_class, item_name, attribute_value)
if name != 'DashPage':
PAGE_CLASSES[name] = new_class
return new_class
class DashPage(metaclass=DashPageMeta):
@abc.abstractmethod
def get_layout(self, *args, **kwargs):
pass
class PageCallbackDecorator:
output = None
inputs = None
state = None
def __init__(self, func):
self.func = func
def __set_name__(self, owner, name):
if not hasattr(owner, 'callback_funcs'):
owner.callback_funcs = []
owner.callback_funcs.append((name, self.output, self.inputs, self.state))
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def page_callback(output=None, inputs=None, state=None):
PageCallbackDecorator.output = output
PageCallbackDecorator.inputs = inputs
PageCallbackDecorator.state = state
return PageCallbackDecorator
And then have 2 functions, one that builds the layout and one that adds the callbacks. My approach is sufficiently different I’m just going to have to provide a general idea of how you would do this:
def build_layout():
layout = []
for page_name, page in PAGE_CLASSES.items():
# Here I pass in the user to page, e.g. page(user)
# allowing me to return custom pages
page_layout = page().get_layout()
# Here you might want to wrap the layout with standard page controls or themes
layout.append(page_layout)
def add_all_callbacks(app):
for page_name, page in PAGE_CLASSES.items():
if not hasattr(page, 'callback_funcs'):
continue
page_instance = page()
for method_name, output, inputs, state in page_instance.callback_funcs:
# You actually need to test if output is an iterable here
original_value = output.component_id
output.component_id = f'page-{page.__dict__["__qualname__"]}-{original_value}'
# Iterate through inputs and states and do the same
# Then add to the real app callback
callback_method = getattr(page_instance, method_name)
app.callback(output=output, inputs=inputs, state=state)(callback_method)
You can probably simplify this a lot, as I say my name-spacing is actually fairly more complicated and dynamic, e.g. I’m passing user configuration in to each page class so each page can be custom.
But hopefully this gives you some ideas.