Complex applications can have deeply nested component hierarchies. Along the way, we may want to encapsulate state at different levels of the component hierarchy, say parent and child components. Further, the children of some containing component may want to react to changes in their parents state. Parameterized classes are the only way to share a components internal state with other components, but its not clear what the best practice is for sharing that state to children in a reactive way.
I’ve come up with the following, but it is ugly and won’t scale well:
import param
import panel as pn
pn.extension()
class StatefulComponent(param.Parameterized):
name = param.String()
def __init__(self, name, children, **params):
super().__init__(**params)
self.name = name
self.children = children
# This is ugly, do I really have to do this?
@param.depends('name')
def update_subcomponent(self):
self.children.parents_name = self.name
def view(self):
return pn.pane.Markdown(f"I am {self.name}")
class StatefulSubcomponent(param.Parameterized):
name = param.String()
# This is ugly too, I have to maintain a
# copy of the parent parameter to retain reactivity
parents_name = param.String()
def __init__(self, name, **params):
super().__init__(**params)
self.name = name
def view(self):
return pn.pane.Markdown(
f'I am {self.name}. My parent is {self.parents_name}')
sub = StatefulSubcomponent(name='Chapter')
main = StatefulComponent(name='Book', children=sub)
pn.Column(
main.view,
sub.view,
main.update_subcomponent # This is also ugly
).servable()
If you create a parameter that references the parent you can directly declare the dependency, e.g. here I’ve added a parent parameter, which I assign to. Now that we have made that declaration we can use param.depends to declare the dependency on the name parameter of the parent:
import param
import panel as pn
pn.extension()
class StatefulComponent(param.Parameterized):
name = param.String()
def __init__(self, name, child, **params):
super().__init__(**params)
self.name = name
self.child = child
self.child.parent = self
def view(self):
return pn.pane.Markdown(f"I am {self.name}")
class StatefulSubcomponent(param.Parameterized):
name = param.String()
parent = param.Parameter()
def __init__(self, name, **params):
super().__init__(**params)
self.name = name
@param.depends('parent.name', 'name')
def view(self):
return pn.pane.Markdown(
f'I am {self.name}. My parent is {self.parent.name}')
sub = StatefulSubcomponent(name='Chapter')
main = StatefulComponent(name='Book', child=sub)
pn.Column(
main.view,
sub.view,
).servable()
The other thing you seem to have missed is that for callbacks with side-effects you do not pass the callback to Panel, instead you declare watch=True in param.depends, i.e. instead of:
@param.depends('name')
def update_subcomponent(self):
self.children.parents_name = self.name
...
pn.Column(
main.view,
sub.view,
main.update_subcomponent # This is also ugly
).servable()
@Marc, thanks for the quick example of how to do this. It seems like the ClassSelector parameter is the key here and it helps to see it in use like this.
I have a question on this topic. Is there anyway to use “a.value” param if I have defined a in the init of class B ? I have been getting NoneType issues
Link here
import param
class A(param.Parameterized):
value = param.Integer()
class B(param.Parameterized):
a_tmp=A()
a = param.ObjectSelector(default=a_tmp,objects=[a_tmp])
value = param.Integer()
def __init__(self, **params):
super().__init__(**params)
@param.depends("a.value", watch=True)
def _update_b_value(self):
print("updating from", self.value)
self.value += self.a.value
print("updating to", self.value)
I defined a_tmp inside class B, because B.a uses a pointer to a_tmp. If you define it outside a class, all instances of B refer to the same instance of A().