Skip to content

[LiveComponent] Advanced Polling Features #2965

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: 2.x
Choose a base branch
from

Conversation

xDeSwa
Copy link
Contributor

@xDeSwa xDeSwa commented Jul 30, 2025

Q A
Bug fix? no
New feature? yes
Docs? yes
Issues
License MIT

Added support for poll lifecycle hooks and pollingDirector control in UX LiveComponent

This PR introduces the following updates to the LiveComponent polling system:

  • Added limit(n) modifier to limit the number of poll iterations.
  • Added new lifecycle events: poll:started, poll:running, poll:paused, poll:stopped, and poll:error.
  • Added the pollingDirector API to start, stop, pause, or resume polling via JavaScript.
  • Added usage examples using Stimulus controllers.
  • Updated documentation with examples and descriptions for all new methods and hooks.

This enhancement introduces support for limiting polling cycles using the limit modifier. Once the limit is reached, the polling will automatically stop.

Additionally, new lifecycle hooks (poll:started, poll:running, poll:paused, poll:stopped, poll:error ) provide full control over the polling process via JavaScript (e.g. Stimulus controllers).

Developers can now track the polling count value to build dynamic UI components such as counters, countdown timers, or progress bars.

Preview:

aaa.mp4

Basic usage

By default, polling runs the $render action every 2000 milliseconds (2 seconds).


    {# Unlimited polling every 2 seconds (default) using $render #}
    <div {{ attributes }} data-poll>...</div>

    {# Poll every 5 seconds, up to 20 times, using $render #}
    <div {{ attributes }} data-poll="delay(5000)|limit(20)|$render">...</div>

    {# Poll a custom action (savePost) every 3 seconds, up to 10 times #}
    <div {{ attributes }} data-poll="delay(3000)|limit(10)|savePost">...</div>

Available Modifiers

  • delay(ms) — The delay between polls in milliseconds (default: 2000)
  • limit(n) — Maximum number of times to run the poll (default: unlimited)
  • actionName — The component action to call (default: $render)

Poll Hooks

The component emits lifecycle hooks during polling. You can listen to these using JavaScript, for example:

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

    export default class extends Controller {
        async connect() {
            this.component = await getComponent(this.element);

            // Disable default error window (optional)
            this.component.on('response:error', (backendResponse, controls) => {
                controls.displayError = false;
            });

            this.component.on('poll:started', ({ actionName, limit }) => {
                console.log(`Polling started: ${actionName}, limit: ${limit}`);
            });

            this.component.on('poll:running', ({ actionName, count, limit }) => {
                console.log(`Polling running: ${actionName} (${count}/${limit})`);
            });

            this.component.on('poll:paused', ({ actionName, count, limit }) => {
                console.log(`Polling paused: ${actionName}`);
            });

            this.component.on('poll:stopped', ({ actionName, finalCount, limit }) => {
                console.log(`Polling stopped: ${actionName}, total runs: ${finalCount}`);
            });

            this.component.on('poll:error', ({ actionName, finalCount, limit, errorMessage }) => {
                console.error(`Polling error on ${actionName}: ${errorMessage}`);
            });
        }
    }

These events are dispatched on the component and can be handled using Stimulus. You must retrieve the component instance with getComponent(this.element) before accessing event listeners.

Handling Poll Actions (Start, Stop, Pause, Resume)

Polling can be programmatically managed using the pollingDirector API.

This allows you to start, pause, resume, or stop polling dynamically for a given action.


    <div {{ attributes.defaults(stimulus_controller('poll')) }} data-poll="delay(5000)|limit(50)|$render">
        <button type="button" data-action="click->poll#start" data-poll-action-param="$render">Start</button>
        <button type="button" data-action="click->poll#stop" data-poll-action-param="$render">Stop</button>
    </div>
    // controllers/poll_controller.js
    import { Controller } from '@hotwired/stimulus';
    import { getComponent } from '@symfony/ux-live-component';

    export default class extends Controller {
        static values = {
            action: String
        }

        async start(event) {
            const actionName = event.params.action;
            (await getComponent(this.element)).pollingDirector.start(actionName);
        }

        async stop(event) {
            const actionName = event.params.action;
            (await getComponent(this.element)).pollingDirector.stop(actionName);
        }

        async pause(event) {
            const actionName = event.params.action;
            (await getComponent(this.element)).pollingDirector.pause(actionName);
        }

        async resume(event) {
            const actionName = event.params.action;
            (await getComponent(this.element)).pollingDirector.resume(actionName);
        }
    }

Available Methods

The pollingDirector API exposes the following methods:

  • component.pollingDirector.start(actionName) — Starts polling for the given action (if previously stopped).
  • component.pollingDirector.pause(actionName) — Temporarily pauses polling. Can be resumed later.
  • component.pollingDirector.resume(actionName) — Resumes a previously paused poll.
  • component.pollingDirector.stop(actionName) — Stops polling entirely. Use start() to restart.

@carsonbot carsonbot added Feature New Feature LiveComponent Status: Needs Review Needs to be reviewed labels Jul 30, 2025
Copy link
Contributor

github-actions bot commented Jul 30, 2025

📊 Packages dist files size difference

Thanks for the PR! Here is the difference in size of the packages dist files between the base branch and the PR.
Please review the changes and make sure they are expected.

FileBefore (Size / Gzip)After (Size / Gzip)
LiveComponent
live_controller.d.ts 7.96 kB / 1.96 kB 8.74 kB+10% 📈 / 2.08 kB+6% 📈
live_controller.js 99.04 kB / 21.35 kB 104.81 kB+6% 📈 / 22.49 kB+5% 📈

@smnandre
Copy link
Member

(won't have time to review attentively before next week, but this looks great!)

@94noni
Copy link
Contributor

94noni commented Aug 6, 2025

reminds me #2448
will try to review/test this asap but what a nice addition !

@smnandre
Copy link
Member

I like these features very much!

@xDeSwa Did you read discussion here #2898 ? What do you think about it ?

That could be a good idea to release both together, no ?

cc @norkunas

Comment on lines +2562 to +2596
// controllers/poll_controller.js
import { Controller } from '@hotwired/stimulus';
import { getComponent } from '@symfony/ux-live-component';

export default class extends Controller {
async connect() {
this.component = await getComponent(this.element);

// Disable default error window (optional)
this.component.on('response:error', (backendResponse, controls) => {
controls.displayError = false;
});

this.component.on('poll:started', ({ actionName, limit }) => {
console.log(`Polling started: ${actionName}, limit: ${limit}`);
});

this.component.on('poll:running', ({ actionName, count, limit }) => {
console.log(`Polling running: ${actionName} (${count}/${limit})`);
});

this.component.on('poll:paused', ({ actionName, count, limit }) => {
console.log(`Polling paused: ${actionName}`);
});

this.component.on('poll:stopped', ({ actionName, finalCount, limit }) => {
console.log(`Polling stopped: ${actionName}, total runs: ${finalCount}`);
});

this.component.on('poll:error', ({ actionName, finalCount, limit, errorMessage }) => {
console.error(`Polling error on ${actionName}: ${errorMessage}`);
});
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the whole block here ? Maybe one block and then comments would be enough ?

Something like that ?

  this.component.on('poll:started', ({ actionName, limit }) => {
      console.log(`Polling started: ${actionName}, limit: ${limit}`);
  });
  
  // this.component.on('poll:started', ({ actionName, limit }) => { ... 
  // this.component.on('poll:running', ({ actionName, count, limit }) => { ... 
  // this.component.on('poll:paused', ({ actionName, count, limit }) => { ... 
  // this.component.on('poll:stopped', ({ actionName, finalCount, limit }) => { ...
  // this.component.on('poll:error', ({ actionName, finalCount, limit, errorMessage }) => { ... 


.. note::

These events are dispatched on the component and can be handled using Stimulus. You must retrieve the component instance with ``getComponent(this.element)`` before accessing event listeners.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
These events are dispatched on the component and can be handled using Stimulus. You must retrieve the component instance with ``getComponent(this.element)`` before accessing event listeners.
These events are dispatched on the component element and can be handled using Stimulus. You must retrieve the component instance with ``getComponent(this.element)`` before accessing event listeners.

Comment on lines +2604 to +2606
Starting from version 2.29, polling can be programmatically managed using the ``pollingDirector`` API.

This allows you to start, pause, resume, or stop polling dynamically for a given action.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A real-life example could help readers understand when / why this feature is the most helpful

Comment on lines +2617 to +2645
// controllers/poll_controller.js
import { Controller } from '@hotwired/stimulus';
import { getComponent } from '@symfony/ux-live-component';

export default class extends Controller {
static values = {
action: String
}

async start(event) {
const actionName = event.params.action;
(await getComponent(this.element)).pollingDirector.start(actionName);
}

async stop(event) {
const actionName = event.params.action;
(await getComponent(this.element)).pollingDirector.stop(actionName);
}

async pause(event) {
const actionName = event.params.action;
(await getComponent(this.element)).pollingDirector.pause(actionName);
}

async resume(event) {
const actionName = event.params.action;
(await getComponent(this.element)).pollingDirector.resume(actionName);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need as many full examples in the documentation.

Could we just list the methods/hooks for both, and use one example to illustrate how to use a Stimulus controller to listen to these events/actions ?

@@ -1,8 +1,14 @@
# CHANGELOG

## 2.29.0

- Added new modifier `limit` and new Hooks for Poll System.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Added new modifier `limit` and new Hooks for Poll System.
- Add new modifier `limit` and new Hooks for Poll System:

## 2.28.0

- Add new modifiers for input validations, useful to prevent unnecessary HTTP requests:
- Add new modifiers for input validations, useful to prevent uneccessary HTTP requests:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Add new modifiers for input validations, useful to prevent uneccessary HTTP requests:
- Add new modifiers for input validations, useful to prevent unneccessary HTTP requests:

It seems unneccessary contains two N: https://en.wiktionary.org/wiki/unnecessary

Comment on lines 372 to +373
- Fixed bug where sometimes a live component was broken after hitting "Back:
in your browser - #436.
in your browser" - #436.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be more like this no ?

Suggested change
- Fixed bug where sometimes a live component was broken after hitting "Back:
in your browser - #436.
in your browser" - #436.
- Fixed bug where sometimes a live component was broken after hitting "Back"
in your browser - #436.

But i'm not 100% sure we should change items that old 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature New Feature LiveComponent Status: Needs Review Needs to be reviewed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants