Livewire Nested Components Sub-Form - laravel

I'm having trouble understanding Nested Components. I'm mainly using my nested components to provide a Modal (nested component) in my main components. Ex: a Modal form for some additional info in my Forms on main components.
I hope what I'm trying to accomplish is possible. I have a "partial form" for Checks input in many components. Each component has the same exact validation rules and required Model quieres. I want to remove all of those, put them in 1 nested component and then call something like #livewire('checks.checks-form') in each main component.
Something like this:
Main Component TimesheetForm
View livewire.timesheets.payment-form :
some TimesheetForm inputs here
#livewire('checks.checks-form')
some TimesheetForm inputs here
Although the data from my nested component (ChecksForm) is loaded onto the main component views, the main component views still try to load the validation rules from itself instead of the nested component. I was really hoping to avoid repeting both, my nested component date (which seems to work) AND my nested component validation rules.
Is this something that can be achieved? (I'm sure since anything I have ever struggled with in the TALL stack always comes back with positive results). Thank you for any pointers in the right direction! - Patryk
App\Http\Livewire\TimesheetPaymentForm:
class TimesheetPaymentForm extends Component
{
public User $user;
protected function rules()
{
return [
'user.full_name' => 'nullable',
'weekly_timesheets.*.checkbox' => 'nullable',
'employee_weekly_timesheets.*.checkbox' => 'nullable',
'user_paid_expenses.*.checkbox' => 'nullable',
];
}
...
public function render()
{
return view('livewire.timesheets.payment-form');
}
payment-form.blade.php:
{{-- FORM --}}
{{-- ROWS --}}
<x-forms.row
wire: model"user.full_name"
errorName="user.full_name"
name="user.full_name"
text="Payee"
type="text"
disabled
>
</x-forms.row>
#livewire('checks.checks-form')
App\Http\Livewire\Checks: Want to use this inside my TimesheetPaymentForm
{
public $check = NULL;
public $check_input = NULL;
protected function rules()
{
return [
'check.date' => 'required',
'check.paid_by' => 'required_without:check.bank_account_id',
'check.bank_account_id' => 'required_without:check.paid_by',
'check.check_type' => 'required_with:check.bank_account_id',
'check.check_number' => 'required_if:check.check_type,Check',
'check.invoice' => 'required_with:check.paid_by',
];
}
...
public function render()
{
$bank_accounts = BankAccount::where('type', 'Checking')->get();
$employees = auth()->user()->vendor->users()->where('is_employed', 1)->whereNot('users.id', auth()->user()->id)->get();
return view('livewire.checks._payment_form', [
'bank_accounts' => $bank_accounts,
'employees' => $employees,
]);
}
_payment.form.blade.php:
...
<x-forms.row
wire:model="check.paid_by"
errorName="check.paid_by"
name="paid_by"
text="Paid By"
type="dropdown"
>
<option value="" readonly>{{auth()->user()->vendor->business_name}}</option>
#foreach ($employees as $employee)
<option value="{{$employee->id}}">{{$employee->first_name}}</option>
#endforeach
</x-forms.row>
<div
x-data="{ open: #entangle('check.paid_by') }"
x-show="!open"
x-transition.duration.150ms
>
#include('livewire.checks._include_form')
</div>
<div
x-data="{ open: #entangle('check.paid_by') }"
x-show="open"
x-transition.duration.150ms
>
<x-forms.row
wire:model="check.invoice"
name="check.invoice"
errorName="check.invoice"
text="Reference"
type="text"
>
</x-forms. Row>
</div>
...

Related

Laravel : how to get $payment_gateway value

I'm trying to create a feature where, when i create a new booking i can choose the payment method like via xendit or transfer. But when i tried to submit the output of the payment method is still offline payment because of this code {{$row->gatewayObj ? $row->gatewayObj->getDisplayName() : ''}} , and not xendit. How do i fix this??
The Controller :
public function create(Request $request){
// $this->checkPermission('news_create');
$allServices = get_bookable_services();
$whatsAppBookableServices = ["art", "food", "gear", "car", "hotel"];
$payment_gateway = ["xendit", "offline payment"];//tambahan Nicho
$row = new BookingOffline();
$row->fill([
'status' => 'publish',
]);
$data = [
// 'categories' => NewsCategory::get()->toTree(),
'row' => $row,
'breadcrumbs' => [
[
'name' => __('Report'),
'url' => 'admin/module/report/booking'
],
[
'name' => __('Add Booking By WA'),
'class' => 'active'
],
],
'bookableServices' => array_keys($allServices),
'whatsAppBookableServices' => $whatsAppBookableServices,
'payment_gateway' => $payment_gateway,//tambahan Nicho
];
return view('Report::admin.booking.create', $data);
}
The Blade file :
<td>
{{$row->gatewayObj ? $row->gatewayObj->getDisplayName() : ''}}
</td>
The gatewayObj :
function get_payment_gateway_obj($payment_gateway)
{
$gateways = get_payment_gateways();
if (empty($gateways[$payment_gateway]) or !class_exists($gateways[$payment_gateway])) {
return false;
}
$gatewayObj = new $gateways[$payment_gateway]($payment_gateway);
return $gatewayObj;
}
There are still missing pieces to the puzzle, so I cannot provide you with a code snippet to implement.
However, I think you should be able to diagnose it this way:
Check your controller.
Do a die-dump of the $data just above the line containing return view.... Like so: dd($data['payment_gateway'])
Then refresh the page in your browser and see if the $data object is exactly how you want it. The value should be ["xendit", "offline payment"].
Check your form
I suppose you have a form element like a <select></select>, which is iterating over the values of the $data['payment_gateway'] array. If you do not have this, how are your users choosing between the payment options?
Next, make sure that each iteration of payment gateway is being submitted properly. YOu did not include the snippet that handles form submission, but if you're using a <select> element, the options each need to be submitted with a value.
If we hardcode the select, you will have something like this:
<select name="payment_gateway">
<option value="xendit">Xendit</option>
<option value="offline">Offline Payment</option>
</select>
So when the server receives this form information, it knows the exact value of payment gateway to use. Dynamically, it could look like this:
<select name="payment_gateway">
#foreach($data['payment_gateways'] as $gateway)
<option value="{{ $gateway }}">{{ $gateway }}</option>
#endforeach
</select>
Intercept the request and check that your payment_gateway is being submitted properly.
Go to the controller method that handles your form, and do something like dd($request->all())
Then inspect the value of payment_gateway.

How to dynamically add input fields with livewire

I want add new Inputbox to be created when the user clicks the add button.
I tried the following code but it didn't work, And I get the following error:
get_class(): Argument #1 ($object) must be of type object, string
given
class Test extends Component
{
public $tests=[''];
public function render()
{
return view('livewire.test');
}
public function mount()
{
$this->tests = Test::all();
}
public function add()
{
$this->tests[] = '';
}
}
<div>
<form wire:submit.prevent="submit">
#foreach($tests as $index=>$test)
<div>
<label for="title">
<input wire:model="title.{{$index}}" type="text">
</label>
</div>
#endforeach
<button type="submit">Save</button>
</form>
<button wire:click="add()">ADD MORE</button>
</div>
There are a few issues to address here, so I will provide some background information and try to be as descriptive as possible.
In your mount function you assign a collection of Test objects to your tests array property:
public function mount()
{
$this->tests = Test::all(); // <-- returns a collection of Test models
}
This overrides the default value and type you have assigned to tests which is an array:
/*
* This declares $tests to be an array
* but this becomes a Collection due to your mount method
*/
public $tests = [''];
Yet in your add function you're adding a string:
public function add()
{
$this->tests[] = '';
}
So you're getting the error because you're trying to add a string to a variable that is a collection of Test models. What you likey want to do is add a new empty Test model.
public function add()
{
$this->tests->push(Test::make());
}
However, the above will not work as you're working with an existing database collection and so you'll get the following error:
Queueing collections with multiple model connections is not supported.
Therefore to achieve your goal of adding a new Test we need to approach this differently.
app\Http\Livewire\Test.php
<?php
namespace App\Http\Livewire;
use Illuminate\Support\Collection;
use Livewire\Component;
class Test extends Component
{
// A collection of your models from the database
public Collection $tests;
// A conditional variable that we use to show/hide the add new model inputs
public bool $isAdding = false;
// A variable to hold our new empty model during creation
public \App\Models\Test $toAdd;
// Validation rules
// These are required in order for Livewire to successfull bind to model properties using wire:model
// Add any other validation requirements for your other model properties that you bind to
protected $rules = [
'tests.*.title' => ['required'],
'toAdd.title' => ['required']
];
// Livewires implementation of a constructor
public function mount()
{
// Set some default values for the component
$this->prepare();
}
public function render()
{
return view('livewire.test');
}
//
public function add()
{
// Set the show/hide variable to true so that the new inputs section is shown
$this->isAdding = true;
}
// Save the new model data
public function save()
{
// Save the model to the database
$this->toAdd->save();
// Clean things up
$this->prepare();
}
// Just a helper method for performing repeated functionality
public function prepare()
{
// Get all the required models from the database and assign our local variable
$this->tests = \App\Models\Test::all();
// Assign an empty model to the toAdd variable
$this->toAdd = \App\Models\Test::make();
// Set the isAdding show/hide property to false, which will hide the new model inputs
$this->isAdding = false;
}
}
resources\views\livewire\test.blade.php
<div>
<!-- loop over all your test models -->
#foreach ($tests as $index => $test)
<!-- the :key attribute is essential in loops so that livewire doesnt run into DOM diffing issues -->
<div :key="tests_{{ $index }}">
<label for="tests_{{ $index }}_title">Title {{ $index }}
<input type="text" id="tests_{{ $index }}_title" name="tests_{{ $index }}_title"
wire:model="tests.{{ $index }}.title" :key="tests_{{ $index }}_title" />
</label>
</div>
#endforeach
<!-- Only show the new model inputs if isAdding is true -->
#if ($isAdding)
<!-- this doesnt need a :key as its not in a loop -->
<div>
<label for="title">Title
<input type="text" id="title" name="title" wire:model="toAdd.title" />
</label>
<!-- triggers the save function on the component -->
<button type="button" wire:click="save">Save</button>
</div>
#endif
<!-- triggers the add function on the component -->
<button type="button" wire:click="add">Add More</button>
</div>
Hopefully, with the comments I have added the above makes sense.
Update
Is it possible to create several input boxes and finally save all the data at once?
That is entirely possible yes. Due to some limitations with Livewire, specifically it not knowing how to hydrate Eloquent Models in a Collection correctly, we need to use a little wizardry to make it work.
app\Http\Livewire\Test.php
<?php
namespace App\Http\Livewire;
use Illuminate\Support\Collection;
use Livewire\Component;
class Test extends Component
{
// A collection of your models from the database
public Collection $tests;
// A conditional variable that we use to show/hide the add new model inputs
public bool $isAdding = false;
// A variable to hold our new empty models during creation
public Collection $toAdd;
// Validation rules
// These are required in order for Livewire to successfull bind to model properties using wire:model
// Add any other validation requirements for your other model properties that you bind to
protected $rules = [
'tests.*.title' => ['required'],
'toAdd.*.title' => ['required']
];
// Livewires implementation of a constructor
public function mount()
{
$this->cleanUp(true);
}
public function hydrate()
{
// Get our stored Test model data from the session
// The reason for this is explained further down
$this->toAdd = session()->get('toAdd', collect());
}
public function render()
{
return view('livewire.test');
}
public function add()
{
$this->isAdding = true;
// Push (add) a new empty Test model to the collection
$this->toAdd->push(\App\Models\Test::make());
// Save the value of the toAdd collection to a session
// This is required because Livewire doesnt understand how to hydrate an empty model
// So simply adding a model results in the previous elements being converted to an array
session()->put('toAdd', $this->toAdd);
}
public function save($key)
{
// Save the model to the database
$this->toAdd->first(function ($item, $k) use ($key) {
return $key == $k;
})->save();
// Remove it from the toAdd collection so that it is removed from the Add More list
$this->toAdd->forget($key);
// Clean things up
$this->cleanUp(!$this->toAdd->count());
}
public function saveAll()
{
$this->toAdd->each(function ($item) {
$item->save();
});
$this->cleanUp(true);
}
// Just a helper method for performing repeated functionality
public function cleanUp(bool $reset = false)
{
// Get all the required models from the database and assign our local variable
$this->tests = \App\Models\Test::all();
// If there are no items in the toAdd collection, do some cleanup
// This will reset things on page refresh, although you might not want that to happen
// If not, consider what you want to happen and change accordingly
if ($reset) {
$this->toAdd = collect();
$this->isAdding = false;
session()->forget('toAdd');
}
}
}
resources\views\livewire\test.blade.php
<div>
<!-- loop over all your test models -->
#foreach ($tests as $index => $test)
<!-- the :key attribute is essential in loops so that livewire doesnt run into DOM diffing issues -->
<div :key="tests_{{ $index }}">
<label for="tests_{{ $index }}_title">Title {{ $index }}
<input type="text" id="tests_{{ $index }}_title" name="tests_{{ $index }}_title"
wire:model="tests.{{ $index }}.title" :key="tests_{{ $index }}_title" />
</label>
</div>
#endforeach
<!-- Only show the new model inputs if isAdding is true -->
#if ($isAdding)
<div>
#foreach ($toAdd as $index => $value)
<div :key="toAdd_{{ $index }}">
<label for="toAdd_{{ $index }}_title">New Title {{ $index }}
<input type="text" id="toAdd_{{ $index }}_title"
name="toAdd_{{ $index }}_title" wire:model="toAdd.{{ $index }}.title"
:key="toAdd_{{ $index }}_title" />
</label>
<!-- triggers the save function on the component -->
<button type="button" wire:click="save({{ $index }})" :key="toAdd_{{ $index }}_save">Save</button>
</div>
#endforeach
<button type="button" wire:click="saveAll">Save All</button>
</div>
#endif
<!-- triggers the add function on the component -->
<button type="button" wire:click="add">Add More</button>
</div>
I've not removed some of the previous comments and not commented on the code that should be pretty obvious what it is doing.
I did rename the prepare function to cleanUp and it seemed more appropriate.
I also provided functions for saving an individual Test model, or all at the same time. You might want to be able to do one or the other or both at some point so seemed useful.

Error trying to get property 'users' of non-object

I have a problem, i am doing a shift history of the user logged in the application.
#foreach($eventos as $evento)
if($evento->solicitud->users->id == (Auth::user()->id))
<div class="item-timeline">
<div class="t-meta-date">
<p class="">{{$evento->fecha}}</p>
</div>
<div class="t-dot">
</div>
<div class="t-text">
<p>{{$evento->servicio->servicio}}</p>
<p>{{$evento->personal}}</p>
</div>
</div>
#endif
#endforeach
I get all shifts with:
public function perfil (){
$eventos = Eventos::All();
$data = [
'category_name' => 'users',
'page_name' => 'profile',
'has_scrollspy' => 0,
'scrollspy_offset' => '',
];
// $pageName = 'profile';
return view('users.user_profile',compact('eventos'))->with($data);
}
Model Eventos:
public function solicitud()
{
return $this->belongsTo(Solicitudes::class);
}
Model Solicitudes:
public function evento()
{
return $this->belongsTo(Eventos::class);
}
and then filter with if condition.
But it shows me error Trying to get property 'users' of non-object
dd capture: https://i.imgur.com/WUVOaO6.png
data base: https://i.imgur.com/J5YSvxQ.png
I need to get the User_id from the solicitudes table.
The relation is: Eventos table-> solicitud_id with the ID column of the Solicitudes table.
from the code which you have provided, $evento-> solicitud-> users means I assume that you have used a nested relationship and the following things are missing
there is no 'users' relation in 'solicitudes' class
you didn't call relations in controller while fetching eventos (that is $eventos=Eventos::with('solicitud.users')->get()
correct me if my assumption is wrong

how to bind the model relation value in livewire wire:model?

There is a Product model with hasmany descriptions relationship.
descriptions relation has two columns, count, body.
How I must define the wire:model value to show the selected product descriptions values in Livewire Update component?
I must mention getting data from relation works fine just can't show data in the input tag of wire:model attribute! I think the problem is on the key definition of protected $rules or wire:model value!
the Update Class:
public $product;
protected $listeners = ['selectedProduct'];
public function selectedProduct($id){
$this->product = Product::with('descriptions')->findOrFail($id);
}
protected $rules = [
"description.count" => "required",
"description.body" => "required",
];
the livewire view:
#if($product)
#foreach($product->descriptions as $description)
<input type="text" wire:model="description.count">
<texatarea wire:model="description.body"></texatarea>
#endforeach
#endif
the loop and number of filed is repeated correct but no data is shown!
There are a few things I'd like to mention, first is that your view should always only have one root HTML element - and subsequent elements that is generated in a loop should also have one unique root element inside, that has wire:key on it. The wire:key should be something unique on your page.
Then we need to have a look at the rules - its correct that any models needs a rule to be visible in input-elements, but you have a collection of elements, so the rules need to reflect that. To do that, you specify a wildcard as the key
protected $rules = [
"description.*.count" => "required",
"description.*.body" => "required",
];
The fields then has to be bound towards an index, so Livewire knows its in a collection.
<div>
#if ($product)
#foreach($product->descriptions as $index=>$description)
<div wire:key="product-description-{{ $description->id }}">
<input type="text" wire:model="descriptions.{{ $index }}.count">
<texatarea wire:model="descriptions.{{ $index }}.body"></texatarea>
</div>
#endforeach
#endif
</div>
Finally, you need to declare the descriptions as a public property on the class, and store them there.
public $product;
public $descriptions;
protected $listeners = ['selectedProduct'];
protected $rules = [
"description.*.count" => "required",
"description.*.body" => "required",
];
public function selectedProduct($id){
$this->product = Product::with('descriptions')->findOrFail($id);
$this->descriptions = $this->product->descriptions;
}

Repopulating user inputs after successful validation

I am trying to create a search form where the user has to select from some dropdown menus and enter text in one of a few fields. The problem is I am redisplaying the search page with results below it. To do this I am not redirecting, I am just returning a view with the datasets I need compacted along with it.
Is there any way to get to retrieve input similar to how you would do this Input::old('x') when you were redirecting after failed validation?
The routes are:
Route::get('search', ['as' => 'main.search.get', 'uses' => 'MainController#showSearchPage']);
Route::post('search', ['as' => 'main.search.post', 'uses' => 'MainController#showSearchResults']);
Example of code I have in the view:
{!! Form::open(array('route' => 'main.search.post', 'class' => 'form-inline align-form-center', 'role' => 'form')) !!}
<div class="form-group">
{!! Form::label('product_code', 'Product Code: ',['class' => 'control-label label-top']) !!}
{!! Form::text('product_code', Input::old('product_code'), ['class' => 'form-control input-sm']) !!}
</div>
So when you submit a search, it calls showSearchResults which then returns a view if it succeeds, if it fails validation via my SearchRequest class it gets redirected to the main.search.get route, errors are printed and input is returned to the fields.
I have done a lot of searching and have come up more or less empty handed, it would be nice if there was a way to say ->withInput() when returning a view (not redirecting) or something.
Currently my only solution is to Input::flash() but since I am not redirecting that data persists for an extra refresh. That isn't a terribly big deal at this point, but I was wondering if anyone else had a better solution.
Edit - Code below from controller where view is returned:
...
Input::flash();
return view('main.search', compact('results', 'platformList', 'versionList', 'customerList', 'currencyList', 'customer', 'currency'));
}
Thank you
I had the same problem. The solution that worked for me was to add the following line into the controller.
session(['_old_input' => $request->input()]);
Now I'll explain how it works.
In the view, the global function old() is called:
<input type="username" id="username" class="form-control" name="username" value="{{ old('username') }}" placeholder="Username" autofocus>
This function is in vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
function old($key = null, $default = null)
{
return app('request')->old($key, $default);
}
This calls Illuminate\Http\Request->old():
public function old($key = null, $default = null)
{
return $this->session()->getOldInput($key, $default);
}
Which calls Illuminate\Session\Store->getOldInput():
public function getOldInput($key = null, $default = null)
{
$input = $this->get('_old_input', []);
return Arr::get($input, $key, $default);
}
This call is looking for _old_input in the session. So the solution is to add the input to the session using this value.
Hope this helps.
You can use request instead of old since its the post request
change {{old('product_code')}} to {{request('product_code')}}

Resources