Skip to content

Commit d7e3a8a

Browse files
authored
Merge pull request #7101 from coollabsio/next
v4.0.0-beta.440
2 parents 8c4bfeb + a45e674 commit d7e3a8a

File tree

14 files changed

+1281
-403
lines changed

14 files changed

+1281
-403
lines changed

.cursor/rules/frontend-patterns.mdc

Lines changed: 349 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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
272619
class 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

Comments
 (0)