Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/LiveComponent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 3.1

- Re-emit the `render:started` and `render:finished` JS hooks as bubbling
`live:render:started` / `live:render:finished` DOM events on the component's
root element, alongside the existing `live:connect` / `live:disconnect`.
- Use `aria-busy` attribute during component re-render

## 3.0.0
Expand Down
10 changes: 10 additions & 0 deletions src/LiveComponent/assets/dist/live_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2218,6 +2218,16 @@ var LiveControllerDefault = class LiveControllerDefault extends Controller {
].forEach((plugin) => {
this.component.addPlugin(plugin);
});
this.component.on("render:started", (html, backendResponse, controls) => {
this.dispatchEvent("render:started", {
html,
backendResponse,
controls
});
});
this.component.on("render:finished", () => {
this.dispatchEvent("render:finished");
});
}
connectComponent() {
this.component.connect();
Expand Down
7 changes: 7 additions & 0 deletions src/LiveComponent/assets/src/live_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,13 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
plugins.forEach((plugin) => {
this.component.addPlugin(plugin);
});

this.component.on('render:started', (html, backendResponse, controls) => {
this.dispatchEvent('render:started', { html, backendResponse, controls });
});
this.component.on('render:finished', () => {
this.dispatchEvent('render:finished');
});
}

private connectComponent() {
Expand Down
35 changes: 35 additions & 0 deletions src/LiveComponent/assets/test/unit/controller/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,41 @@ describe('LiveController rendering Tests', () => {
expect(test.component.valueStore.getOriginalProps()).toEqual({ firstName: 'Kevin' });
});

it('dispatches live:render:started and live:render:finished DOM events', async () => {
const test = await createTest(
{ firstName: 'Ryan' },
(data: any) => `
<div ${initComponent(data)}>
<span>Name: ${data.firstName}</span>
<button data-action="live#$render">Reload</button>
</div>
`
);

let startedTriggered = false;
let finishedTriggered = false;
test.element.addEventListener('live:render:started', (event: any) => {
startedTriggered = true;
expect(event.bubbles).toStrictEqual(true);
expect(typeof event.detail.html).toEqual('string');
expect(event.detail.controls).toEqual({ shouldRender: true });
});
test.element.addEventListener('live:render:finished', (event: any) => {
finishedTriggered = true;
expect(event.bubbles).toStrictEqual(true);
});

test.expectsAjaxCall().serverWillChangeProps((data: any) => {
data.firstName = 'Kevin';
});

getByText(test.element, 'Reload').click();

await waitFor(() => expect(test.element).toHaveTextContent('Name: Kevin'));
expect(startedTriggered).toBe(true);
expect(finishedTriggered).toBe(true);
});

it('conserves the value of model field that was modified after a render request', async () => {
const test = await createTest(
{ title: 'greetings', comment: '' },
Expand Down
26 changes: 26 additions & 0 deletions src/LiveComponent/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,32 @@ The following hooks are available (along with the arguments that are passed):
* ``loading.state:finished`` args ``(element: HTMLElement)``
* ``model:set`` args ``(model: string, value: any, component: Component)``

DOM Events
~~~~~~~~~~

The ``connect``, ``disconnect``, ``render:started`` and ``render:finished`` JavaScript
hooks are also re-emitted as bubbling DOM events on the component's root element,
prefixed with ``live:``. This lets external code subscribe via plain
``addEventListener`` without needing to grab the ``Component`` instance through
``getComponent()``:

.. code-block:: javascript

element.addEventListener('live:render:finished', (event) => {
// event.detail.component — the (proxied) Component instance
// event.detail.controller — the Stimulus controller
});

The ``live:render:started`` event additionally carries ``html``, ``backendResponse``
and ``controls`` in its ``detail``, matching the JS hook signature.

The following DOM events are dispatched:

* ``live:connect``
* ``live:disconnect``
* ``live:render:started``
* ``live:render:finished``

Loading States
--------------

Expand Down
Loading