DEMOS / LiveComponent

Inline Editing

Inline editing? Simple. Use LiveComponents to track if you're in "edit" mode, let the user update any fields on your entity, and save through a LiveAction.

Banana 🍌


The Banana 🍌 has 23 votes! Yum!

// ... use statements hidden - click to show
use App\Entity\Food;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\LiveComponent\ValidatableComponentTrait;

#[AsLiveComponent]
class InlineEditFood extends AbstractController
{
    use DefaultActionTrait;
    use ValidatableComponentTrait;

    /** This allows us to have a data-model="food.name" */
    #[LiveProp(writable: ['name'])]
    /** When we validate, we want to also validate the Food object */
    #[Assert\Valid]
    public Food $food;

    /** Tracks whether the component is in "edit" mode or not */
    #[LiveProp]
    public bool $isEditing = false;

    /**
     * A temporary message to show to the user.
     *
     * This is purposely not a LiveProp: this is a "temporary" value that
     * will only show one time.
     */
    public ?string $flashMessage = null;

    #[LiveAction]
    public function activateEditing()
    {
        $this->isEditing = true;
    }

    #[LiveAction]
    public function save(EntityManagerInterface $entityManager)
    {
        // if validation fails, this throws an exception & the component re-renders
        $this->validate();

        $this->isEditing = false;
        $this->flashMessage = 'Saved! Well, not actually because this is a demo (if you refresh, the value will go back).';

        // in a real app, we would save!
        // $entityManager->flush();
    }
}
<div {{ attributes }}>
    {% if flashMessage %}
        <div class="alert alert-success">{{ flashMessage }}</div>
    {% endif %}

    <div class="d-inline-flex">
        {% if isEditing %}
            {# The form isn't used, but allows the user to hit enter to save. #}
            <form class="row g-3">
                <div class="input-group mb-3 col">
                    {% set error = this.getError('food.name') %}

                    <div class="form-floating">
                        <input
                            type="text"
                            data-model="food.name"
                            class="form-control form-control-lg{{ error ? ' is-invalid' }}"
                            autofocus
                            id="food_name"
                        />
                        <label for="food_name">Food name</label>
                    </div>

                    <button
                        data-action="live#action:prevent"
                        data-live-action-param="save"
                        class="btn btn-outline btn-outline-{{ error ? 'danger' : 'secondary' }}"
                    >Save</button>

                    {% if error %}
                        <div class="invalid-feedback">{{ error.message }}</div>
                    {% endif %}
                </div>
                <div class="form-text">Clear the field to trigger validation!</div>
            </form>
        {% else %}
            <h2>{{ food.name }}</h2>
            <button
                data-action="live#action"
                data-live-action-param="activateEditing"
                class="btn btn-link"
                title="Click to edit!"
            >
                <twig:Icon name="pencil" />
            </button>
        {% endif %}
    </div>

    <hr>
    <p>
        The <strong>{{ food.name }}</strong> has {{ food.votes }} votes! Yum!
    </p>
</div>