Closed robert-hoffmann closed 1 year ago
Hi there!
This is a really interesting problem. I think the problem is not in LiveCompontents, but with our Chart.js controller itself. The behavior we want is for the Chart.js instance to update (with animations!) whenever the "data" we're feeding into it changes - i.e. whenever the Stimulus "attributes" on the canvas element change. Currently, we read this view
value (which contains all of the chart data) and render Chart.js - https://github.com/symfony/ux/blob/2.x/src/Chartjs/assets/src/controller.ts#L27
But, if that attribute ever changed... nothing happens. So, you're not doing anything wrong, we just need to make the Stimulus chart.js controller smarter! The idea would be to listen to the view
value to change - https://stimulus.hotwired.dev/reference/values#change-callbacks - then use that to modify the chart
instance - https://www.chartjs.org/docs/latest/developers/updates.html. After doing this, you'd even be able to "mess" with the HTML attribute data on your canvas
element and watch your chart update - pretty cool.
I think (famous last words) that this should be pretty easy! It's possible something else might need to be done in live components itself as well, but I'm quite sure we need this first part to start.
Thanks for the response
Any way i might be able to hook into the chart element and trigger a chart.update() myself via the live component ?
You can definitely listen to the render:finished
hook from a custom Stimulus controller. I'd actually be interested how well this works. I think (?) that, when the Live Component updates, it will "update" the existing canvas
element to have the new attributes, but I'm not entirely sure about that - canvas
elements are special element and I haven't used them yet. You mentioned:
The first problem i have is that any rerender of the live component makes the chart invisible, and i have to resize the window so that it rerenders correctly.
If by "rerenders correctly" you mean that it DOES show the updated chart, then that actually makes me think that the canvas
element might replaced with the new one... then the new one is completely creating a new Chart. If that's the case, what I mentioned earlier about the "smart chart updating" isn't needed (it would be nice because the updates would animate, but not strictly required). So then, why doesn't the chart show up until you resize? You could try checking here - https://stackoverflow.com/questions/48343189/data-is-shown-on-chartjs-vue-only-after-resizing-browser-or-opening-developer-to#answers - it might be as simple as hooking into render:finished
and calling chart.update()
. But I'm doing a lot of guessing.
Not really sure how to hook into that because this is double imbricked
StatsController.php (routes)
return $this->render('stats/stats.html.twig');
stats/stats.html.twig
{{ component('stats') }}
components/stats.html.twig
{{ render_chart(this.chart) }}
...no clue how to hook into the render_chart() object via a controller (well i could just use that chart straight up via a stimulus-controller, but like i said i wanted to try and use it via a live-component, to keep js to a minimum, and not have to write a bunch of webservices)
Is there a way to get the instance of the symfony--ux-chartjs--chart controller or the chart object via some kind of document.queryselector manipulation ?
I found this, but there is no chart instance on the page/window
https://stackoverflow.com/questions/36608208/how-to-retrieve-chartjs-instance-after-initialization
You'll need to "extend" chartjs with a Stimulus controller - like shown here - https://symfony.com/bundles/ux-chartjs/current/index.html#extend-the-default-behavior - that'll give you access to the Chart instance.
Then, inside there, you'll use the getComponent()
method - https://symfony.com/bundles/ux-live-component/current/index.html#working-with-the-component-in-javascript - to hook into the live component. The tricky this is that you won't be able to use this.element
like shown in that example, because this is your chartjs controller, so this.element
will be the canvas
element, not the live controller element. A simple thing would be to add an id=""
attribute to your live component, then find it via:
const component = await getComponent(document.getElementById();
This is the kind of thing that should "just work". But it doesn't (yet) for some reason, hence needing some extra JS to work around the issue.
Pretty cool, i like how to get access to the components like this
Unfortunately nothing is updating
I have this in the controller (seems to be working)
export default class extends Controller {
#chart;
connect() {
this.element.addEventListener('chartjs:connect', (event) => {
this.#chart = event.detail.chart;
});
}
async initialize() {
this.component = await getComponent(document.querySelector('div[data-controller="live"]'));
this.component.on('render:finished', () => {
this.#chart.update();
});
}
}
Then in the component i have something like this
#[LiveAction]
public function update()
{
$this->chart->setData([
'labels' => ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
'datasets' => [
[
'label' => $this->sampleText,
'backgroundColor' => 'rgb(255, 99, 132, .4)',
'borderColor' => 'rgb(255, 99, 132)',
'data' => [2, 10, 5, 18, 20, 30, 45],
'tension' => 0.4,
],
[
'label' => 'Km walked 🏃♀️',
'backgroundColor' => 'rgba(45, 220, 126, .4)',
'borderColor' => 'rgba(45, 220, 126)',
'data' => [10, 15, 4, 3, 25, 41, 25],
'tension' => 0.4,
],
],
]);
}
I tied this to a live action, to be sure that sampletext (comes from a live input) is up to date, and i trigger the changes manually to be sure there arent any race conditions
but it's always just the same chart
Ok, then it goes back to my original guess :). LiveComponents (correctly) doesn't replace the canvas
element, it just updates the attributes (which hold the chart data). You can verify this by inspecting element on the canvas after live components loads and look carefully to see if the data has been changed.
The problem is back to this:
I think the problem is not in LiveCompontents, but with our Chart.js controller itself. The behavior we want is for the Chart.js instance to update (with animations!) whenever the "data" we're feeding into it changes - i.e. whenever the Stimulus "attributes" on the canvas element change. Currently, we read this view value (which contains all of the chart data) and render Chart.js - https://github.com/symfony/ux/blob/2.x/src/Chartjs/assets/src/controller.ts#L27 But, if that attribute ever changed... nothing happens
So we need to make the chartjs controller smarter. To work around it, in your controller, you could read the data-symfony--ux-chartjs--chart-view-value
directly and try to use the data to update the chart:
this.component = await getComponent(document.querySelector('div[data-controller="live"]'));
this.component.on('render:finished', () => {
const newChartData = JSON.parse(this.element.dataset['symfony-UxChartjs-ChartViewValue']);
// this will have keys like "type", "data" and "options". Example usage:
this.#chart.data = newChartData.data;
});
this.#chart.update();
});
Let me know if this helps :) - code above may not be perfect!
Nice ;-)
This is working as far as updating data goes
stats_controller.js
export default class extends Controller {
#chart;
// https://symfony.com/bundles/ux-chartjs/current/index.html
connect() {
this.element.addEventListener('chartjs:connect', (event) => {
this.#chart = event.detail.chart;
});
}
async initialize() {
this.component = await getComponent(document.querySelector('div[data-controller="live"]'));
this.component.on('render:finished', (component) => {
// do something after the component re-renders
const newChartData = JSON.parse(this.element.dataset['symfony-UxChartjs-ChartViewValue']);
// this will have keys like "type", "data" and "options". Example usage:
this.#chart.data = newChartData.data;
this.#chart.update('active');
});
}
}
templates/components/stats.html.twig
<div {{ attributes }}>
<h2>{{this.sampleText}}</h2>
<div style="height: 300px;">
{{ render_chart(this.chart, {'data-controller': 'stats'}) }}
</div>
<input
type="text"
class="form-control"
data-model="debounce(100)|sampleText"
/>
</div>
components/StatsComponent.php
private function update()
{
$this->chart->setData([
'labels' => ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
'datasets' => [
[
'label' => $this->sampleText,
'backgroundColor' => 'rgb(255, 99, 132, .4)',
'borderColor' => 'rgb(255, 99, 132)',
'data' => [2, 10, 5, 18, 20, 30, 45],
'tension' => 0.4,
],
[
'label' => 'Km walked 🏃♀️',
'backgroundColor' => 'rgba(45, 220, 126, .4)',
'borderColor' => 'rgba(45, 220, 126)',
'data' => [10, 15, 4, 3, 25, 41, 25],
'tension' => 0.4,
],
],
]);
}
//#endregion
//#region Properties
#[LiveProp(writable: true)]
public string $sampleText = 'Chart';
//#endregion
//#region Actions
//#endregion
//#region Getters
public function getSampleText()
{
$this->update();
return $this->sampleText;
}
Last problem is the canvas blanking out, and me having to resize the window manually to get the chart to rerender correctly after updating
initial render
after automatic update
after manual window resize
was just missing this
this.#chart.resize();
So this is the final code, which can probably be made generic to handle being used inside a Live Component
<div {{ attributes }}>
<div style="height: 300px;">
{{ render_chart(this.chart, {'data-controller': 'stats'}) }}
</div>
</div>
stats_controller.js
export default class extends Controller {
#chart;
connect() {
this.element.addEventListener('chartjs:connect', (event) => {
this.#chart = event.detail.chart;
});
}
async initialize() {
this.component = await getComponent(document.querySelector('div[data-controller="live"]'));
this.component.on('render:finished', (component) => {
const newChartData = JSON.parse(this.element.dataset['symfony-UxChartjs-ChartViewValue']);
this.#chart.data = newChartData.data;
this.#chart.update('active');
this.#chart.resize();
});
}
}
Ah, thank you for sharing this! We will likely need that .resize()
also after we make our Chartjs controllers smart enough to update themselves :)
My pleasure, I'm finding live components interesting. Hope to see some cool UX updates in the future 👍
Only thing missing is making this more robust, to allow for multiple components
document.querySelector('div[data-controller="live"]')
Btw, in the next version - 2.8.0 - the chart will automatically re-render itself when the view
value (i.e. that attribute on the <canvas>
changes. It works out-of-the-box with live components - in large part thanks to this conversation :)
Cool, looking forward to it 😊
PS. have a look at this one https://github.com/symfony/ux/issues/704
This is probably not a bug.
I'm just running into a problem where i'm trying to use chart.js in the context of a live component (without a stimulus controller)
Basically i'd like to have input fields around the chart that control the data that is rendered in the chart, so on modifications of a live prop, the chart should rerender based on the new input.
The first problem i have is that any rerender of the live component makes the chart invisible, and i have to resize the window so that it rerenders correctly.
Second problem, is i still haven't figured out how to actually change the data of the chart, and i don't really want to use a stimulus controller, which IMO defeats the purpose of a live component.
Any pointers for me ? Thanks !