symfony / ux

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

[LiveComponent] CKEditor field not updated #606

Closed bastien70 closed 1 year ago

bastien70 commented 1 year ago

Hello,

In a project, we have a step form, with several tabs.

On one of the tabs we have a wysiwyg field. We opted for CKEditor 4.

We are using LiveComponent, so if we go to a field, modify its value, then leave that field, the ajax request should start.

It works fine with all other fields except wysiwyg.

Here is a very minimalist small reproducer of the code set up :

The FormType :

<?php
class SchoolClassStep3Type extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('goals', CKEditorType::class, [
                'required' => false,
                'label' => 'Goals',
            ])
            ->add('applyUrl', UrlType::class, [
                'required' => false,
                'label' => 'Apply Url',
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => SchoolClass::class,
            'attr' => [
                'id' => 'school-class-form',
                'data-live-ignore' => '',
            ],
        ]);
    }
}

The LiveComponent :

<?php
#[AsLiveComponent('school_class_form', template: 'components/school_class_form.html.twig', csrf: false)]
class SchoolClassFormComponent extends AbstractController
{
    use DefaultActionTrait;
    use ComponentWithFormTrait;

    #[LiveProp(fieldName: 'field_school_class')]
    public ?SchoolClass $schoolClass = null;

    #[LiveProp(fieldName: 'field_step')]
    public ?int $step = 1;

    #[LiveAction]
    public function getTemplate(): string
    {
        return 'components/school-class/form_step'.$this->step.'.html.twig';
    }

    public static function getFormFromStep(int $step = 1): string
    {
        return match ($step) {
            1 => SchoolClassStep1Type::class,
            2 => SchoolClassStep2Type::class,
            3 => SchoolClassStep3Type::class,
            4 => SchoolClassStep4Type::class,
            5 => SchoolClassStep5Type::class,
            6 => SchoolClassStep6Type::class,
            default => throw new \Exception('Invalid step for SchoolClassFormComponent'),
        };
    }

    protected function instantiateForm(): FormInterface
    {
        return $this->createForm(self::getFormFromStep($this->step), $this->schoolClass);
    }

    #[LiveAction]
    public function save(#[LiveArg] ?int $nextStep, EntityManagerInterface $em): Response
    {
        // Submit form
        $this->submitForm();

        // Get and save school class
        $schoolClass = $this->getFormInstance()->getData();
        $schoolClass->validate();
        $em->persist($schoolClass);
        $em->flush();

        // Go to next step
        return $this->redirectToRoute('admin', [
            EA::ENTITY_ID => $schoolClass->getId(),
            EA::CRUD_ACTION => 'advancedForm',
            EA::CRUD_CONTROLLER_FQCN => SchoolClassCrudController::class,
            'step' => $nextStep,
        ]);
    }
}

And our template for the step :

{% form_theme form 'admin/bootstrap_form_theme.html.twig' %}
{{ form_start(form) }}
<div class="row">
    <div class="col-md-12 mb-4">
        <div class="form-panel">
            <div class="form-panel-body mt-2">
                <h6 class="mt-3">Les objectifs</h6>
                {{ form_widget(form.goals) }}
            </div>
        </div>
    </div>
    <div class="col-md-12 col-lg-6 col-xl-6 col-xxl-6 mb-4">
        <div class="form-panel">
            <div class="form-panel-header">
                <div class="form-panel-title">
                    <a href="#" class="not-collapsible">Les urls</a>
                </div>
            </div>
            <div class="form-panel-body mt-2">
                {{ form_row(form.applyUrl) }}
            </div>
        </div>
    </div>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}

And here is a demonstration video. In this one there are more fields than I put in the example above, it was to avoid putting useless info.

But if you watch the video carefully, if I update my URL type fields, the AJAX request is launched. If I update the CKEditor field, no Ajax request.

If I change tabs and come back to the one I was on, my URL type field has been updated, not CKEditor's.

Obviously I'm talking about CKEditor here, but it's exactly the same with the basic wysiwyg provided by EasyAdmin

https://user-images.githubusercontent.com/53140475/207026317-bee58dd6-b491-47c8-abd9-a6b1826383fe.mp4

weaverryan commented 1 year ago

Hi there!

Here's my first guess at the problem :) - look at the bottom of this section - https://symfony.com/bundles/ux-live-component/current/index.html#working-with-the-component-in-javascript

Basically, when you type into CKEditor, behind the scenes, it probably sets the value on the real (hidden) textarea behind the scenes (and then when your form submits, everything is happy!). But, when CKEditor does this, my guess is that it simply does yourField.value = thenewValue. This, unfortunately, does not fire the input JS event like normally typing into a box would. And so, you need to do extra work so that LiveComponents is aware of this change. You'll likely need to write a little extra JS for this.

Let me know if that helps :)

Cheers!

bastien70 commented 1 year ago

Hello !

Okay thank you, we will see that! :)

weaverryan commented 1 year ago

Please let me know if this solves the problem :)

bastien70 commented 1 year ago

Hello @weaverryan , currently, we updated our assets/app.js file by adding this :

CKEDITOR.on('instanceReady', function (ev) {
    ev.editor.on('blur', function (ev) {
        console.log('blur event fired');
        const el = ev.editor.element;
        const textarea = document.getElementById(el.$.id);
        textarea.innerHTML = ev.editor.getData();
        textarea.dispatchEvent(new Event('change', { bubbles: true }));
        ev.editor.updateElement();
    });
});

It works ! :D

weaverryan commented 1 year ago

Yup! That's really annoying to need that, but that's the idea! Having a UX component for CKEditor, where we do something like this FOR you, would be awesome. Thanks for reporting back!