С появлением поддержки Vue.js в новом October, открылись новые возможности по созданию виджетов и прочих интересных вещей в админке нашей любимой CMS.

Так как документации по поводу использования Vue.js в настоящий момент еще нет и автор не претендует на звание Кунг-Vue мастера, то публикуем этот гайд исходя из опыта, полученного в процессе изучения исходников новой системы.

Задача

Задача простая, бесполезная, но сгодится для примера - это создание formWidget'а для получения адреса, с использованием Яндекс.Карт

Подготовка

Перед тем как начать - создадим тестовый плагин, назовем его Address.
Данный материал предполагает, что читатель знаком с базовыми командами по созданию плагинов, моделей и пр. в OctoberCMS.

// Создадим плагин для экспериментов, назовем его Sandbox

php artisan create:plugin sandbox.address
  
// Создадим модель и контроллер для плагина
php artisan create:model sandbox.address address
php artisan create:controller sandbox.address addresses

Далее ускоренный прогон - миграция, модель, контроллер.

// Миграция create_addresses_table.php

<?php namespace Sandbox\Address\Updates;

use Schema;
use October\Rain\Database\Schema\Blueprint;
use October\Rain\Database\Updates\Migration;

class CreateAddressesTable extends Migration
{
    public function up()
    {
        Schema::create('sandbox_address_addresses', function (Blueprint $table) {
            $table->engine = 'InnoDB';
            $table->increments('id');
            $table->string('address');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('sandbox_address_addresses');
    }
}

// =========================================
// Модель Address

<?php namespace Sandbox\Address\Models;

use Model;

/**
 * Address Model
 */
class Address extends Model
{
    /**
     * @var string table associated with the model
     */
    public $table = 'sandbox_address_addresses';
}

// =========================================
// Контроллер Addresses

<?php namespace Sandbox\Address\Controllers;

use BackendMenu;
use Backend\Behaviors\FormController;
use Backend\Behaviors\ListController;
use Backend\Classes\Controller;

/**
 * Addresses Back-end Controller
 */
class Addresses extends Controller
{
    public $implement = [
        FormController::class,
        ListController::class
    ];

    public $formConfig = 'config_form.yaml';
    public $listConfig = 'config_list.yaml';

    public function __construct()
    {
        parent::__construct();

        BackendMenu::setContext('Sandbox.Address', 'address', 'addresses');
    }
}

Не забываем обновить файл updates/version.yaml

1.0.1: 
    - First version of address
    - create_addresses_table.php

и выполнить:

php artisan plugin:refresh sandbox.address

Не стану описывать, что и как - мы только что создали простой скелет, для того, чтобы было куда "прицепить" наш formWidget.

Создание виджета

Пробежимся по созданию виджета - следуя документации. Назовем наш виджет MapPicker

php artisan create:formwidget sandbox.address MapPicker

В итоге у нас будет создан шаблон нашего виджета, с ним и будем работать.

Сразу зарегистрируем виджет (согласно документации) в файле Plugin.php нашего плагина SandBox:


public function registerFormWidgets()
{
    return [
        Sandbox\Address\FormWidgets\MapPicker::class => 'mapPicker',
    ];
}

И добавим поле address с типом mapPicker в yaml-файл sandbox/address/models/address/fields.yaml

# ===================================
#  Form Field Definitions
# ===================================

fields:
    id:
        label: ID
        disabled: true
    address:
        label: MapPicker
        type: mapPicker

Теперь в браузере перейдем на страницу /backend/sandbox/address/addresses/create и увидим стандартный сгенерированный шаблон виджета. С ним и будем работать.

Итак, что мы хотим здесь видеть:

  1. Поле ввода адреса
  2. Кнопку, которая откроет в модальном окне карту
  3. Собственно карту, при клике по которой, мы получим адрес и вставим его в поле ввода

Поле и кнопка

Поправим стандартный шаблон партиалки, добавив в него кнопку и немного css.

// sandbox/address/formwidgets/mappicker/partials/_mappicker.htm

<?php if ($this->previewMode): ?>

    <div class="form-control">
        <?= $value ?>
    </div>

<?php else: ?>

    <div id="widget-<?= $this->getId('input') ?>" class="mappicker-widget">
        <input
            type="text"
            id="<?= $this->getId('input') ?>"
            name="<?= $name ?>"
            value="<?= $value ?>"
            class="form-control"
            autocomplete="off"/>
        <button type="button" class="btn btn-primary">
            Карта
        </button>
    </div>

<?php endif ?>
/*
 * sandbox/address/formwidgets/mappicker/assets/css/mappicker.css
 */

.mappicker-widget{
    display: flex;
    gap: 15px;
}

Получили примерно то, что надо =)

Далее используем стандартный компонент <backend-component-modal>, немного покопавшись в исходниках модуля editor и подглядев, как его вызывать.

Немного модифицируем нашу партиалку:

// sandbox/address/formwidgets/mappicker/partials/_mappicker.htm

<?php if ($this->previewMode): ?>

    <div class="form-control">
        <?= $value ?>
    </div>

<?php else: ?>

    <div id="widget-<?= $this->getId('input') ?>" class="mappicker-widget">
        <input
            type="text"
            id="<?= $this->getId('input') ?>"
            name="<?= $name ?>"
            value="<?= $value ?>"
            class="form-control"
            autocomplete="off"/>
        <button type="button" class="btn btn-primary" @click="$refs.modal.show()">
            Карта
        </button>

        <backend-component-modal
            ref="modal"
            :aria-labeled-by="modalTitleId"
            :unique-key="uniqueKey"
            size="large"
            :draggable="false"
        >
            <template v-slot:content>
                <div class="modal-header">
                    <h4 class="modal-title">Заголовок</h4>
                </div>
                <div class="modal-body">Текст</div>
            </template>
        </backend-component-modal>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function () {
            new Vue({
                el: '#widget-<?= $this->getId('input') ?>',
                data: function () {
                    return {
                        uniqueKey: $.oc.domIdManager.generate('modal-mappicker'),
                        modalTitleId: $.oc.domIdManager.generate('modal-mappicker-title'),
                    };
                },
            });
        });
    </script>

<?php endif ?>

Проверяем:

Ура, получилось! Дело за малым, наполнить модалку картой и функционалом.

Яндекс.Карты

Достаточно просто и красиво всё описано тут - https://vue-yandex-maps.github.io/

Немного модифицируем файл виджета, добавив в метод загрузки ассетов одну строку

// sandbox/address/formwidgets/MapPicker.php


public function loadAssets()
{
    $this->addCss('css/mappicker.css', 'sandbox.address');
    $this->addJs('js/mappicker.js', 'sandbox.address');
    $this->addJs('https://unpkg.com/vue-yandex-maps', 'sandbox.address');
}

Ну вот и всё - согласно документации, мы подключили плагин карт напрямую, и теперь дело техники, подпилить нашу партиалку для работы с ним.

Долго описывать не будем - покажем результат модифицированной партиалки

// sandbox/address/formwidgets/mappicker/partials/_mappicker.htm

<?php if ($this->previewMode): ?>

    <div class="form-control">
        <?= $value ?>
    </div>

<?php else: ?>
    <div id="widget-<?= $this->getId('input') ?>" class="mappicker-widget">
        <input
            type="text"
            id="<?= $this->getId('input') ?>"
            name="<?= $name ?>"
            value="<?= $value ?>"
            class="form-control"
            v-model="address"
            autocomplete="off"/>
        <button type="button"
                class="btn btn-primary"
                @click="$refs.modal.show()"
        >
            Карта
        </button>

        <backend-component-modal
            ref="modal"
            :aria-labeled-by="modalTitleId"
            :unique-key="uniqueKey"
            size="large"
            :draggable="false"
        >
            <template v-slot:content>
                <div class="modal-header">
                    <h4 class="modal-title">
                        <span v-if="address">{{ address }}</span>
                        <span v-else>Кликните по карте, для получения адреса</span>
                    </h4>
                </div>
                <div class="modal-body">
                    <div id="yandex-map">
                        <yandex-map
                            :coords="coords"
                            :controls="controls"
                            zoom=10
                            @click="onMapClick"
                        >
                            <ymap-marker
                                :coords="coords"
                                marker-id="marker-<?= $this->getId('input') ?>"
                                :icon="markerIcon"
                                :key="address"
                            />
                        </yandex-map>
                    </div>
                    <button type="button"
                            class="btn btn-primary pull-right m-b"
                            :disabled="!address"
                            @click="$refs.modal.hide()"
                    >
                        Выбрать этот адрес
                    </button>
                </div>
            </template>
        </backend-component-modal>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function () {
            new Vue({
                el: '#widget-<?= $this->getId('input') ?>',
                data: function () {
                    return {
                        address: '',
                        uniqueKey: $.oc.domIdManager.generate('modal-mappicker'),
                        modalTitleId: $.oc.domIdManager.generate('modal-mappicker-title'),
                        coords: [55.753994, 37.622093],
                        controls: [],
                        markerIcon: {
                            content: '',
                        }
                    };
                },
                methods: {
                    onMapClick(event) {
                        this.coords = event.get('coords');
                        this.getAddress(this.coords);
                    },
                    getAddress(coords) {
                        vm = this;
                        vm.markerIcon.content = '';
                        ymaps.geocode(this.coords).then(function (res) {
                            let firstGeoObject = res.geoObjects.get(0);
                            vm.address = firstGeoObject.getAddressLine();
                            vm.markerIcon.content = vm.address;
                        });
                    }
                },
                async mounted() {
                    await window['vue-yandex-maps'].loadYmap({
                        apiKey: "YOUR-API-KEY",
                        lang: "ru_RU",
                        coordorder: "latlong",
                        version: "2.1"
                    });
                },
            });
        });
    </script>
<?php endif ?>

И немного докинем стилей в css

// sandbox/address/formwidgets/mappicker/assets/css/mappicker.css

/*
 * This is a sample StyleSheet file used by MapPicker
 *
 * You can delete this file if you want
 */
.mappicker-widget {
    display: flex;
    gap: 15px;
}

#yandex-map {
    height: 400px;
    margin-bottom: 20px;
}

.ymap-container {
    height: 100%;
}

Результат

Собственно, чтобы не придумывать велосипед - небольшое видео-демонстрация

Ссылки

Оригинал статьи
Исходный код примера

Спасибо, очень интересно!