jrdavidson's avatar

Sorting A Livewire Component with a Concatenated String

I'm looking to find out how I can sort a field inside of my Livewire component that contains an accessor. The accessor is called full_name which is made up of title, first_name and last_name. Currently inside of my view I am displaying the name as title+first_name+last_name, however for a listing of people's names it should be viewed as last_name,+title+first_name.

What is suggested to change the order of how the full name is shown on the page? Should I create a different accessor for this?

<?php

namespace App\Http\Livewire;

use App\Models\Teacher;
use Livewire\Component;
use Livewire\WithPagination;

class TeachersList extends Component
{
    use WithPagination;

    protected $paginationTheme = 'bootstrap';
    public $sortField = 'last_name';
    public $sortDirection = 'asc';

    public function sortBy($field)
    {
        if ($this->sortField === $field) {
            $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
        } else {
            $this->sortDirection = 'asc';
        }

        $this->sortField = $field;
    }

    public function render()
    {
        $teachers = Teacher::orderBy($this->sortField, $this->sortDirection)->paginate(10);

        return view('livewire.teachers.teachers-list', [
            'teachers' => $teachers
        ]);
    }
}
<x-slot name="head">
    <x-table.heading sortable hasSymbol wire:click="sortBy('last_name')" :direction="$sortField == 'last_name' ? $sortDirection : null">Name</x-table.heading>
</x-slot>

... 

<div class="ml-4">
    <div class="mb-0 text-dark-75 font-weight-bolder font-size-lg">{{ $teacher->full_name }}</div>
    <a class="text-muted font-weight-bold text-hover-primary" href="#">{{ $teacher->email }}</a>
</div>
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Teacher extends Model
{
    use HasFactory;

    ...

    protected $appends = ['name'];

    /**
    * Get the user's full name.
    *
    * @return string
    */
    public function getFullNameAttribute()
    {
        return "{$this->title } {$this->first_name} {$this->last_name}";
    }

    /**
    * Get the user's name.
    *
    * @return string
    */
    public function getNameAttribute()
    {
        // return "{$this->first_name} {$this->last_name}";
        return $this->attributes['name'] = "{$this->first_name} {$this->last_name}";
    }
}

0 likes
7 replies
rodrigo.pedra's avatar

As you are paginating your results you should apply the order by on your database query, where your model's accessors are not available.

One solution is to use the Query Builder's ->when(...) method:

public function render()
{
    $teachers = Teacher::query()
        ->when(
            $this->sortField === 'full_name',
            function ($query) {
                $query->orderByRaw("CONCAT(last_name, ', ', title, ' ', first_name) " . $this->sortDirection);
            },
            function ($query) {
                $query->orderBy($this->sortField, $this->sortDirection);
            },
        )
        ->paginate(10);

    return view('livewire.teachers.teachers-list', [
        'teachers' => $teachers,
    ]);
}

The ->when(...) method checks for a condition and if it is true it runs the first callback, else it runs the second one.

So in this case we check if the sort field is full_name, then we apply a raw ORDER BY clause to the query's underlying SQL. Otherwise we use the regular ->orderBy(...) method.

The in your livewire component, you can use wire:click as you would for the other fields:

<div class="ml-4">
    <div class="mb-0 text-dark-75 font-weight-bolder font-size-lg"  wire:click="sortBy('full_name')">{{ $teacher->full_name }}</div>
    <a class="text-muted font-weight-bold text-hover-primary" href="#">{{ $teacher->email }}</a>
</div>

Note I used the order you mentioned in your post for ordering (CONCAT(last_name, ', ', title, ' ', first_name)) which is different then the order you are rendering in your model's accessor ({$this->first_name} {$this->last_name})

Hooe it is clear and helps somehow.

1 like
jrdavidson's avatar

@rodrigo.pedra Thank you for such a great response. That is what I"m looking for however the problem with the code right now is that by default it thinks $this->sortField is null which it is by default but after watching a screencast about datatables with livewire I don't understand what I'm missing for it to not render.

https://laravel-livewire.com/screencasts/s7-sorting

<?php

namespace App\Http\Livewire;

use App\Models\Teacher;
use Livewire\Component;
use Livewire\WithPagination;

class TeachersList extends Component
{
    use WithPagination;

    protected $paginationTheme = 'bootstrap';
    public $sortField;
    public $sortDirection = 'asc';

    public function sortBy($field)
    {
        if ($this->sortField === $field) {
            $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
        } else {
            $this->sortDirection = 'asc';
        }

        $this->sortField = $field;
    }

    public function render()
    {
        $teachers = Teacher::query()
                        ->when(
                            $this->sortField === 'full_name',
                            function ($query) {
                                $query->orderByRaw("CONCAT(last_name, ', ', title, ' ', first_name) " . $this->sortDirection);
                            },
                            function ($query) {
                                $query->orderBy($this->sortField, $this->sortDirection);
                            },
                        )
                        ->paginate(10);

        return view('livewire.teachers.teachers-list', [
            'teachers' => $teachers
        ]);
    }
}
<x-slot name="head">
    <x-table.heading sortable hasSymbol wire:click="sortBy('full_name')" :direction="$sortField === 'full_name' ? $sortDirection : null">Name</x-table.heading>
</x-slot>
@props([
    'sortable' => null,
    'direction' => null,
    'hasSymbol' => null
])

<th
    {{ $attributes->merge(['class' => 'datatable-cell datatable-cell-sort'])->only('class') }}
>
    @unless ($sortable)
        <span style="width: 130px">{{ $slot }}</span>
    @else
        @unless ($hasSymbol)
            <span style="width: 295px">
                {{ $slot }}
            </span>
            @if ($direction === 'asc')
                <i class="flaticon2-arrow-down"></i>
            @elseif ($direction === 'desc')
                <i class="flaticon2-arrow-up"></i>
            @else
                <i></i>
            @endif
        @else
            <span style="width: 250px" {{ $attributes->except('class') }}>
                {{ $slot }}
                @if ($direction === 'asc')
                    <i class="flaticon2-arrow-down"></i>
                @elseif ($direction === 'desc')
                    <i class="flaticon2-arrow-up"></i>
                @else
                    <i></i>
                @endif
            </span>
        @endif
    @endif
</th>

rodrigo.pedra's avatar

You can use a default value for it:

<?php

namespace App\Http\Livewire;

use App\Models\Teacher;
use Livewire\Component;
use Livewire\WithPagination;

class TeachersList extends Component
{
    use WithPagination;

    protected $paginationTheme = 'bootstrap';
    public $sortField;
    public $sortDirection = 'asc';

    public function sortBy($field)
    {
        if ($this->sortField === $field) {
            $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
        } else {
            $this->sortDirection = 'asc';
        }

        $this->sortField = $field ?? 'name'; // <<< CHANGED HERE
    }

    public function render()
    {
        $teachers = Teacher::query()
                        ->when(
                            $this->sortField === 'full_name',
                            function ($query) {
                                $query->orderByRaw("CONCAT(last_name, ', ', title, ' ', first_name) " . $this->sortDirection);
                            },
                            function ($query) {
                                $query->orderBy($this->sortField ?? 'name', $this->sortDirection); // <<<< CHANGED HERE
                            },
                        )
                        ->paginate(10);

        return view('livewire.teachers.teachers-list', [
            'teachers' => $teachers
        ]);
    }
}
1 like
jrdavidson's avatar

@rodrigo.pedra Right but I don't understand what I"m doing differently in mine that what is happening inside of the video because as far as I can see there is no default value but yet he doesn't get an error due to $this->sortField being null.

rodrigo.pedra's avatar

Yeah, i was skimming through the video and noticed that too.

Although he first set it to title (at 01:47) and does a hard-refresh, and then later it is not clear he does a hard refresh on just refresh from the Livewire component which will persist the public state between calls.

This might be missing from the video, or was cut out when he edited it.

As I am not a sponsor I cannot check the GitHub repository to see what the final code is.

I guess you'll need to either have a initial value, or use the null-coalescing for now.

If you are a sponsor, please check how the final code is on the GitHub repository.

jrdavidson's avatar

@rodrigo.pedra I looked into this and became a sponsor and without divulging code the sortField became a sorts array. So now I need to figure out how to handle this. I also added full_name_listing to the $appends array, however the query orderByRaw is giving an sql error.

->when(
    collect($this->sorts)->keys()->contains('full_name_listing'),
    function ($query) {
        $query->orderByRaw("CONCAT(last_name, ', ', title, ' ', first_name)" . $this->sorts['full_name_listing']);
    },
);
rodrigo.pedra's avatar

Is $this->sorts['full_name_listing'] either ASC or DESC ?

If so, maybe there is a missing space after the CONCAT's closing parenthesis ?

->when(
    collect($this->sorts)->keys()->contains('full_name_listing'),
    function ($query) {
        $query->orderByRaw("CONCAT(last_name, ', ', title, ' ', first_name) " . $this->sorts['full_name_listing']);
    },
);

look the space I added right before the closing quote on the CONCAT expression.

If it does not work yet, change the final ->paginate(10); call, to ->toSql(); and post the results.

You might need to dd the variable to see the SQL query, based on a previous code sample would be something like this:

        $teachers = Teacher::query()
->when(
    collect($this->sorts)->keys()->contains('full_name_listing'),
    function ($query) {
        $query->orderByRaw("CONCAT(last_name, ', ', title, ' ', first_name) " . $this->sorts['full_name_listing']);
    },
);
                        ->toSql(10);

dd($teachers);

Please or to participate in this conversation.