JS dependency management #670
Replies: 1 comment 3 replies
-
It comes down to these three features:
Support for fragmentsNormally when we render an HTML, it already comes with all scripts and styles included: <html>
<head>
<!-- Styles here -->
</head>
<body>
<main>
...
</main>
<!-- Scripts here -->
</body>
</html> But for the fragments, they may have specific JS / CSS scripts they need. But if the fragment response is also just an HTML <div>
...
</div> Then it won't automatically load the JS / CSS. So there needs to be something on the client that tracks which JS / CSS has been already loaded, and it loads missing JS / CSS when a fragment is inserted into the HTML and some of the fragments dependencies are missing. This is what we discussed back in #478. And this is what the Support for CSS variablesWith the support of CSS variables, there will be more As seen here, if I defined CSS variables like so: class MyComp(Component):
def get_css_data(self, color):
return {
"background": color,
} Then for each different return value, we'd generate a separate DYNAMIC CSS file: [data-comp-css-f3f3eg9] {
--background: red;
} [data-comp-css-0ab0fc] {
--background: blue;
} etc. What we achieve this way is that:
Support for JS variables + Running inlined JS per componentHere, the same applies as for CSS, but then JS goes one step further. For JS we'd do the same, that the static JS (inlined JS and JS from class MyComp(Component):
def get_js_data(self, count):
return {
"count": count,
} For each different return value, we'd generate a separate DYNAMIC JS file. And while for CSS I used an HTML attribute (e.g. Components.registerComponentData("table", "a1b2c3", () => {{
# NOTE: The content inside `JSON.parse` is the JSONified result from `MyComp.get_js_data`
return `JSON.parse`('{ "count": 2 }');
}}); Components.registerComponentData("table", "0fc3da", () => {{
return JSON.parse('{ "count": 5 }');
}}); etc. This is what we do here. The outcome is the same as for CSS vars - the static JS is rendered only once even if there's multiple instance of the same component, and it works also with fragments. Now, here is where the difference between JS and CSS start:For CSS we didn't need to explicitly connect the CSS variables with the inlined CSS. But for JS we have to. Above, you could see that for registering JS data, we used Components.registerComponentData("table", "0fc3da", () => {{
return JSON.parse('{ "count": 5 }');
}}); Here, But we also need to register the inlined JS script. Because if we have an inlined JS script like this: console.log("count: " + count); Then it doesn't know where the So, behind the scenes, we wrap the inlined JS script like so: Components.registerComponent("table", async ({ $id, $name, $data, $els }) => {
// ORIGINAL SCRIPT HERE
}); So we end up with Components.registerComponent("table", async ({ $id, $name, $data, $els }) => {
// NOTE: Changed `count` to `$data.count`
console.log("count: " + $data.count);
});
So, by now, we have:
The last thing that remains is to actually call the component's JS.The last piece of puzzle is that the inlined JS script should be able to access the HTML element of the component. For that reason the root HTML elements of the component are tagged with unique identifier, e.g.: <main data-comp-id-3ab0af>
<div>
<h3>My title</h3>
</div>
</main> And so, when we are rendering a component, we know:
So we can then put it all together, and call Components.callComponent("table", "3ab0af", "0fc3da"); Which basically says: "Call inlined JS script. Use the script from component name 'table'. Pass in the data from JS input with hash '0fc3da'. And pass in the reference to the HTML elements that match the CSS query
Dependencies here mean the JS and CSS files used by components. As described in the previous point, with the introduction of these features, a useful way to look at it is that we will now have STATIC and DYNAMIC dependencies. STATIC in this context means dependencies that are known ahead of time. E.g. the user-written inlined JS / inlined CSS or files linked in On the other hand, the dependencies that hold the JS / CSS variables are DYNAMIC - we will generate them at runtime, depending on the inputs. Same goes for the JS logic that calls /* E.g. code like this is known only at runtime: */
[data-comp-css-0ab0fc] {
--background: blue;
} However, to support the HTML fragments, even these dynamic dependencies need to be fetch-able via URL. So the the client dependency manager can fetch them if missing, or skip if already fetched. So we have those JS/CSS scripts that are generated at runtime, but we also needs to be able to fetch them via URL afterwards... So for this reason I decided to store the dynamic dependencies in Django's cache. And there will be an endpoint which takes the component name, and JS / CSS input hash, and returns the dynamically-generated content. So e.g. [data-comp-css-0ab0fc] {
--background: blue;
}
If you look at the package.json, the script has no runtime dependencies, only dev dependencies. So unless we plan on updating the dev dependencies (IMO no need for that), I don't think it will be breaking any time soon. Quite the opposite. I could have gone with just plain JS script, but then we'd have no tests in place, which would be a lot more prone to breakings. And TypeScript also adds it another layer of robustness. IMO we need to ensure that it's well documented, so even contributors coming from mainly Python background would be able to fix potential bugs in the JS code. But otherwise it's more of a "set and forget" piece of code. |
Beta Was this translation helpful? Give feedback.
-
@JuroOravec Starting a thread here not to mess with code discussions in your latest MR.
What does dependency management mean in this case? What responsibility are you suggesting we should take on? Currently we just manage a list of URL:s to JS (and CSS) files, and passes anything else to django's staticfiles app, or a third-party one like django-compressor.
This adds a 10.000 line lockfile with all the dependencies of this setup to the project. I would have suggested a much lighter vanilla JS way of doing this, without any dependencies. I'm not looking forward to maintaining a new full environment that will break continously :/
I'm guessing this fits together with other features you are planning in the future, could you give some ideas of how this fits with the bigger pitcture?
Beta Was this translation helpful? Give feedback.
All reactions