livewire: Binding deep nested data broken.
Description
First of all, I love this package! It takes a way the hurdle of JS in dynamic interfaces.
My problem: I have build a very dynamic form. This is the context: A candidate can have multiple employers and at a employer he can have (had) multiple positions. Therefore we have to bind the form item of a position on the following way:
protected $rules = [ 'employers.*.employer_name' => 'required|string|min:3', 'employers.*.positions.*.position_title' => 'required', ];
The binding of the employer name works. That is also synced between the frontend and the Livewire component.
However when loading the page the position title is not displayed. It stays blank.
I can conclude that the bind is successful because of two reasons:
- as soon as I enter data in the form field after a few seconds the input disappears.
- when I change the name of the field, an error pops up that I have to bind the input with $rules.
In my opinion this means that somewhere in the package the data is incorrectly handled. I have tried to figure out the behavior by looking into the hydrate / dehydrate method for dotNotedData. However by checking the data via dd() debugging all seems fine. I even see an update object with the original value (which is not displayed in the text input) and the new input.
This supports my hypothesis that somewhere in the source of Livewire the data is not getting correctly placed. However I ran into the limits of my debugging skills and PHP skills.
I hope someone can help me figure this out and find a solution. I am more then willing to submit a PR for this problem however I am unable to find the core issue.
Exact steps to reproduce
This is a example of the data structure I use:
0 => array:6 [▼
"id" => 91
"employer_name" => "Employer 1"
"candidate_id" => 33
"created_at" => "2020-08-24T09:06:39.000000Z"
"updated_at" => "2020-08-24T09:06:39.000000Z"
"positions" => []
]
1 => array:6 [▼
"id" => 92
"employer_name" => "Employer 2"
"candidate_id" => 33
"created_at" => "2020-08-24T09:06:58.000000Z"
"updated_at" => "2020-08-24T09:06:58.000000Z"
"positions" => array:2 [▼
0 => array:11 [▼
"id" => 70
"position_title" => "Example position"
"description" => """
Test description
"""
"start_date" => "2016-03-24"
"end_date" => null
"date_notation" => "m-Y"
"is_current_position" => "true"
"employer_id" => 92
"candidate_id" => 33
"created_at" => "2020-08-24T09:06:58.000000Z"
"updated_at" => "2020-08-24T09:06:58.000000Z"
]
1 => array:11 [▼
"id" => 71
"position_title" => "Example position"
"description" => """
Testing engineer\r\n
"""
"start_date" => "2006-12-24"
"end_date" => "2006-03-24"
"date_notation" => "Y"
"is_current_position" => null
"employer_id" => 92
"candidate_id" => 33
"created_at" => "2020-08-24T09:06:58.000000Z"
"updated_at" => "2020-08-24T09:06:58.000000Z"
]
]
]
2 => array:6 [▼
"id" => 93
"employer_name" => "Employer 3 "
"candidate_id" => 33
"created_at" => "2020-08-24T09:06:58.000000Z"
"updated_at" => "2020-08-24T09:06:58.000000Z"
"positions" => array:1 [▼
0 => array:11 [▼
"id" => 72
"position_title" => "Engineer"
"description" => ""Description"
"start_date" => "1992-08-24"
"end_date" => "2006-08-24"
"date_notation" => "Y"
"is_current_position" => null
"employer_id" => 93
"candidate_id" => 33
"created_at" => "2020-08-24T09:06:58.000000Z"
"updated_at" => "2020-08-24T09:06:58.000000Z"
]
]
]
Stripped-down, copy-pastable code snippets
This is the (simplified) Livewire component i use:
namespace App\Http\Livewire\Candidate;
use App\Models\Candidate\CandidateEmployer;
use App\Models\Candidate\CandidatePosition;
use App\Models\Candidate\CandidateResume;
use Livewire\Component;
class ResumeWorkExperienceEdit extends Component
{
public $resume;
public $candidate;
public $employers = [];
protected $rules = [
'employers.*.employer_name' => 'required|string|min:3',
'employers.*.positions.*.position_title' => 'required|string|min:3',
];
public function mount()
{
$this->resume = CandidateResume::where('candidate_id', $this->candidate->id)->first();
$this->employers = old('education', $this->resume->employers);
}
public function resumeEdited()
{
$this->emit('resumeEdited');
}
public function render()
{
return view('livewire.candidate.resume-work-experience-edit');
}
}
This is the frontend code:
@foreach($employers as $i => $employer)
<div class="{{ !$loop->first ? 'border-t border-gray-200' : ''}} py-4">
<h3 class="text-md leading-6 font-medium text-gray-900">
Werkgever {{ $loop->iteration }}
<button type="button" wire:click.prevent="removeEducation({{ $i }})"
class="float-right relative inline-flex items-center px-1 py-1 rounded-md border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-purple-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
<svg class="h-4 w-4 text-gray-400" x-description="Heroicon name: bookmark"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z">
</path>
</svg>
</button>
</h3>
<div class="grid grid-cols-6 gap-4">
<div class="col-span-6">
<x-form-input name="employers.{{ $i }}.employer_name"
wire:model="employers.{{ $i }}.employer_name" label="Naam werkgever" />
</div>
</div>
@foreach($employer->positions as $i_position => $position)
<div class="grid grid-cols-6 gap-4">
<div class="col-span-6">
<x-form-input name="employers.{{ $i }}.positions.{{ $i_position }}.position_title"
wire:model="employers.{{ $i }}.positions.{{ $i_position }}.position_title"
label="Naam werkgever" />
</div>
</div>
@endforeach
</div>
@endforeach
Context
- PHP: 7.4.10
- Livewire version: v2.3.0
- Laravel version: v8.10.0
- Browser: Safari
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Comments: 20 (5 by maintainers)
@CoffeeKing68 oh yeah arrays are fine. Sorry should have been more clear that it is eloquent models/collections that are limited.
So the reason it’s limited is, when you bind to models or eloquent collections, you have to specify in the rules what data can be accessed by the front end.
But for that to actually work, Livewire iterates through the model/collection properties and only collects and sends to the front end any that are in that list (acting as a whitelist), which is a security feature to prevent all data, such as a user’s password field, being sent to the frontend automatically. Instead the developer needs to be deliberate about it.
The iteration code currently only supports shallow nesting as it is manually specified to a certain depth. To change that to deep nesting requires changing that code to a recursive function then making sure edge cases are covered and ensuring that there isn’t excessive data sent to the front end.
I have a proof of concept working with the recursive function that supports something like
posts.*.comments.*.replies.messagefor example, but it currently breaks if I do something likeposts.*.comments.*.replies.author.name. Hopefully will find the time soon to finish it off 🙂@CoffeeKing68 nah that is definitely related, so this issue is fine. Livewire currently doesn’t support deep nesting so
contents.*.tasks.*.namewon’t work.I’m currently working on deep nesting support, which I have about half finished. I just haven’t had the time the last month or so to finish it off.
Hope this helps!
@joshhanley yep it works with 2.3.6, great!
I did not try any of the given examples. But my experience with these kind of problems is that whenever working with nested data it is better to create ‘child’ components. So in your case it would be wise to have a separate Employers component, etc.