Triumph-Tech / Triumph-Helix

A repo that allows for reporting issues related to Helix.
https://helix.triumph.tech
5 stars 0 forks source link

Lava-Form Validation Issue with Nested HTMX Element #4

Open zackdutra opened 2 days ago

zackdutra commented 2 days ago

Description

When a lava-form has an htmx element inside of it as well as a text input lava shortcode, validation is triggered on the initial load for the input, and that nested htmx is never rendered.

Actual Behavior

When a lava-form has an htmx element inside of it as well as a text input lava shortcode, validation is triggered on the initial load for the input, and that nested htmx is never rendered. Replacing with

fixes the issue.

Expected Behavior

Validation on the text input should only occur when the form is submitted.

Steps to Reproduce

  • Create a new lava application with a slug of test

  • Create an endpoint with a slug of form, HTTP method of GET, and Security Mode of Application View. Set the Code Template to:

    <lava-form>
    {[ textbox name:'eventname' label:'Event Name' value:'' ]}
    <div id="test" hx-get="^/test/second-endpoint" hx-trigger="load">Loading...</div>
    </lava-form>
  • Create another endpoint with a slug of second-endpoint, HTTP method of GET, and Security Mode of Application View. Set the Code template to anything, such as "content goes here"

  • Create a page with a Lava Application Content block. Set the application to the one you just created and set the content to:

    <div id="main" hx-get="^/test/form" hx-trigger="load" >
    </div>
  • On saving and reloading the page, you should see the rendered content below, as well as a successful network request to the form endpoint image

  • Go back to the form endpoint and change the lava-form opening and closing tags to regular HTML form closing tags, such as below:

    <form>
    {[ textbox name:'eventname' label:'Event Name' value:'' ]}
    <div id="test" hx-get="^/test/second-endpoint" hx-trigger="load">Loading...</div>
    </form>
  • Notice the page now loads as expected, with network requests to both of our endpoints. image

Issue Confirmation

  • [X] Perform a search on the Github Issues to see if your bug or enhancement is already reported.
  • [X] Try to reproduce the problem on a fresh install or on the demo site.

Rock Version

16.6

zackdutra commented 2 days ago

If I am incorrectly using HTMX to load additional fields into my form, please let me know what the proper way to format this is.

jonedmiston commented 2 days ago

Couple of thoughts on this:

  1. In the use case above I would recommend that you return the entire lava form from the ^/test/second-endpoint. Would seem to make more sense or have the 'loading' logic in the actual content load on the block.
  2. I do think though that we need to limit form validation to only non-GET requests. We could also consider adding our own attribute that you could add to your trigger that could optionally suppress validation, even for non-GET requests. Open for feedback on this.
zackdutra commented 2 days ago

Hey Jon, thanks for those thoughts. The actual endpoints I'm using have quite a bit more content, this is just a simplified example. In the real version of the second endpoint, I have the content below as an image file upload section of the form. It's unfinished and a little sloppy - I only got through some basic setup when I hit this issue. I post the full content just to give you an idea of the length of content. Ideally I'd be able to load in this field set separately because of how much content there is.

Also in the real world use case, the first endpoint that I mentioned is only exposed after clicking on "edit", after which you get the name input and the image uploads.

I'm open to your feedback on how to best handle that.

{% comment %}Set file type Id here.{% endcomment %}
{% assign fileTypeId = 3 %}

{% comment %}Load existing artwork from attributes.{% endcomment %}
{% assign eventItemId = QueryString.eventitemid %}
{% eventitem Id:'{{ eventItemId }}' %}{% endeventitem %}
{% if eventitem %}
    {% assign artworkSquare = eventitem | Attribute:'ArtworkSquare' %}
    {% assign artworkWide = eventitem | Attribute:'ArtworkWide' %}
    {% assign artworkBanner = eventitem | Attribute:'ArtworkBanner' %}
{% endif %}

{% capture defaultImageSvg %}
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
        <path fill-rule="evenodd" d="M16.3 1A2.7 2.7 0 0 1 19 3.7v12.6a2.7 2.7 0 0 1-2.7 2.7H3.7A2.7 2.7 0 0 1 1 16.3V3.7A2.7 2.7 0 0 1 3.7 1h12.6zm-7.708 9.83l-5.466 4.192a1.12 1.12 0 0 1-.125.082L3 16.3a.7.7 0 0 0 .7.7h12.6a.7.7 0 0 0 .7-.7v-2.281l-.033-.022-1.976-1.527-2.045 1.535a1 1 0 0 1-1.22-.014l-.091-.083-3.043-3.078zM16.3 3H3.7a.7.7 0 0 0-.7.7v8.897l5.074-3.89a1 1 0 0 1 1.227.007l.093.083 3.05 3.085 1.956-1.469a1 1 0 0 1 1.1-.066l.111.075L17 11.495V3.7a.7.7 0 0 0-.7-.7zm-2.8 3a1.5 1.5 0 1 1-.001 3.001A1.5 1.5 0 0 1 13.5 6z"></path>
    </svg>
{% endcapture %}

{% comment %}Define JSON data for uploaders{% endcomment %}
{% assign uploadersJson = '[ 
    { "id": "1", "width": "1024", "height": "1024", "aspectRatio": "1-1" }, 
    { "id": "2", "width": "1920", "height": "1080", "aspectRatio": "16-9" }, 
    { "id": "3", "width": "1920", "height": "692", "aspectRatio": "1920-692" } 
]' %}

{% assign uploaders = uploadersJson | FromJSON %}

{% comment %}Render each uploader using the JSON data{% endcomment %}
{% for uploader in uploaders %}
    <div class="drop-zone" data-width="{{ uploader.width }}" data-height="{{ uploader.height }}" onclick="document.getElementById('fileInput{{ uploader.id }}').click();">
        <div id="previewContainer{{ uploader.id }}" class="preview-container aspect-ratio-{{ uploader.aspectRatio }}">
            {{ defaultImageSvg }}
        </div>
        <input type="file" id="fileInput{{ uploader.id }}" class="input-button-hidden" hidden onchange="uploadFile(this, 'previewContainer{{ uploader.id }}')">
        <p>
            <span><strong>Drag and drop your file here or click to select a file.</strong></span><br>
            <span>{{ uploader.width }} x {{ uploader.height }} • 15 MB maximum</span><br>
            <span>Supported: PNG, JPG</span>
        </p>
    </div>
{% endfor %}

<script>
function uploadFile(fileInputElement, previewContainerId) {
    var fileInput = fileInputElement;
    var file = fileInput.files[0];
    var dropZone = fileInput.closest('.drop-zone');
    var requiredWidth = parseInt(dropZone.getAttribute('data-width'));
    var requiredHeight = parseInt(dropZone.getAttribute('data-height'));

    // Check if the file is an image
    if (file && file.type.match('image.*')) {
        var img = new Image();
        img.onload = function() {
            // Check the image dimensions
            if (img.width === requiredWidth && img.height === requiredHeight) {
                // Proceed to upload
                var formData = new FormData();
                formData.append('file', file);

                var binaryFileTypeId = {{ fileTypeId }};

                fetch(`/api/BinaryFiles/Upload?binaryFileTypeId=${binaryFileTypeId}`, {
                    method: 'POST',
                    body: formData,
                    headers: { 'Accept': 'application/json' }
                })
                .then(response => response.json())
                .then(data => {
                    console.log('Success:', data);
                    var reader = new FileReader();
                    reader.onload = function (e) {
                        var previewContainer = document.getElementById(previewContainerId);
                        var imgElement = previewContainer.querySelector('img.preview-image');
                        if (!imgElement) {
                            imgElement = document.createElement('img');
                            imgElement.classList.add('preview-image');
                            previewContainer.innerHTML = ''; // Clear the SVG icon
                            previewContainer.appendChild(imgElement);
                        }
                        imgElement.src = e.target.result;
                    };
                    reader.readAsDataURL(file);
                })
                .catch(error => {
                    console.error('Error:', error);
                    var previewContainer = document.getElementById(previewContainerId);
                    var imgElement = previewContainer.querySelector('img.preview-image');
                    if (imgElement) {
                        imgElement.remove(); // Remove the image if the upload fails
                    }
                    alert('Failed to upload the image.');
                });
            } else {
                // Image dimensions do not match
                alert(`Image must be exactly ${requiredWidth} x ${requiredHeight} pixels.`);
                // Clear the file input
                fileInput.value = '';
            }
        };
        img.onerror = function() {
            alert('Invalid image file.');
            fileInput.value = '';
        };
        // Read the file as Data URL
        img.src = URL.createObjectURL(file);
    } else {
        alert('Please select a valid image file.');
        fileInput.value = '';
    }
}

// Adding Drag and Drop functionality
var dropZones = document.querySelectorAll('.drop-zone');
dropZones.forEach(function(dropZone) {
    dropZone.addEventListener('dragover', function(e) {
        e.preventDefault();
        dropZone.classList.add('active');
    });

    dropZone.addEventListener('dragleave', function(e) {
        dropZone.classList.remove('active');
    });

    dropZone.addEventListener('drop', function(e) {
        e.preventDefault();
        dropZone.classList.remove('active');
        var files = e.dataTransfer.files;
        if (files.length) {
            // Find the corresponding file input
            var fileInput = dropZone.querySelector('input[type="file"]');
            fileInput.files = files;
            // Trigger the upload function
            uploadFile(fileInput, dropZone.querySelector('.preview-container').id);
        }
    });
});

</script>
zackdutra commented 2 days ago

When loaded correctly with the form tag, the real use case looks like the form below. image