基于下拉筛选器动态渲染图表:Livewire+Chart.js getContext报错解决
问题描述
我不需要刷新Blade上已有的图表,场景为:每个模块包含问题、选项及用户选择的答案列表,通过下拉菜单按模块筛选问题。
页面加载时第一个模块可成功生成动态图表,但切换到模块2时,chartData已更新并通过emit传递给脚本,却无法更新Blade,报错信息如下:
Uncaught (in promise) TypeError: Cannot read properties of null (reading 'getContext') at 4:2072:66
报错出自代码行:
const ctx = document.getElementById(canvasId).getContext('2d');
我尝试过销毁Chart实例的方案但问题依旧,相关代码如下:
Livewire组件代码
public function mount() { $this->modules = $this->session->course->modules->where('status', 'PUBLISHED'); $this->selectedModule = $this->modules->first()->id; $this->GetQuestions(); $this->preparePieChartData(); } public function preparePieChartData() { $singleChoiceQuestions = $this->questions->where('type', 'single_choice'); $data = []; foreach ($singleChoiceQuestions as $question) { if ($question->answers->count() > 0) { $options = $question->options; $questionData = [ 'question' => $question->question_text, 'options' => [], ]; foreach ($options as $option) { $count = $question->answers ->where('option_uuid', $option->uuid) ->where('session_id', $this->session->id) ->count(); $questionData['options'][] = [ 'option' => $option->option_text, 'count' => $count, ]; } $data[] = $questionData; } } $this->chartData = $data; $this->emitSelf('chartDataUpdated', $data); } public function updatedSelectedModule() { $this->GetQuestions(); $this->preparePieChartData(); }
Blade视图代码
<select class="form-control-sm form-control" wire:model="selectedModule"> @foreach($modules as $module) <option value="{{ $module->id }}">{{ $module->title }}</option> @endforeach </select> @foreach($questions as $question) @if ($question->type == 'single_choice') <canvas id="pieChart{{ $loop->index }}" style="max-height: 300px; display: block;"></canvas> @endif @endforeach
Blade底部脚本代码
<script> document.addEventListener('livewire:load', function () { const chartInstances = []; function GenerateCharts(chartData) { console.log('drawing charts...'); chartData.forEach((chart, index) => { console.log(index); console.log(chart); const config = { type: 'pie', data: { labels: chart.options.map(option => option.option), datasets: [{ label: 'Total', data: chart.options.map(option => option.count), }] }, options: { plugins: {} }, } const canvasId = `pieChart${index}`; if (chartInstances[canvasId]) { chartInstances[canvasId].destroy(); delete chartInstances[canvasId]; } const ctx = document.getElementById(canvasId).getContext('2d'); chartInstances[canvasId] = new Chart(ctx, config); }); } GenerateCharts(@js($chartData)); @this.on('chartDataUpdated', (data) => { GenerateCharts(data); }); }); </script>
解决方案
问题根源
- 切换模块时,Livewire会重新渲染
questions循环,原canvas元素被替换,但chartDataUpdated事件触发时,Livewire的DOM更新可能尚未完成,导致找不到对应ID的canvas元素,抛出null错误。 - 使用
$loop->index作为canvas ID的一部分,切换模块后新问题列表的索引会从头开始,容易出现ID冲突和实例管理混乱。
修复步骤
- 等待DOM更新完成后生成图表
修改Livewire组件的emit代码,从emitSelf改为普通emit:
// 替换原emitSelf代码 $this->emit('chartDataUpdated', $data);
脚本中用Livewire的$nextTick确保DOM渲染完成后再执行图表生成:
@this.on('chartDataUpdated', (data) => { @this.$nextTick(() => { GenerateCharts(data); }); });
- 使用稳定的canvas ID
改用问题的唯一标识(如$question->id)作为canvas ID,避免索引重复:
@foreach($questions as $question) @if ($question->type == 'single_choice') <canvas id="pieChart-{{ $question->id }}" style="max-height: 300px; display: block;"></canvas> @endif @endforeach
在preparePieChartData中给questionData添加question_id字段:
$questionData = [ 'question_id' => $question->id, // 新增该行 'question' => $question->question_text, 'options' => [], ];
脚本中修改ID生成逻辑:
chartData.forEach((chart) => { const canvasId = `pieChart-${chart.question_id}`; // 改用question_id // 其余代码不变 });
- 完善实例清理逻辑
生成新图表前,先销毁所有旧实例并清空存储,避免内存泄漏:
function GenerateCharts(chartData) { // 销毁所有旧图表实例 Object.values(chartInstances).forEach(instance => { instance.destroy(); }); // 清空实例存储 Object.keys(chartInstances).forEach(key => delete chartInstances[key]); chartData.forEach((chart) => { const canvasId = `pieChart-${chart.question_id}`; const ctx = document.getElementById(canvasId)?.getContext('2d'); if (!ctx) return; // 增加空判断,避免报错 const config = { type: 'pie', data: { labels: chart.options.map(option => option.option), datasets: [{ label: 'Total', data: chart.options.map(option => option.count), }] }, options: { plugins: {} }, } chartInstances[canvasId] = new Chart(ctx, config); }); }
内容的提问来源于stack exchange,提问作者TheLazyUnicorn
相关产品推荐
相关产品推荐

