Open JuroOravec opened 2 months ago
I think this discussion would be easier if there was a concrete problem that you are trying to solve, that you made clear. It's always so hard to design for "problems that people might have", because if you get the problem wrong, you will get the solution wrong too.
Some things to consider:
This above that I agree with:
Given the above, I'm suggesting the API be something like:
class MyComp(Component):
def get_context_data(self, name, address, city):
return {"name": name, "address": address}
def get_context_data_js(self, name, address, city):
return {"name": name, "address": address}
def get_context_data_css(self, *args):
return self.get_context_data(*args)
js = ""
css = ""
I think this discussion would be easier if there was a concrete problem that you are trying to solve, that you made clear. It's always so hard to design for "problems that people might have", because if you get the problem wrong, you will get the solution wrong too.
Yeah, agree. I feel I will have to do a full end-to-end proof of concept (that includes also the client-side dependency manager) to understand thing a bit better and expand on this. Because there are some constraints, e.g. so far it looks that, to support this feature, we would users to ALWAYS use the middleware (or call an equivalent function on the rendered content).
I think passing variables to CSS makes sense too
This is one thing that I was thinking about, but don't think there's a non-breaking solution, because to make it work reliably, the CSS variable would have to be scoped under the top-level element of the HTML template. And for that we'd need to parse the HTML with beautifulsoup or similar to insert a class or other identifier to that top-level element.
Maybe this will all work out fine, but I am yet to run benchmarks to see how much overhead it would introduce.
We would also need to place constraints on how people write HTML templates - each template would need to be a valid HTML.
IMO this is probably a step in a good direction. But overall it will be a big update.
Given the above, I'm suggesting the API be something like:
class MyComp(Component): def get_context_data(self, name, address, city): return {"name": name, "address": address} def get_context_data_js(self, name, address, city): return {"name": name, "address": address} def get_context_data_css(self, *args): return self.get_context_data(*args) js = "" css = ""
I like on my proposed approach that 1. the function arguments have to be defined only once, and 2. that we don't have to re-compute get_context_data
if there's shared data.
On the other hand, I think having dedicated functions like these will make it clear to people that js and css data need to be JSON-serializable.
Also, something which I haven't shared yet, but is relevant here, is that I also wrote a bit of validation logic for typed components. So when someone specifies the generics like:
Args = Tuple[int, str]
class Kwargs(TypedDict):
key: NotRequired(str)
class MyComp(Component[Args, Kwargs, ...]):
...
Then the args, kwargs, slots, and data returned from get_context_data
is type-checked against the declared types.
Now, the relevant point is that the validation will make it possible for people to use the types for declaring function signature of get_context_data
. So one could then leverage this to avoid having to duplicately define types and function signatures for all 3 get_context_data
s, people could instead use self.input
, so like this:
class MyComp(Component): def get_context_data(self, *args, **kwargs): return {"name": self.input.kwargs["name"], "address": self.input.kwargs["address"]} def get_context_data_js(self, *args, **kwargs): return {"name": self.input.kwargs["name"], "address": self.input.kwargs["address"]} def get_context_data_css(self, *args, **kwargs): return self.get_context_data(*args, **kwargs) js = "" css = ""
So that would address my point 1) "the function arguments have to be defined only once".
Update on this:
This is one thing that I was thinking about, but don't think there's a non-breaking solution, because to make it work reliably, the CSS variable would have to be scoped under the top-level element of the HTML template. And for that we'd need to parse the HTML with beautifulsoup or similar to insert a class or other identifier to that top-level element.
Maybe this will all work out fine, but I am yet to run benchmarks to see how much overhead it would introduce.
Just came across this blog post, where selectolax came out to be much, much faster, taking only about 0.02 seconds to parse the page. I haven't run the script myself, but looking at the webpage they scraped, it currently has a size of 750 kB
So that's about about 0.026 seconds per MB of HTML. Assuming it might be same to convert it back to HTML, then the cost of modifying the outgoing HTML might about 0.050 seconds/MB.
In exchange, we'd gain:
Sounds like a good deal to me!
@JuroOravec I'm not following, why would we need to parse the HTML?
This is one thing that I was thinking about, but don't think there's a non-breaking solution, because to make it work reliably, the CSS variable would have to be scoped under the top-level element of the HTML template. And for that we'd need to parse the HTML with beautifulsoup or similar to insert a class or other identifier to that top-level element.
Yeah, so imagine that we want to use variables from Python in component's CSS:
.my-class {
background: var(--my-var)
}
If there was multiple components with perhaps diffferent values of my-var
, then this would generate multiple CSS files, e.g.
.my-class {
background: red;
}
.my-class {
background: blue;
}
But because of how CSS works, these two definitions would conflict, and in the end ALL instance of the component would end up having the same value.
So one way to avoid conflict is what e.g. Vue does with scoped css.
In Vue, when you write
<style scoped>
.example {
color: red;
}
</style>
<template>
<div class="example">hi</div>
</template>
then it renders
<style>
.example[data-v-f3f3eg9] {
color: red;
}
</style>
<template>
<div class="example" data-v-f3f3eg9>hi</div>
</template>
So it does two things:
data-v-f3f3eg9
.example[data-v-f3f3eg9]
.Now, Vue's approach is more complex then what I want to do. Dunno if they put the unique identifier on ALL HTML elements, or only the ones mentioned in the CSS. But basically I don't know how they know that .example
belongs to the top-level element.
I think what we could get away is to:
data-v-f3f3eg9
[data-v-f3f3eg9] {
--my-var: red;
}
Then, it should be enough for users to just write:
.my-class {
background: var(--my-var)
}
and different instance of the same component will be able to have different values of --my-var
.
But for this all to work, we need to parse the output of the component as HTML, and then insert that data-v-f3f3eg9
to top-level elements.
I am no expert at all on this but wonder if css-scope-inline be an alternative for css? Longer description of what it does at https://blog.logrocket.com/simplifying-inline-css-scoping-css-scope-inline/
First of all, let's split JS variable support, and CSS variable support into two different features. They can be worked on and released independently.
Hmm... lots of options for CSS here. I wonder if there's a way to do this without necessarily including the CSS over and over again for each time we include a tag.
Here's one very light-touch idea:
Given this component "example":
<div class="example">Text</div>
.example { background: var(--my-var, red) }
And the following calls:
{% component "example" / %}
{% component "example" my_var="blue" / %}
{% component "example" my_var="yellow" / %}
{% component "example" my_var="yellow" / %}
{% component "example" my_var="red" / %}
We would like to:
We could render this:
<style>
/* Fully cachable and only needs to be included once */
.example.comp-f3f3eg9 {
background: var(--my-var, red)
}
</style>
<div class="example comp-f3f3eg9">Text</div>
<div class="example comp-f3f3eg9" style="--my-var: blue">Text</div>
<div class="example comp-f3f3eg9" style="--my-var: yellow">Text</div>
<div class="example comp-f3f3eg9" style="--my-var: yellow">Text</div>
<div class="example comp-f3f3eg9" style="--my-var: red">Text</div>
This would mean that we need to parse the HTML and the CSS. When we do, we add a unique identifier to the HTML and CSS, set any CSS variables with a style tag.
Update: We could in fact let the use handle setting the style by modifying their template:
<div class="example" style="--my-var: {{ my_var|css_safe }}">Text</div>
Thoughts?
This would mean that we need to parse the HTML and the CSS. When we do, we add a unique identifier to the HTML and CSS, set any CSS variables with a style tag.
@EmilStenstrom I was thinking of this too, but:
Otherwise I agree with everything!
@EmilStenstrom Sorry, update, I didn't read it properly, I'm a bit hungover today 😅
This part I fully agree with:
We would like to:
- Scope all CSS to our components so it doesn't leak to the rest of the page
- Allow changing the CSS on a component by component basis
- Maximize the cacheability of the CSS
- Minimize the size of the generated HTML/CSS
When it comes to this:
<style> /* Fully cachable and only needs to be included once */ .example.comp-f3f3eg9 { background: var(--my-var, red) } </style> <div class="example comp-f3f3eg9">Text</div> <div class="example comp-f3f3eg9" style="--my-var: blue">Text</div> <div class="example comp-f3f3eg9" style="--my-var: yellow">Text</div> <div class="example comp-f3f3eg9" style="--my-var: yellow">Text</div> <div class="example comp-f3f3eg9" style="--my-var: red">Text</div>
Personally I'd go with example below:
<!-- User's component CSS -->
<style>
.example {
background: var(--my-var, red)
}
</style>
<!-- CSS variables -->
<style>
[data-css-f3f3eg9] {
--my-var: red;
}
</style>
<style>
[data-css-03ab3f] {
--my-var: blue;
}
</style>
<style>
[data-css-bb22cc] {
--my-var: yelllow;
}
</style>
...
<div class="example" data-css-f3f3eg9>Text</div>
<div class="example" data-css-03ab3f>Text</div>
<div class="example" data-css-bb22cc>Text</div>
...
While it is more verbose, it doesn't touch the style
attribute. I prefer not touch style
, because I'd prefer to avoid mixing library's internals with user's definitions.
I'm imagining that someone would be using django_components and doing snapshot testing. And where they define component's HTML something like:
<div class="example" style="background: red">
Text
</div>
Then they would get back e.g.:
<div class="example" style="--my-var: blue; --my-other-var: red; --my-other-other-var: green; background: red">
Text
</div>
Instead, if we just insert the identifiers, there'd be less potential for clashes:
<div class="example" style="background: red" data-comp-f3f3eg9>
Text
</div>
Update: We could in fact let the use handle setting the style by modifying their template:
Template:
<div class="example" style="--my-var: {{ my_var|css_safe }}">Text</div>
Don't like this as much, for 3 reasons:
If there was multiple top-level elements, they would have to do it for each of them.
One would have to track the CSS variable through Python, HTML, and CSS files. On the other hand, if we defined the CSS vars in get_context_css_data
, and then used them in the CSS files, it'd touch only the Python and CSS files.
It feels like a user would need to have more experience / knowledge to know how to do this. On the other hand it feels like a lower mental barrier if we hide the style="--my-var: {{ my_var|css_safe }}"
away.
I am no expert at all on this but wonder if css-scope-inline be an alternative for css? Longer description of what it does at blog.logrocket.com/simplifying-inline-css-scoping-css-scope-inline
@dalito That's an interesting one! Reading about it, I realized there's actually 2 separate feature ideas floating around in this discussion on CSS:
In one of my comments above, I mixed the two when I talked about Vue's scoped. My bad.
So when it comes to "scoped CSS", I can imagine that there could be 2 modes of operation:
By default, the CSS would NOT be scoped (like described here).
User could set the CSS to be scoped for given component. In that case, we would add some identifier to the CSS's <style>
tag, e.g. <style data-css-03ab3f> ... </style>
. And we would modify the script of css-scope-line
to work only with the style
tags that start with this identifier. The identifier would also be used to find the corresponding HTML elements.
So, this way, we should be able to support both, CSS variables, and scoped CSS.
Ayooo, I've got the proof of concept working!
Also got the scoped CSS working 🎉
One more remaining change is to allow to merge the JS / CSS, because currently the CSS / JS for every component and every input is fetched separately:
And got that working too!
Now, I took a different approach than we discussed in https://github.com/EmilStenstrom/django-components/issues/478. Instead of specifying which JS / CSS to load via request headers, I inserted a <script>
tag that is generated to call the client-side API.
With this, IMO a support for HTMX fragments could be implemented as so:
Component.fragment()
and Component.fragment_to_response()
methods, which accept all the same inputs as Component.render()
and Component.render_to_response()
, except also an additional fragment
kwarg (required), specifying the CSS selector of the fragment.fragment()
would call render()
to get the rendered HTML.{% component_dependencies %}
.
Background
I feel I've addressed most of the items that were blocking me for the the UI component library (there's about 5-6 MRs I've got half-ready in the pipeline), and kinda the bigger pieces that are still remaining for v1 is the rendering of CSS / JS dependencies and then documentation. So I've started looking into the CSS / JS rendering.
IMO before I get back to some of the ideas we discussed before, regarding the dependency middleware and such (https://github.com/EmilStenstrom/django-components/issues/478), it first make sense to add desired CSS- / JS-related features to django_components , and only then think about how to send the CSS / JS over to the client.
So one thing I'd like to have is to use the variables from
get_context_data()
in CSS / JS files. Let's break it down.How static files are loaded
When it comes to static files, so those that are served by the server AS IS, those are managed by Django's
staticfiles
app. My experience was that the files are "collected" / generated usingcollectstatic
, which physically puts them in a certain directory. And then, when the server starts, it serves the files from that directory.Now, if we want to treat CSS / JS as Django templates, we cannot pre-process them with
collectstatic
. Instead, what would be generated would depend on the components inputs. Same as it is with the HTML template.How CSS / JS is inserted into the HTML
Both CSS and JS can be "injected" into an HTML file it two ways:
Inlined, meaning that I embed the full definition within the HTML, e.g.
Linked , meaning that I only declare the URL where the CSS / JS can be found, and the browser fetches it
Static CSS / JS is linked. This is done by the
ComponentDependencyMiddleware
, which works like this:<!-- _RENDERED {name} -->
<!-- _RENDERED {name} -->
comments within the HTML, and retrieves corresponding component classes from the registry.<style> ... </style>
STATICFILES_DIRS
.Considerations
Suggestion
One approach that could fulfill the above is using Django's cache plus defining an endpoint / view that returns the cached file.
It would work like this:
When I define a component, I could specify an attribute like ~
dyn_js
~ where I could write an inlined JS script, same as we currently do with thejs
attribute.js
attribute, and only definejs_vars
~At render time, at the same time as we're rendering the HTML template, we'd also treat the JS file as a Template, and render it.~
We'd put the result string into Django's cache, using the component inputs to generate a hash key.
Then there would also be an endpoint defined by django_components, that the user would have to register at installation.
The endpoint could be something like:
/components/cache/table.a7df5a67e.css
/components/
would scope all django_component's endpoints/cache/<compname>.<hash>.<suffix>
would be a path to fetch the cached item.And the same way as we currently insert
<!-- _RENDERED {name} -->
into the rendered template, we would instead insert<!-- _RENDERED {name}.{js_hash}.js -->
or<!-- _RENDERED {name}.{css_hash}.css -->
.Thus, once this HTML would get to
ComponentDependencyMiddleware
, it would parse these comments, and:/components/cache/{name}.{js_hash}.js
(or.css
)Declaring used variables
But to be able to effective cache the JS / CSS files, one way could be to declare which variables are intended to be used in JS / CSS, e.g. like so:
So in the example above, only
name
andaddress
context variables would be available for use in the JS script. And it also means that if either of the two would change, then this would result in a miss in the cache, and a new JS script would have to be rendered.Cool thing about using something like
js_vars
is that we could say that the default is[]
, and then we wouldn't needjs_dyn
attribute, and instead we could just use thejs
attribute we already use.So current behavior, which is static JS script, would be the same as
Passing declared variables to JS
Don't know if others share this sentiment with me, but IMO formatting JS script with Python / Django is weird and hard to read.
So maybe a better way would be to pass those declared variables to JS.
So if my original JS script was this:
Then what we'd actually send to the browser is:
So:
$component
, which is an object of thejs_vars
keys and their values piped through JSON de/serializationSo in the declared JS file, one could use e.g.
$component.name
:Reusing JS script with different inputs
This maybe raises a question - use JS vars or Django vars? At first I was inclined to allow both - thinking that maybe users may have some complex filters or tags that could be difficult to rewrite in JS, and likewise some data manipulation could be easier in JS than in Django template.
However, I am also considering the option of using only JS vars. So people could still use Django filters and tags in the JS template, but would not be able to access any variables. The reason is that this would work nicely with the client-side JS / CSS manager, as discussed in https://github.com/EmilStenstrom/django-components/issues/478. Because then we'd need to send any JS script to the browser only once. And if there were multiple component instances using the same script, they would just invoke that given script with different arguments.
How will the JS script know which HTML template it is associated with?
I'd say that Django / Django components are loosely coupled with HTML. Meaning that while do have e.g. the
html_attrs
tag or separate HTML, JS, and CSS, django components could still be theoretically used to render anything. Markdown, LaTeX, other code, etc...This isn't bad on it's own, but it means that rendering HTML with it has its limits.
For example, because the rendered HTML is passed around as string, some components could have an HTML template that defines multiple root elements. Or none. Or would not render closing tags, because they would expect the closing tag to be supplied outside the component. Crazy, but it could happen.
The point is, we can't reliably tell if there's any valid HTML in the template. And so we cannot mark the generated string in any way that would make it possible for the JS / CSS to know what which HTML it is scoped to.
It would be possible to parse the generated string, but I'm concerned that that could give unnecessary overhead to each request.
It would be maybe possible for some system where the user would opt-in, signalling that the component's template IS a valid HTML. ...But then we'd still need to parse the HTML string to find the root element
Since Django's templating is completely independent from HTML, there would always have to be 2 parsing rounds (django -> HTML and HTML -> DOM) if we wanted scoped CSS / JS.
But I guess if we ever did go in this direction, it would still be compatible with the approach of wrapping the JS script in a function, and calling it with different inputs. E.g. then we could allow users to access the root component(s) under
$els
What about CSS?
This is already quite long post, so won't go into CSS. But much of the same applies as does for JS.
⚠️ UPDATE ⚠️: I'm now actually NOT in favour of treating the JS as a template, so the only way to pass data from Python component to JS would be via the exposed variables. Because otherwise, it would make sense to allow django syntax also in standalone JS files. But including Django syntax in JS or CSS would mess up the linters.