Laravel livewire trouble when using mutators and accessor - accessor

I try to input a model the properties of which are float numbers that are stored in database in a definite unit e.g. for volume : liters.
I want the user being able to see and enter the values in a unit of their choice e.g. US Gallons instead of liters letting the possibility of choosing an other unit at any time e.g. US Pint or hectoliter.
To achieve this I allow every user to store a unit set for various pysical quantities.
What I intend to do is to create mutators and accessors based on the untis the user has chosen to transform custom units to liters before saving and to transform liters to custom units at getting.
Here is an excerpt of my model
class Equipment extends Model
{
use HasFactory;
protected $table = "equipments";
protected $fillable = [
'name',
...
'boiler_volume',
'boiler_dead_volume',
...
];
protected $appends = ['boiler_volume','boiler_dead_volume'];
//get the user's unit transformation factor (it works and return a float number)
private function getKVolume()
{
$units = Unitset::where('user_id', auth()->user()->id)->first();
return explode(',', $units->volume)[2];
}
//BOILER-----------------
public function setBoilerVolumeAttribute($value)
{
if (is_numeric($value) and $value !== null and $value != 0) {
$this->attributes['boiler_volume'] = $value * $this->getKVolume();
}
}
public function getBoilerVolumeAttribute($value)
{
if (is_numeric($value)) {
return $value / $this->getKVolume();
}
}
//------------------
public function setBoilerDeadVolumeAttribute($value)
{
if (is_numeric($value) and $value !== null and $value != 0) {
$this->attributes['boiler_dead_volume'] = $value * $this->getKVolume();
}
}
public function getBoilerDeadVolumeAttribute($value)
{
if (is_numeric($value)) {
return $value / $this->getKVolume();
}
}
Now comes the rules in the livewire component
protected $rules =[
...
'equipment.boiler_volume'=>'required|numeric',
'equipment.boiler_dead_volume'=>'required|numeric',
...
];
and finally the relevant part of the form in the blade view
<x-input.group label="{{ __('Boiler Tun Volume ') }} ({{ explode(',', $unitset->volume)[0] }})" for='
boiler_volume' :error="$errors->first('equipment.boiler_volume')"
class="flex flex-col w-full p-2 border border-jbrown-peachpuf bg-jbrown-darker rounded-md ">
<x-input.text wire:model.lazy="equipment.boiler_volume" type="number" step="0.001"
class="border border-red-200 bg-red-100" name="boiler_volume" id="boiler_volume" value="" />
</x-input.group>
<x-input.group label="{{ __('Boiler Tun Dead Volume ') }} ({{ explode(',', $unitset->volume)[0] }})" for='
boiler_dead_volume' :error="$errors->first('equipment.boilen_dead_volume')"
class="flex flex-col w-full p-2 border border-jbrown-peachpuf bg-jbrown-darker rounded-md ">
<x-input.text wire:model="equipment.boiler_dead_volume" type="text" class="border border-red-200 bg-red-100"
name="boiler_dead_volume" id="boiler_dead_volume" value="" />
</x-input.group>
As you can see the first input use a number type while the second on use a text type.
This is only for explanation as the trouble is the same either with number input type or text input type. But now let me say what the trouble is.
What is the trouble?
The trouble is that for the second field equipment.boiler_dead_volume as soon as you have input a value you cannot delete it (for example for correction). You can delete all the digits you have entered but the first one. Nevertheless, if you use the left arrow key to place the cursor at the beginning of the field, you can enter a new value, then delete the last char but never let the field empty. It's rather annoying for the user.
I must add that this behavior doesn't exist if mutators and accessors are not used. In addition, the first field for boiler_volume works normally even with accessor and mutator.
Last and final, all other fields behaves the same i.e. wrongly.
Note
The line
protected $appends=...
doesn't change anything.
Other strange behavior
Looking at the mutators and accessors you can see some checking that the value is numeric and not null. This is due to the fact that without this checks, everytime a field is emptied, the application complains that the value is not numeric.
I would be happy to get some help troubleshooting this!

It seems that wire:model.lazy is mandatory. In the first group I used it at another time without being aware of it.
So, lazyness is mandatory in this case.

Related

Use vue to access a Laravel model method in a v-for loop

I am learning how to use vue with laravel. I have basic loops working well to pull direct model relationships, but I can't figure out how to access model methods in a loop. Many of my Larvel models have basic information formulated with a method pulling data from related models. I've tried to research it and think the answer might be some combination of eager loading, preformating the answer as a json response or maybe something with axios, but the snipits I've found aren't clear on what goes where, or what needs to be in place for them to work correctly. I've tried both eager loading and using a json response and neither has worked. I can access methods in simple vue components that are just text, but not in a loop where the variable isn't part of the page.
Example: I want to use Vue to display a list of ingredients on a recipe's page. The ingredient "title" is a method pulling the information from a related model.
RecipeController.php
public function show(Recipe $recipe)
{
$ingredients = $recipe->ingredients;
$view = $this->view('recipes.show');
//(variable in the view, variable defined in current function)
$view->with('recipe', $recipe);
$view->with('ingredients', $ingredients);
return $view;
}
Recipe.php
public function ingredients()
{
return $this->hasMany('App\Models\Ingredient', 'recipe_id', 'recipe_id');
}
Ingredient.php
public function title()
{
$title = $this->item->title();
return $title;
}
public function vueTitle()
{
$title = Ingredient::title()->get();
return response()->json($title );
}
Recipes/show.php
<div>
<ul>
<li
is="test-li"
v-for="ingredient in {{ $ingredients }}"
v-bind:key="ingredient.ingredient_id"
v-bind:title= "ingredient.vueTitle"
v-bind:id="ingredient.ingredient_id"
></li>
</ul>
</div>
I'd prefer to reuse the same methods, but created a new one to try converting to json first but that didn't work (or I'm doing it wrong). I tried eager loading, but it either did nothing, or generated an error (Call to a member function on null) if I tried to eager load the specific method. I've tried various combinations of binding and not binding the title component. I've also tried title= "{{ingredient->title()}}" but that syntax errors.
How can I get the result of the Laravel method in a Vue loop?
After more searching, I found this post which described how to add an accessor to a model. Doing so allowed me to access my custom method as if it were a standard direct relationship. It was a straightforward modification and will reduce complexity in a number of places. I made the following modifications:
Ingredient.php
Added the get..Attribute() function and appended the array
...
protected $table = 'ingredients';
...
protected $appends = array('title');
// Access methods as direct relationships
public function getTitleAttribute()
{
return $this->title();
}
Recipes/show.php
Bound the title prop to the ingredient title as if it were a direct relationship.
<div>
<ul>
<li
is="test-li"
v-for="ingredient in {{ $ingredients }}"
v-bind:key="ingredient.ingredient_id"
v-bind:title= "ingredient.title"
v-bind:id="ingredient.ingredient_id"
></li>
</ul>
</div>
Another example, hope one may find it helpful:
Model.php
/**
* Accessor for Age.
*/
protected $appends = ['age'];
public function getAgeAttribute()
{
return Carbon::parse($this->attributes['dob'])->age;
}
VueFile.vue
<td>
<span v-bind:age="user.age"> {{user.age}} </span>
</td>

how construct route pattern for an unknown number of tags - Laravel & Conner/Taggable

I have a blog and a quotationfamous sayings repository on one site.
The quotations are tagged and the entries are tagged too.
I use this rtconner/laravel-tagging package.
Now, what I want to do is to display ALL Quotation models which share the same tags as article.
The Eloquent syntax is simple, as the original docs provide an example:
Article::withAnyTag(['Gardening','Cooking'])->get();
possible solution
Optional routing parameters. The asker-picked answer in this question gives a solution:
//in routes.php
Route::get('/{book?}/{chapter?}/{topic?}/{article?}', 'controller#func');
//in your controller
public function func($book = null, $chapter = null, $topic = null, $article = null) {
...
}
my problem
In my app the shared tags might count more than 3 or 5. I will soon get an example with even 10 tags. Possibly more
My question
Does it mean that I have to construct an URL with 10 optional routing parameters? Do I really need sth like this:
Route::get('quotations/tags/{tag1?}/{tag2?}/{tag3?}/{tag4?}/{tag5?}/{tag6?}/{tag7?}', 'controller#func');
my question rephrased
I could create a form with only a button visible, and in a hidden select field I could put all the tags. The route would be a POST type then and it would work. But this solution is not URL-based.
I think you could process the slashes, as data:
Route::get('quotations/tags/{tagsData?}', 'controller#func')
->where('tagsData', '(.*)');
Controller:
public function controller($tagsData = null)
{
if($tagsData)
{
//process
}
}
Ok, this is my solution. As I have a tagged model, I dont't need to iterate through tags in url to get the whole list of tags.
The enough is this:
// Routes file:
Route::get('quotations/all-tags-in/{itemtype}/{modelid}', 'QuotationsController#all_tagged_in_model');
Then in my controller:
public function all_tagged_in_topic($itemtype, $id) {
if($itemtype == 'topic') {
$tags = Topic::find($id)->tags->pluck('name')->all();
$topic = Topic::find($id);
}
if($itemtype == 'quotation') {
$tags = Quotation::find($id)->tags->pluck('name')->all();
$quotation = Quotation::find($id);
}
// dd($tags);
$object = Quotation::withAnyTag($tags)->paginate(100);;
And it is done.
Now, the last issue is to show tags in the URL.
TO do that, the URL should have an extra OPTIONAL parameter tags:
// Routes file:
Route::get('quotations/all-tags-in/{itemtype}/{modelid}/{tags?}', 'QuotationsController#all_tagged_in_model');
And in the {url?} part you can just write anything which won't break the pattern accepted by route definition.
In your view you might generate an URL like this:
// A button to show quotes with the same set of tags as the article
// generated by iteration through `$o->tags`
<?php
$manual_slug = 'tag1-tag2-tag3-tag4`;
?>
<a href="{{ URL::to('quotations/all-tags-in/article/'.$o->id.'/'.$manual_slug) }}" class="btn btn-danger btn-sm" target="_blank">
<i class="fa fa-tags icon"></i> Tagi:
</a>

Keeping select values across pages

I have a couple of routes
Route::get('/route_one', 'IndexController#index');
Route::get('/route_two', 'IndexController#index');
They call the same controller function because these pages need the same array of data. This function is as follows
public function index()
{
$user = Auth::user();
if( $user ) {
$fileData = $this->fillArrayWithFileNodes(new DirectoryIterator( public_path(). '/images'));
$currentPath= Route::getFacadeRoot()->current()->uri();
if(!empty($fileData)) {
return view('index', compact('fileData', 'currentPath'));
}
} else {
return view('auth.login');
}
}
Now the index view is pretty straight forward, but it does has this part
#if($currentPath == 'route_one')
#include('layouts.searchbarRouteOne')
#endif
#if($currentPath == 'route_two')
#include('layouts.searchbarRouteTwo')
#endif
So depending on what route is called, a different sidebar is displayed. Now the sidebars essentially contain a load of select inputs like the following
<div class="col-lg-3">
<div class="form-group">
<label>Year</label>
<select id="year" class="form-control">
<option value=""></option>
#foreach($fileData["route_one"] as $year => $products)
<option value="{{ $year }}">{{ $year }}</option>
#endforeach
</select>
</div>
</div>
Both sidebars have different selects. When select options are selected, an ajax call is made to display an image. This all works fine.
This is my problem. I have a link to get to route_one or route_two. As the page refreshes when the link is clicked, the selects are at their default state. What I would like to do somehow is keep the last state of the select inputs. I am not storing this data within a database which may be an issue?
Furthermore, route_two relies on the select options from route_one. So when route_two is selected, I need to pass it route_ones options.
What would be the best way to achieve what I am after?
Thanks
Think what you are trying to accomplish here: remember the old input values.
You could send the form when clicking the link and flash the data in your controller or use JavaScript saving input values to the browser's storage.
Simple example using plain JavaScript
// Get all select-elements
var inputs = document.querySelectorAll('select');
// Loop through them
for (var i = 0; i < inputs.length; i++) {
// Set the old input value
inputs[i].value = localStorage.getItem(inputs[i].name);
// Start listening changes
inputs[i].addEventListener('change', store);
}
// The function to call when the value has changed
function store(event) {
// Set the new value to the browser's storage
localStorage.setItem(event.target.name, event.target.value);
}
In that example your form elements are required to have unique name attributes. Of course it can be switched out using e.g. id attribute.

Checking First Character of a String with Blade Template

Right now, in my controller, I'm passing some alphabetized data to my explore view.
public function browse()
{
return View::make('explore')
->with('artists', Artist::orderBy('artist', 'ASC')->get());
}
And then in the view, I'm using Blade to loop through that information.
<ul>
#foreach($artists as $artist)
<li>{{ $artist->artist }}</li>
#endforeach
</ul>
I want to run a conditional that checks the first letter of each artist so I can further group the data under the correct starting letter. How would you do that with Blade? I saw the helper "starts_with" but I'm not quite sure how to implement that into a conditional.
This logic really belongs somewhere other than the view/template layer. A View Composer could be a better location, or perhaps even a model method to return your data separated by starting letter - thus returning a hash of arrays containing artists under each letter.
Example code:
class Artist extends Eloquent
{
public static function allByFirstCharacter()
{
$artists = Artist::orderBy('artist', 'ASC')->get();
return $artists->reduce(function($sortedArtists, $artist) {
$firstChar = $artist->artist[0];
if (!isset($sortedArtists[$firstChar])) {
$sortedArtists[$firstChar] = [];
}
$sortedArtists[$firstChar][] = $artist;
return $sortedArtists;
}, []);
}
}
// Controller
$sortedArtists = Artist::allByFirstCharacter(); // Then pass it to the view
// In view
#foreach ($sortedArtists as $letter => $artists)
<!-- Some menu item or something similar here -->
#foreach ($artist as $artist)
<!-- Print out details of artist -->
#endforeach
#endforeach
Note that this example only fills in existing first characters. If you'd like to have all characters in the alphabet/numbers you'd need to prepopulate the hash and then reduce it.

Laravel - get id from the right database record

I have laravel project. When I click my view button, I want to see full description of my record. But I don't know how to pass the right id. My database table is called - csstable.
I have model:
<?php
class CssTable extends Eloquent
{
protected $table = 'csstable';
}
View button on each post (I get all of my posts from database, so each of them have id):
<div class="view">
<a href="{{ action('CssController#show') }}" ></a>
</div>
CssController with this show function:
public function show()
{
$csstable = CssTable::all();
return View::make('cssposts/right_post', compact('csstable'));
}
My Route:
Route::get('/css/id_of_right_post', 'CssController#show' );
Right_post, where I want information from description column from row, with id, that i clicked (In this field, I see just last record's description:
<h1 style="color:#fff">{{ $css->description }}</h1>
I have tried to put something like this
public function show($id)
{
$csstable = CssTable::find($id);
return View::make('cssposts/right_post', compact('csstable'));
}
But then there is an error - missing 1 argument in show function. So I want to know, how to pass correct id!
The way to do this involves three steps. First let's go for the route:
Route::get('css/{id}', 'CssController#show');
The {id} there means it's a matching parameter - it'll match a full URI segment (basically anything between slashes) and use that to pass into he method passed. So on to the controller:
class CssController
{
public function show($id)
{
$csstable = CssTable::findOrFail($id);
return View::make('cssposts/view', compact('csstable));
}
}
That controller method accepts a (required) single parameter. You can call it whatever you want, but here I'm going for id as it's an ID for a model. Finally, the last part of the puzzle is how to link to such a route:
// view button for each csstable
<div class="view">
{{ link_to_action('CssController#show', $item->title, array($item->getKey())) }}
</div>
As you can see, I'm using the link_to_action helper, but your method with <a href="{{{ action('CssController#show', array($item->getKey())) }}}"> will work too. After the controller action name, you pass an array that contains all of the parameters in the URI to fill in (in order). In our case we have one parameter, to it's an array with one item. I think in these cases you could also use a string and Laravel will turn it into an array with one element for you. I prefer to be explicit.
Hopefully that's helped you work out how to use the parameter-based routing system in Laravel.

Resources