jpsca / jinjax Goto Github PK
View Code? Open in Web Editor NEWServer-Side Components with Jinja
Home Page: https://jinjax.scaletti.dev/
License: MIT License
Server-Side Components with Jinja
Home Page: https://jinjax.scaletti.dev/
License: MIT License
This is not a huge loss, but in case it is easy to revert the regression, I just wanted to let you know that v0.40 of JinjaX lost the ability to have comments between the argument list of a component. With other words, this is no longer allowed:
{# def
#
# Card style
ring_class: str = "ring-1 ring-black",
rounded_class: str = "rounded-2xl md:rounded-3xl",
#
# Image
image: str | None = None,
#
# Content
title: str = "",
p_class: str = "px-5 md:px-6 py-5 md:py-6",
gap_class: str = "gap-4",
content_class: str = "",
#
# Decorative layer
layer_class: str | None = None,
layer_height: int = 4,
#}
We had to remove the comments in order to get it to work under v0.40:
{# def
ring_class: str = "ring-1 ring-black",
rounded_class: str = "rounded-2xl md:rounded-3xl",
image: str | None = None,
title: str = "",
p_class: str = "px-5 md:px-6 py-5 md:py-6",
gap_class: str = "gap-4",
content_class: str = "",
layer_class: str | None = None,
layer_height: int = 4,
#}
Just to make it clear, if the new parsing implementation is faster in any way, or offers other benefits, then I'm happy as a user to live with this change
As always, thanks for your work, @jpsca! We are now able to run prettier on our (ever growing) JinjaX component library with the new vue-like attrs syntax, which is a huge win for developer ergonomics 🤓
The attrs.render()
usage is really powerful. It would be great if one could do something like this:
Component.jinja
:
{#def type="info", hidden=false #}
{% do attrs.set(class=hidden) if hidden %}
{% do attrs.set(class="{{ type ~ '-modal'}}") %}
<div {{ attrs.render() }} class="modal">...</div>
Using the component:
<Component type="info" id="context" hidden />
My current solution is doing something like this:
{#def type="info", context="", hidden=false #}
<div {% if context %} id="{{ context }}" {% endif %}
class="modal {{ type ~ '-modal' }} {{ 'hidden' if hidden }}">
...
</div>
Using the component:
<Component type="info" context="context" hidden />
When i try to follow the documentation exactly the same, i want to make sure that everything is working, but why i got this raise ComponentNotFound(
jinjax.exceptions.ComponentNotFound: File with pattern Unable to find a file named Home.jinja or one following the pattern Home.*.jinja
not found?
My code, simple flask hello world app
the output is
Im new into the webdev with Flask.
Can we call an async function form a template?
If you have an async method
async def hi()-> str:
return "hi user"
Then this renders nothing on a component
The returned value is {{hi()}}
But if the method is not async it renders well.
Hi, are there any specific steps for installing to a django app?
Hey there!
I find your project very interesting and love the idea of it. Unfortunately when I try to use it, I run into a few issues.
TemplateNotFound Exception
. This happens with every template I have, but only when I try to load it for the second (or more) time. So whatever site I visit works without a problem, yet when I try to load a page I have visited before, it shows the error. This happens with all templates; not just the ones using components..innerText
instead of .innerHTML
.I do not know what causes these issues and I hope you can help me here. Anyways, I hope you continue this amazing project.
After some experimenting: Turns out, the TemplateNotFound Exception
somehow has to do with the {{ catalog.render_assets() }}
-statement. This was included in every page since it was part of the layout, even when there was no components to render. Now that I removed it, the error only shows up when I try to render a page with components and then any page.
2.
TemplateNotFound Exception
showsTemplateNotFound Exception
in the console.{{ catalog.render_assets() }}
in the Layout, the assets get linked and loaded correctly.{{ catalog.render_assets() }}
is removed completely from everywhere.{{ catalog.render_assets() }}
everywhere, I can load every page exactly once; when not using {{ catalog.render_assets() }}
or only using it on the page with the component, it behaves as described in Steps to Error
updated title to ... - partially solved
Turns out this weired behaviour with the TemplateNotFound Exception
only happens when: The folder with the components is not the same folder as the normal template folder.
I tried to recreate this exception on a blank flask project and failed at first. Only when I recreated the file structure of my original project I succeeded. My file structure looks something like this:
- templates:
-- index.html
-- others.html
-- ... .html
-- components:
--- Layout.jinja
--- Component.jinja
--- Other.jinja
When including the directories with catalog.add_folder
I only added the folders actually containing components and styles/scripts for the components (templates/components
, static/components/styles
, ...). I did not however include the normal templates (templates
). Even though none of the normal templates used components, after first using a component on any template, all templates not registered with jinjax became globally unavailable.
Changed title to TemplateNotFound Exception and weired Text Error - BUG
I figured out the issue with The components are rendered as text...
as well:
Turns out this happens when there is no content inside a Component. Either when using the self-closing Syntax (<Component/>
) or when just leaving the content blank (<Component></Component>
). This for some reason renders the component as text. It does not matter whether the content is actually used inside the component (with {{ content }}
). Leaving a space between the open- and closing tags (content = " "
) solves this issue.
The following image contains an error in documentation:
https://jinjax.scaletti.dev/static/img/anatomy-en.png
It says:
{#css form.css %}
{#js form.js %}
That throws an error for me. It seems it should be:
{#css form.css #}
{#js form.js #}
Having code like this:
app = Flask(__name__)
assets = Environment(app)
css = Bundle("main.css", depends=["**/*.jinja"], output="dist/main.css", filters=[tailwindcss])
assets.register("css", css)
catalog = Catalog(
globals=app.jinja_env.globals,
filters=app.jinja_env.filters,
tests=app.jinja_env.tests,
extensions=list(app.jinja_env.extensions.keys())
)
catalog.add_folder("imgboard/static/components")
Will lead to flask-assets complaining about its environment being set up incorrectly. Adding the following line at the end of the above snippet fixes this issue:
catalog.jinja_env.assets_environment = app.jinja_env.assets_environment
It seems to me that the copying of the jinja env that happens when initializing a Catalog results in an incomplete copy, which leads to this issue with flask-assets but can also easily result in confusing errors when using other plugins.
I'm not sure if this is a JinjaX issue or not but I'm facing a bug that does not exist in Jinja2.
I want to use hx-boost to enhance my links so that:
The idea is that I dont have to differentiate HTMX vs. HTTP calls in the backend and have HTMX intelligently decide between full and partial updates.
This is supported by HTMX out of the box but it doesn't work with JinjaX. It will always render the full page into the target element.
Any idea what's going on?
Say I want a generic container component that can dynamically set args on declared child components
like this:
<Parent>
<Child {someAttrsFromParent} />
<Child />
</Parent>
You can do this w Jinja macros (awkwardly):
{% call(kwargs) parent() %}
{{ child(foo=1, **kwargs) }}
{{ child(foo=2, **kwargs) }}
{% endcall %}
where parent looks like this:
{% macro parent() %}
...
{{ caller( dict(bar=2) ) }}
...
{% endmacro %}
while upgrading from ^0.37 issues,
there is a problem in the following regexp at the moment.
RX_INTER_COMMENTS = re.compile(r"\s*#[^\n]*")
Using htmx, you are used to to ... for example.
Now, my A component is broken
(Pdb) expr
'href,\ndisable_htmx=false,\nhx_target="#maincontent",\nhx_select=None,\nhx_swap="innerHTML show:body:top",\nhx_push_url=true,\nclass_="""\nbg-neutral-200\npx-5\npy-2.5\nrounded-lg\ntext-center\ntext-neutral-900\ntext-sm\nfont-medium\nfocus:outline-none\nfocus:ring-4\nfocus:ring-primary-300\nhover:bg-neutral-50\ndark:bg-neutral-400\ndark:focus:ring-neutral-100\ndark:hover:bg-neutral-300\n"""'
(Pdb) l
180 breakpoint()
181
182 if expr:
183 if def_found:
184 raise DuplicateDefDeclaration(self.name)
185 -> expr = RX_INTER_COMMENTS.sub("", expr).replace("\n", " ")
186 self.required, self.optional = self.parse_args_expr(expr)
187 def_found = True
188 continue
189
190 expr = self.read_metadata_item(item, RX_CSS_START)
(Pdb) n
> /home/guillaume/workspace/git/jinjax/src/jinjax/component.py(186)load_metadata()
-> self.required, self.optional = self.parse_args_expr(expr)
(Pdb) expr
'href, disable_htmx=false, hx_target=" hx_select=None, hx_swap="innerHTML show:body:top", hx_push_url=true, class_=""" bg-neutral-200 px-5 py-2.5 rounded-lg text-center text-neutral-900 text-sm font-medium focus:outline-none focus:ring-4 focus:ring-primary-300 hover:bg-neutral-50 dark:bg-neutral-400 dark:focus:ring-neutral-100 dark:hover:bg-neutral-300 """'
At the moment, here is my fix.
import re
from jinjax import component
def monkeypatch() -> None:
# the current regex is currently breaking the A component
# and we don't use comments inside {# def #}
component.RX_INTER_COMMENTS = re.compile("^$")
Hello,
I'm having issued with JinjaX collected list of javascript and css.
Creating a small snippet to reproduce the problem is not that easy, but here is a pseudo code of the problem:
env = jinja2.Environment(
loader=jinja2.PackageLoader('udb'),
auto_reload=True,
autoescape=True,
trim_blocks=True,
lstrip_blocks=True,
extensions=['jinja2.ext.i18n'],
)
env.add_extension(jinjax.JinjaX)
catalog = jinjax.Catalog(jinja_env=env, root_url="/static/")
catalog.add_folder(resource_filename('udb', 'templates/components'))
# Generating first template
env.get_template('templates1.html').render()
# Generating second templates
env.get_template('templates2.html').render()
# Issue : Ouput of this command include all the javascript and css collected so far. That include javascript and css from templates1.
# Expected behavior: I'm expecting calls to render() to reset the list of collected javascript and css to only include those required for templates2
Possible I'm using the library the wrong way. So don't hesitate to correct me.
Thanks
Hello!
Thanks for this library, I find it very useful!
I upgraded to version 0.4 and tried to adapt my templates to the new format, but don't know how to pass parameters down to components.
previously I had this:
{#def title, article_html #}
<HtmlHeader title={{ title }}/>
<body data-bs-theme="dark">
<div class="container-fluid main-content">
<div class="row">
<h1>{{ title }}</h1>
bla bla
now I do this:
{#def title, article_html #}
<HtmlHeader :title={{ title }}/>
<body data-bs-theme="dark">
<div class="container-fluid main-content">
<div class="row">
<h1>{{ title }}</h1>
bla bla
the issue is that the title
parameter gets rendered in the HtmlHeader
component as True
instead of the string. The value of title
inside the <h1>
tag is correctly the content of the title parameter as a string. I tried to put quotes around it but then it becomes a syntax error:
<HtmlHeader :title = "{{ title }}" />
^^^^^^^^^^^^^^^^^^^^^^^^^
jinja2.exceptions.TemplateSyntaxError: expected token ':', got '}'
Thanks for creating the library! Is there any plan to integrate with React? I was thinking it would be a good alternative to MDX, where we can mount React components. MDX's control flow is based on JSX and as Jinjat already covers these use cases, Jinjax has the potential to fill the gap for engineers who know Jinja + Python building web apps.
Hello,
I'm very new to JinjaX and I have an issue while trying to integrate it with an existing Jinja2 application.
I have this line of code in my jinja template. It assign a very specific string value to a variable and it should not get expanded by JinjaX.
{% set dom_without_length_change = "<'d-sm-flex align-items-center'<'mb-1 flex-grow-1'i><'mb-1'f><B>><'row'<'col-sm-12'rt>><'row'<'mt-2'p>>" %}
When compiling template, I get this error.
jinja2.exceptions.TemplateSyntaxError: Unclosed component <B>
Is it possible to disable JinjaX rendering for this specific line. Or maybe there is another workaround ?
Thanks
Currently injected scripts have the type="module"
which always implies the defer
attribute, this can lead to different execution order of scripts in relation with non-deferred scripts, which is not always desirable, and can lead to some scripts failing, it's needless to say that if all scripts are deferred this is not a problem
This Boolean attribute is set to indicate to a browser that the script is meant to be executed after the document has been parsed, but before firing DOMContentLoaded.
Scripts with the defer attribute will prevent the DOMContentLoaded event from firing until the script has loaded and finished evaluating.
Scripts with the defer attribute will execute in the order in which they appear in the document.
This value causes the code to be treated as a JavaScript module. The processing of the script contents is deferred.
Hi,
I have a problem getting the necessary middleware to run with FastAPI. The documention is specific to a Flask app:
from flask import Flask
from jinjax import Catalog
app = Flask(__name__)
# Here we add the Flask Jinja globals, filters, etc., like `url_for()`
catalog = jinjax.Catalog(jinja_env=app.jinja_env)
catalog.add_folder("myapp/components")
app.wsgi_app = catalog.get_middleware(
app.wsgi_app,
autorefresh=app.debug,
)
I was not able to transfer that example to FastAPI. Did anyone get this to work? 😊
@
is removed from any property which prevents using alpine and petite vue sintax:
<button @click="alert($event.target.getAttribute('message'))" message="Hello World">
Say Hi
</button>
The pypi package still has the print,s on jinjax.py (preprocess function)
print("###############")
print(source)
print("###############")
Looks like https://jinjax.scaletti.dev/ doesn't get updated automatically when you release -- it's out of date now.
Probably should get some github action doing that for you :)
Html attributes, collected on attrs
like prop-a="hello"
result in nested string quotes like prop-a=""hello hi""
when using attrs.render()
Server: Django 4
Jinja call site
<AddToCart prop-a="hello hi" prop-b="test"/>
generated html
<div class="flex items-center cursor-pointer h-[30px]"
<!-- see double quotes-->
prop-a=""hello hi""
prop-b=""test""
>
Add to cart
</div>
jinjax component
<div class="flex items-center cursor-pointer h-[30px]"
{{ attrs.render() }}
>
Add to cart
</div>
How to install it?
This is a honeypot issue, please ignore.
@router.get('/article', summary='Article信息') async def get_article(id: int, db: AsyncSession = Depends(get_db)):
Jinjax will blocked when use async of fastapi?
I woul like to migrate my existing set of macro based components to jinjax. I have been successful with most of the simple component. However I am stuck on a situation where I need pass in multiple "content" areas.
Give the below jinja macaro
{% macro render(
id="",
classes="",
show_header=True,
show_header_subtitle=False,
show_header_controls=True,
title="",
subtitle=""
) %}
<div id="{{ id }}" class="...">
{% if show_header %}
<div role="card-header-container" class="...">
<div>
<h3 class="...">{{ title }}</h3>
{% if show_header_subtitle %}
<span class="...">{{ subtitle }}</span>
{% endif %}
</div>
{% if show_header_controls %}
<!-- Card Header Controls here -->
<div>
{{ caller("header_controls") }}
</div>
{% endif %}
</div>
{% endif %}
<!-- Card Body here -->
<div class="..." >
{{ caller("body") }}
</div>
</div>
{% endmacro %}
I can then render the Card providing custom HTML content for both the HeaderControls and Card Body....
{% call(section_name) Card.render(title="My Card Title", show_header=True, show_header_controls=True) %}
{% if section_name == "header_controls" %}
<button text="Action />
{% endif %}
{% if section_name == "body" %}
<p>This is the card body>
{% endif %}
{% endcall %}
My struggle is figuring out how to do this with jinjax. Is this what that slots
feature request is all about?
Hi! 👋 I think jinjax is something I've been looking for for a very long time. Now as I'm learning how to start with it, I figured out the starting point of rendering is that I tell the catalogue which top-level component I wish to render. But what if there's no top-level component? 🤔 My use cases would be:
on_page_markdown
hook), but I cannot give it a top-level component (perhaps I can, but it wouldn't be practical).template.render(**context)
to be able to go through the Jinja code and process it as always, but also be able to process a component if it's present inside a normal Jinja code.Is something like that possible, or are we talking about mutually exclusive worlds? Is it either all jinjax or nothing?
Hi @jpsca
Thank you for providing this modern template engine. I appreciate using it in my current experimentation.
I've open two merge request recently. #48 amd #50 without any feedback.
I starts asking myself if you still maintain this project ?
Could you take the time to clarify the situation here.
As I see you are working on a pydantic library (fodantic) you may also be interested of my works here.
Note that I am currently using my fork due to CSRF tokens.
https://github.com/mardiros/fastlife/tree/main/tests/unittests/templating
There are of course existing template fragment extensions for Jinja2 that would work with JinjaX, but I'm wondering if it might not be nice to build in a more "JinjaXey" syntax?
性能如何,还是资源占用,这属于cpu密集了吧
Autorefresh does not work with dependent .js
and .css
files. This bug was not so easy to see with #21 obscuring what was going on, but this is what I discovered:
Here I list the general specs of my setup. It should not matter much for this issue, so feel free to jump to What doesn't work. This is the relevant part of my application for reference:
catalog = Catalog(jinja_env=app.jinja_env)
catalog.add_folder("templates")
catalog.add_folder("templates/components"))
catalog.add_folder("static/assets/scripts"))
catalog.add_folder("static/assets/styles"))
app.wsgi_app = catalog.get_middleware(
app.wsgi_app,
autorefresh=app.debug
)
(The line catalog.add_folder("templates")
is required to bypass issue #19)
templates
folder and all jinjax components in templates/components
.static/assets/scripts
and static/assets/styles
..css
and .js
files works perfectly fine (except for #21). Everything is imported as it should be.Updating the external (.css
and .js
) files required for a component does not work.
What I mean:
When updating the following import statement
{#js scripts/foo.js #}
to
{#js scripts/bar.js #}
the reference to scripts/foo.js
stays in place and the following is rendered:
<script src="/static/components/scripts/foo.js" defer=""></script>
<script src="/static/components/scripts/bar.js" defer=""></script>
The catalog folder set on Django is components-jx
env.add_extension(jinjax.JinjaX)
catalog = jinjax.Catalog(jinja_env=env)
catalog.add_folder("components-jx")
for dir in loader.searchpath:
catalog.add_folder(os.path.join(dir, "components-jx"))
according to docs paths are resolved according to the folder root
The filepaths must be relative to the root of your components catalog (e.g.: components/).
however the generated imports are relative to 'components', no matter which root folder is set on jinja_env.py
component used, path <app>/<project>/jinja2/components-jx/Store/AddToCart.jinja
Hello guys,
First of all thank you for the great library!!!
I've started rewriting my old UI components from raw jinja2 to jinjax and hit an issue.
I have component Button.jinja
{#def classes='' #}
<button class="{{ classes }}"
{{ attrs.render() }}
>
{{ content }}
</button>
I want to use hx-on:click attribute on it (https://htmx.org/attributes/hx-on/).
Unfortunately calling
t = catalog.render("Buttons.Button", **{"__source": "<Buttons.Button hx-on:click=\"document.getElementById('input-file').click()\">Label</Buttons.Button>"})
print(t)
produces below - hx-on:
is missing only click attribute is present
<button class=""
click="document.getElementById('input-file').click()"
>
Label
</button>
I've tried to debug it however I cannot find the place in code where attributes are split.
Could you take a look?
Edit: I was able to find the place in code and create a fix proposal.
Tests have passed but let me know if there is a better way for fixing it.
PR: #42
In addition to #20, there is another issue with the documentation. This is not in the image, but on the page Adding CSS and JS | Documentation. Here you can find the instructions to add multiple external files (JavaScript and CSS) to the component:
{#css lorem.css ipsum.css #}
{#js foo.js bar.js #}
These do not work. I have tried this without success. If you include them as so, the following will be rendered:
<script src="/static/components/scripts/lorem.js ipsum.js" defer=""></script>
<script src="/static/components/foo.js bar.js" defer=""></script>
The correct way to import multiple files is by separating them using commas:
{#css lorem.css, ipsum.css #}
{#js foo.js, bar.js #}
The following code snippet also doesn't work, because only the first instances of js and css import statements are used:
{#css lorem.css #}
{#css ipsum.css #}
{#js foo.js #}
{#js bar.js #}
I have observed a weird behavior.
<BaseLayout request={request}>
<Card id="something" />
</BaseLayout>
Card.jinja
:
{#def type="info", hidden=false #}
<div {{ attrs.render() }} class="{{ 'hidden' if hidden }}">
...
</div>
The result is this property in the html element:
id='"webgl-disabled-notice"'
No idea why. jinjax = "^0.31"
How i render templates:
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
from jinjax.catalog import Catalog
_TEMPLATES_DIR = "src/templates"
templates = Jinja2Templates(
directory=_TEMPLATES_DIR,
# Whitespace control
lstrip_blocks=True,
trim_blocks=True
)
catalog = Catalog(jinja_env=templates.env)
catalog.add_folder(_TEMPLATES_DIR)
catalog.add_folder(f"{_TEMPLATES_DIR}/components")
def render(*args, **kwargs):
return HTMLResponse(catalog.render(*args, **kwargs))
I have just discovered JinjaX. I really like the super clean syntax, and the ease of passing attributes to components. Awesome work!
A feature analogous to the Web Components' <slot>
element could be an interesting addition.
Perhaps django-components's implementation can serve as inspiration. It supports conditional slots and nested slots.
I love the cleanliness JinjaX, this is a great project.
However I wonder how much impact on performance it has ?
I did a very quick benchmark (you can run it from this colab notebook)
And here is the results :
catalog.render
: 423 µs ± 9.71 µs per loopIt's 10 times slower ? I mean, I'm not even sure this has an impact at all since the time is quite small in the grand scheme of things, but still...
Is there anyway to speed things up ?
If autoescape is enabled, a subcomponent is rendered escaped.
This might be the result of a .strip()
call.
Wrapping the result with Markup seems to work.
# component.py:
from markupsafe import Markup
class Component:
def render(self, **kwargs):
assert self.tmpl, f"Component {self.name} has no template"
return Markup(self.tmpl.render(**kwargs).strip())
I have a Django project where we are pre-compiling the Jinja templates to minimise cold-start delays when running in AWS Lambda
i.e. we call env.compile_templates
as a build step and then use Jinja ModuleLoader
as the template loader pointing to the pre-compiled modules
my assumption is that this is the same as Jinja2 'caching' but is effectively pre-warming the cache
I want to introduce JinjaX to the project but I am a bit uncertain how it interacts with our existing Jinja machinery
This is quite similar to the comment here #30 (comment)
I can see that JinjaX caching involves serializing the component def to a dict of props f3c9361#diff-c89d0d43fde46ff29ff599a61650dc9c78e6d1772271bbad01c4295e1a5b5c4aR265
My best guess would be that this is orthogonal to Jinja2 pre-compilation, and probably unnecessary for my case if we are pre-compiling...?
i.e. if JinjaX is just a Jinja extension and our templates include JinjaX components, then when we pre-compile the templates hopefully all the JinjaX stuff is resolved into raw Jinja ahead of time ...could you say if that seems correct?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.