Home > other >  A better way to update javascript in Laravel Livewire?
A better way to update javascript in Laravel Livewire?

Time:11-15

I recently ran into an issue when building a Laravel Livewire component where the javascript portion wouldn't update when a select input changed. The component is a chart from the Chartist.js library and it displays on load but when I change the select input the chart disappears. I came up with a solution but it feels dirty, anyone have a better solution to this.

line-chart.blade.php

<div class="mt-6">
    <h2 class="text-xl text-gray-600 py-3 font-bold">Location Views</h2>
    <div class="bg-white rounded-lg shadow overflow-hidden overflow-y-scroll">
        <div class="flex justify-end px-10 py-6">
            <select wire:model="days" class="block w-40 pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-yellow-500 focus:border-yellow-500 sm:text-sm rounded-md">
                <option value="30">Last 30 Days</option>
                <option value="365">Last 12 Months</option>
            </select>
        </div>
        <div id="line-chart" class="relative ct-chart">
            <div class="hidden absolute inline-block chartist-tooltip bg-white text-xs shadow text-center px-3 py-1 rounded-md w-36">
                <span class="chartist-tooltip-key"></span><br>
                <span class="chartist-tooltip-date"></span><br>
                <span class="chartist-tooltip-value"></span>
            </div>
        </div>
    </div>
</div>

@push('styles')
    <link rel="stylesheet" href="//cdn.jsdelivr.net/chartist.js/latest/chartist.min.css">
@endpush

@push('scripts')
    <script src="//cdn.jsdelivr.net/chartist.js/latest/chartist.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chartist-plugin-tooltips@0.0.17/dist/chartist-plugin-tooltip.min.js"></script>
@endpush

@push('js')
    <script>
        document.addEventListener('livewire:load', function () {
            setTimeout(() => {
                Livewire.emit('updateJS')
            })

            Livewire.on('updateJS', function () {
                var data = {
                    labels: @this.labels,

                    // Our series array that contains series objects or in this case series data arrays
                    series: @this.data,
                };

                var options = {
                    height: 300,
                    fullWidth: true,
                    chartPadding: 40,
                    axisX: {
                        offset: 12,
                        showGrid: true,
                        showLabel: true,
                    },
                    axisY: {
                        offset: 0,
                        showGrid: true,
                        showLabel: true,
                        onlyInteger: true,
                    },
                }

                new Chartist.Line('#line-chart', data, options).on("draw", function (data) {
                    if (data.type === "point") {
                        data.element._node.addEventListener('mouseover', e => {
                            const tooltip = document.getElementsByClassName('chartist-tooltip')

                            tooltip[0].style.top = data.y - 75   'px'
                            tooltip[0].style.left = data.x > 200 ? data.x - 150   'px' : data.x   'px'

                            tooltip[0].classList.remove('hidden')

                            const key = document.getElementsByClassName('chartist-tooltip-key')
                            key[0].innerHTML = data.meta[1]

                            const meta = document.getElementsByClassName('chartist-tooltip-date')
                            meta[0].innerHTML = data.meta[0]

                            const value = document.getElementsByClassName('chartist-tooltip-value')
                            value[0].innerHTML = data.value.y === 1 ? data.value.y   ' view' : data.value.y   ' views'
                        })

                        data.element._node.addEventListener('mouseleave', e => {
                            const tooltip = document.getElementsByClassName('chartist-tooltip')
                            tooltip[0].classList.add('hidden')
                        })
                    }
                })
            })
        })
    </script>
@endpush

LineChart.php

<?php

namespace App\Http\Livewire\Components;

use Carbon\Carbon;
use Carbon\CarbonPeriod;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use Livewire\Component;

class LineChart extends Component
{
    /**
     * @var Collection
     */
    public Collection $data;

    /**
     * @var array
     */
    public array $labels;

    /**
     * @var int
     */
    public int $days = 30;

    /**
     *
     */
    public function mount()
    {
        $this->data();

        $this->labels = $this->labels();
    }

    /**
     * Trigger mount when days is updated.
     */
    public function updatedDays()
    {
        $this->mount();

        $this->emit('updateJS');
    }

    /**
     * @return Application|Factory|View
     */
    public function render()
    {
        return view('livewire.components.line-chart');
    }

    /**
     * Generates the chart data.
     */
    public function data()
    {
        $locations = request()->user()->locations;

        if ($this->days === 30) {
            $this->data = $locations->map(function ($location) {
                return $this->getDatesForPeriod()->map(function ($date) use ($location) {
                    return [
                        'meta' => [
                            Carbon::parse($date)->format('M j'),
                            $location->name
                        ],
                        'value' => $location->views->filter(function ($view) use ($date) {
                            return $view->viewed_on->toDateString() === $date;
                        })->count()
                    ];
                })->toArray();
            });
        }

        if ($this->days === 365) {
            $this->data = $locations->map(function ($location) {
                return $this->getDatesForPeriod()->map(function ($date) use ($location) {
                    return [
                        'meta' => [
                            Carbon::parse($date)->format('M'),
                            $location->name
                        ],
                        'value' => $location->views->filter(function ($view) use ($date) {
                            return $view->viewed_on->month === Carbon::parse($date)->month;
                        })->count()
                    ];
                })->toArray();
            });
        }
    }

    /**
     * Creates the labels for the chart.
     *
     * @return array
     */
    protected function labels()
    {
        return $this->getDatesForPeriod()->map(function ($date) {
            if ($this->days === 30) {
                return Carbon::parse($date)->format('M j');
            } else {
                return Carbon::parse($date)->format('M');
            }
        })->toArray();
    }

    /**
     * Gets the dates for the specified period.
     *
     * @return Collection
     */
    protected function getDatesForPeriod()
    {
        if ($this->days === 30) {
            return collect(CarbonPeriod::create(now()->subDays($this->days)->toDateString(), now()->toDateString()))
                ->map(function ($date) {
                    return $date->toDateString();
                });
        }

        if ($this->days === 365) {
            return collect(now()->startOfMonth()->subMonths(11)->monthsUntil(now()))
                ->map(function ($date) {
                    return $date->toDateString();
                });
        }
    }
}

If I change document.addEventListener('livewire:load', function () {} to livewire:update then the chart works as expected when I use the select input, but then the chart doesn't display on load. So to get around this I had to set a timeout and trigger an event that displays the chart on load but will also display the chart on update. I feel like there is a better way to do this, I'm just missing something.

CodePudding user response:

I never used the chartist, but I usually use this approach (let me know if the code make sense to you).

<div class="mt-6" x-data="{
    labels: @entangle('labels'), 
    series: @entangle('data'),
    chart: null
}"
x-init="() => {
    const options = {}; // I'll omit part of the code here
    chart = new Chartist.Line('#line-chart', data, options);
},
$watch('series', (dataChart) => {
     // I usually put both, the series data and the labels in an associative array on the livewire component back-end
    const { series, labels } = dataChart;
    chart.update(dataChart);
"
>
    <h2 class="text-xl text-gray-600 py-3 font-bold">Location Views</h2>
    <div class="bg-white rounded-lg shadow overflow-hidden overflow-y-scroll">
        <div class="flex justify-end px-10 py-6">
            <select wire:model="days" class="block w-40 pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-yellow-500 focus:border-yellow-500 sm:text-sm rounded-md">
                <option value="30">Last 30 Days</option>
                <option value="365">Last 12 Months</option>
            </select>
        </div>
        <div id="line-chart" class="relative ct-chart">
            <div class="hidden absolute inline-block chartist-tooltip bg-white text-xs shadow text-center px-3 py-1 rounded-md w-36">
                <span class="chartist-tooltip-key"></span><br>
                <span class="chartist-tooltip-date"></span><br>
                <span class="chartist-tooltip-value"></span>
            </div>
        </div>
    </div>
</div>

I think that watching a value from the livewire component it's easier to debug. The use of events to make the communication between back and front-end can make debug challenge when the page gets bigger (and with a lot of events).

With this code your char will be loaded after livewire initial load (you don't need to check the 'livewire:load' event). The chart instance will not be mounted every time you dispatch the updateJS event, only the data will be update.

Ps: I don't know how Chartist works, so I didn't put the code on chart.update(...), but since it's a chart lib it should have an options to update the data. I just want to show you my approach, hope it helps.

CodePudding user response:

To get this to work I removed the setTimeout() and the Livewire.On('updateJS') and added another event listener inside of livewire:load that listens for livewire:update and in here we're updating the chart with

chart.update({labels: @this.labels, series: @this.data})

Here is the entire code snippet. I feel like this is a much cleaner solution and doesn't feel dirty lol.

<script>
document.addEventListener('livewire:load', function () {
    var data = {
        labels: @this.labels,

        // Our series array that contains series objects or in this case series data arrays
        series: @this.data,
    };

    var options = {
        height: 300,
        fullWidth: true,
        chartPadding: 40,
        axisX: {
            offset: 12,
            showGrid: true,
            showLabel: true,
        },
        axisY: {
            offset: 0,
            showGrid: true,
            showLabel: true,
            onlyInteger: true,
        },
    }

    const chart = new Chartist.Line('#line-chart', data, options).on("draw", function (data) {
       if (data.type === "point") {
           data.element._node.addEventListener('mouseover', e => {
               const tooltip = document.getElementsByClassName('chartist-tooltip')

               tooltip[0].style.top = data.y - 75   'px'
               tooltip[0].style.left = data.x > 200 ? data.x - 150   'px' : data.x   'px'

               tooltip[0].classList.remove('hidden')

               const key = document.getElementsByClassName('chartist-tooltip-key')
               key[0].innerHTML = data.meta[1]

               const meta = document.getElementsByClassName('chartist-tooltip-date')
               meta[0].innerHTML = data.meta[0]

               const value = document.getElementsByClassName('chartist-tooltip-value')
               value[0].innerHTML = data.value.y === 1 ? data.value.y   ' view' : data.value.y   ' views'
           })

           data.element._node.addEventListener('mouseleave', e => {
               const tooltip = document.getElementsByClassName('chartist-tooltip')
               tooltip[0].classList.add('hidden')
           })
        }
    })

    document.addEventListener('livewire:update', function () {
        chart.update({labels: @this.labels, series: @this.data})
    })
})
</script>
  • Related