Add `Component.parent`, `Component.root`, `Component.ancestors`
@EmilStenstrom Let me know what you think!
I lightly mentioned this feature here and there, so want to discuss it before implementing it. This is one of the 2 last changes I want to add to the Component class (the other might be to add render_js() and render_css() to allow to configure how to wrap JS / CSS in <script> / <link> tags).
When rendering components, they are now stored as proper Component instances.
The way components are rendered is that nested components practically can access the Component tree that they are rendered within.
But right now it's not easy, and requires accessing our internal fields and internal context variables.
So I want to allow users to access these through Component.parent, Component.root, and Component.ancestors:
-
Component.parent- the parent component (instance) of the current component.Noneif current component is root. -
Component.root- the root component (instance) of the current component.selfif current component is root. -
Component.ancestors- the list (or iterator?) of all the parent components (instances) of the current component.
So basically this would work similarly to:
- Vue's
$parentand$root - AlpineJS
$root - Browser's parentElement
These parent / root properties + args / kwargs etc will be very powerful combo when creating component libraries.
For example, this is how one could write a Table component whose styling is decided by the Theme component it's nested in:
class Theme(Component):
...
class Table(Component):
def on_render_before(self, context, template):
# Do something based on the root component's args/kwargs/slots
theme_comp = self.closest(Theme)
if theme_comp.kwargs.get("show_header"):
self.slots["header"] = self.root.kwargs["header"]
Disclaimer: The above is not recommended. Data should be still passed top-down through provide / inject, as that allows the ancestor/parent to decide what data to provide.
I think a more realistic scenario for this will be when there will be a need to resolve compatibility issues between 2 sets of component libraries:
- Let's say I am using a Vuetify-like UI component library.
- This UI library uses provide / inject to pass theming data from ancestors to children.
- I write a custom component in my project for integrating a Markdown editor with martor.
- For some reason the CSS set by the UI library breaks the Markdown editor CSS, so I write some custom CSS to fix it.
- But then I discover that my CSS fix in turn breaks the Markdown editor when the Vuetify CSS is NOT applied.
- Thus, I would need a way to check if I need to apply the CSS fix depending on whether the Markdown editor IS or IS NOT nested in Vuetify theme component.
class MarkdownEditor(Component):
def get_template_data(self, args, kwargs, slots, context):
is_nested_in_vuetify = [isinstance(comp, VuetifyTheme) for comp in self.ancestors]
if is_nested_in_vuetify:
css_fix = "width: 200px; display: flex"
else:
css_fix = ""
return {
"css_fix": css_fix,
}
template = """
<div style="{{ css_fix }}">
...
</div>
"""
So ancestors would be children but not grandchildren, right? ...else a tree-structure is needed for ancestors.
@dalito Nope, by ancestors I meant those components that are less and less nested (going towards the root).
E.g. if we have a component structure like this:
- Page (root)
- Layout
- Card
- Button
Then inside the Button, the ancestors would be the parent, the parent's parent, etc, going upward, so:
comp.ancestors == [Card, Layout, Page]
In other words, it would be the same as doing following:
ancestors = []
curr_comp = comp
while curr_comp.parent is not None:
ancestors.append(curr_comp.parent)
curr_comp = curr_comp.parent
@JuroOravec I like it! I'm a little worried that the examples you provide feel contrived, and that the first one you specifically recommend against. I understand that this is something that exists elsewhere, does it have to exist here?
I'd say yes. I didn't think of it at first, but this will be needed for the django-debug-toolbar integration (https://github.com/django-components/django-components/issues/957)
There, having the parent attribute will make it really easy to construct the component tree, so that the profiler would show components in the order as they were rendered.
class DebugToolbarProfilerExtension(ComponentExtension):
name = "debug_toolbar_profiler"
def __init__(self):
self.tree = {}
def on_component_input(self, ctx):
child_id = ctx.component.id
parent_id = ctx.component.parent.id
# This is sufficient to build up the component tree (graph)
# See https://docs.python.org/3.9/library/graphlib.html
self.tree[child_id] = parent_id
@JuroOravec Great use-case, let's go!