symfony / ux

Symfony UX initiative: a JavaScript ecosystem for Symfony
https://ux.symfony.com/
MIT License
845 stars 310 forks source link

[LiveComponent] Reinitializing Choices.js plugins #1410

Closed disceney closed 9 months ago

disceney commented 9 months ago

Hello everyone,

I'm facing a small issue with one of my Twig Live Components. I have a simple form with a Choice/Input/Select field.

For the Select field, I want to implement the Choices.js plugin. So, I added an additional controller to my Twig Component to load the Choice plugin on "select" fields. Here's my code:

// form_controller.js

import { Controller } from "@hotwired/stimulus";
import Choices from "choices.js";

export default class extends Controller {
    connect () {
        this.initSelectInput();
    }

    initSelectInput () {
        let selectInput = $("form select");

        if (selectInput.length > 0) {
            selectInput.each(function () {
                new Choices(this);
            });
        }
    }
}
<div {{ attributes }}>
    {{ form_start(form, {attr: {
        "novalidate": true,
        "data-action": "live#action",
        "data-action-name": "prevent|save",
        "data-controller": "form"
    }}) }}

    <div class="card">
        <div class="card-body">
            <div class="row">
                {% include "admin/themes/input.html.twig" with {"form": form.identifier} only %}

                {% include "admin/themes/input.html.twig" with {"form": form.percentage} only %}

                {% if status.id %}
                    {% include "admin/themes/input.html.twig" with {"form": form.preview} only %}
                {% endif %}
            </div>
        </div>
    </div>

    <div class="card">
        <div class="card-body">
            <div class="row">
                {% include "admin/themes/input.html.twig" with {"form": form.status} only %}

                {% include "admin/themes/input.html.twig" with {"form": form.color} only %}

                {% include "admin/themes/input.html.twig" with {"form": form.font} only %}
            </div>
        </div>
    </div>

    {% if status.isLock == false %}
        {% include "admin/themes/button.html.twig" with {"type": "button", "translate": "user.status.action.submit", "color": "success", "icon": "form.submit", "disabled": this.hasErrors} only %}
    {% endif %}

    {{ form_widget(form, {"attr": {"class": "d-none"}}) }}
    {{ form_end(form) }}
</div>

So, during the first loading, everything went perfectly fine. Here is the result:

Capture d’écran 2024-01-19 à 3 49 49 PM

So far, everything is going perfectly fine (well, almost). The form reloads correctly to validate my constraints and display error messages. However, since I implemented the Choices.js plugin, whenever I change any of the values in my input fields, it automatically recreates another field without the plugin, resulting in a duplicate.

Capture d’écran 2024-01-19 à 3 51 40 PM

As you can see, it's the same duplicate field, but this time without using the plugin. I haven't been able to find a solution for the different elements, or perhaps I haven't understood them correctly. I would like to know if the method I've implemented is correct, or if I have been mistaken from the beginning?

Thank you for your assistance.

P.S.: I would like to clarify that I started from the initial elements provided here: https://ux.symfony.com/demos/live-component/auto-validating-form

weaverryan commented 9 months ago

Hi!

Hmm, yea, this stuff where an external plugin modifies the HTML can be tricky. LiveComponents using a smart rendering system but it still doesn't handle everything perfectly.

Question: When you live component re-renders (i.e. an Ajax all after changing a field), is the connect() method called again on your Stimulus controller? Also, if you could create a small reproducer project, that'd help me debug faster.

Cheers

disceney commented 9 months ago

Hello!

I apologize for the delayed response, but in fact, the "connect" method is not called after changing a field.

A code is worth a thousand words, so here is the project in question along with an example similar to my original code. You can see that as soon as you touch and modify the value of a field, you encounter the duplication issue I mentioned earlier.

Thank you very much for your help :)

https://github.com/disceney/SymfonyUX

smnandre commented 9 months ago

I think i understand.

You created a stimulus controller on the form so it trigger only once the "connect" event.

And it's executed after the live controller, which then consider the DOM updated by Choice.js as its starting point.

But, when the component rerenders, as it does not find the select anymore (the DOM is entirely updated by choices.js), it has to recreate what it does not consider as beeing the same element as originally.

I would advice the following things:

disceney commented 9 months ago

Hello @smnandre !

I wanted to let you know that thanks to your advice, I was able to resolve my issue. Here are the steps I followed:

  1. I moved my "form" controller directly onto the parent component
  2. I imported "@symfony/ux-live-component" and applied the "initialize" method with the "render:finished" event
  3. I added the "data-live-id" attribute to the input in the FormType
  4. I also specified a value for the "empty_data" attribute in the FormType
  5. I modified my "initSelectInput" function to only affect the Choices.js plugin if it hadn't already been initialized

To thoroughly test this method, I added a second Choice input and a Datetime input with flatpickr, and everything appears to be working correctly.

Could you please confirm if this is the correct functioning or if there are still optimization possibilities? Thank you very much for your assistance.

You can review the changes I made by following this link: https://github.com/disceney/SymfonyUX/commit/a353ad667403b213c56222adac5d06f89c512410

smnandre commented 9 months ago

Happy to help :)

Your modifications seems good. I'll take a look at your code later but if it works for you it's great!