Есть много руководств в сети, рассказывающих про создание произвольных виджетов, но, к сожалению, большая их часть редко выходит за рамки основ. Именно по этой причине в сегодняшнем руководстве я покажу вам, как создать виджет рекомендаций с поддержкой неограниченного количества отзывов.
Проблема с повторяющимися полями заключается в том, что нужно правильно их именовать, что может быть достаточно сложным действием.
Есть два возможных решения этой проблемы, однако в обучающих целях мы пойдем самым сложным путем: воспользуемся Backbone. Это легкий JS-фреймворк, который позволит вам легко написать фронтэнд-приложение. Конечно, мы могли бы воспользоваться jQuery, однако, поверьте мне, как только мы это сделаем, вы будете благодарны мне за то, что я показал вам такой путь.
Создание и подключение необходимого JS-файла
Перед тем, как начать, нам нужно сделать некоторую базовую подготовку. Нам нужно создать JS-файл (в папке js с вашей темой) и назвать его admin-testimonials.js. Вставьте следующий код в файл functions.php:
/** * Enqueue admin testimonials javascript */ function testimonials_enqueue_scripts() { wp_enqueue_script( 'admin-testimonials', get_template_directory_uri() . '/js/admin-testimonials.js', array( 'jquery', 'underscore', 'backbone' ) ); } add_action( 'admin_enqueue_scripts', 'testimonials_enqueue_scripts' );
Помимо нашего недавно созданного файла, нам также требуется массив зависимостей, в нашем случае нам нужен backbone, который уже идет вместе с WordPress.
Определяем класс
Далее нам нужно создать новый файл class-testimonial-widget.php, поместить его в нашу директорию inc с темой и потребовать его в нашем файле functions.php:
/** * Load Testimonial Widget */ require get_template_directory() . '/inc/class-testimonial-widget.php';
Открываем файл. В нем нам нужно будет определить новый класс для нашего виджета рекомендаций. Поместите в него следующий код:
<?php // Prevent direct access to this file defined( 'ABSPATH' ) or die( 'Nope.' ); /** * Register the widget with WordPress */ add_action( 'widgets_init', function(){ register_widget( 'Testimonial_Widget' ); }); class Testimonial_Widget extends WP_Widget { }
Перед тем, как мы напишем какие-либо методы в этом классе, не лишним будет посмотреть официальную документацию Widget API. Нам понадобятся следующие методы:
- __construct() – метод-конструктор. В нем нам нужно вызвать родительский (WP_Widget) конструктор и передать ему id (или false, и он сгенерирует id автоматически), название нашего виджета, а также необязательный массив параметров.
- widget() – метод обработки, отвечающий за то, что видят посетители.
- update() – как и предполагает название, этот метод отвечает за обновление значений полей. Очистка ввода (санитизация) проходит именно здесь.
- form() – используется для обработки и представления нашей формы в панели администратора, через которую вы управляем нашими данными виджетов – самая важная часть этого руководства.
Определяем конструктор
В том случае, если вы не знаете, конструктор – это специальный метод, который вызывается автоматически при создании экземпляра класса (с помощью ключевого слова new).
public function __construct() { parent::__construct( false, 'Testimonials', array( 'description' => 'My Testimonials Widget' ) ); }
Есть много опций, которые вы можете передавать родительскому конструктору (WP_Widget::__construct), однако в данном руководстве мы не будем их все рассматривать. Если вы сохраните метод, ваша панель виджетов будет иметь следующий вид:
Определяем метод обновления
Теперь давайте определим метод, который будет отвечать за безопасность. В случае приема любых данных от пользователей необходимо обязательно очистить их.
Вот небольшой пример того, как это делается:
public function update( $new_instance, $old_instance ) { $instance = array(); $instance['header'] = wp_kses_post( $new_instance['header'] ); $instance['testimonials'] = $new_instance['testimonials']; return $instance; }
Здесь мы создаем новый массив, который содержит заголовок экземпляра, а также еще один массив, который содержит все наши рекомендации.
В данном случае экземпляр – это уникальная копия виджета, поскольку вы можете иметь массу виджетов одного и того же типа в различных сайдбарах. Каждый из них будет экземпляром.
Домашнее задание: в данном методе я очистил заголовок, однако передал все рекомендации в исходном виде. Когда вы закончите разбираться с этим руководством, я советую вам попробовать применить очистку и к рекомендациям. Информацию по функциям очистки в WordPress можно найти здесь.
Определяем метод обработки формы в консоли
Теперь мы подошли к самой важной части: форме рекомендаций. В нем мы должны позаботиться о следующих возможностях:
- Получение данных или задание некоторых стандартных значений, если требуемых данных не существует
- Вывод любых неповторяющихся полей (как заголовок), если они есть
- Поскольку мы используем Blackbone (JS), нам нужно также задать шаблон, который будет использоваться для каждой отдельной рекомендации
- Вывод заполнителя, который будет иметься у всех отзывов
- Инициализация получения данных, как только все будет обработано
Скопируйте следующий метод и вставьте его в класс (объяснение приведено ниже):
public function form( $instance ) { // segment #1 $header = empty( $instance['header'] ) ? 'Testimonials' : $instance['header']; $testimonials = isset( $instance['testimonials'] ) ? array_values( $instance['testimonials'] ) : array( array( 'id' => 1, 'quote' => '', 'author' => '', 'image' => '' ) ); ?> <!— segment #2 —> <p> <label for="<?= $this->get_field_id( 'header' ); ?>">Header</label> <input class="widefat" id="<?= $this->get_field_id( 'header' ); ?>" name="<?= $this->get_field_name( 'header' ); ?>" type="text" value="<?= esc_attr( $header ); ?>" /> </p> <!— segment #3 —> <script type="text/template" id="js-testimonial-<?= $this->id; ?>"> <p> <label for="<?= $this->get_field_id( 'testimonials' ); ?>-<%- id %>-quote">Quote:</label> <textarea rows="4" class="widefat" id="<?= $this->get_field_id( 'testimonials' ); ?>-<%- id %>-quote" name="<?= $this->get_field_name( 'testimonials' ); ?>[<%- id %>][quote]"><%- quote %></textarea> </p> <p> <label for="<?= $this->get_field_id( 'testimonials' ); ?>-<%- id %>-author">Author:</label> <input class="widefat" id="<?= $this->get_field_id( 'testimonials' ); ?>-<%- id %>-author" name="<?= $this->get_field_name( 'testimonials' ); ?>[<%- id %>][author]" type="text" value="<%- author %>" /> </p> <p> <input name="<?= $this->get_field_name( 'testimonials' ); ?>[<%- id %>][id]" type="hidden" value="<%- id %>" /> <a href="#" class="js-remove-testimonial"><span class="dashicons dashicons-dismiss"></span>Remove Testimonial</a> </p> </script> <!— segment #4 —> <div id="js-testimonials-<?= $this->id; ?>"> <div id="js-testimonials-list" style="padding: 0px 15px; background: #fafafa;"></div> <p> <a href="#" class="button" id="js-testimonials-add">Add New Testimonial</a> </p> </div> <!— segment #5 —> <script type="text/javascript"> var testimonialsJSON = <?= json_encode( $testimonials ) ?>; myWidgets.repopulateTestimonials( '<?= $this->id; ?>', testimonialsJSON ); </script> <?php }
Давайте пройдемся по этому методу и посмотрим, что он делает.
В первом сегменте мы определяем заголовок, который будет использоваться во фронтэнде. Вы можете легко добавить сюда больше полей, к примеру, если у вас есть слайдер с рекомендациями, после чего вы можете добавить некоторые опции конфигурации слайдера, как, к примеру, интервал повторения. Сегмент 2 просто обрабатывает эти поля.
Сегмент 3 – то место, где все выполняется. Вы можете заметить, что весь вывод здесь обернут в тег script. Так как мы используем Backbone для обработки повторяющихся полей, нам нужен шаблон, который будет применяться к каждому объекту (в нашем случае это рекомендации). Мы используем два поля в нашем руководстве – quote и author – однако вы можете использовать столько полей, сколько захотите. Также нам нужна будет возможность удаления рекомендаций, поэтому мы задаем еще один элемент – кнопку. Вы можете видеть, что она имеет пустой параметр href; сделано это по той причине, что мы используем JS для асинхронных обращений к WordPress.
Четвертый сегмент – это заполнитель для списка рекомендаций. Помимо этого, я также решил обработать здесь кнопку New Testimonial, однако вы можете поместить ее в любое другое место.
В последнем, пятом, сегменте мы создаем JSON объект из наших существующих рекомендаций, после чего вызывает функцию, которая будет обрабатывать его. Мы передаем id нашего экземпляра виджета и объект JSON.
Если вы попробуете обновить панель виджетов в данный момент, она выдаст вам ошибки JS, поскольку наша функция repopulateTestimonials пока не существует. Но перед тем, как задать ее, давайте сначала…
Определяем метод обработки сайта
Следующий метод используется для вывода наших рекомендаций в сайдбаре:
public function widget( $args, $instance ) { $header = apply_filters( 'widget_title', empty( $instance['header'] ) ? '' : $instance['header'], $instance, $this->id_base ); ?> <h3><?= $header ?></h3> <?php foreach ( $instance['testimonials'] as $testimonial ): ?> <blockquote> <p><?= $testimonial['quote'] ?></p> <footer>— <?= $testimonial['author'] ?></footer> </blockquote> <?php endforeach; }
Здесь не нужны длительные объяснения. Мы просто применяем стандартный фильтр к нашему заголовку и выводим его, после чего проходим в цикле по всем нашим рекомендациям и выводим цитату и ее автора.
Пришло время отложить в сторону JS и погрузиться в мир Backbone – можете закрыть php файл, поскольку нам он больше не понадобится.
Backbone для нашего виджета
Помните файл admin-testimonials.js, который мы создали ранее? Сейчас мы кое-что с ним сделаем.
Если вы не знакомы с Backbone, не бойтесь, мы коснемся здесь лишь самых основ. Однако если вы не понимаете некоторые участки кода, обратитесь к официальной документации Backbone – это всего лишь одна страница (пусть и достаточно длинная).
Первый шаг, который мы сделаем, это создадим пространство имен.
var myWidgets = myWidgets || {};
Если вы не знакомы с данной техникой, то это просто такой способ организации вашего кода, который не загрязняет ваше глобальное пространство имен – подробнее об этом вы можете прочитать в следующей статье.
Перед тем, как начать писать код для наших рекомендаций, нам нужно еще раз привести наши требования:
- Нам нужен способ сохранения и заполнения каждой рекомендации
- Нам нужен способ обновления рекомендаций
- Нам нужна возможность создания новой рекомендации или удаления уже существующей
В мире разработки некоторый произвольный объект обычно называется моделью, и Backbone поддерживает моделирование данных «из коробки»:
myWidgets.Testimonial = Backbone.Model.extend({ defaults: { 'quote': '', 'author': '' } });
Здесь мы определили модель Testimonial, которая расширяет стандартное поведение, устанавливаемое для моделей в Backbone. В примере мы используем ключ defaults, который не всегда требуется, но который позволяет нам визуально определить схему наших данных. Первое требование выполнено.
Теперь нам нужно определить два Views. Первый будет отвечать за отдельные рекомендации и их поведение, а второй – за то, что наш список в порядке. View – терминология JS/Backbone, которая означает некоторые логические условия, используемые для управления HTML-выводом элемента (или группы их). С этим обычно связана некоторая путаница, поскольку в большинстве серверных языков View – это обработанный HTML, а логические условия обработки называются контроллером. И View в таком случае означает шаблон. Довольно запутанно.
Для начала нам нужно создать View, отвечающий за каждый объект с отдельной рекомендацией:
myWidgets.TestimonialView = Backbone.View.extend( { className: 'testimonial-widget-child', events: { 'click .js-remove-testimonial': 'destroy' }, initialize: function ( params ) { this.template = params.template; this.model.on( 'change', this.render, this ); return this; }, render: function () { this.$el.html( this.template( this.model.attributes ) ); return this; }, destroy: function ( ev ) { ev.preventDefault(); this.remove(); this.model.trigger( 'destroy' ); }, } );
Давайте посмотрим, что делает этот View:
- className определяет класс для каждой отдельной рекомендации в нашем списке
- events задает список событий и соответствующие действия, которые должны быть инициированы, если событие произошло
- initialize вызывается всякий раз, когда создается новая рекомендация (с JS). Ей требуется параметр template. Также мы прикрепляем к ней листенер, который позволит заново обработать рекомендацию, если она была изменена.
- Render формирует HTML, соединяя данные с шаблоном
- Destroy удаляет HTML и связанные с ним данные
Теперь мы должны позаботиться об отдельных рекомендациях. Давайте остановимся на представлении списка:
myWidgets.TestimonialsView = Backbone.View.extend( { events: { 'click #js-testimonials-add': 'addNew' }, initialize: function ( params ) { this.widgetId = params.id; this.$testimonials = this.$( '#js-testimonials-list' ); this.testimonials = new Backbone.Collection( [], { model: myWidgets.Testimonial } ); this.listenTo( this.testimonials, 'add', this.appendOne ); return this; }, addNew: function ( ev ) { ev.preventDefault(); var testimonialId = 0; if ( ! this.testimonials.isEmpty() ) { var testimonialsWithMaxId = this.testimonials.max( function ( testimonial ) { return testimonial.id; } ); testimonialId = parseInt( testimonialsWithMaxId.id, 10 ) + 1; } var model = myWidgets.Testimonial; this.testimonials.add( new model( { id: testimonialId } ) ); return this; }, appendOne: function ( testimonial ) { var renderedTestimonial = new myWidgets.TestimonialView( { model: testimonial, template: _.template( jQuery( '#js-testimonial-' + this.widgetId ).html() ), } ).render(); this.$testimonials.append( renderedTestimonial.el ); return this; } } );
Этот View более сложный, поскольку он управляет добавлением новых рекомендаций и удаляет уже существующие рекомендации:
- Initialize, как и в случае с отдельным представлением, автоматически вызывается, когда мы создаем новый экземпляр, и сохраняет все наши рекомендации в виде DOM-объекта и в виде коллекции (обратите внимание на символ $). Также она следит за коллекциями и инициирует функцию add всякий раз, когда новый объект рекомендации добавляется в коллекцию.
- addNew вызывается всякий раз, когда мы щелкаем по кнопке Add New. Также он позволяет сохранить ID (который увеличивается на единицу).
- appendOne, как вы можете предположить, создает новое отдельное представление с рекомендацией, которую мы передали, и добавляет ее к DOM.
В данный момент у нас отсутствует только один важный элемент – функция repopulateTestimonials, которую мы вызываем из PHP-шаблона, определенного ранее:
myWidgets.repopulateTestimonials = function ( id, JSON ) { var testimonialsView = new myWidgets.TestimonialsView( { id: id, el: '#js-testimonials-' + id, } ); testimonialsView.testimonials.add( JSON ); };
Мы создаем наше представление списка, передавая в функцию ID (полезно в том случае, если мы используем более одного экземпляра виджета в одном и том же сайдбаре) и DOM-элемент, к которому мы хотим добавить наш список. Наконец, мы вносим все наши существующие рекомендации в представление (View).
Сохраняем файл и обновляем панель виджетов. Если вы увидите что-то подобное, значит вы сделали все верно!
Весь код приведен в следующем gist.
Заключение
Достаточно просто, верно? Мы могли бы использовать для этого jQuery, однако Backbone позволяет нам правильно структурировать наше небольшое JS-приложение, что является огромным преимуществом.