✊🏿 Black Lives Matter. Please consider donating to Black Girls Code today.
⚡️ Concerned about the grid? Kyle Baranko teaches how to predicting peak loads using XGBoost. Register for the August webinar!

Build a composed component in python

Hello,

I would like to build a new dash component that would encapsulate a set of standard dash component (from dcc or dbc) yet be able to declare it as a component with custom fields.
For instance, a HockeyField component that would have a players and a positions field and that would render this as a table and graphviz element. So that I could write Output(“my-hockey”, “players”) to update the component.

So essentially, is it possible to compose different dash components into a single new component, all in pure python?

1 Like

I think the proper approach would be to create the component in JavaScript. But it would be cool if it was possible in Python too (:

Hi, I do not think this is possible as of today, but this is a tremendous idea!

Would there be some documentation of the interface that a component should expose to be able to be used in Input/Output/State calls?

The best documentation, that i know of on this subject, is the component boiler plate repository,

Tx @Emil, I already had a look at that but I find only js files. I think there is a whole framework encapsulating these js files to generate a python class (dynamically?).
I was more thinking about creating a python class with the proper interface to interact with the Input/Output/State callables.

Yes, the Python code is generated dynamically. But since you need JavaScript running on the client side, you can’t make components directly in pure Python. To achieve this kind of interface, you would need some mechanism to generate appropriate JavaScript for the component.

EDIT: If you are only targeting component composition, i.e. you are seeking a behavior which is already possible in Dash, i guess it would be possible to create a class like you have indicated simply be writing a thin wrapper that maps your desired syntax into standard Dash syntax.

And come with a class like

def MyComponent(SomeMagicClass):
   layout=html.Label(id="my-label")
   properties={"value":"my-label.value", ...})
   def input_handling(self,...):
       # do some magic 

after some extra thoughts on the problem, I was thinking of the following interface to declare a new component (a bit like an “embedded app” with its own layout and callbacks):

class SmartLabelInput(ComposedComponent):
    """Useless component that displays a label with an input field and
    that updates the label by appending the number of characters in the input."""

    # properties declared on the composed component (ie properties that can be used to track events)
    properties = ["value"]

    def __init__(self, label, *args, **kwargs):
        super().__init__(*args, *kwargs)
        self.label = label

    @property
    def layout(self):
        """Return the layout of the component.
        
        remark: The ids of the internal components should be mangled to avoid clashing with id of other components
        """
        return html.Div([html.Label(id="my-label", children=self.label), dcc.Input(id="my-input")])

    @ComposedComponent.callback(
        [
            Output(component_id="self", component_property="value"),
            Output(component_id="my-label", component_property="children"),
        ],
        [Input(component_id="my-input", component_property="value")],
    )
    def manage_changes_in_children_components(self, my_input_value):
        """This will update the property of the component (self.value) as well
        as the internal component label 'my-label.children'."""
        return my_input_value, f"{self.label} ({len(my_input_value)} chars)"

You can check out with I did for the components of my explainerdashboard library here: https://explainerdashboard.readthedocs.io/en/latest/components.html

It has a number of features that are specific to this library (dependencies, explainer objects, etc), but a stripped down version would be:

class DashComponent:
    """DashComponent is a bundle of a dash layout and callbacks.

    An DashComponent can have DashComponent subcomponents, that
    you register with register_components(). 
    
    Each DashComponent adds a unique uuid name string to all elements, so 
    that there is never a name clash even with multiple ExplanerComponents of 
    the same type in a layout. 
    Important:
        define your callbacks in _register_callbacks() and
        DashComponent will register callbacks of subcomponents in addition
        to _register_callbacks() when calling register_callbacks()
    """
    def __init__(self, title=None, name=None):
        self.title = title
        self.name = name
        if self.name is None:
            self.name = shortuuid.ShortUUID().random(length=10)
        self._components = []

    def register_components(self, *components):
        """register subcomponents so that their callbacks will be registered
        and dependencies can be tracked"""
        if not hasattr(self, '_components'):
            self._components = []
        for comp in components:
            if isinstance(comp, DashComponent):
                self._components.append(comp)
            elif hasattr(comp, '__iter__'):
                for subcomp in comp:
                    if isinstance(subcomp, DashComponent):
                        self._components.append(subcomp)
                    else:
                        print(f"{subcomp.__name__} is not a  DashComponent so not adding to self.components")
            else:
                print(f"{comp.__name__} is not a DashComponent so not adding to self.components")

    def layout(self):
        """layout to be defined by the particular DashComponent instance.
        All element id's should append +self.name to make sure they are unique."""
        return None

    def _register_callbacks(self, app):
        """register callbacks specific to this DashComponent"""
        pass

    def register_callbacks(self, app):
        """First register callbacks of all subcomponents, then call
        _register_callbacks(app)
        """
        if not hasattr(self, '_components'):
            self._components = []
        for comp in self._components:
            comp.register_callbacks(app)
        self._register_callbacks(app)

(note that you need shortuuid package to make sure all id’s are unique)

You could then write a custom component as:


class CustomComponent(DashComponent):
    def __init__(self, starting_text="hello world!"):
        super().__init__(title="Custom Component")
        self.starting_text = starting_text

    def layout(self):
        return html.Div([
            html.Div(id='output-div-'+self.name, children=self.starting_text),
            html.Button("Add !", id='button-'+self.name),
        ])
    
    def _register_callbacks(self, app):
        @app.callback(
            Output('output-div-'+self.name, 'children'),
            Input('button-'+self.name, 'n_clicks'),
            State('output-div-'+self.name, 'children')
        )
        def update_div(n_clicks, old_text):
            return old_text + '!'

And combine multiple components into a single layout:

class CustomDashboard(DashComponent):
    def __init__(self, starting_text1='Hello world', starting_text2='Bye World'):
        super().__init__(title="! Adder")
        self.custom1 = CustomComponent(starting_text1)
        self.custom2 = CustomComponent(starting_text2)
        
        self.register_components(self.custom1, self.custom2)
        
    def layout(self):
        return html.Div([
            html.H1(self.title),
            self.custom1.layout(),
            self.custom2.layout()
            
        ]) 

And start your dashboard:

db = CustomDashboard()

app = dash.Dash()
app.title = db.title
app.layout = db.layout()
db.register_callbacks(app)
app.run_server()

thanks @oegedijk, your proposal/code is indeed close to what I would like to be able to use.
but can your composed component have its own “properties” (that are linked behind the scene to some property of a child component ? in order for the component to be a kind of “native component” (and less an “embedded app”)

Not sure exactly what you mean, but you can definitely pass properties of the parent component down to the child properties. You can also even define callbacks between components, that can use each other’s properties.

Here’s a quick example of a Team edit dashboard, where the parent component holds a team (a list of members), one component can add members to the team, and another component displays the team. A connector makes sure that whenever a member gets added the display gets updated.

Might seem a bit cumbersome for this particular example, but for larger projects may be nice to keep components separated and abstracted in this way. You should be careful to keep the components stateful though, otherwise your application will break if you start using e.g. gunicorn with multiple workers.

import shortuuid
import dash
import dash_html_components as html
import dash_core_components as dcc

from dash.dependencies import Input, Output, State

from dash.exceptions import PreventUpdate

class EditTeamComponent(DashComponent):
    def __init__(self, team):
        super().__init__()
        self.team = team

    def layout(self):
        return html.Div([
            html.Div(id='dummy-div-'+self.name, style=dict(display="none")),
            dcc.Input(id='input-'+self.name, type="text", placeholder="add team member"),
            html.Button("Add Team Member", id='button-'+self.name),
        ])
    
    def _register_callbacks(self, app):
        @app.callback(
            Output('dummy-div-'+self.name, 'children',),
            Input('button-'+self.name, 'n_clicks'),
            State('input-'+self.name, 'value'),
        )
        def update_div(n_clicks, input_name):
            if input_name is not None:
                self.team.append(input_name)
                return "" # this is just to trigger the EditDisplayConnector callback
            raise PreventUpdate

class DisplayTeamComponent(DashComponent):
    def __init__(self, team):
        super().__init__()
        self.team = team

    def layout(self):
        return html.Div([
            html.Div(id='output-div-'+self.name, children=str(self.team)),
        ])
    

class EditDisplayConnector(DashComponent):
    def __init__(self, edit_component, display_component):
        super().__init__()
        self.edit_component = edit_component
        self.display_component = display_component
        
    def _register_callbacks(self, app):
        @app.callback(
            Output('output-div-'+self.display_component.name, 'children'),
            Input('dummy-div-'+self.edit_component.name, 'children',),
        )
        def update_div(dummy_div):
            return str(self.display_component.team)


class TeamDashboard(DashComponent):
    def __init__(self, team):
        super().__init__(title="Team Dashboard")
        
        self.edit = EditTeamComponent(team) 
        self.display = DisplayTeamComponent(team)
        self.connector = EditDisplayConnector(self.edit, self.display)
        self.register_components(self.edit, self.display, self.connector)
        
    def layout(self):
        return html.Div([
            html.H1(self.title),
            self.edit.layout(),
            self.display.layout()
            
        ]) 

team = ['player1', 'player2']
db = TeamDashboard(team)

app = dash.Dash()
app.title = db.title
app.layout = db.layout()
db.register_callbacks(app)
app.run_server()

Decided to flesh out the ideas a bit over the weekend and built a new library: dash_oop_components

Check it out on github : https://github.com/oegedijk/dash_oop_components

I think this could help you?

1 Like

Great, I will have a look at it.
I also fleshed some ideas leveraging the dash-extensions project, see the PR https://github.com/thedirtyfew/dash-extensions/pull/26