ux icon indicating copy to clipboard operation
ux copied to clipboard

[LiveComponent] How to reload parent component when child component form submitted?

Open benr77 opened this issue 4 years ago • 6 comments

I have a child component which has an "add item" form, and when this submits, it persists a new entity. This works fine.

However, the parent component is showing a list of the persisted entities - how can I reload the parent component to show the updated list when the child component finished submitting? (it's a "Choose product" form, and then "Add to basket" type thing.)

I've read the docs about the parent and child both having a model property with the same name etc, but there is no model to share here. I would have thought this is a common behaviour, but I just cannot find a way to get it working. Any ideas?

Thanks

benr77 avatar Nov 13 '21 18:11 benr77

Hmm. This case makes sense - but I hadn't thought about it yet. So the question is, what's the best way to support it?

We could dispatch an event from the child when its model changes. Then, on the parent, we could add a way to listen to that event and call a re-render. For example:

<div class="something-inside-parent-element" data-action="live:update-model->live#$render">
    .. child component inside here
</div>

Unless I've messed something up (very possible), this should actually work. We already dispatch an event called live:update-model when a model is updated, which is how we handle the "parent and child both having a model property with the same name" situation. And so, the above code should "listen" to that same event and call $render on the parent controller when it happens.

Let me know if it works - it would be a great thing to document :)

weaverryan avatar Nov 16 '21 17:11 weaverryan

Thanks for your help Ryan. Unfortunately I don't think this is working, for my example at least.

I have tried surrounding the child component with a div as you suggest, but nothing is updating.

I've also tried putting the data-action on the containing div in the parent component but no joy here either.

<div {{ init_live_component(this) }} data-action="live:update-model->live#$render">
    {{ component('add_product_to_order', {
        orderId: this.order.orderId
    }) }}
</div>

Any thoughts?

benr77 avatar Nov 16 '21 19:11 benr77

I have only been able to get this to work with a less than elegant solution:

Create a custom Stimulus controller called basket-reload-controller or whatever:

import { Controller } from 'stimulus'

export default class extends Controller {
    reload() {
        setTimeout(() => {
            const event = new CustomEvent("update-basket")
            window.dispatchEvent(event)
        }, 300)
    }
}

Call this custom controller on click or whatever in the child component

<button data-controller="basket-reload" data-action="click->basket-reload#reload">

Then in the parent, on the DIV that contains the component, listen to this custom event to trigger re-render:

<div {{ init_live_component(this) }} data-action="update-basket@window->live#$render">

It's a bit hacky but it works.

benr77 avatar Nov 25 '21 07:11 benr77

Running into this as well right now while trying to create a somewhat more complex GUI that has some nested components with separate forms (building an RBAC assignment interface that has a child component where the user can type in an email address and then the parent interface wants to re-render to show roles and so on for that user).

benr77's workaround is interesting but I can't get it to work properly.

This seems like a very desirable feature. I also don't see an easy way to work around this (apart from inverting the entire component nesting tree, which doesn't make a whole lot of sense from a namespacing and software design perspective).

An event that is emitted by the child and can be caught by the parent to hint a re-render seems like the best way to go about this. Is there any progress on this issue?

Regardless, great adaption and looking forward to utilizing live components in future applications.

Sylvannes avatar Aug 11 '22 10:08 Sylvannes

An event that is emitted by the child and can be caught by the parent to hint a re-render seems like the best way to go about this. Is there any progress on this issue?

This is what I was thinking as well. Can you tell me the exact use-case you have? Like, when exactly do you want to re-render the parent component? Is it when a live action is completed by a child template? Or when a child template's model changes (and if so, any model change, or a specific model change)?

Btw, another option might just be a syntax like this:

<button
    data-action="live#$render"
    data-action-target="parent"
>

I'm a bit hesitant, since data-action is raw Stimulus right now... and so we would be adding something on top of Stimulus (data-action-target) that doesn't currently exist.

weaverryan avatar Aug 11 '22 19:08 weaverryan

Can you tell me the exact use-case you have?

Thanks for the reply. My exact use-case comes from trying to create many different reusable components with the form trait, for example an input field to find a user by email address. This component I try to use as a child in a larger interface that wants to obtain the result of the child interface (i.e. a found user).

In the parent component and child component I have an $email attribute and I then try to do something like....

{% if this.email %}
... do something with the email address...
{% else %}
    {{ component('form-email-text', {
        form: emailTextForm
    }) }}
{% endif %}

Where the child component is its own form with its own submit button that prevents the form submit but performs the live#update:

<div {{ attributes }} id="form-email-text">
    {{ form_start(form) }}
    {{ form_row(form.email, {
        attr: {
            'data-model': 'email',
            'data-action': 'live#update',
            'value': this.email
        }
    }) }}
    {{ form_errors(form.email) }}
    <button
            class="btn btn-primary w-100 btn-lg"
            data-action="live#action"
            data-action-name="prevent|submit"
            data-loading="addAttribute(disabled)"
    >
        {{ submitText }}
    </button>
    {{ form_end(form) }}
</div>

This already works well through the live:update-model event. The parent component upon re-rendering will have the email value and the flow works well. The only problem is there is no way (outside of constantly polling or creating an additional "re-render" button) to re-render the parent when the form in the child component submits.

Is it when a live action is completed by a child template? Or when a child template's model changes (and if so, any model change, or a specific model change)?

Basically, what would also work is if the parent "knows" when an attribute is changed through live:update-model or the fact that it uses updateDeferred is somehow configurable (e.g. configure the parent to explicitly re-render when an attribute cahnges through live:update-model. That way it doesn't cross any boundaries between the parent and child perhaps?

edit: One of the reasons setting a bigger component up with multiple child components this way seemed like an interesting approach to me is because it allows me to create a bunch of highly reusable components that each have their own form and do their own validation. These can then be used to string together larger GUI's.

I can work around the problem by incorporating the forms manually in the parent component instead of using nested child components of course, but that is much less reusable (building a large company CRM).

Sylvannes avatar Aug 12 '22 09:08 Sylvannes

This is what I was thinking as well. Can you tell me the exact use-case you have? Like, when exactly do you want to re-render the parent component?

I can give another use case as an example. I have a component (and respective doctrine entity) called Survey. A Survey has multiple child components (and respective one-to-many property 'questions') called Question. Each of the Question child components has a delete button. Clicking the button should send an event to the parent component containing the id of the child Question (entity). Parent component should then delete the Question entity and re-render.

StrahilRuychev avatar Dec 22 '22 14:12 StrahilRuychev

I think we need an event-like system like Livewire has: https://laravel-livewire.com/docs/2.x/events

But, for this exact situation:

Each of the Question child components has a delete button. Clicking the button should send an event to the parent component containing the id of the child Question (entity). Parent component should then delete the Question entity and re-render.

I think this could be accomplished IF we allowed you to call an action on a parent component. So, to take my previous comment, but modify it a bit, something like this:

<!-- inside the child component -->
<div>
    <button
        data-action="live#action"
        data-action-name="parent.deleteQuestion"
    >
</div>

This doesn't work currently - just throwing that out as an idea.

weaverryan avatar Jan 10 '23 15:01 weaverryan

I think this could be accomplished IF we allowed you to call an action on a parent component. So, to take my previous comment, but modify it a bit, something like this:

<!-- inside the child component -->
<div>
    <button
        data-action="live#action"
        data-action-name="parent.deleteQuestion"
    >
</div>

I have solved the problem by having a shared prop between the parent and the child - questionToDeleteId - holding the ID of the question. In the parent the prop is private and has a getter and setter. In the setter I do the actual deleting. It works, bit is a bit hacky. Your approach would be better. But events I think, would be best. As a side mention, I could not make the delete work if the child component (Question) has a prop question that holds the actual Question entity. When deleting the question, error is thrown as somehow the questions collection of the Survey object contains a null element in the place of the deleted Question object. It worked when I changed my approach and only passed the question id to the child component and loaded it from the db independently.

StrahilRuychev avatar Jan 10 '23 16:01 StrahilRuychev

Thanks for sharing that! Your use-case sounds like a perfectly nice one, and one that we need to get working smoothly and easily. I've added it to a "Live component demo ideas" list - https://gist.github.com/weaverryan/31a581652c8eec40bc1a8ee9ec7cc8f2

weaverryan avatar Jan 11 '23 19:01 weaverryan