在 Laravel Livewire 中更新 javascript 的更好方法?

A better way to update javascript in Laravel Livewire?

我最近 运行 在构建 Laravel Livewire 组件时遇到一个问题,其中 javascript 部分不会在 select 输入更改时更新。该组件是来自 Chartist.js 库的图表,它在加载时显示,但是当我更改 select 输入时,图表消失了。我想出了一个解决方案,但感觉很脏,任何人都有更好的解决方案。

行-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();
                });
        }
    }
}

如果我将 document.addEventListener('livewire:load', function () {} 更改为 livewire:update,那么当我使用 select 输入时图表会按预期工作,但图表不会在加载时显示。所以为了解决这个问题,我必须设置一个超时并触发一个事件,该事件在加载时显示图表,但也会在更新时显示图表。我觉得有更好的方法来做到这一点,我只是错过了一些东西。

我从未使用过图表师,但我通常使用这种方法(如果代码对您有意义,请告诉我)。

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

我认为从 livewire 组件观察值更容易调试。当页面变大(并且有很多事件)时,使用事件进行后端和前端之间的通信会给调试带来挑战。

使用此代码,您的字符将在 livewire 初始加载后加载(您无需检查 'livewire:load' 事件)。每次派发 updateJS 事件时不会挂载图表实例,只会更新数据。

Ps:我不知道 Chartist 是如何工作的,所以我没有把代码放在 chart.update(...) 上,但由于它是一个图表库,它应该有更新数据的选项.我只是想告诉你我的方法,希望对你有帮助。

为了让它工作,我删除了 setTimeout()Livewire.On('updateJS') 并在 livewire:load 中添加了另一个监听 livewire:update 的事件监听器,在这里我们'用

重新更新图表
chart.update({labels: @this.labels, series: @this.data})

这是完整的代码片段。我觉得这是一个更干净的解决方案,不会让人觉得脏哈哈。

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