symfony / ux

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

[LiveComponent] save model values on "onUpdated" param #1289

Open bastien70 opened 10 months ago

bastien70 commented 10 months ago

Hello, in my LiveComponent, I've this :

#[LiveProp(writable: true, onUpdated: 'onQueryUpdated')]
    public ?string $query = null;

    #[LiveProp]
    public array $items = [];

    public function onQueryUpdated($previousValue): void
    {
        $this->loadItems();
        $this->dispatchBrowserEvent('items:loaded');
    }

The goal is to simply trigger an event on my stimulus controller as soon as the "query" model is updated.

The "loadItems" method will do all the processing and assign to the "items" model.

The problem is that just before the dispatchBrowserEvent, if I dump the content of the "items" model, I have the new content, but not from my stimulus controller, which keeps the old value

window.addEventListener('items:loaded', (event) => {
            loadChart(this.component);
        });

        function loadChart(component)
        {
            var jsonData = component.getData('items');
            console.log(jsonData); // still the previous value
      }

Is this normal behavior? Is there a method to run from the component to tell it to "flush" the new model values ​​so that from the stimulus controller, I can retrieve it?

smnandre commented 10 months ago

Could you create a little small reproducer, it's easier to understand what you are doing / expecting.

But right now i'd say the component has not been re-rendered, so this.component still contain original items ?

bastien70 commented 10 months ago

This is a reproducer : https://github.com/bastien70/live-component-onUpdated-reproducer

Description

In this simple and unnecessary example, we will display a date in either French or English format, depending on the checkbox.

The update of the component's "date" model will be carried out at postMount but also when the "displayAsFrench" model is updated.

Installation

composer install yarn install --force yarn run build symfony serve

Files

Component :

#[AsLiveComponent('date', template: 'components/date.html.twig')]
final class DateComponent
{
    use DefaultActionTrait;
    use ComponentToolsTrait;

    #[LiveProp(writable: true, onUpdated: 'onDisplayAsFrenchUpdated')]
    public bool $displayAsFrench = false;

    #[LiveProp]
    public ?string $date = null;

    #[PostMount]
    public function postMount(): array
    {
        $this->reloadDate();

        return [];
    }

    public function onDisplayAsFrenchUpdated($previousValue): void
    {
        // $this->query already contains a new value
        // and its previous value is passed as an argument
        $this->reloadDate();
        dump($this->date);
        $this->dispatchBrowserEvent('refresh:date');
    }

    public function reloadDate(): void
    {
        $date = new \DateTime();

        if($this->displayAsFrench)
        {
            $this->date = $date->format('d/m/Y H:i:s');
        } else {
            $this->date = $date->format('Y-m-d H:i:s');
        }
    }
}

Stimulus controller :

// assets/controllers/some-custom-controller.js
// ...
import { Controller } from '@hotwired/stimulus';
import { getComponent } from '@symfony/ux-live-component';

export default class extends Controller {

    async initialize() {
        this.component = await getComponent(this.element);

        // Get live component
        updateValue(this.component);

        this.component.on('render:started', (component) => {
            // do something after the component re-renders
        });

        this.component.on('render:finished', (component) => {
            // loadChart(component);
        });

        window.addEventListener('refresh:date', (event) => {
            updateValue(this.component);
        });

        function updateValue(component) {
            let date = component.getData('date')

            console.log(date);
            document.querySelector('#dateValue').innerHTML = date;
        }
    }
}

template :

<div{{ attributes.add(stimulus_controller('display-date')) }}>
    <div class="form-check form-switch">
        <input class="form-check-input" type="checkbox" role="switch" id="displayAsFrench" data-model="displayAsFrench">
        <label class="form-check-label" for="displayAsFrench">Display as French</label>
    </div>

    <b>Value setted from controller :</b>
    <div id="dateValue"></div>
</div>

Usage

Just go to https://127.0.0.1:8000/ The date is displayed in English format. Click on the “Display as French” checkbox. The onUpdated is triggered, and the code of the onDisplayAsFrenchUpdated method is launched. The date is then updated. The value is dumped, you can find it in the profiler. A dispatchBrowserEvent is fired to update the contents of the HTML, based on the contents of "date", but it retains the old value. (check console)

Demo

This is what I have on the browser, and in the console initially. Good values

image image

Now, if I click on the checkbox, the onDisplayAsFrenchUpdated method is triggered. It renews the date, according the "displayAsFrench" new value, dump the value, and and dispatches the browser event which will refresh the HTML with the value.

In the profiler, I can see the (correct) new "date" value : image

But, in the browser, I've still the previous value :

image

If you're looking on the "console" tab, the "console.log(date)" displays the same old value :

image

In this function :

function updateValue(component) {
            let date = component.getData('date')

            console.log(date);
            document.querySelector('#dateValue').innerHTML = date;
        }

However, we have recovered the content of the "date" model from the component, but which does not seem to have been updated here whereas when we dump it from the component, we have the new value.

Hope this all helps you

smnandre commented 10 months ago

Yes i understand.

You should start by making it work without the custom controller JS first (with an input text for example)

As you do not have any model in the HTML for the display date, you should follow those instructions if you want to update your model... or it'll keep the first data i think:

--> https://symfony.com/bundles/ux-live-component/current/index.html#working-with-the-component-in-javascript

smnandre commented 10 months ago

Hmm but you're right, as you want to do it .. it probably won't work.

Maybe instead of this event cascade you can use a simple action ?

bastien70 commented 10 months ago

It was just a (completely stupid) example that makes no sense, to show the "problem". Yes I can do otherwise here, sure, but the goal is to show that there is a problem with the onUpdated.

So for cases where we really need onUpdated (me or someone else), the problem will be present :)

smnandre commented 10 months ago

Well you are right. For now what you want to do will not work..

i'd still advise you to use LiveActions when you want to update your model (without persisting it) ... or to persist your data as you seem to require state storage

carsonbot commented 4 months ago

Thank you for this issue. There has not been a lot of activity here for a while. Has this been resolved?

carsonbot commented 3 months ago

Friendly reminder that this issue exists. If I don't hear anything I'll close this.

carsonbot commented 3 months ago

Hey,

I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!