@@ -267,18 +267,365 @@ For complete documentation, see **[form-components.mdc](mdc:.cursor/rules/form-c
267267
268268## Form Handling Patterns
269269
270+ ### Livewire Component Data Synchronization Pattern
271+
272+ **IMPORTANT**: All Livewire components must use the **manual `syncData()` pattern** for synchronizing component properties with Eloquent models.
273+
274+ #### Property Naming Convention
275+ - **Component properties**: Use camelCase (e.g., `$gitRepository`, `$isStatic`)
276+ - **Database columns**: Use snake_case (e.g., `git_repository`, `is_static`)
277+ - **View bindings**: Use camelCase matching component properties (e.g., `id="gitRepository"`)
278+
279+ #### The syncData() Method Pattern
280+
281+ ```php
282+ use Livewire\Attributes\Validate;
283+ use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
284+
285+ class MyComponent extends Component
286+ {
287+ use AuthorizesRequests;
288+
289+ public Application $application;
290+
291+ // Properties with validation attributes
292+ #[Validate(['required'])]
293+ public string $name;
294+
295+ #[Validate(['string', 'nullable'])]
296+ public ?string $description = null;
297+
298+ #[Validate(['boolean', 'required'])]
299+ public bool $isStatic = false;
300+
301+ public function mount()
302+ {
303+ $this->authorize('view', $this->application);
304+ $this->syncData(); // Load from model
305+ }
306+
307+ public function syncData(bool $toModel = false): void
308+ {
309+ if ($toModel) {
310+ $this->validate();
311+
312+ // Sync TO model (camelCase → snake_case)
313+ $this->application->name = $this->name;
314+ $this->application->description = $this->description;
315+ $this->application->is_static = $this->isStatic;
316+
317+ $this->application->save();
318+ } else {
319+ // Sync FROM model (snake_case → camelCase)
320+ $this->name = $this->application->name;
321+ $this->description = $this->application->description;
322+ $this->isStatic = $this->application->is_static;
323+ }
324+ }
325+
326+ public function submit()
327+ {
328+ $this->authorize('update', $this->application);
329+ $this->syncData(toModel: true); // Save to model
330+ $this->dispatch('success', 'Saved successfully.');
331+ }
332+ }
333+ ```
334+
335+ #### Validation with #[Validate] Attributes
336+
337+ All component properties should have `#[Validate]` attributes:
338+
339+ ```php
340+ // Boolean properties
341+ #[Validate(['boolean'])]
342+ public bool $isEnabled = false;
343+
344+ // Required strings
345+ #[Validate(['string', 'required'])]
346+ public string $name;
347+
348+ // Nullable strings
349+ #[Validate(['string', 'nullable'])]
350+ public ?string $description = null;
351+
352+ // With constraints
353+ #[Validate(['integer', 'min:1'])]
354+ public int $timeout;
355+ ```
356+
357+ #### Benefits of syncData() Pattern
358+
359+ - **Explicit Control**: Clear visibility of what's being synchronized
360+ - **Type Safety**: #[Validate] attributes provide compile-time validation info
361+ - **Easy Debugging**: Single method to check for data flow issues
362+ - **Maintainability**: All sync logic in one place
363+ - **Flexibility**: Can add custom logic (encoding, transformations, etc.)
364+
365+ #### Creating New Form Components with syncData()
366+
367+ #### Step-by-Step Component Creation Guide
368+
369+ **Step 1: Define properties in camelCase with #[Validate] attributes**
370+ ```php
371+ use Livewire\Attributes\Validate;
372+ use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
373+ use Livewire\Component;
374+
375+ class MyFormComponent extends Component
376+ {
377+ use AuthorizesRequests;
378+
379+ // The model we're syncing with
380+ public Application $application;
381+
382+ // Component properties in camelCase with validation
383+ #[Validate(['string', 'required'])]
384+ public string $name;
385+
386+ #[Validate(['string', 'nullable'])]
387+ public ?string $gitRepository = null;
388+
389+ #[Validate(['string', 'nullable'])]
390+ public ?string $installCommand = null;
391+
392+ #[Validate(['boolean'])]
393+ public bool $isStatic = false;
394+ }
395+ ```
396+
397+ **Step 2: Implement syncData() method**
398+ ```php
399+ public function syncData(bool $toModel = false): void
400+ {
401+ if ($toModel) {
402+ $this->validate();
403+
404+ // Sync TO model (component camelCase → database snake_case)
405+ $this->application->name = $this->name;
406+ $this->application->git_repository = $this->gitRepository;
407+ $this->application->install_command = $this->installCommand;
408+ $this->application->is_static = $this->isStatic;
409+
410+ $this->application->save();
411+ } else {
412+ // Sync FROM model (database snake_case → component camelCase)
413+ $this->name = $this->application->name;
414+ $this->gitRepository = $this->application->git_repository;
415+ $this->installCommand = $this->application->install_command;
416+ $this->isStatic = $this->application->is_static;
417+ }
418+ }
419+ ```
420+
421+ **Step 3: Implement mount() to load initial data**
422+ ```php
423+ public function mount()
424+ {
425+ $this->authorize('view', $this->application);
426+ $this->syncData(); // Load data from model to component properties
427+ }
428+ ```
429+
430+ **Step 4: Implement action methods with authorization**
431+ ```php
432+ public function instantSave()
433+ {
434+ try {
435+ $this->authorize('update', $this->application);
436+ $this->syncData(toModel: true); // Save component properties to model
437+ $this->dispatch('success', 'Settings saved.');
438+ } catch (\Throwable $e) {
439+ return handleError($e, $this);
440+ }
441+ }
442+
443+ public function submit()
444+ {
445+ try {
446+ $this->authorize('update', $this->application);
447+ $this->syncData(toModel: true); // Save component properties to model
448+ $this->dispatch('success', 'Changes saved successfully.');
449+ } catch (\Throwable $e) {
450+ return handleError($e, $this);
451+ }
452+ }
453+ ```
454+
455+ **Step 5: Create Blade view with camelCase bindings**
456+ ```blade
457+ <div>
458+ <form wire:submit="submit">
459+ <x-forms.input
460+ canGate="update"
461+ :canResource="$application"
462+ id="name"
463+ label="Name"
464+ required />
465+
466+ <x-forms.input
467+ canGate="update"
468+ :canResource="$application"
469+ id="gitRepository"
470+ label="Git Repository" />
471+
472+ <x-forms.input
473+ canGate="update"
474+ :canResource="$application"
475+ id="installCommand"
476+ label="Install Command" />
477+
478+ <x-forms.checkbox
479+ instantSave
480+ canGate="update"
481+ :canResource="$application"
482+ id="isStatic"
483+ label="Static Site" />
484+
485+ <x-forms.button
486+ canGate="update"
487+ :canResource="$application"
488+ type="submit">
489+ Save Changes
490+ </x-forms.button>
491+ </form>
492+ </div>
493+ ```
494+
495+ **Key Points**:
496+ - Use `wire:model="camelCase"` and `id="camelCase"` in Blade views
497+ - Component properties are camelCase, database columns are snake_case
498+ - Always include authorization checks (`authorize()`, `canGate`, `canResource`)
499+ - Use `instantSave` for checkboxes that save immediately without form submission
500+
501+ #### Special Patterns
502+
503+ **Pattern 1: Related Models (e.g., Application → Settings)**
504+ ```php
505+ public function syncData(bool $toModel = false): void
506+ {
507+ if ($toModel) {
508+ $this->validate();
509+
510+ // Sync main model
511+ $this->application->name = $this->name;
512+ $this->application->save();
513+
514+ // Sync related model
515+ $this->application->settings->is_static = $this->isStatic;
516+ $this->application->settings->save();
517+ } else {
518+ // From main model
519+ $this->name = $this->application->name;
520+
521+ // From related model
522+ $this->isStatic = $this->application->settings->is_static;
523+ }
524+ }
525+ ```
526+
527+ **Pattern 2: Custom Encoding/Decoding**
528+ ```php
529+ public function syncData(bool $toModel = false): void
530+ {
531+ if ($toModel) {
532+ $this->validate();
533+
534+ // Encode before saving
535+ $this->application->custom_labels = base64_encode($this->customLabels);
536+ $this->application->save();
537+ } else {
538+ // Decode when loading
539+ $this->customLabels = $this->application->parseContainerLabels();
540+ }
541+ }
542+ ```
543+
544+ **Pattern 3: Error Rollback**
545+ ```php
546+ public function submit()
547+ {
548+ $this->authorize('update', $this->resource);
549+ $original = $this->model->getOriginal();
550+
551+ try {
552+ $this->syncData(toModel: true);
553+ $this->dispatch('success', 'Saved successfully.');
554+ } catch (\Throwable $e) {
555+ // Rollback on error
556+ $this->model->setRawAttributes($original);
557+ $this->model->save();
558+ $this->syncData(); // Reload from model
559+ return handleError($e, $this);
560+ }
561+ }
562+ ```
563+
564+ #### Property Type Patterns
565+
566+ **Required Strings**
567+ ```php
568+ #[Validate(['string', 'required'])]
569+ public string $name; // No ?, no default, always has value
570+ ```
571+
572+ **Nullable Strings**
573+ ```php
574+ #[Validate(['string', 'nullable'])]
575+ public ?string $description = null; // ?, = null, can be empty
576+ ```
577+
578+ **Booleans**
579+ ```php
580+ #[Validate(['boolean'])]
581+ public bool $isEnabled = false; // Always has default value
582+ ```
583+
584+ **Integers with Constraints**
585+ ```php
586+ #[Validate(['integer', 'min:1'])]
587+ public int $timeout; // Required
588+
589+ #[Validate(['integer', 'min:1', 'nullable'])]
590+ public ?int $port = null; // Nullable
591+ ```
592+
593+ #### Testing Checklist
594+
595+ After creating a new component with syncData(), verify:
596+
597+ - [ ] All checkboxes save correctly (especially `instantSave` ones)
598+ - [ ] All form inputs persist to database
599+ - [ ] Custom encoded fields (like labels) display correctly if applicable
600+ - [ ] Form validation works for all fields
601+ - [ ] No console errors in browser
602+ - [ ] Authorization checks work (`@can` directives and `authorize()` calls)
603+ - [ ] Error rollback works if exceptions occur
604+ - [ ] Related models save correctly if applicable (e.g., Application + ApplicationSetting)
605+
606+ #### Common Pitfalls to Avoid
607+
608+ 1. **snake_case in component properties**: Always use camelCase for component properties (e.g., `$gitRepository` not `$git_repository`)
609+ 2. **Missing #[Validate] attributes**: Every property should have validation attributes for type safety
610+ 3. **Forgetting to call syncData()**: Must call `syncData()` in `mount()` to load initial data
611+ 4. **Missing authorization**: Always use `authorize()` in methods and `canGate`/`canResource` in views
612+ 5. **View binding mismatch**: Use camelCase in Blade (e.g., `id="gitRepository"` not `id="git_repository"`)
613+ 6. **wire:model vs wire:model.live**: Use `.live` for `instantSave` checkboxes to avoid timing issues
614+ 7. **Validation sync**: If using `rules()` method, keep it in sync with `#[Validate]` attributes
615+ 8. **Related models**: Don't forget to save both main and related models in syncData() method
616+
270617### Livewire Forms
271618```php
272619class ServerCreateForm extends Component
273620{
274621 public $name;
275622 public $ip;
276-
623+
277624 protected $rules = [
278625 'name' => 'required|min:3',
279626 'ip' => 'required|ip',
280627 ];
281-
628+
282629 public function save()
283630 {
284631 $this->validate();
0 commit comments