diff --git a/.cursor/rules/frontend-patterns.mdc b/.cursor/rules/frontend-patterns.mdc index 663490d3b..4730160b2 100644 --- a/.cursor/rules/frontend-patterns.mdc +++ b/.cursor/rules/frontend-patterns.mdc @@ -267,18 +267,365 @@ For complete documentation, see **[form-components.mdc](mdc:.cursor/rules/form-c ## Form Handling Patterns +### Livewire Component Data Synchronization Pattern + +**IMPORTANT**: All Livewire components must use the **manual `syncData()` pattern** for synchronizing component properties with Eloquent models. + +#### Property Naming Convention +- **Component properties**: Use camelCase (e.g., `$gitRepository`, `$isStatic`) +- **Database columns**: Use snake_case (e.g., `git_repository`, `is_static`) +- **View bindings**: Use camelCase matching component properties (e.g., `id="gitRepository"`) + +#### The syncData() Method Pattern + +```php +use Livewire\Attributes\Validate; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; + +class MyComponent extends Component +{ + use AuthorizesRequests; + + public Application $application; + + // Properties with validation attributes + #[Validate(['required'])] + public string $name; + + #[Validate(['string', 'nullable'])] + public ?string $description = null; + + #[Validate(['boolean', 'required'])] + public bool $isStatic = false; + + public function mount() + { + $this->authorize('view', $this->application); + $this->syncData(); // Load from model + } + + public function syncData(bool $toModel = false): void + { + if ($toModel) { + $this->validate(); + + // Sync TO model (camelCase → snake_case) + $this->application->name = $this->name; + $this->application->description = $this->description; + $this->application->is_static = $this->isStatic; + + $this->application->save(); + } else { + // Sync FROM model (snake_case → camelCase) + $this->name = $this->application->name; + $this->description = $this->application->description; + $this->isStatic = $this->application->is_static; + } + } + + public function submit() + { + $this->authorize('update', $this->application); + $this->syncData(toModel: true); // Save to model + $this->dispatch('success', 'Saved successfully.'); + } +} +``` + +#### Validation with #[Validate] Attributes + +All component properties should have `#[Validate]` attributes: + +```php +// Boolean properties +#[Validate(['boolean'])] +public bool $isEnabled = false; + +// Required strings +#[Validate(['string', 'required'])] +public string $name; + +// Nullable strings +#[Validate(['string', 'nullable'])] +public ?string $description = null; + +// With constraints +#[Validate(['integer', 'min:1'])] +public int $timeout; +``` + +#### Benefits of syncData() Pattern + +- **Explicit Control**: Clear visibility of what's being synchronized +- **Type Safety**: #[Validate] attributes provide compile-time validation info +- **Easy Debugging**: Single method to check for data flow issues +- **Maintainability**: All sync logic in one place +- **Flexibility**: Can add custom logic (encoding, transformations, etc.) + +#### Creating New Form Components with syncData() + +#### Step-by-Step Component Creation Guide + +**Step 1: Define properties in camelCase with #[Validate] attributes** +```php +use Livewire\Attributes\Validate; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Livewire\Component; + +class MyFormComponent extends Component +{ + use AuthorizesRequests; + + // The model we're syncing with + public Application $application; + + // Component properties in camelCase with validation + #[Validate(['string', 'required'])] + public string $name; + + #[Validate(['string', 'nullable'])] + public ?string $gitRepository = null; + + #[Validate(['string', 'nullable'])] + public ?string $installCommand = null; + + #[Validate(['boolean'])] + public bool $isStatic = false; +} +``` + +**Step 2: Implement syncData() method** +```php +public function syncData(bool $toModel = false): void +{ + if ($toModel) { + $this->validate(); + + // Sync TO model (component camelCase → database snake_case) + $this->application->name = $this->name; + $this->application->git_repository = $this->gitRepository; + $this->application->install_command = $this->installCommand; + $this->application->is_static = $this->isStatic; + + $this->application->save(); + } else { + // Sync FROM model (database snake_case → component camelCase) + $this->name = $this->application->name; + $this->gitRepository = $this->application->git_repository; + $this->installCommand = $this->application->install_command; + $this->isStatic = $this->application->is_static; + } +} +``` + +**Step 3: Implement mount() to load initial data** +```php +public function mount() +{ + $this->authorize('view', $this->application); + $this->syncData(); // Load data from model to component properties +} +``` + +**Step 4: Implement action methods with authorization** +```php +public function instantSave() +{ + try { + $this->authorize('update', $this->application); + $this->syncData(toModel: true); // Save component properties to model + $this->dispatch('success', 'Settings saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } +} + +public function submit() +{ + try { + $this->authorize('update', $this->application); + $this->syncData(toModel: true); // Save component properties to model + $this->dispatch('success', 'Changes saved successfully.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } +} +``` + +**Step 5: Create Blade view with camelCase bindings** +```blade +