Hello there
I am working on a app, which has a offCanvas (AIO component) coponent as filter’s container. This component is used on each page .
In this OffCanvas, I have checkLists for filtering data, the checkLists are one AIO component.
Now the problem is I can’t find a optimal way of adding select all option to the AIO checklist, because in the offCanvas component I refer to multiple checklists objects, which I collect selections from.
In order to get this working, I had to give up the select all option and set AIO_List id as id of components from offCanvas. It seems that lack of select options might be deal breaker for some users.
I know it is a little bit confusing, here some code that might help grasp the problem:
components / AIO_List_BASIC (the commented code is for select all, but not working) :
from dash import Dash, Output, Input, State, html, dcc, callback, MATCH, ctx, html, callback_context
import uuid
import dash_bootstrap_components as dbc
import base64
from components.grids import grid_1x2
import dash_mantine_components as dmc
#from utils.cache import set_specyfic_cache
# All-in-One Components should be suffixed with 'AIO'
class AIO_List(html.Div): # html.Div will be the "parent" component
#list_of_values = []
# A set of functions that create pattern-matching callbacks of the subcomponents
class ids:
checklist = lambda aio_id: {
'component': 'AIO_List',
'subcomponent': 'checklist',
'aio_id': aio_id
}
div = lambda aio_id: {
'component': 'AIO_List',
'subcomponent': 'div',
'aio_id': aio_id
}
select_all = lambda aio_id: {
'component': 'AIO_List',
'subcomponent': 'select_all',
'aio_id': aio_id
}
# Make the ids class a public class
ids = ids
# Define the arguments of the All-in-One component
def __init__(
self,
available_list,
choices_list,
list_title = '',
aio_id=None,
):
#self.component_id = component_id
self.available_list = available_list
self.choices_list = choices_list
self.list_title = list_title
if aio_id is None:
aio_id = str(uuid.uuid4())
title = html.Div(self.list_title)
#print('self.ids.checklist(aio_id)', self.ids.checklist(aio_id))
main_checklist = dcc.Checklist(
id = aio_id
#id = self.ids.checklist(aio_id)
,options=self.available_list
,value=self.choices_list
,inline = False
,persistence= True
,persistence_type='session'
)
# select_all_checkbox = dcc.Checklist(
# id=self.ids.select_all(aio_id),
# options=[{'label': 'Select All', 'value': 'all'}],
# value=[],
# inline=False,
# persistence= True,
# persistence_type='session'
# )
# Define the component's layout
super().__init__([ # Equivalent to `html.Div([...])`
html.Br(),
title,
#select_all_checkbox,
html.Div(main_checklist, style = {"height":"100%",
"overflow-y": "auto",
"scrollbar-width":"thin",
"display":"flex",
}),
html.Br()
]
,style = {'height': "25vh" ,
})
# @callback(
# Output(ids.checklist(MATCH), 'value'),
# [Input(ids.select_all(MATCH), 'value'),
# State(ids.main_checklist_id(MATCH), "options")]
# )
# def select_all(selected_values, av_values):
# if 'all' in selected_values:
# # If "Select All" is checked, return all option values
# return [option['value'] for option in av_values
# else:
# # If "Select All" is unchecked, return an empty list
# return []
components / AIO_OffCanvas_BASIC
import dash_bootstrap_components as dbc
from components.AIO_List import AIO_List
from dash import Output, Input, State, html, callback, MATCH, html
import uuid
import dash_bootstrap_components as dbc
import dash_mantine_components as dmc
import json
def grid_1x2(i1j1, i1j2):
"""insert html objects to cells of i'th rows and j'th columns"""
return html.Div(
[
dbc.Row(
[
dbc.Col(
html.Div(
html.Div(i1j1, className='box' ,style={"height": "100%"})
, className='box', style={'margin' : '10px', "height": "100%", 'padding':'10px'})
,xs={'size': 12, 'offset': 0, 'order': 1}
,sm={'size': 12, 'offset': 0, 'order': 1}
,md={'size': 6, 'offset': 0, 'order': 1}
,lg={'size': 6, 'offset': 0, 'order': 1}
,xl={'size': 6, 'offset': 0, 'order': 1}
),
dbc.Col(
html.Div(
html.Div(i1j2, className='box' ,style={"height": "100%"})
, className='box', style={'margin' : '10px', "height": "100%", 'padding':'10px'})
,xs={'size': 12, 'offset': 0, 'order': 2}
,sm={'size': 12, 'offset': 0, 'order': 2}
,md={'size': 6, 'offset': 0, 'order': 2}
,lg={'size': 6, 'offset': 0, 'order': 2}
,xl={'size': 6, 'offset': 0, 'order': 2}
),
],
className='row justify-content-center'
#,style={'margin' : '10px'}
)
,html.Br()
],
className='grid-container'
,style={ "background-color": "#FFF", 'padding-left':'2%'}
)
# All-in-One Components should be suffixed with 'AIO'
class AIO_OffCanvas(html.Div): # html.Div will be the "parent" component
btn_style_openCanvas = {
"width":"1.5%",
"height":"1800px",
"border-radius":"0",
"position": "absolute",
"text-align":"left",
"padding":"0 0 0 0",
"margin": "0 0 0 0"
}
# A set of functions that create pattern-matching callbacks of the subcomponents
class ids:
div = lambda aio_id: {
'component': 'AIO_OffCanvas',
'subcomponent': 'div',
'aio_id': aio_id
}
title = lambda aio_id: {
'component': 'AIO_OffCanvas',
'subcomponent': 'title',
'aio_id': aio_id
}
button_applyFilters = lambda aio_id: {
'component': 'AIO_OffCanvas',
'subcomponent': 'button_applyFilters',
'aio_id': aio_id
}
filtersDiv = lambda aio_id: {
'component': 'AIO_OffCanvas',
'subcomponent': 'filtersDiv',
'aio_id': aio_id
}
offCanvas = lambda aio_id: {
'component': 'AIO_OffCanvas',
'subcomponent': 'offCanvas',
'aio_id': aio_id
}
offCanvasOpenButton = lambda aio_id: {
'component': 'AIO_OffCanvas',
'subcomponent': 'offCanvasOpenButton',
'aio_id': aio_id
}
yearsFilter = lambda aio_id: {
'component': 'AIO_OffCanvas',
'subcomponent': 'yearsFilter',
'aio_id': aio_id
}
monthsFilter = lambda aio_id: {
'component': 'AIO_OffCanvas',
'subcomponent': 'monthsFilter',
'aio_id': aio_id
}
daysFilter = lambda aio_id: {
'component': 'AIO_OffCanvas',
'subcomponent': 'daysFilter',
'aio_id': aio_id
}
costCenterFilter = lambda aio_id: {
'component': 'AIO_OffCanvas',
'subcomponent': 'costCenterFilter',
'aio_id': aio_id
}
# Make the ids class a public class
ids = ids
# Define the arguments of the All-in-One component
def __init__(
self,
# component_id,
userName,
pageName,
#filters,
aio_id=None,
):
self.userName = userName
if aio_id is None:
aio_id = str(uuid.uuid4())
self.navi_choices = ["Dashboard", "Details", 'Map']
self.navi_value = pageName
self.navi_choices = html.Div([
#html.Div("Choose the page", style = {'text-align':'center'}),
html.Div([
html.Br(),
dmc.ChipGroup([dmc.Chip(html.A(x, href=f'/{x.lower()}/', target='_self') , value=x) for x in self.navi_choices],
value=self.navi_value,
style = {'align-items':'center', 'justify-content': 'center', 'display': 'flex'},
),
html.Br()]
, style = {'border-style':'outset', 'border-radius':'15px', 'border-width': '5px'}),
html.Br()
])
self.YearsFilter = AIO_List([2018,2019,2020,2021,2022,2023], [2023], list_title = 'Years', aio_id=self.ids.yearsFilter(aio_id))
self.MonthsFilter = AIO_List([1,2,3,4,5,6,7,8,9,10,11,12], [8], list_title = 'Months', aio_id=self.ids.monthsFilter(aio_id))
self.DaysFilter = AIO_List([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31], [20], list_title = 'Days', aio_id=self.ids.daysFilter(aio_id))
self.CostCenterFilter = AIO_List(['12312321', '13123123', '123123'], ['13123123'], list_title = 'CostCenters', aio_id=self.ids.costCenterFilter(aio_id))
# Define the component's layout
super().__init__([ # Equivalent to `html.Div([...])`
html.Div(
[
dbc.Button(" ➠", id=self.ids.offCanvasOpenButton(aio_id), n_clicks=0,
style = self.btn_style_openCanvas
),
dbc.Offcanvas([
html.P(id=self.ids.title(aio_id)),
html.P(
"""This is Dash Eliot, the following report should give you knowledge about this this and that."""
),
self.navi_choices,
dbc.Accordion(
[
dbc.AccordionItem(
children=[
"This is the content of the first section",
html.Div(dbc.Button("Apply changes", id = self.ids.button_applyFilters(aio_id)), n_clicks=0 , style = {'text-align': 'right'}),
grid_1x2(self.YearsFilter,
self.MonthsFilter
),
#self.DaysFilter,
grid_1x2(self.DaysFilter, self.CostCenterFilter),
html.Br(),
html.Div(id=self.ids.filtersDiv(aio_id), children = ''),
]
, title="Filters"
),
dbc.AccordionItem(
["Content ",
'one two three',
], title="User preferences"
),
dbc.AccordionItem(
["Content "
, 'Here should be: '
], title="Additional settings"
),
],
start_collapsed=True,
always_open=True,
),
html.Br(),
html.Br(),
],
id=self.ids.offCanvas(aio_id),
title= 'Hello '+ self.userName,
is_open=False,
scrollable=True,
backdrop = False
),
]
)
])
############################################### CALLBACKS
@callback(
Output(ids.offCanvas(MATCH), "is_open"),
Input(ids.offCanvasOpenButton(MATCH), "n_clicks"),
[State(ids.offCanvas(MATCH), "is_open")],
)
def toggle_offcanvas(n1, is_open):
if n1:
return not is_open
return is_open
@callback(
Output( ids.filtersDiv(MATCH), 'children'),
[Input(ids.yearsFilter(MATCH), 'value'),
Input(ids.monthsFilter(MATCH), 'value'),
Input(ids.daysFilter(MATCH), 'value'),
Input(ids.costCenterFilter(MATCH), 'value'),
Input(ids.button_applyFilters(MATCH), 'n_clicks')
]
)
def updateFilters(years, months, days, costCenter,n_clicks):
response = {'Years': sorted(years),
'Months': sorted(months),
'Days':sorted(days),
'CostCenter':sorted(costCenter),
'n_clicks':n_clicks}
return json.dumps(response)
Example usage main.py:
import dash
from dash import dcc, html
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output
from components.AIO_OffCanvas_BASIC import AIO_OffCanvas
from components.AIO_List_BASIC import AIO_List
app = dash.Dash(__name__ ,external_stylesheets=[
dbc.themes.BOOTSTRAP,
])
app.layout = html.Div([
AIO_OffCanvas('testUser', 'MainPage', 'MainPage123'),
html.Div(id='output-selected-values', children=[
html.H1('MainPage', style = {"text-align":"center"})
] )
])
if __name__ == '__main__':
app.run_server(debug=True)
Requirements:
dash==2.12.1
dash-bootstrap-components==1.4.1
This AIO_OffCanvas can be instantiated in dash page and will work.
Any ideas how I can still make callbacks with AIO_List in AIO_OffCanvas and have working callback inside AIO_List for select all?
The goal here is to make it all reusable and maintainable.
When trying to implement solutions I get bunch of errors about MATCH wildcard.
Much love, Kamil