Search code examples
laravellaravel-livewire

A better way to update javascript in Laravel Livewire?


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.


Solution

  • 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>