Skip to content

Single file components #183

@vb8448

Description

@vb8448
Contributor

Have you ever thought to implement a single file component, similar to vuejs ?

Eg.:

@component.register("calendar")
class Calendar(component.Component):
    # This component takes one parameter, a date string to show in the template
    def get_context_data(self, date):
        return {
            "date": date,
        }
   
    template = """
        <div class="calendar-component">Today's date is <span>{{ date }}</span></div>
    """
    css = """
        .calendar-component { width: 200px; background: pink; }
        .calendar-component span { font-weight: bold; }
    """
    js = """
        (function(){
            if (document.querySelector(".calendar-component")) {
                document.querySelector(".calendar-component").onclick = function(){ alert("Clicked calendar!"); };
            }
        })()
    """

Activity

EmilStenstrom

EmilStenstrom commented on Oct 17, 2022

@EmilStenstrom
Collaborator

This is a great idea. I have thought of ways of making this happen. I don't think I like having everything as strings, because you lose all syntax highlighting in editors. Is there a way to get syntax highlighting some other way?

vb8448

vb8448 commented on Oct 17, 2022

@vb8448
ContributorAuthor

you lose all syntax highlighting in editors. Is there a way to get syntax highlighting some other way?

Pycharm seems to support language injections and I guess also vscode does. Probably a specific extension will be needed.

EmilStenstrom

EmilStenstrom commented on Oct 18, 2022

@EmilStenstrom
Collaborator

I did some reading on this, and found a couple of crazy solutions such as https://github.com/twidi/mixt/ which have solved this in a mix of crazy and fantastic. But there's really nothing good out there. Language injections works in pycharm, but not in vscode. This also bypasses Django's template loading system, which loses caching.

Overall, I think using strings like in your quick example would quickly break down for bigger components. So right now it's a "wontfix".

I do really like to have a solution for this, but I just don't see a solution I'd like to recommend to users. Losing syntax highlighting in editors seems like the major one right now.

aymericderbois

aymericderbois commented on Nov 2, 2022

@aymericderbois

I love the idea of having the template directly in a component variable. This is especially useful when you want to make small components!
Unfortunately for syntax highlighting, I think there is no solution. Even with Pycharm, the support of language injections is limited.

Nevertheless, as this is the kind of functionality I could use, I tried to make some tests and I made a quick development that allows to have "inline template".

Here is a "countdown" component, with the template directly in the component :

@component.register("countdown")
class Countdown(component.Component):
    template_string = """
        <span
          data-ended-at-timestamp="{{ until.timestamp }}"
          x-data="{
                interval: null,
                init() {
                    this.endTimestamp = parseInt($el.dataset.endedAtTimestamp);
                    this.processCountdown();
                    this.interval = setInterval(() => this.processCountdown(), 1000);
                },
                processCountdown() {
                    const currentTimestamp = Math.floor(Date.now() / 1000)
                    const remainingTime = this.endTimestamp - currentTimestamp;
                    if ($refs.days === undefined && this.interval !== null) {
                        clearInterval(this.interval)
                    } else {
                        $refs.days.innerHTML = Math.floor(remainingTime / 60 / 60 / 24)
                        $refs.hours.innerHTML = Math.floor(remainingTime / 60 / 60) % 24
                        $refs.minutes.innerHTML = Math.floor(remainingTime / 60) % 60
                        $refs.seconds.innerHTML = Math.floor(remainingTime) % 60
                    }
                }
            }"
        >
            <span x-ref="days"></span>j
            <span x-ref="hours"></span>h
            <span x-ref="minutes"></span>ms
            <span x-ref="seconds"></span>s
        </span>
    """

    def get_context_data(self, until: datetime):
        return {
            "until": until,
        }

The commit applying the modification is here : aymericderbois@d650a92

The advantage of this implementation is to keep the use of the Django template system. I did this quickly, so I guess it could be greatly improved.

EmilStenstrom

EmilStenstrom commented on Nov 4, 2022

@EmilStenstrom
Collaborator

Thanks for taking the time to show how it could look like.

I really don't like the developer ergonomics of having to write html, css, and js inside of python strings. If there was (when there is?) a way to enable syntax highlighting in some clever way, I would be OK with this change. But as it stands, I don't think we should encourage that kind of code, and instead only support having separate files in a directory.

Since all the changes are in the Component class, I think you could make your own component base, and inherit from that inside your project? That way, we wouldn't have to build this as part of the core django-components.

EmilStenstrom

EmilStenstrom commented on Dec 17, 2022

@EmilStenstrom
Collaborator

Hmm. I keep thinking about how to get this done, because it would a lot simpler. Maybe we should write a custom VSCode extension that can highlight our custom file format?

EmilStenstrom

EmilStenstrom commented on Dec 19, 2022

@EmilStenstrom
Collaborator

I have opened the following PR to try to get editors to highlight this code: microsoft/vscode#169560

EmilStenstrom

EmilStenstrom commented on Feb 3, 2023

@EmilStenstrom
Collaborator

The Python extension people with VSCode are currently thinking about adding support for this (!). All we need right now are more upvotes on this ticket: https://www.github.com/microsoft/pylance-release/issues/3874

Is this something that you, or your colleague, or maybe your grandmother would find useful? Please add a 👍 to that issue, and tell your friends! We're doing this!!

rtr1

rtr1 commented on May 15, 2023

@rtr1

I was able to get syntax highlighting to work using https://github.com/samwillis/python-inline-source

image

EmilStenstrom

EmilStenstrom commented on May 16, 2023

@EmilStenstrom
Collaborator

@rtr1 This is FANTASTIC news! This solved the major pain point I see with having inline components like this. I'm definitely open to allowing inline css/javascript now! Maybe also template code?

rtr1

rtr1 commented on May 17, 2023

@rtr1

I got two Tailwind VS Code extensions to work inside Python strings.

Now with Tailwind, Alpine, and HTMX, I'm making Django components with only the .py file. I love it!

In VS Code's settings.json:

     "editor.quickSuggestions": {
        "strings": true
     },
     "tailwindCSS.includeLanguages": {
        "python": "html"
      },
      "[py]": {
        "tailwindCSS.experimental.classRegex": [
          "\\bclass=\"([^\"]*)\"",
        ]
    },
    "inlineFold.supportedLanguages": [
        "python"
    ]

And then using Tailwind CLI, in tailwind.config.js:

module.exports = {
  content: [
    './apps/**/templates/**/*.html',
    './components/**/*.html',
    './components/**/*.py'
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
EmilStenstrom

EmilStenstrom commented on May 17, 2023

@EmilStenstrom
Collaborator

@rtr1 Sounds great! Do you run a fork of django-components that supports inline strings? Would you mind submitting a PR?

7 remaining items

nerdoc

nerdoc commented on Jul 24, 2023

@nerdoc

I dislike the idea of single file components, not because it is not convenient, but it has major drawbacks for devs. Even when syntax highlighting works, you loose things like auto suggestions, click-to-navigate to code, IDE's understanding of the code structure etc.
There is another framework: Tetra, that solves this using the VSCode plugin. PyCharm does not works at all yet besides Syntax highlighting (manually). So -1 for this, efforts are better spent into other features IMHO...

rtr1

rtr1 commented on Jul 24, 2023

@rtr1

I see at least two distinct scenarios.

  1. One is what @nerdoc brings up. I think if one has extensive JS or CSS in a component, this a very valid point, and becomes more valid as the complexity of those pieces of the puzzle increase.

  2. Two is using a “low/no JS” approach, like utilizing Tailwind and Alpine. This scenario can basically already be done with existing tooling, IDE extensions, etc.

So it may be the case that, the only practical application of SFC is one which is already possible, and no further changes would be needed. At least, it’s worth considering if this is the case or not.

EmilStenstrom

EmilStenstrom commented on Jul 24, 2023

@EmilStenstrom
Collaborator

@nerdoc To be clear: The suggestion here is not to replace all existing multi-file components with single-file components. but to add single-file components as an option. If you have a complex component, that would likely be in multiple files, but simple ones could be in a single file. Again, this would be optional for you as a user, and I wouldn't want to break backwards compatibility by removing multi-file components.

nerdoc

nerdoc commented on Jul 25, 2023

@nerdoc

Yeah, thats what I thought. As long as the "traditional" approach stays, a single file approach would reallly be a perfect addition.

dylanjcastillo

dylanjcastillo commented on Dec 17, 2023

@dylanjcastillo
Collaborator

Would it be too crazy to have something in between? Not exactly a SFC, more like a single-file UI component + python component:

Something like this:

UI component:

<style>
    ...
</style>
<template>
    <div id="table-view-inner">
        <div class="flex flex-row justify-end">
            <p>
                Showing <span class="font-medium">{{ table_data|length }}</span> results
            </p>
        </div>
        <form class="flex flex-row align-baseline mb-4"
              hx-get="{% url 'table_view' %}"
              hx-target="#table-view-inner"
              hx-trigger="submit, change from:select[name='search_sorting']">
            ....
            <select id="search_sorting"
                    name="search_sorting"
                    class="w-1/5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
                <option value="most-recent"
                        {% if widget_state.search_sorting == 'most-recent' %}selected{% endif %}>Most recent</option>
                <option value="oldest"
                        {% if widget_state.search_sorting == 'oldest' %}selected{% endif %}>Oldest</option>
                <option value="most-cited"
                        {% if widget_state.search_sorting == 'most-cited' %}selected{% endif %}>Most cited</option>
                <option value="least-cited"
                        {% if widget_state.search_sorting == 'least-cited' %}selected{% endif %}>Least cited</option>
            </select>
        </form>
        {% for journal in table_data %}
            {% component 'table_row' journal=journal %}
        {% endfor %}
    </div>
</template>
<script>
    (function() {
        document.addEventListener("htmx:configRequest", function(event) {
            console.log(event.detail.triggeringEvent);
            if (event.detail.triggeringEvent.submitter) {
                event.detail.parameters["event_source"] =
                    event.detail.triggeringEvent.submitter.id;
            } else {
                event.detail.parameters["event_source"] =
                    event.detail.triggeringEvent.target.id;
            }
        });
    })();
</script>

Python Component:

from typing import Any, Dict
from django_components import component


@component.register("table_view")
class Calendar(component.Component, use_ui_component=True):
    template_name = "table_view/table_view.html"

    def get_context_data(self, table_data, widget_state) -> Dict[str, Any]:
        return {
            "table_data": table_data,
            "widget_state": widget_state,
        }

The UI component would be an HTML file, so you get syntax highlighting, etc for free but you also get more LOB on the UI side of things.

EmilStenstrom

EmilStenstrom commented on Dec 17, 2023

@EmilStenstrom
Collaborator

@dylanjcastillo With HTMX and Tailwind, you are not as likely to need any style and js files at all. So I guess you can do that already...

I think the value here is having the python file contain everything you need. If it's two or four files matters less. The VSCode extension above solves inlines color highlighting nicesly.

So to me, this is just a matter of who will write the PR?

dylanjcastillo

dylanjcastillo commented on Dec 19, 2023

@dylanjcastillo
Collaborator

Hey @EmilStenstrom, I agree that the single Python file is ideal, but you'd lose things like auto-formatting of the file (e.g., Prettier, djlint), which would reduce this to only very simple components.

OTOH, I'd like to get a bit more understanding of the codebase, and this could be a good starting point. So after I finish a project by the end of the month, I'll take a shot at this.

ekaj2

ekaj2 commented on Jan 5, 2024

@ekaj2
Contributor
  1. Neovim has this plugin which works great for this concept: https://github.com/AndrewRadev/inline_edit.vim/
  2. I'd much rather have a single component in an html file than a python file as including <script> tags is so common that most editors LSP + highlighting support it out of the box. The python bits could be inferred based on filename, etc. With HTMX and Tailwind, I almost always just copy/paste the boilerplate, rename the class + component to match my file name.
EmilStenstrom

EmilStenstrom commented on Jan 6, 2024

@EmilStenstrom
Collaborator

@ekaj2 Thanks for the neovim link, lots of good inspiration!

As I've said before, my view is that it's already possible to embed script and css in the template file if you want. Just inline the CSS with tailwind and put the js in a script tag. The python file is then likely small enough to be easy to do. I've created an issue to maybe make this workflow even easier #337, but I'd say it's a nice to have.

@ALL Single file components in the python file is probably best for small components, with just a few lines of template code, or CSS, which in my experience most components are. This means that automatic formatting within those strings is not as useful, although if someone finds a way to support that it would of course be nice.

So let's focus on putting everything in the python file for in this issue.

Design suggestion

There are four languages that we want to support in this single file: Python, HTML, CSS, and JS.

Python: Since we already are within a python file, that case is solved :) ✅
HTML: Importing Template from django.template as in #183 (comment) solves this nicely I think. No need for a separate template file.
CSS and JS: This is what I'd like to fix, supporting putting your code in strings instead of as links to separate files.

Syntax idea:

@component.register("location_menu")
class LocationMenu(component.Component):
    def get_context_data(self):
        return {'locations': Location.objects.all().order_by("name")}
    
    def get_template(self, *args, **kwargs):
        return Template("""
            <ul class="menu">
                {% for location in locations %}
                    <li><a href="/navigate/{{ location.id }}/">{{ location.name }}</a></li>
                {% endfor %}
            </ul>
        """)

    def get_css(self, *args, **kwargs):
        return """
            .menu {
                display: inline-block;
                width: 100px;
            }
        """

    def get_js(self, *args, **kwargs):
        return """
            window.addEventListener("load", (event) => {
                console.log("Page loaded!");
            });
        """

And maybe there should be an even simpler API where you just put your code in a variable with a special name:

@component.register("location_menu")
class LocationMenu(component.Component):
    def get_context_data(self):
        return {'locations': Location.objects.all().order_by("name")}
    
    template = """
        <ul class="menu">
            {% for location in locations %}
                <li><a href="/navigate/{{ location.id }}/">{{ location.name }}</a></li>
            {% endfor %}
        </ul>
    """)
    
    css = """
        .menu {
            display: inline-block;
            width: 100px;
        }
    """
    
    js = """
        window.addEventListener("load", (event) => {
            console.log("Page loaded!");
        });
    """
EmilStenstrom

EmilStenstrom commented on Jan 14, 2024

@EmilStenstrom
Collaborator

Fixed by #361, thanks @dylanjcastillo!

EmilStenstrom

EmilStenstrom commented on Jan 14, 2024

@EmilStenstrom
Collaborator

This is now released as version 0.32 on PyPI: https://pypi.org/project/django-components/0.32/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @EmilStenstrom@aymericderbois@nerdoc@dylanjcastillo@rtr1

        Issue actions

          Single file components · Issue #183 · django-components/django-components