DEV.RULES
Digital agency
376
Laravel

Делаем ссылки в навигации активными с помощью blade-директивы

От Святослав Торн, публикация от 1 July, 2020

Автор: Ryan Chandler

Ссылка на ресурс: ryangjchandler.co.uk

Перевод: Святослав Торн

  1. Перевод статьи
  2. Реальное использование - от переводчика
  3. Активация от несокльких роутов - от переводчика

Позвольте мне показать как я делаю добавление класса "active" к элементам меню в своих приложениях. 

Самый простой способ сделать это - сравнить текущий маршрут с ожидаемым.

Отлично, у Laravel уже есть класс Illuminate\Support\Facades\Route, в котором есть метод currentRouteName() благодаря которому мы сможем выполнить это сравнение.

Создадим простую переиспользуемую функцию (в любом месте нашего приложения).

use Illuminate\Support\Facades\Route;

function active_route(string $name): bool
{
    return Route::currentRouteName() === $name;
}

Эта функция уже по сути решает часть нашего вопроса, но еще не на столько хороша. Давайте вторым параметром передадим необходимый CSS класс (например 'bg-gray-900').

use Illuminate\Support\Facades\Route;

function active_route(string $name, string $classes): bool
{
    if ($active = Route::currentRouteName() === $name) {
        echo $classes;
    }
  
    return $active;
}

Огонь! Теперь в нашу функцию мы передаем два параметра. И если сравнение будем успешным, мы распечатываем наш класс переданный вторым параметром.

Давайте завернем это все в Blade директиву. Для этого в любом сервис провайдере ServiceProvider::boot() в секции boot() объявим нашу новую директиву.

Blade::directive('active', function ($expression) {
    return "<?php \active_route({$expression}); ?>"
});

Теперь внутри нашего Blade шаблона просто используем это

@active('projects.index', 'bg-gray-900 hover:bg-gray-500')

Это решение будет работать точно также как и прямой вызов функции active_route().

Немного расширим применение.

Что если мы хотим показывать какие то UI элементы в зависимости от текущей страницы?

Один из способов это сделать, проверить сколько параметров получила функция на вход. Если параметр всего один, то можно рассматривать это как условие if.

Давайте сделаем второй параметр active_route() необязательным и добавим несколько проверок, чтобы увидеть, сколько параметров получает наша директива:

use Illuminate\Support\Facades\Route;

function active_route(string $name, string $classes = null): bool
{
    if ($active = Route::currentRouteName() === $name && $classes) {
        echo $classes;
    }
  
    return $active;
}
use Illuminate\Support\Facades\Blade;

Blade::directive('active', function ($expression) {
    $parts = explode(',', str_replace(['(', ')'], '', $expression));
  
    if (count($parts) === 1) {
        return "<?php if (active_route({$expression})) : ?>";
    }
  
    return "<?php \active_route({$expression}); ?>"
});

Происходящее может показаться запутанным, но я объясню:

  1. Когда наш обработчик пользовательских директив вызывается, он получает $expression в виде строки. Например @active ('projects.index') будет означать выражение ('projects.index'). Скобки включены в строку, поэтому нам нужно удалить их перед запуском функции explode().
  2. explode(',', ...) разделит строку после каждого ,. Если приводятся два аргумента, то они должны быть разделены запятой. Вы можете столкнуться с проблемами здесь, если будете использовать запятые в названиях маршрутов, но я никогда не видел, чтобы кто-то делал так. В том случае, если запятые отсутствуют, $parts будет просто массивом с одним элементом.
  3. Если существует только 1 элемент в массиве, то тогда это будет if else конструкция. Иначе будет отдана строка с классом.

Сейчас пробуем сделать вот так:

@active('projects.index') bg-gray-900 hover:bg-gray-500 @else bg-gray-400 @endif

@endactive против @endif

Можно не заморачиваться и использовать endif - это будет отлично работать. Но чтобы код был красиво и понятно оформлен мы можем ввести еще одну директиву.

Blade::directive('endactive', function () {
    return '<?php endif; ?>';
});

 

Пример реального использования!

Решил попробовать сделать так как писал автор этой статьи, посмотрим что из этого вышло :)

Раньше я делал возможно так же как и вы:

{{Request::is(['admin']) ? 'active-nav-link' : 'opacity-75 hover:opacity-100'}}

<a href="{{ route('admin.home') }}" class="flex items-center {{Request::is(['admin']) ? 'active-nav-link' : 'opacity-75 hover:opacity-100'}} text-white py-4 pl-6 nav-item transition duration-150">
    <i class="fas fa-tachometer-alt mr-3"></i>
    Dashboard
</a>

Теперь во вью это выглядит так:

@active('notes', 'text-gray-500 cursor-not-allowed')

<a href="{{ route('notes') }}" class="@active('notes', 'text-gray-500 cursor-not-allowed')  py-2 px-6 flex hover:text-blue-500 transition duration-200">
    Заметки
</a>
<?php
namespace App\Providers;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        ...
        Blade::directive('active', static function ($expression) {
            return "<?php \App\Functions\Func::active_route({$expression}); ?>";
        });
    }
}
<?php
namespace App\Functions;
use Illuminate\Support\Facades\Route;

class Func
{
    public static function active_route(string $name, string $classes): bool
    {
        if ($active = (Route::currentRouteName() === $name)) {
            echo $classes;
        }

        return $active;
    }
}

Супер! Легко и просто, пользуйтесь.

Усложним задачу!

У меня блог с заметками, и есть два роута, страница с карточками заметок и с самой заметкой. А что если роутов еще больше, я немного переработал функцию автора и вот что вышло:

// Через запятую перечислим возможные роуты
@active('notes,note.page', 'text-gray-500 cursor-not-allowed')
<?php
namespace App\Functions;
use Illuminate\Support\Facades\Route;

class Func
{
    public static function active_route(string $name, string $classes): bool
    {
        // Получаем массив роутов
        $variants = explode(',', $name);
        // Проходим в цикле, сравнивая с текущим роутом, и выходим из цикла как только будет найдено первое совпадение, иначе false.
        foreach ($variants as $variant){
            if($active = (Route::currentRouteName() === $variant)) {
                echo $classes;
            }
        }
        return $active;
    }
}

Йухху:)