Verification: a143cc29221c9be0

Php check which checkboxes are checked

Php check which checkboxes are checked

Фильтр для товаров категории

Поскольку у нас теперь товары имеют атрибуты new, hit и sale, мы можем их отбирать по этим атрибутам. Другими словами — реализовать фильтр товаров. Давайте создадим шаблон filter.blade.php в директрии views/catalog/part и подключим его в шаблоне catalog.category.

@extends('layout.site', ['title' => $category->name])

@section('content')
    

{{ $category->name }}

{{ $category->content }}
class="row"> @foreach ($category->children as $child) @include('catalog.part.category', ['category' => $child]) @endforeach
class="bg-info p-2 mb-4">
method
="get" action="{{ route('catalog.category', ['category' => $category->slug]) }}"> @include('catalog.part.filter') href="{{ route('catalog.category', ['category' => $category->slug]) }}" class="btn btn-light">Сбросить
class="row"> @foreach ($products as $product) @include('catalog.part.product', ['product' => $product]) @endforeach
{{ $products->links() }} @endsection

="price" class="form-control d-inline w-25 mr-4" title="Цена">
    ="0">Выберите цену
    ="min"@if(request()->price == 'min') selected @endif>Дешевые товары
    ="max"@if(request()->price == 'max') selected @endif>Дорогие товары


class="form-check form-check-inline"> type="checkbox" name="new" class="form-check-input" id="new-product" @if(request()->has('new')) checked @endif value="yes"> ="form-check-label" for="new-product">Новинка
class="form-check form-check-inline"> type="checkbox" name="hit" class="form-check-input" id="hit-product" @if(request()->has('hit')) checked @endif value="yes"> ="form-check-label" for="hit-product">Лидер продаж
class="form-check form-check-inline "> type="checkbox" name="sale" class="form-check-input" id="sale-product" @if(request()->has('sale')) checked @endif value="yes"> ="form-check-label" for="sale-product">Распродажа
="submit" class="btn btn-light">Фильтровать

Изменим метод category() контроллера CatalogController:

class CatalogController extends Controller {
    /* ... */
    public function category(Request $request, Category $category) {
        $descendants = $category->getAllChildren($category->id);
        $descendants[] = $category->id;
        $builder = Product::whereIn('category_id', $descendants);

        // дешевые или дорогие товары
        if ($request->has('price') && in_array($request->price, ['min', 'max'])) {
            $products = $builder->get();
            $count = $products->count();
            if ($count > 1) {
                $max = $builder->get()->max('price'); // цена самого дорогого товара
                $min = $builder->get()->min('price'); // цена самого дешевого товара
                $avg = ($min + $max) * 0.5;
                if ($request->price == 'min') {
                    $builder->where('price', ', $avg);
                } else {
                    $builder->where('price', '>=', $avg);
                }
            }
        }
        // отбираем только новинки
        if ($request->has('new')) { 
            $builder->where('new', true);
        }
        // отбираем только лидеров продаж
        if ($request->has('hit')) {
            $builder->where('hit', true);
        }
        // отбираем только со скидкой
        if ($request->has('sale')) {
            $builder->where('sale', true);
        }

        $products = $builder->paginate(6)->withQueryString();
        return view('catalog.category', compact('category', 'products'));
    }
    /* ... */
}

Фильтр по цене вычисляет среднюю цену товара в категории как среднее арифметическое максимальной и минимальной цены. Если выбраны дешевые товары — будут показаны товары, у которых цена меньше или равна средней арифметической, если выбраны дорогие товары — будут показаны товары, у которых цена больше или равна средней арифметической.

У этого алгоритма есть недостаток — если в категории большинство товаров дешевые (например — 100.00, 200.00, 300.00 и 400.рублей), а дорогих товаров мало (например — только один 1000.00 рублей), то средняя цена будет 550.00 рублей. Когда выбраны дешевые товары — будут показано четыре товара, а когда дорогие — только один.

Возможно, есть смысл вычислить среднюю цену как среднее арифметическре всех цен в разделе каталога, тогда распределение по дешевым и дорогим будет более равномерным. Еще один алгоритм отбора дешевых и дорогих товаров — делить их ровно пополам. То есть 50% товаров попадают в дешевые, а еще 50% — попадают в дорогие.

class CatalogController extends Controller {
    /* ... */
    public function category(Request $request, Category $category) {
        $descendants = $category->getAllChildren($category->id);
        $descendants[] = $category->id;
        $builder = Product::whereIn('category_id', $descendants);

        // дешевые или дорогие товары
        if ($request->has('price') && in_array($request->price, ['min', 'max'])) {
            $products = $builder->get()->sortBy('price')->values();
            $count = $products->count();
            if ($count > 1) {
                $half = intdiv($count, 2);
                if ($count % 2) {
                    // нечетное кол-во товаров, надо найти цену товара, который ровно посередине
                    $avg = $products[$half]['price'];
                } else {
                    // четное количество, надо найти такую цену, которая поделит товары пополам
                    $avg = 0.5 * ($products[$half - 1]['price'] + $products[$half]['price']);
                }
                if ($request->price == 'min') {
                    $builder->where('price', ', $avg);
                } else {
                    $builder->where('price', '>=', $avg);
                }
            }
        }
        // отбираем только новинки
        if ($request->has('new')) { 
            $builder->where('new', true);
        }
        // отбираем только лидеров продаж
        if ($request->has('hit')) {
            $builder->where('hit', true);
        }
        // отбираем только со скидкой
        if ($request->has('sale')) {
            $builder->where('sale', true);
        }

        $products = $builder->paginate(6)->withQueryString();
        return view('catalog.category', compact('category', 'products'));
    }
    /* ... */
}

Рефакторинг кода

Теперь метод category() контроллера выглядит запутанно, и выполняет слишком много работы. Давайте вынесем фильтрацию товаров в отдельный класс app/Helpers/ProductFilter:

namespace App\Helpers;

use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Builder;

class ProductFilter {

    private $builder;
    private $request;

    public function __construct(Builder $builder, Request $request) {
        $this->builder = $builder;
        $this->request = $request;
    }

    public function apply() {
        foreach ($this->request->query() as $filter => $value) {
            if (method_exists($this, $filter)) {
                $this->$filter($value);
            }
        }
        return $this->builder;
    }

    private function price($value) {
        if (in_array($value, ['min', 'max'])) {
            $products = $this->builder->get();
            $count = $products->count();
            if ($count > 1) {
                $max = $this->builder->get()->max('price'); // цена самого дорогого товара
                $min = $this->builder->get()->min('price'); // цена самого дешевого товара
                $avg = ($min + $max) * 0.5;
                if ($value == 'min') {
                    $this->builder->where('price', ', $avg);
                } else {
                    $this->builder->where('price', '>=', $avg);
                }
            }
        }
    }

    private function new($value) {
        if ('yes' == $value) {
            $this->builder->where('new', true);
        }
    }

    private function hit($value) {
        if ('yes' == $value) {
            $this->builder->where('hit', true);
        }
    }

    private function sale($value) {
        if ('yes' == $value) {
            $this->builder->where('sale', true);
        }
    }
}
class CatalogController extends Controller {
    /* ... */
    public function category(Request $request, Category $category) {
        $descendants = $category->getAllChildren($category->id);
        $descendants[] = $category->id;
        $builder = Product::whereIn('category_id', $descendants);

        $products = (new ProductFilter($builder, $request))->apply()->paginate(6)->withQueryString();

        return view('catalog.category', compact('category', 'products'));
    }
    /* ... */
}

Используем скоупы

Сейчас, чтобы получить товары категории, мы используем следующий код:

class CatalogController extends Controller {
    /* ... */
    public function category(Request $request, Category $category) {
        $descendants = $category->getAllChildren($category->id);
        $descendants[] = $category->id;
        $builder = Product::whereIn('category_id', $descendants);

        $products = (new ProductFilter($builder, $request))->apply()->paginate(6)->withQueryString();

        return view('catalog.category', compact('category', 'products'));
    }
    /* ... */
}

При использовании локального скоупа (scope), получаем уже такой код:

class CatalogController extends Controller {
    /* ... */
    public function category(Request $request, Category $category) {
        $descendants = $category->getAllChildren($category->id);
        $descendants[] = $category->id;
        $builder = Product::categoryProducts($descendants);

        $products = (new ProductFilter($builder, $request))->apply()->paginate(6)->withQueryString();

        return view('catalog.category', compact('category', 'products'));
    }
    /* ... */
}
class Product extends Model {
    /**
     * Позволяет выбирать товары категории и всех ее потомков
     *
     * @param \Illuminate\Database\Eloquent\Builder $builder
     * @param array $parents
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeCategoryProducts($builder, $parents) {
        return $builder->whereIn('category_id', $parents);
    }
    /* ... */
}

Кажется, что в этом нет особого смысла, но давайте добавим еще один скоуп:

class CatalogController extends Controller {
    /* ... */
    public function category(Category $category, ProductFilter $filters) {
        $descendants = $category->getAllChildren($category->id);
        $descendants[] = $category->id;
        // получаем товары категории и ее потомков, потом применяем фильтры
        $builder = Product::categoryProducts($descendants)->filterProducts($filters);

        $products = $builder->paginate(6)->withQueryString();

        return view('catalog.category', compact('category', 'products'));
    }
    /* ... */
}
class Product extends Model {
    /**
     * Позволяет выбирать товары категории и всех ее потомков
     *
     * @param \Illuminate\Database\Eloquent\Builder $builder
     * @param array $parents
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeCategoryProducts($builder, $parents) {
        return $builder->whereIn('category_id', $parents);
    }

    /**
     * Позволяет фильтровать товары по нескольким условиям
     *
     * @param \Illuminate\Database\Eloquent\Builder $builder
     * @param $filters
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeFilterProducts($builder, $filters) {
        return $filters->apply($builder);
    }
    /* ... */
}

Мы внедряем экземпляр класса ProductFilter в метод category() контроллера. Создаем экземпляр построителя запроса, используя скоуп scopeCategoryProducts() и отбираем товары категории. Но скоуп scopeFilterProducts() используем немного не так, как принято делать. Вместо того, чтобы добавить условие where() — вызываем метод apply() класса ProductFilter. Который вернет нам все тот же объект построителя запроса, к которому добавлено несколько условий where() — это наши фильтры.

class ProductFilter {

    private $builder;
    private $request;

    public function __construct(Request $request) {
        $this->request = $request;
    }

    public function apply($builder) {
        $this->builder = $builder;
        foreach ($this->request->query() as $filter => $value) {
            if (method_exists($this, $filter)) {
                $this->$filter($value);
            }
        }
        return $this->builder;
    }

    /* ... */
}

Только вызов метода Category::getAllChildren() все портит — давайте сделаем этот метод статическим. И будем его вызывать в методе scopeCategoryProducts() модели Product, а не в методе category() контроллера CatalogController.

class Category extends Model {
    /**
     * Возвращает всех потомков категории с идентификатором $id
     */
    public static function getAllChildren($id) {
        // получаем прямых потомков категории с идентификатором $id
        $children = self::where('parent_id', $id)->with('children')->get();
        $ids = [];
        foreach ($children as $child) {
            $ids[] = $child->id;
            // для каждого прямого потомка получаем его прямых потомков
            if ($child->children->count()) {
                $ids = array_merge($ids, self::getAllChildren($child->id));
            }
        }
        return $ids;
    }
}
class Product extends Model {
    /**
     * Позволяет выбирать товары категории и всех ее потомков
     *
     * @param \Illuminate\Database\Eloquent\Builder $builder
     * @param integer $id
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeCategoryProducts($builder, $id) {
        $descendants = Category::getAllChildren($id);
        $descendants[] = $id;
        return $builder->whereIn('category_id', $descendants);
    }

    /**
     * Позволяет фильтровать товары по нескольким условиям
     *
     * @param \Illuminate\Database\Eloquent\Builder $builder
     * @param \App\Helpers\ProductFilter $filters
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeFilterProducts($builder, $filters) {
        return $filters->apply($builder);
    }
    /* ... */
}

Вот теперь метод category() контроллера выглядит вполне прилично:

class CatalogController extends Controller {
    /* ... */
    public function category(Category $category, ProductFilter $filters) {
        $products = Product::categoryProducts($category->id) // товары категории и всех ее потомков
            ->filterProducts($filters) // фильтруем товары категории и всех ее потомков
            ->paginate(6)
            ->withQueryString();
        return view('catalog.category', compact('category', 'products'));
    }
    /* ... */
}

Фильтр для товаров бренда

Аналогичный фильтр можем сделать для товаров бренда, для этого добавляем форму в шаблон catalog.brand:

@extends('layout.site', ['title' => $brand->name])

@section('content')
    

{{ $brand->name }}

{{ $brand->content }}
class="bg-info p-2 mb-4">
method
="get" action="{{ route('catalog.brand', ['brand' => $brand->slug]) }}"> @include('catalog.part.filter') href="{{ route('catalog.brand', ['brand' => $brand->slug]) }}" class="btn btn-light">Сбросить
class="row"> @foreach ($products as $product) @include('catalog.part.product', ['product' => $product]) @endforeach
{{ $products->links() }} @endsection

И изменяем метод brand() контроллера CatalogController:

class CatalogController extends Controller {
    /* ... */
    public function brand(Brand $brand, ProductFilter $filters) {
        $products = $brand
            ->products() // возвращает построитель запроса
            ->filterProducts($filters)
            ->paginate(6)
            ->withQueryString();
        return view('catalog.brand', compact('brand', 'products'));
    }
    /* ... */
}

Как осуществляется создание кастомного чекбокса или переключателя

Данный процесс осуществляется посредством скрытия стандартного элемента и создания с помощью CSS другого «поддельного», такого как мы хотим.

Но как же это будет работать, если стандартный input скрыть? Это можно выполнить благодаря тому, что в HTML переключить состояние checked можно не только с помощью самого элемента input, но и посредством связанного с ним label.

В HTML связывание label с input выполняется одним из 2 способов:

1. Посредством помещения элемента input в label:


2. Посредством задания элементу input атрибута id, а labelfor с таким же значением как у id.


В этой статье мы подробно разберём шаги по кастомизации checkbox и radio, в которых label с input свяжем по 2 варианту. Создание «поддельного» чекбокса выполним с использованием псевдоэлемента ::before, который поместим в label. При этом никакие дополнительные элементы в разметку добавлять не будем.

Создание стильного чекбокса

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

Шаг 1. Создадим разметку.


При создании разметки очень важно соблюдать последовательность расположения элементов. Это необходимо, потому что в зависимости от того, как они расположены мы будем составлять выражения для выбора элементов в CSS и назначать им стили.

В этом примере элемент label расположен после input. Связь label с input осуществляется посредством соответствия значения for элемента label с id элемента input.

В примере к элементу input добавлен класс custom-checkbox. Данный класс мы будем использовать при составлении селекторов и тем самым с помощью него определять элементы к которым следует добавить стилизованный чекбокс вместо обычного. Т.е. его присутствие или отсутствие будет определять с каким чекбоксом (со стандартным или поддельным) будет выводится элемент input с type="checkbox".

Вид чекбокса в браузере по умолчанию

Шаг 2. Напишем стили для скрытия стандартного элемента input.

Вид чекбокса после скрытия

.custom-checkbox {
  position: absolute;
  z-index: -1;
  opacity: 0;
}

Мы не будем использовать display: none, а установим ему стили, с помощью которых уберём его из потока (position: absolute), поместим его ниже существующих элементов (z-index: -1), а также сделаем его полностью прозрачным (opacity: 0). Зачем это нужно? Это нам необходимо для того, чтобы мы могли получить состояние фокуса, а затем стилизовать «подделный» checkbox или radio, когда он будет находиться в нём.

Шаг 3. Создадим поддельный чекбокс.

Вид кастомного чекбокса

.custom-checkbox+label {
  display: inline-flex;
  align-items: center;
  user-select: none;
}
.custom-checkbox+label::before {
  content: '';
  display: inline-block;
  width: 1em;
  height: 1em;
  flex-shrink: 0;
  flex-grow: 0;
  border: 1px solid #adb5bd;
  border-radius: 0.25em;
  margin-right: 0.5em;
  background-repeat: no-repeat;
  background-position: center center;
  background-size: 50% 50%;
}

Создание «поддельного» чекбокса выполним с помощью псевдоэлемента ::before. Посредством CSS зададим ему размеры (в данном случае 1emx1em), а затем нарисуем его с помощью border: 1px solid #adb5bd. Свойства начинающие со слова background будут определять положение самого флажка (когда checkbox будет в состоянии checked).

Первое правило необходимо для вертикального центрирования флажка и надписи к нему. Это действие в примере выполнено через CSS Flexbox.

Шаг 4. Создадим стили при нахождении элемента в состоянии checked.

Вид стилизованного чекбокса в состоянии checked

.custom-checkbox:checked+label::before {
  border-color: #0b76ef;
  background-color: #0b76ef;
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e");
}

В этом коде при получении элементом состояния checked применим к псевдоэлементу ::before находящемуся в label стили, посредством которых установим цвет границы, цвет фону и фоновую картинку (флажок) в формате svg.

Шаг 5. Добавим код для стилизации чекбокса при нахождении его в состояниях hover, active, focus и disabled.

Вид кастомного чекбокса в состояниях hover, active, focus и disabled

/* стили при наведении курсора на checkbox */
.custom-checkbox:not(:disabled):not(:checked)+label:hover::before {
  border-color: #b3d7ff;
}
/* стили для активного состояния чекбокса (при нажатии на него) */
.custom-checkbox:not(:disabled):active+label::before {
  background-color: #b3d7ff;
  border-color: #b3d7ff;
}
/* стили для чекбокса, находящегося в фокусе */
.custom-checkbox:focus+label::before {
  box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
/* стили для чекбокса, находящегося в фокусе и не находящегося в состоянии checked */
.custom-checkbox:focus:not(:checked)+label::before {
  border-color: #80bdff;
}
/* стили для чекбокса, находящегося в состоянии disabled */
.custom-checkbox:disabled+label::before {
  background-color: #e9ecef;
}

Открыть пример

Разработка кастомного переключателя

Стилизация переключателя (input с type="radio") выполняется аналогично, т.е. посредством тех же шагов которые мы применяли при кастомизации чекбокса.

Вид стилизованного чекбокса в браузере по умолчанию и в состоянии checked

Итоговый набор стилей для кастомного оформления input с type="radio":


Открыть пример

Примеры стилизации флажков с ресурса codepen.io

Как работает стилизация

В этом примере видно, где расположен флажок, который обычно скрывается с помощью свойства display: none, и как стилизованы label. Откройте вкладку CSS и проанализируйте код.

See the Pen [Pure CSS] Delightful Checkbox Animation by Dylan Raga (@dylanraga) on CodePen.18892

Простое решение для флажков и переключателей от Jon Kantner.

See the Pen CSS Checkboxes and Radio Buttons by Jon Kantner (@jkantner) on CodePen.18892

Близкие к стандартным варианты флажков, переключателей и выпадающих списков от Kenan Yusuf

See the Pen Completely CSS: Custom checkboxes, radio buttons and select boxes by Kenan Yusuf (@KenanYusuf) on CodePen.18892

Зачеркивание при выборе флажка с анимацией

See the Pen Animated Fill and Strikethrough Checkboxes by Jon Kantner (@jkantner) on CodePen.18892

Несколько интересных решений для флажков и переключателей от Brad Bodine

See the Pen CSS3 Checkbox Styles by Brad Bodine (@bbodine1) on CodePen.18892

Анимированное переключение состояний флажка в виде пилюли

See the Pen Pill Switch 💊 by Elen (@ambassador) on CodePen.18892

Круглая кнопка с анимацией от mandycodestoo

See the Pen 100dayscss-66 by @mandycodestoo (@mandycodestoo) on CodePen.18892

Анимация текста флажков и радио-кнопок от Raúl Barrera

See the Pen Pure CSS Fancy Checkbox/Radio by Raúl Barrera (@raubaca) on CodePen.18892

Подсветка текста label + анимация выбора чекбокса от Adam Quinlan

See the Pen chippy checkbox inputs by Adam Quinlan (@quinlo) on CodePen.18892

Анимированные флажки с изменением цвета на основе css-переменных от Stas Melnikov

See the Pen #CodePenChallenge | Pure CSS Checkboxes by Stas Melnikov (@melnik909) on CodePen.18892

Анимация флажков с переворотом

See the Pen Flip checkbox by Elen (@ambassador) on CodePen.18892

Анимация флажков и переключателей в стиле Material Design от Matt Sisto

See the Pen CSS "Ripple/Wave" checkbox and radio button by Matt Sisto (@msisto) on CodePen.18892

Перекатывающийся шарик от Jon Kantner

Вариант 1

See the Pen Toy Toggle Switch by Jon Kantner (@jkantner) on CodePen.18892

Вариант 2 со скрепкой

See the Pen Paper Clip Toggle Switch by Jon Kantner (@jkantner) on CodePen.18892

Карандаш для отметки выбора флажка

Еще один вариант анимации от Jon Kantner, но с появлением карандаша. Отличное решение для тестов, например.

See the Pen Pencil and Paper Checkboxes by Jon Kantner (@jkantner) on CodePen.18892

Меняем цветовую схему переключателем

Автор Jon Kantner предлагает вашему вниманию вариант кода, при котором клик на чекбоксе меняет цвет фона.

Вариант 1

See the Pen Toggle Switch with Rolling Label by Jon Kantner (@jkantner) on CodePen.18892

Вариант 2

See the Pen Light/Dark Mode Toggle With Curtain Effect by Jon Kantner (@jkantner) on CodePen.18892

Переключатель энергии

See the Pen Blocky Toggle Switch by Jon Kantner (@jkantner) on CodePen.18892

Реализация аккордеона на чистом CSS с использованием чекбоксов от Alex Bergin

See the Pen CSS + HTML only Accordion Element by Alex Bergin (@abergin) on CodePen.18892