Git Product home page Git Product logo

jinjax's Introduction

From chaos to clarity: The power of components in your server-side-rendered Python web app.

Documentation: https://jinjax.scaletti.dev/

Write server-side components as single Jinja template files. Use them as HTML tags without doing any importing.

Roadmap

Planned

  • Slots
  • Type checking at runtime
  • ...

Done

  • Autoloading assets (optional?) (Card.jinja autoloads Card.css and/or Card.js if exists)

jinjax's People

Contributors

ahnafcodes avatar cemrehancavdar avatar jakobgm avatar jodal avatar jpsca avatar kamcio181 avatar mardiros avatar ramonvg avatar rix1 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

jinjax's Issues

Render without parent component?

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:

  1. I'd like to drop components inside Markdown, effectively achieving something like MDX. The Markdown file lives e.g. in MkDocs and it's a plain Markdown text except of a few places where I want to include a component. I can pre-process such file with Jinja (MkDocs have a on_page_markdown hook), but I cannot give it a top-level component (perhaps I can, but it wouldn't be practical).
  2. I have a rather large project where I'd like to start using jinjax. It's a mess of Jinja macros and whatnot. There are, of course, no top-level components at the moment. I cannot go and rewrite everything, I need to roll the change gradually. I need the old code to live alongside the new code. In such environment, I need the standard 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?

It'd be nice if could pass attrs from parent to child at compose time

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 %}

Jinjax fails with python3.9

The recent version (0.32) of jinjax fails with python3.9, even though pyproject.toml includes this version:

image

Autorefresh not working properly

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:

Setup

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)

  • I have most of my templates in the templates folder and all jinjax components in templates/components.
  • I have all scriptfiles and stylesheets in static/assets/scripts and static/assets/styles.
  • Importing .css and .js files works perfectly fine (except for #21). Everything is imported as it should be.
  • Updating the code of components works fine and displays the correct updates in the client.

What doesn't work

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>

Question re how JinjaX interacts with Jinja2 pre-compiled templates (ModuleLoader)

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?

How to disable jinjax for specific lines

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

How to work with Fastapi

@router.get('/article', summary='Article信息') async def get_article(id: int, db: AsyncSession = Depends(get_db)):

Jinjax will blocked when use async of fastapi?

Update package 0.19 on pypi

The pypi package still has the print,s on jinjax.py (preprocess function)

        print("###############")
        print(source)
        print("###############")

Ability to customize <script> type

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

defer from MDN

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.

type="module" from MDN

This value causes the code to be treated as a JavaScript module. The processing of the script contents is deferred.

image

Support for mounting React components

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.

Append new attributes by default, add option to override behaviour

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 />

TemplateNotFound Exception and weired Text Error - BUG

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.

  1. I get a random 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.
  2. The components are rendered as text instead of actual html components. Not sure what causes this. Looks similar like using .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.

EDIT:

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.

Steps to Error:

  • open any pages without components; no matter how often -> works fine
  • open a page using components -> still works fine except for 2.
  • open any page (either reload or switch to any other page) -> TemplateNotFound Exception shows

About the error:

  • I am using flask
  • There are no exceptions before or after the TemplateNotFound Exception in the console.
  • When using {{ catalog.render_assets() }} in the Layout, the assets get linked and loaded correctly.
  • The error also occurs when {{ catalog.render_assets() }} is removed completely from everywhere.
  • When using {{ 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

Edit 2:

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.

More information on this:

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.

Steps to recreate:

  1. Create a blank flask project with the default project structure
  2. Create some jinja2 templates (without using jinjax)
  3. Add a folder for components and fill it with at least one jinjax component
  4. register the jinjax component folder with the catalog, but not the templates folder
  5. create a template using the component
  6. start the app
  7. navigate between the pages not using jinjax components without problems
  8. open a page using jinjax components - still works
  9. try to open any other page in the templates folder - flask breaks

Edit 3:

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.

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

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

image

the output is

image

Im new into the webdev with Flask.

hx-boost not working with JinjaX

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:

  • If HTMX is available it will do a AJAX request + partial update
  • If not or if someone opens the link directly, it will do a normal request + full render

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?

JinjaX does not work with autoescape mode

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())


Performance impact

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 :

  • JinaX, catalog.render : 423 µs ± 9.71 µs per loop
  • FastAPI Jinja2Templates : 44.3 µs ± 1.03 µs per loop

It'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 ?

Using `catalog.get_middleware` with FastAPI

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? 😊

Slots

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.

How to pass arguments down to components on 0.40

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 '}'

Django Setup?

Hi, are there any specific steps for installing to a django app?

attrs.render() attributes result it nested quoted string: p=""v""

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>

Issue with rendering hx-on:click

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

Documentation issue

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>

Correction

The correct way to import multiple files is by separating them using commas:

{#css lorem.css, ipsum.css #}
{#js foo.js, bar.js #}

What also doesn't work

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 #}

Regression: no longer able to have comments in component argument list

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 🤓

Calling async function from template

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.

Passing arguments and rendering wraps them in... one extra double quote in html?

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))

Django

How to install it?
This is a honeypot issue, please ignore.

maintenance status

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

Jinjax does not work with flask-assets by default

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.

Asset root path does not honor catalog folder root

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

image

component used, path <app>/<project>/jinja2/components-jx/Store/AddToCart.jinja

image

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.