EmilStenstrom / django-components

Create simple reusable template components in Django.
MIT License
1.1k stars 74 forks source link

Crazy idea: Auto-convert RadixUI from JSX to django-components with LLM? #656

Closed EmilStenstrom closed 4 days ago

EmilStenstrom commented 1 week ago

RadixUI is a great design system built up with fully designed JSX-components that can be composed into a full interface. It would we very nice to have a set of components that would compose like they do, and have a full system behind it like RadixUI does.

Idea: I wonder if we could use an LLM to convert all of RadixUI:s components from JSX to django-components?

JuroOravec commented 1 week ago

Unfortunately it won't be that simple :/ This is the same problem I faced with vuetify.

While converting the templating itself could be maybe done with LLM, the biggest challenge is translating the component JS logic. And without it, the components would be much less useful, maybe even stop working.

The RedixUI is built on React. E.g.see this component .

The component above sets up React bindings (useState, etc), and renders the template in a single step / single function call. This was the same with vuetify.

For this to be useable with Django components we'd need to do 4 things:

And only after all this is done, then the last step would be to generate Django components that render the HTML, whereas the HTML would initialize the runtime components once loaded in browser.

For vuetify it was about 3 weeks of part-time (if you're passionate and know the library) to get to the point where the only thing remaining is the django layer.

The cool thing about this approach tho, is that the same UI component library could then be used in other server-side framework and even other languages. E.g. one could have a Go web server and Go port that would define the same components as the Django one.

I recommend this to be built by someone who would be using this setup right away in their project(s). As there's a lot or things that can go wrong across the whole stack. So the easiest way to debug is if you just NEED the things to work.

EmilStenstrom commented 1 week ago

Thanks for sharing! I'm convinced that a straight conversation isn't a great idea unless you're willing to put in all the necessary work, like you have with vuetify!

Another approach is to see the list of components as inspiration and try to build a separate set of server-backed versions of them. That wouldn't lean too heavily on the actual JSX code, but more on the fact that a set of components like that is probably a pretty good view of what components are in most demand.

For fun, here's Claude 3.5 Sonnet's best translation of the Form component above into django-components. It actually knows the django-components API rather well, likely from reading the README during training...

START CLAUDE OUTPUT:

Certainly! I'll try to expand this component to include more features similar to the RadixUI version. This will involve creating multiple interconnected components and adding more advanced functionality. Keep in mind that this is still a simplified version and may not capture all the nuances of the RadixUI component, but it should give you a more comprehensive starting point.

Here's an expanded version:

from django_components import component
from django import forms
from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe
import json

# Custom form field for enhanced validation
class EnhancedCharField(forms.CharField):
    def __init__(self, *args, **kwargs):
        self.custom_validators = kwargs.pop('custom_validators', [])
        super().__init__(*args, **kwargs)

    def validate(self, value):
        super().validate(value)
        for validator in self.custom_validators:
            if not validator(value):
                raise ValidationError(f"Custom validation failed: {validator.__name__}")

# Main Form Component
@component.register("form")
class FormComponent(component.Component):
    template_name = "components/form.html"

    class Form(forms.Form):
        name = EnhancedCharField(
            max_length=100,
            custom_validators=[lambda x: len(x) > 3]
        )
        email = forms.EmailField()

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'] = self.Form()
        return context

    def post(self, request, *args, **kwargs):
        form = self.Form(request.POST)
        if form.is_valid():
            # Process the valid form data
            pass
        return self.render_to_response({'form': form})

    class Media:
        css = {
            'all': ('css/form.css',)
        }
        js = ('js/form.js',)

# Form Field Component
@component.register("form_field")
class FormFieldComponent(component.Component):
    template_name = "components/form_field.html"

    def get_context_data(self, field, **kwargs):
        return {
            'field': field,
            'field_id': f"id_{field.name}",
            'label_id': f"label_{field.name}",
            'error_id': f"error_{field.name}",
        }

# Form Label Component
@component.register("form_label")
class FormLabelComponent(component.Component):
    template_name = "components/form_label.html"

    def get_context_data(self, field, label_id, field_id, **kwargs):
        return {
            'field': field,
            'label_id': label_id,
            'field_id': field_id,
        }

# Form Control Component
@component.register("form_control")
class FormControlComponent(component.Component):
    template_name = "components/form_control.html"

    def get_context_data(self, field, field_id, error_id, **kwargs):
        return {
            'field': field,
            'field_id': field_id,
            'error_id': error_id,
        }

# Form Message Component
@component.register("form_message")
class FormMessageComponent(component.Component):
    template_name = "components/form_message.html"

    def get_context_data(self, field, error_id, **kwargs):
        return {
            'field': field,
            'error_id': error_id,
        }

# Form Submit Component
@component.register("form_submit")
class FormSubmitComponent(component.Component):
    template_name = "components/form_submit.html"

# Template files

# components/form.html
{% load django_components %}

<form method="post" id="dynamic-form">
    {% csrf_token %}
    {% for field in form %}
        {% component "form_field" field=field %}
    {% endfor %}
    {% component "form_submit" %}
</form>

# components/form_field.html
{% load django_components %}

<div class="form-field" data-field-name="{{ field.name }}">
    {% component "form_label" field=field label_id=label_id field_id=field_id %}
    {% component "form_control" field=field field_id=field_id error_id=error_id %}
    {% component "form_message" field=field error_id=error_id %}
</div>

# components/form_label.html
<label id="{{ label_id }}" for="{{ field_id }}">
    {{ field.label }}
</label>

# components/form_control.html
{{ field }}

# components/form_message.html
<div id="{{ error_id }}" class="error-message" style="display: none;"></div>

# components/form_submit.html
<button type="submit">Submit</button>

# Static files

# static/js/form.js
document.addEventListener('DOMContentLoaded', function() {
    const form = document.getElementById('dynamic-form');

    form.addEventListener('submit', function(e) {
        e.preventDefault();
        validateForm();
    });

    function validateForm() {
        const formData = new FormData(form);

        fetch(form.action, {
            method: 'POST',
            body: formData,
            headers: {
                'X-Requested-With': 'XMLHttpRequest',
            }
        })
        .then(response => response.json())
        .then(data => {
            if (data.valid) {
                form.submit();
            } else {
                displayErrors(data.errors);
            }
        });
    }

    function displayErrors(errors) {
        for (const [fieldName, fieldErrors] of Object.entries(errors)) {
            const errorElement = document.getElementById(`error_${fieldName}`);
            if (errorElement) {
                errorElement.textContent = fieldErrors.join(', ');
                errorElement.style.display = 'block';
            }
        }
    }
});

# static/css/form.css
.form-field {
    margin-bottom: 1rem;
}

.error-message {
    color: red;
    font-size: 0.8rem;
}

This expanded version includes:

  1. A main FormComponent that handles the overall form structure and submission.
  2. Separate components for form fields, labels, controls, and error messages.
  3. An EnhancedCharField that allows for custom validation.
  4. Client-side form submission and validation using JavaScript.
  5. Basic CSS for styling.

To use this in your Django project:

  1. Ensure you have django-components installed and configured.
  2. Place the component classes in an appropriate Python file (e.g., components.py).
  3. Create the HTML templates in your templates directory.
  4. Place the JS and CSS files in your static files directory.
  5. In your Django template, you can now use:
{% load django_components %}

{% component "form" %}

This expanded version provides more flexibility and closer alignment with the structure of the RadixUI component. However, it's still not a complete replication and lacks some of the more advanced features. To fully replicate the RadixUI component's functionality, you would need to implement:

  1. More advanced state management on the client-side.
  2. More sophisticated accessibility features.
  3. Support for nested form fields.
  4. A more comprehensive system for custom validators and error messages.

Remember that Django's server-side rendering model means that some features of the RadixUI component (which is designed for client-side rendering) may not be directly translatable or may require significant client-side JavaScript to implement.

JuroOravec commented 1 week ago

Ha, nice one!


Thanks for sharing! I'm convinced that a straight conversation isn't a great idea unless you're willing to put in all the necessary work, like you have with vuetify!

Another approach is to see the list of components as inspiration and try to build a separate set of server-backed versions > of them. That wouldn't lean too heavily on the actual JSX code, but more on the fact that a set of components like that is probably a pretty good view of what components are in most demand.

Yeah, totally can serve as inspo :)