Open JuroOravec opened 2 months ago
This looks like an elegant way to define fragments. But why would you need fragments? Is there a use-case you have in mind?
Not for me, but seems like the combo of Django + HTMX + fragments keeps bouncing around Django forums / reddits.
Hello! I haven't been around in a while, but just wanted to say this would be a great feature for HTMX users!
That, plus dynamic loading of JS/CSS, would make django-components by far the best way to build Django+HTMX projects.
Also, it's amazing how much this has grown so fast. Hats off to you both!
I hope to start contributing again in Q4.
@dylanjcastillo Can you shed some light into why one would use fragements, and not whole components? I'm thinking it must be easier to reload the whole component compared to keeping track of minor fragments within a component?
Here's an example (copied verbatim from this article from the creator of HTMX):
<html>
<body>
<div hx-target="this">
#fragment archive-ui
#if contact.archived
<button hx-patch="/contacts/${contact.id}/unarchive">Unarchive</button>
#else
<button hx-delete="/contacts/${contact.id}">Archive</button>
#end
#end
</div>
<h3>Contact</h3>
<p>${contact.email}</p>
</body>
</html>
Based on that template, you can render the full component:
Contact c = getContact();
ChillTemplates.render("/contacts/detail.html", "contact", c);
Or the archive-ui
fragment:
Contact c = getContact();
ChillTemplates.render("/contacts/detail.html#archive-ui", "contact", c);
The benefit would be all the logic could live in the same component/file, giving you a better locality of behavior.
Maybe with the latest changes to the library, there's a way to have something like this without fragments? If that's the case, documenting it would likely be enough.
@dylanjcastillo Thanks for the example!
There's a link to six python/django packages at the end of that link, I'll to through them below:
{% include %}
-tag, and not have anything to do with fragments.All in all the reasoning from people is that fragments is an ALTERNATIVE to components. Instead of breaking things out into smaller components, just reuse your existing full-page templates, and pull in parts of them.
Based on that, I think the current way of rendering a component independently via HTTP will be enough to make django_components a great fit with HTMX devs.
The way I would look at this feature is how easy / difficult it is to implement it for users.
When we consider only the HTML, I agree with @EmilStenstrom that fragments could be easily done as separate components. One would maybe need to reuse some logic in get_context_data
in both the parent and child component, but this could be done by refactoring it out:
def shared_logic(abc, xyz): -> Dict:
return {"abc": abc, "xyz": xyz}
class MyComp(Component):
def get_context_data(self, abc, xyz):
return shared_logic(abc, xyz)
class MySubComp(Component):
def get_context_data(self, abc, xyz):
return shared_logic(abc, xyz)
Let's say someone had template that used the same CSS variables in and outside of the fragment. Then I could achieve this by sharing the CSS variables similarly as above:
def shared_css(color): -> Dict:
return {"background": color}
class MyComp(Component):
def get_context_css(self, color):
return shared_css(color)
class MySubComp(Component):
def get_context_css(self, color):
return shared_css(color)
A more complex case could be if I use {% component %}
INSIDE the fragment's HTML, whereas the component referenced via {% component %}
would expect to have CSS, maybe also CSS vars, that's shared in both the root component, and the fragment. In that case, we can still achieve this, but would already lead to some duplication:
def shared_css(color): -> Dict:
return {"background": color}
shared_css: types.css = """
.shared-class {
background: var(--background);
}
"""
class MyComp(Component):
def get_context_css(self, color):
return shared_css(color)
css: types.css = f"""
{shared_css}
.my-class {
text: blue;
background: var(--background);
}
"""
class MySubComp(Component):
def get_context_css(self, color):
return shared_css(color)
css: types.css = f"""
{shared_css}
.my-frag {
text: red;
background: var(--background);
}
"""
If I imagine I have a list of items, where the items can be rendered as fragments, then I'd expect that it is the fragment's JS that registers the item into the list (so that the parent component could run some logic that applies to all list items).
On the other hand, if there was a single component, then it would need to know somehow whether the JS that we're running is for the whole component, or for a specific fragment. For that I suggested the $fragment
variable in the component's JS in the original post:
if ($fragment === "header") {
doSomething();
} else if ($fragment === "footer") {
doSomethingElse();
} else {
processAsFullComponent();
}
If there was a different component for the top-level component, and for the fragment, then I think that in terms of JS, it would be just a matter of splitting the JS script, and moving the relavant block to the fragment's component.
There may also be shared CSS and JS scripts defined in Components.Media.js/css
, I expect this would be 1-3 scripts at most, so could be shared
Actually, I think if there was a lot of shared logic, scripts and CSS, it would probably make more sense to define the fragments as subclasses, to avoid defining all the methods duplicately:
class MyComp(Component):
class Media:
js = ["script1.js", "script2.js"]
css = ["styl1.css", "style2.css"]
# CSS includes also fragment's styling
css: types.css = """
.shared-class {
background: var(--background);
}
.my-class {
text: blue;
background: var(--background);
}
.my-frag {
text: red;
background: var(--background);
}
"""
class MySubComp(MyComp):
# JS / CSS media are the same. Same as:
# Media = MyComp.Media
# CSS is the same. Same as:
# css = MyComp.css
...
Although subclassing might not work with component typing. Because if I define typing for the top-level component:
class MyCompKwargs(TypedDict):
...
class MyCompSlots(TypedDict):
...
class MyCompData(TypedDict):
...
class MyComp(Component[EmptyTuple, MyCompKwargs, MyCompSlots, MyCompData]):
...
Then I would want to inherit from MyComp
, but that one already has the types set for args, kwargs, slots, data
class MySubCompKwargs(TypedDict):
...
# How to specify that `MySubomp` accepts `MySubCompKwargs` for kwargs?
class MySubComp(MyComp):
...
I can think of 2 ways to work around that:
Use cast(SubComp, Component), e.g.
class MySubCompKwargs(TypedDict):
...
# How to specify that `MySubComp` accepts `MySubCompKwargs` for kwargs?
# Like this:
MySubCompType = Component[EmptyTuple, MySubCompKwargs, MyCompSlots, MyCompData]
class MySubComp(cast(MySubCompType, MyComp)):
...
But subclassing means that MySubComp
might also inherit the same get
, post
, etc, methods. And if MySubComp
has different inputs, I would assume that the get
/ post
/ etc methods would also differ.
So my approach would be to define a "base class", that defines the shared JS, CSS, etc. So then the top-level component could use different get
/ post
/ etc methods:
class _MyComp:
class Media:
js = ["script1.js", "script2.js"]
css = ["styl1.css", "style2.css"]
# CSS includes also fragment's styling
css: types.css = """
.shared-class {
background: var(--background);
}
.my-class {
text: blue;
background: var(--background);
}
.my-frag {
text: red;
background: var(--background);
}
"""
class MyComp(Component[EmptyTuple, MyCompKwargs, MyCompSlots, MyCompData], _MyComp):
def get(self, request)
return MyComp.render_to_response(kwargs={"background": request.GET['background'] })
...
class MySubComp(Component[EmptyTuple, MySubCompKwargs, EmptyDict, MySubCompData], _MyComp):
...
Note, though, that there might still be some issues with typing in _MyComp
. So maybe it might actually subclass with types provided (Component[...]
). In which case we would still need to do cast()
, so the fragment's signature might look like so:
MyCompType = Component[EmptyTuple, MyCompKwargs, MyCompSlots, MyCompData]
# Base component, using the type of the top-level component
class _MyComp(MyCompType):
...
# Top-level component
class MyComp(_MyComp):
...
MySubCompType = Component[EmptyTuple, MySubCompKwargs, EmptyDict, MySubCompData]
# Fragment component
class MySubComp(MySubCompType, _MyComp):
...
So overall, it SHOULD be possible to use fragments also without explicitly supporting them, though there might be some workaround to make the typing work.
Using a Fragment
nested class to define the fragments would definitely be cleaner, as for the typing we'd need to define types only for the top-level component:
class MyComp(Component[EmptyTuple, MySubCompKwargs, EmptyDict, MySubCompData]):
class Fragments:
sub_frag = "#frag-id"
...
But all of the above is just hypothetical, I haven't tried it in-code yet.
@JuroOravec I like the idea of having "class Fragment" where you specify CSS selectors. It's much cleaner than targeting {% block %}
or {% partial %}
tags that you have to sprinkle around the templates. I also like the idea of being able to send in a fragment name when rendering, and only have that fragment rendered. The main question here is if this is something that people will use, or if the existing component tree is enough for most use-cases? Then we wouldn't need to complicate it further. This would also encourage larger components with fragments, instead of many smaller ones. What do we recommend?
As soon as we go past the python stuff, and start to mess with CSS or JS, we quickly end up with magic stuff that will be hard to follow. I'm least enthusiastic about those parts.
As soon as we go past the python stuff, and start to mess with CSS or JS, we quickly end up with magic stuff that will be hard to follow. I'm least enthusiastic about those parts.
Oh, just wait till you see the code for the client-side dependency mgmt 😄
But jokes aside
The main question here is if this is something that people will use, or if the existing component tree is enough for most use-cases? Then we wouldn't need to complicate it further. This would also encourage larger components with fragments, instead of many smaller ones. What do we recommend?
Tbh difficult for me to say, @dylanjcastillo and others will have to advocate for this one. Personally, I'm not a big fan of the fragment pattern, and I share the concern about encouraging larger components vs smaller, but that's because, coming from Vue/React, I prefer to split the logic and presentation layers, to have finer control.
But at the same time, currently I have a harder time navigating when there's more than one component in a file, because python formatting requires a lot of empty lines. So if I knew I was rendering a component using .render()
(that is, not via the component tag) personally I would probably go with a single component with extra fragment, over multiple components.
Lastly, I see no problem with adding support for it if there's people coming to Django from other areas, for whom the fragment pattern might be a sufficient / pragmatic approach.
NOTE: I'm jumping way ahead with this one, so really this one becomes relevant only once https://github.com/EmilStenstrom/django-components/issues/622 and similar have been implemented. But since I looked into this today, I wanted to document it.
Building on top of https://github.com/EmilStenstrom/django-components/issues/622#issuecomment-2324233043, I was thinking of what could be an intuitive use of HTML fragments with django_components.
For demonstration, imagine I have a component like so:
JS
Allow to still run component JS, but allow to modify the logic depending on whether and which fragment it is.
As show in the example, where there is
isFragmentA
andisFragmentB
, a single component can be rendered as multiple fragments.In all 3 cases
isFragmentA
,isFragmentB
, andelse
, the$els
would point to different elements. In full render, it is the top-level root elements. InisFragmentA
andisFragmentB
, it would be the fragments' root elements.We can save users some work by figuring for them which fragment is the JS script running.
For this, I suggest to declare the possible fragments and their names on the
Component
class:Thus, when rendering a component as a fragment, user would have to choose from pre-existing options:
We could then pass this info to the JS script and access it e.g. via
$fragment
:Nested components that are fully present in the HTML should run as normal.
Conversely, nested components whose ROOT elements are MISSING should NOT have their JS script ran, since we cannot guarantee correctness.
This is easy to achieve, because how things work now is that we first load shared JS, then register the components, and only at the end actually call the components' JS one-by-one.
One of the inputs for calling component's JS are the root elements, as identified by their
data-comp-id
ID.So if we have a fully rendered HTML like this:
The order of JS execution would be:
data-comp-id-555555
data-comp-id-3c0103
Next, if our fragment selector was
.mycomp
, we would still have the inner component:In this case above, we would first call the root component's JS with the fragment elements, and then the inner component:
data-comp-id-555555 (as fragment)
data-comp-id-3c0103
But if our selector was
myfrag
, thendata-comp-id-3c0103
would no longer be present, because it was neither the component where we calledfragment()
, nor its root present.So order of JS exec would be:
data-comp-id-555555 (as fragment)
CSS
Styles that apply to the fragment even after discarding the rest of the HTML should still be applied.
So I take the original template, and assume that the selector for our fragment is
.myfrag
, then this should left us with:Looking at the CSS, ASSUMING we interpret
me
as the ORIGINAL ROOT, then the CSS looks like:Then, given this, we'd expect
.myfrag
to be still applied.Notes:
For this to work, we will need to include all the same CSS files as if we rendered the component fully, so that the CSS takes the same effect.
Moreover, we will need to do the css-inline-scoping ourselves at server-side, so that the
me
is properly interpreted as the component root. Because if we inserted a CSS with theme
selector inside the fragment, it would incorrect interpret the fragment root asme
.Fragment CSS should still have access to the same CSS variables as original component.
In the example, I used CSS var
--bg_color
. But given how the CSS variables are implemented, then child components should have access to the CSS variables from their parents.Hence, this behavior should be kept even for fragments.
The access to CSS vars is via the
data-comp-css-a1b2c3
HTML attributes.So if I have a rendered HTML like so:
Then the fragment HTML should still have access to the variables defined in
data-comp-css-0d0d0d
anddata-comp-css-1ab2c3
.There might be a smarter way about it, but one approach could be to look for all
data-comp-css-
attributes in the ancestor chain of the root fragment elements, and apply all those attributes to the fragment elements themselves.So we would get something like:
Since we've already established in the previous point that we should include all the same CSS files as the fully rendered component, we WOULD have access to the variables under the IDs
data-comp-css-1ab2c3
anddata-comp-css-0d0d0d
.HTML
Not much to say about HTML. I think I've seen somewhere some lib(s) with special syntax for defining the fragments. But for simplicity I suggest using normal HTML syntax. So users could pick any attribute(s). E.g.: