-
-
Notifications
You must be signed in to change notification settings - Fork 76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use variables in JS / CSS and caching thereof #622
Comments
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 = "" |
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).
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.
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 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 Now, the relevant point is that the validation will make it possible for people to use the types for declaring function signature of
So that would address my point 1) "the function arguments have to be defined only once". |
Update on this:
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?
|
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-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:
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 I think what we could get away is to:
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 But for this all to work, we need to parse the output of the component as HTML, and then insert that |
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":
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:
Thoughts? |
@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:
When it comes to this:
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 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>
Don't like this as much, for 3 reasons:
|
@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:
So, this way, we should be able to support both, CSS variables, and scoped CSS. |
And got that working too! Now, I took a different approach than we discussed in #478. Instead of specifying which JS / CSS to load via request headers, I inserted a With this, IMO a support for HTMX fragments could be implemented as so:
So in the end, if the component looked like this: <main>
<div id="my-fragment">
<h3>My title</h3>
</div>
</main> Then the full rendered component would look like this: <main data-comp-id="3ab0af" data-comp-css="fb082c">
<div id="my-fragment">
<h3>My title</h3>
</div>
</main> And after processing it as a fragment, we should generate something like this: <!-- <main> is not included, because it was outside of the CSS selector -->
<div id="my-fragment">
<h3>My title</h3>
<!-- Appended <script> tag that loads the fragment's dependecies if missing -->
<script>
Components.loadScript(
"js",
'<script src="/path/to/comp.js"></script>'
);
Components.loadScript(
"css",
'<link href="/path/to/comp.css"></link>'
)
</script>
</div> |
There's sooo many different discussions and important decisions going on in one thread here. Here's my take on trying to break them down:
|
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 (#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:
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
where I could write an inlined JS script, same as we currently do with thedyn_js
js
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 #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.
The text was updated successfully, but these errors were encountered: