Hey,
I have come across an interesting bug in my Livewire component. The variables inside the @if statement @if($editingService && $selectedService?->id === $service->id) with the 'Editing' badge inside a foreach loop seem to be stuck and not refreshing on Livewire updates. When I add a @dump() directly above it, it shows the values correctly (changing with LW updates). The issue is that when a user enters the edit form and then leaves it, the 'Editing' badge is still present (even though $editingService is false).
Yes, the Livewire template has only one wrapping .
The problematic block:
@if(!empty($services))
<div class="grid gap-4">
@foreach($services as $service)
<div wire:key="{{ $service->id }}"
@class([
"rounded-xl border",
ring-2 ring-blue-200 dark:ring-blue-800"=> $editingService && $selectedService?->id === $service->id
])>
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ $service->name }}</h3>
<x-services.status :status="$service->status" />
@dump($editingService)
@if($editingService && $selectedService?->id === $service->id) // <-- here
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400">
<x-heroicon-o-pencil class="size-3 mr-1" />
Editing') }}
</span>
@endif
</div>
Livewire template (at least the most of it):
<div class="space-y-6">
<!-- Header with Add Button -->
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ __('Services') }}</h2>
<button wire:click="showAddServiceForm"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-all duration-200 active:scale-95 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
{{ __('Add Service') }}
</button>
</div>
<!-- Flash Messages -->
@if (session()->has('success'))
<div x-data="{ show: true }" x-show="show" x-transition
class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<x-heroicon-o-check-circle class="size-5 text-green-600 dark:text-green-400" />
<p class="text-sm text-green-800 dark:text-green-200">{{ session('success') }}</p>
</div>
<button @click="show = false"
class="text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-200">
<x-heroicon-o-x-mark class="size-5" />
</button>
</div>
</div>
@endif
<!-- Add Service Form -->
<div wire:show="$wire.showAddForm && !$wire.editingService" wire:transition.duration.300ms wire:cloak>
<div class="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Add New Service') }}
</h3>
<button wire:click="cancelServiceForm"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
<x-heroicon-o-x-mark class="size-6" />
</button>
</div>
<form wire:submit="saveService" class="space-y-4">
<!-- Service Name -->
<div>
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ __('Service Name') }}
</label>
<input type="text" id="name" wire:model="name"
class="w-full rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2 text-sm focus:border-blue-500 focus:ring-blue-500 dark:text-gray-100"
placeholder="{{ __('Enter service name') }}" />
@error('name')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<!-- Service Type -->
<div>
<label for="type" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ __('Service Type') }}
</label>
<select id="type" wire:model.live="type"
class="w-full rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2 text-sm focus:border-blue-500 focus:ring-blue-500 dark:text-gray-100">
@foreach($serviceTypes as $serviceType)
<option value="{{ $serviceType->value }}">{{ $serviceType->label() }}</option>
@endforeach
</select>
@error('type')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<!-- Configuration Fields -->
@if($type->getConfigFields())
<div class="space-y-4">
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ __('Configuration') }}</h4>
@foreach($type->getConfigFields() as $field => $fieldConfig)
<div>
<label for="config_{{ $field }}"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ $fieldConfig['label'] }}
@if($fieldConfig['required'])
<span class="text-red-500">*</span>
@endif
</label>
<input type="{{ $fieldConfig['type'] }}" id="config_{{ $field }}"
wire:model="config.{{ $field }}"
class="w-full rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2 text-sm focus:border-blue-500 focus:ring-blue-500 dark:text-gray-100"
placeholder="{{ __('Enter :field', ['field' => strtolower($fieldConfig['label'])]) }}" />
@error("config.{$field}")
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
@endforeach
</div>
@endif
<!-- Secret Fields -->
@if($type->getSecretFields())
<div class="space-y-4">
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ __('Credentials') }}</h4>
@foreach($type->getSecretFields() as $field => $fieldConfig)
<div>
<label for="secret_{{ $field }}"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ $fieldConfig['label'] }}
@if($fieldConfig['required'])
<span class="text-red-500">*</span>
@endif
</label>
<input type="{{ $fieldConfig['type'] }}" id="secret_{{ $field }}"
wire:model="secret.{{ $field }}"
class="w-full rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2 text-sm focus:border-blue-500 focus:ring-blue-500 dark:text-gray-100"
placeholder="{{ __('Enter :field', ['field' => strtolower($fieldConfig['label'])]) }}" />
@error("secret.{$field}")
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
@endforeach
</div>
@endif
<!-- Form Actions -->
<div class="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<button type="button" wire:click="cancelServiceForm"
class="px-4 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
{{ __('Cancel') }}
</button>
<button type="submit" wire:loading.attr="disabled"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900 disabled:opacity-50">
<span wire:loading.remove>
{{ __('Add Service') }}
</span>
<span wire:loading>
{{ __('Saving...') }}
</span>
</button>
</div>
</form>
</div>
</div>
<!-- Services List -->
@if(!empty($services))
<div class="grid gap-4">
@foreach($services as $service)
<div wire:key="{{ $service->id }}"
@class([ "rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-6"
, "ring-2 ring-blue-200 dark:ring-blue-800"=> $editingService && $selectedService?->id === $service->id
])>
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ $service->name }}</h3>
<x-services.status :status="$service->status" />
@dump($editingService)
@if($editingService && $selectedService?->id === $service->id)
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400">
<x-heroicon-o-pencil class="size-3 mr-1" />
{{ __('Editing') }}
</span>
@endif
</div>
@if(!($editingService && $selectedService?->id === $service->id))
<div class="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400 mb-4">
<span class="flex items-center gap-1">
<x-heroicon-o-server class="size-4" />
{{ $service->type->label() }}
</span>
@if($service->version)
<span class="flex items-center gap-1">
<x-heroicon-o-tag class="size-4" />
v{{ $service->version }}
</span>
@endif
<span class="flex items-center gap-1 hs-tooltip hs-tooltip-toggle cursor-help">
<x-heroicon-o-clock class="size-4" />
{{ $service->created_at->diffForHumans() }}
<span
class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-gray-900 text-xs font-medium text-white rounded-md shadow-2xs dark:bg-neutral-700"
role="tooltip">
Added: {{ $service->created_at->format('Y-m-d H:i') }}
</span>
</span>
</div>
<!-- Configuration Preview -->
@if($service->config)
<div class="flex gap-2 items-center text-sm flex-wrap">
@foreach($service->config as $key => $value)
<div
class="flex items-center gap-2 p-2 bg-gray-50 border border-gray-100 rounded-lg dark:bg-sky-900/20">
<span class="text-gray-500 font-medium">{{ ucfirst($key) }}:</span>
<span class="text-gray-900 dark:text-gray-100">{{ $value }}</span>
</div>
@endforeach
</div>
@endif
@endif
</div>
<!-- Actions -->
<div class="flex items-center gap-3 ml-4">
@if($editingService && $selectedService && $selectedService->id === $service->id)
<button wire:click="cancelServiceForm"
class=""
title="{{ __('Cancel Edit') }}">
<x-heroicon-o-x-mark class="size-5" />
</button>
@else
<button wire:click="editService({{ $service->id }})"
class=""
title="{{ __('Edit Service') }}">
<x-heroicon-o-pencil-square class="size-5" />
</button>
<button wire:click="deleteService({{ $service->id }})"
wire:confirm="{{ __('Are you sure you want to delete this service?') }}"
class=""
title="{{ __('Delete Service') }}">
<x-heroicon-c-trash class="size-5" />
</button>
@endif
</div>
</div>
......
PHP file:
<?php
namespace App\Livewire\Servers;
use Illuminate\Validation\Rule;
use Livewire\Component;
use App\Enums\{
ServiceStatus,
ServiceType
};
use App\Models\{
Server,
Service
};
class ServerServices extends Component
{
public Server $server;
// Service form properties
public bool $showAddForm = false;
public bool $editingService = false;
public ?Service $selectedService = null;
public string $name = '';
public ServiceType $type = ServiceType::mysql;
public array $config = [];
public array $secret = [];
protected $listeners = ['serviceAdded' => '$refresh'];
public function mount(Server $server): void
{
$this->server = $server;
}
public function showAddServiceForm(): void
{
$this->reset(['name', 'config', 'secret']);
$this->type = ServiceType::mysql;
$this->showAddForm = true;
$this->editingService = false;
$this->selectedService = null;
$this->initializeConfigFields();
}
public function editService(Service $service): void
{
$this->selectedService = $service;
$this->name = $service->name;
$this->type = $service->type;
$this->config = $service->config ?? [];
$this->secret = $service->secret->toArray() ?? [];
$this->showAddForm = true;
$this->editingService = true;
$this->initializeConfigFields();
}
public function cancelServiceForm(): void
{
$this->reset(['showAddForm', 'editingService', 'selectedService', 'name', 'config', 'secret']);
}
public function updatedType(): void
{
$this->initializeConfigFields();
}
private function initializeConfigFields(): void
{
// Initialize config fields with defaults
foreach ($this->type->getConfigFields() as $field => $fieldConfig) {
if (!isset($this->config[$field]) && isset($fieldConfig['default'])) {
$this->config[$field] = $fieldConfig['default'];
}
}
// Initialize secret fields
foreach ($this->type->getSecretFields() as $field => $fieldConfig) {
if (!isset($this->secret[$field])) {
$this->secret[$field] = '';
}
}
}
public function saveService(): void
{
$this->validate();
$serviceData = [
'name' => $this->name,
'type' => $this->type,
'server_id' => $this->server->id,
'config' => $this->config,
'secret' => $this->secret,
'status' => ServiceStatus::PENDING,
];
if ($this->editingService && $this->selectedService) {
$this->selectedService->update($serviceData);
session()->flash('success', 'Service updated successfully.');
} else {
$serviceData['service_identifier'] = uniqid('service_');
$this->server->services()->create($serviceData);
session()->flash('success', 'Service added successfully.');
}
$this->cancelServiceForm();
$this->dispatch('serviceAdded');
}
public function deleteService(Service $service): void
{
if ($service->server_id !== $this->server->id) {
return;
}
$service->delete();
session()->flash('success', 'Service deleted successfully.');
$this->dispatch('serviceAdded');
}
protected function rules(): array
{
$rules = [
'name' => ['required', 'string', 'max:255'],
'type' => ['required', Rule::enum(ServiceType::class)],
];
// Add validation rules for config fields
foreach ($this->type->getConfigFields() as $field => $fieldConfig) {
$fieldRules = [];
if ($fieldConfig['required']) {
$fieldRules[] = 'required';
}
if ($fieldConfig['type'] === 'number') {
$fieldRules[] = 'numeric';
} else {
$fieldRules[] = 'string';
$fieldRules[] = 'max:255';
}
$rules["config.{$field}"] = $fieldRules;
}
// Add validation rules for secret fields
foreach ($this->type->getSecretFields() as $field => $fieldConfig) {
$fieldRules = [];
if ($fieldConfig['required']) {
$fieldRules[] = 'required';
}
$fieldRules[] = 'string';
$fieldRules[] = 'max:255';
$rules["secret.{$field}"] = $fieldRules;
}
return $rules;
}
protected function validationAttributes(): array
{
$attributes = [
'name' => 'service name',
'type' => 'service type',
];
// Add attributes for config fields
foreach ($this->type->getConfigFields() as $field => $fieldConfig) {
$attributes["config.{$field}"] = strtolower($fieldConfig['label']);
}
// Add attributes for secret fields
foreach ($this->type->getSecretFields() as $field => $fieldConfig) {
$attributes["secret.{$field}"] = strtolower($fieldConfig['label']);
}
return $attributes;
}
public function render()
{
$services = $this->server->services()->orderBy('created_at', 'desc')->get();
return view('livewire.servers.server-services', [
'services' => $services,
'serviceTypes' => ServiceType::cases(),
]);
}
}