[LiveComponent] data-loading and action with parameter
Hi,
Is it possible to use data-loading with an action that has a parameter?
I have a list of items with one button per item.
Each button calls the same action with an item ID.
I would like to be able to use data-loading only on the item button when I click on a button.
I haven't been able to find a way to do this.
For example :
{% for item in items %}
<button {{ live_action('removeItem', { id: item.id }) }}
data-loading="action(removeItem)|addAttribute(disabled)">
Delete
</button>
{% endfor %}
In this example, all buttons are disabled.
Thank's for your help.
In this example, all buttons are disabled.
I'm surprised about this behaviour.
div(`B`)
- item(`B1`)
- item(`B2`) removeItem
- item(`B3`)
Here if the removeItem action was called for item(B2)
... only item(B2) should get the disabled attribute during the request.
But for you B1 B2 and B3 are disabled during the action.
Do you have a nesting / recursive list of items ? If "no", we may have a bug here..
Thanks for your response @smnandre
Here's a simple example :
CategoryList.php :
<?php
namespace App\Twig\Components\Category;
use App\Entity\Category;
use App\Form\CategoryType;
use App\Repository\CategoryRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentWithFormTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;
#[AsLiveComponent('category_list')]
class CategoryList extends AbstractController
{
use DefaultActionTrait;
use ComponentWithFormTrait;
#[LiveProp]
public ?Category $initialFormData = null;
private array $categories;
public function __construct(
private readonly CategoryRepository $categoryRepository,
private readonly EntityManagerInterface $em,
) {
}
protected function instantiateForm(): FormInterface
{
return $this->createForm(CategoryType::class, $this->initialFormData);
}
public function getCategories(): array
{
return $this->categoryRepository->findAll();
}
#[LiveAction]
public function save(): void
{
$this->submitForm();
/** @var Category $category */
$category = $this->getForm()->getData();
$this->em->persist($category);
$this->em->flush();
}
#[LiveAction]
public function delete(#[LiveArg] int $id): void
{
sleep(1);
$category = $this->categoryRepository->find($id);
$this->em->remove($category);
$this->em->flush();
}
}
category_list.html.twig :
<div {{ attributes }}>
{{ form_start(form, {
attr: {
'data-action': 'live#action:prevent',
'data-live-action-param': 'save'
}
}) }}
{{ form_row(form.text) }}
<button type="submit">Add</button>
{{ form_end(form) }}
<ul>
{% for category in this.categories %}
<li>
{{ category.text }}
<button {{ live_action('delete', {'id':category.id}) }}
data-loading="action(delete)|addAttribute(disabled)">
Delete
</button>
</li>
{% endfor %}
</ul>
</div>
If I have several categories, when I click on the delete button for one of the categories, all the delete buttons are disabled, whereas only the delete button for that category should be disabled.
Hmm okay.
The current behaviour makes sense to me as the entire component is waiting for a response and loading... ... but I can understand what you're trying to do here.
(just to be curious: is the goal to prevent double submissions here?)
So, what could be done here is a custom modifier that would restrict the action(..) I suppose.
(illustration purposes only, no idea the best syntax would be)
<button
{# add attribute only if the element did trigger the action "delete" #}
data-loading="action(delete):self|addAttribute(disabled)">
I'm taking inspiration from the event modifier from Stimulus here, we should probably use different syntax here to avoid confusion
This would only require little changes in the LoadingPlugin, enforcing element === targetElement for the modifier if ":self" is detected ?
Would you like to implement something like this ? I can help if you need :)
That's exactly right :)
The idea is to prevent double-clicking or, in some cases, if the process takes a little longer, such as when calling an API, for example.
The type of syntax you suggest seems really great 👍
It would be really cool to be able to do it as you suggest :)
The idea is to prevent double-clicking or, in some cases, if the process takes a little longer, such as when calling an API, for example.
Here your live component won't send any other request before it receives the response for the "delete" action, so locking the other delete buttons too does not seem such a bad idea to me (the entire LiveComponent is loading, not just one button). And doing so would solve the "double-click" problem.
So to answer your other question, if you are planning "long tasks" (more than 1 second), you should probably use an isolated component. LiveComponent are meant to be.... live, and work best when quick exchanges with the server can be made.
On an user experience level, a long task probably deserves some feedback: an indicator showing the request has been received and processing.
Other potential idea to prevent double-submissions, did you try using Stimulus :once modifier of ? (did not test myself but see no reason it would not work)
<button data-action="live#action:once"
Or you could add a dedicated stimulus controller specialized on locking/disabling buttons for a time after they are pressed
Hi @smnandre,
Thank's for your propositions :)
Here your live component won't send any other request before it receives the response for the "delete" action, so locking the other delete buttons too does not seem such a bad idea to me (the entire LiveComponent is loading, not just one button). And doing so would solve the "double-click" problem.
That is indeed a possibility, but from a user experience perspective, it feels like all lines are affected by the deletion.
Here is a very simple example that shows what can be done with live components.
In general, it would be really useful to be able to apply data-loading="action(...) to the current button when the same action is called several times in a component.
Your suggestion :
<button
{# add attribute only if the element did trigger the action "delete" #}
data-loading="action(delete):self|addAttribute(disabled)">
seems like a good idea to me if it's feasible. Perhaps use :current instead of :self, for example, to avoid confusion with :self in stimulus.
So to answer your other question, if you are planning "long tasks" (more than 1 second), you should probably use an isolated component. LiveComponent are meant to be.... live, and work best when quick exchanges with the server can be made.
On an user experience level, a long task probably deserves some feedback: an indicator showing the request has been received and processing.
Indeed, in this case, a dedicated component would allow for better management.
I tested the solution :
<button data-action="live#action:once"
This effectively prevents double-submissions when I click on it. However, something strange happens afterwards. The line that is deleted is replaced by another line (which is normal). Then, when I click on the delete button on that line, nothing happens. It's as if the button on the replacement line is no longer active.
Then, when I click on the delete button on that line, nothing happens. It's as if the button on the replacement line is no longer active.
Hmm ... could you try with an unique id on your button ? This may fix this, with no absolute certitude.
<button id="delete-{{ category.id }}" data-action=" ..
I encountered a similar problem: we have a list of links, each of which calls LiveAction with its own parameters. After clicking on a link, it is required to display a loading indicator (spinner) next to that link. The current data-loading implementation does not allow us to accurately determine which element was clicked.
After analyzing the code, I concluded that an implementation using self would require significant changes, as the data-loading logic is implemented through a plugin and hooks system. The clicked element is only available during the click handling phase.
Currently, I have solved the problem by adding a condition for remapping the action name. Before creating the request, the original method name is saved, and everything that goes after the special symbol "-" is removed from the request (to avoid "method not found" errors). In the plugin, the reverse mapping occurs, and if the original method name matches the method name in data-loading, then the corresponding class is applied.
<a href="#" data-action="live#action" data-live-action-param="addFilter-1" data-live-id-param="1" data-loading="action(addFilter-1)|addClass(btn-with-spinner-right)">Item 1</a>
<a href="#" data-action="live#action" data-live-action-param="addFilter-2" data-live-id-param="2" data-loading="action(addFilter-2)|addClass(btn-with-spinner-right)">Item 2</a>
Modified repo availaible here https://github.com/casskir/ux-live-component.
It's temporary solution.
Hi @smnandre,
Then, when I click on the delete button on that line, nothing happens. It's as if the button on the replacement line is no longer active.
Hmm ... could you try with an unique id on your button ? This may fix this, with no absolute certitude.
<button id="delete-{{ category.id }}" data-action=" ..
I confirm that adding a unique id works with once :). This is very useful for preventing multiple requests when clicking the same button several times.
However, I think it would still be useful to be able to use data-loading individually when the same action is called several times (e.g., displaying a loading (spinner) like @casskir).
I confirm that adding a unique id works with once :).
Nice! Happy to read that