Vue.js v Nette

před 7 měsíci od Ivo Toman     edit

Integrace Vue.js do Nette není nijak složitá a jak brzo zjistíme, dá se zvládnout během pár minut. Na příkladu vám ukážu, jak si vytvořit vlastní vue komponentu pro vyhledávání s našeptávačem.

Pokud máte SPA, tak se vám Nette pravděpodobně stará o API, se kterým si Vue mění informace a de facto jsou obě aplikace na sobě nezávislé. Já jsem však Vue chtěl použít na klasickém webu ve spojení s latte šablonami v komponentách, což tedy vyžaduje určitou integraci.

Začneme zprovozněním webpacku na našem projektu. Mimochodem, pokud nemáte rádi klasickou konfiguraci webpacku stejně jako já, můžete použít Symfony Encore, které je imho PHP programátorům mnohem bližší. Zde je integrace Encore do Nette, kterou jsem použil já.

Vue komponenta

Pokud nám webpack již builduje assety, nastal čas si vytvořit Vue SFC (Single File Component)

SFC je komponenta (např. autocomplete.vue), kterou je možné členit na 3 části. Jedna je určena html šabloně, druhá je pro javascript a třetí pak pro css styly. Ačkoliv patří do jednoho souboru, budu jednotlivé části popisovat odděleně pro větší přehlednost.

1/3 template

<template>
    <div class="autocomplete">
        <form autocomplete="off"><!--required for disable google chrome auto fill-->
        <input
                type="text"
                :placeholder="placeholder"
                :name="inputname"
                class="form-control"
                v-model="searchquery"
                @input="autoComplete"
                @keydown.down="onArrowDown"
                @keydown.up="onArrowUp"
                @keydown.enter="onEnter"
        >
        <div class="autocomplete-result" v-show="isOpen">
            <ul class="list-group">
                <li class="list-group-item"
                    v-for="(result, i) in results"
                    :key="result.id"
                    @click="setResult(result)"
                    :class="{ 'is-active': i === arrowCounter }"
                >
                    {{ result[choice] }}
                </li>
            </ul>
        </div>
        </form>
    </div>
</template>

Nejprve jsme si tedy v template definovali šablonu komponenty. Zajímavé na tomto kódu je snad jen ta část s :, což je zkratka pro v-bind, která zajišťuje reaktivitu – propojení s daty a props. Pokud vás zajímá část se @, tak je to zkratka pro v-on , což navěsí posluchače události na daný element a v případě dané události spustí patřičnou metodu – viz dále.

Nezapomeňme, že ve Vue musí být pouze jeden root element.

2/3 javascript

<script>
    import axios from 'axios';

    export default {
        name: "autocomplete",
        props: {
            source: {
                type: String,
                required: true,
            },
            choice: {
                type: String,
                required: true,
            },

            placeholder: String,
            inputname: String
        },
        data: function () {
            return {
                searchquery: '',
                results: [],
                isOpen: false,
                arrowCounter: -1,
            }
        },
        methods: {
            autoComplete() {
                this.$emit('autocomplete-start');
                this.results = [];
                if (this.searchquery.length > 2) {
                    axios.get(this.source, {params: {[this.inputname]: this.searchquery}})
                        .then(response => {
                            this.results = response.data.data;
                            this.isOpen = true;
                        })
                        .catch((error) => {
                            // console.log(error);
                        });
                }
            },
            setResult(item) {
                this.isOpen = false;
                this.searchquery = item[this.choice];
                this.product_id = item.id;
                this.$emit('selected', item);
            },
            onArrowDown() {
                if (this.arrowCounter < this.results.length - 1) {
                    this.arrowCounter = this.arrowCounter + 1;
                }
            },
            onArrowUp() {
                if (this.arrowCounter > 0) {
                    this.arrowCounter = this.arrowCounter - 1;
                }
            },
            onEnter() {
                this.setResult(this.results[this.arrowCounter]);
                this.arrowCounter = -1;
            },
            handleClickOutside(evt) {
                if (!this.$el.contains(evt.target)) {
                    this.isOpen = false;
                    this.arrowCounter = -1;
                }
            }
        },
        mounted() {
            document.addEventListener('click', this.handleClickOutside)
        },
        destroyed() {
            document.removeEventListener('click', this.handleClickOutside)
        }
    }
</script>

Nejprve jsme si naimportovali axios, což je knihovna pro http komunikaci (ajax). Dále jsme si připravili props, což jsou informace, které se do komponenty předají z vnějšku – při spuštění komponenty. Máme také nastavená data, která se promítají v templatu (tady pozor, musíme data mít jako return funkce, jinak se mohou u více komponent vzájemně přebíjet).

A nakonec jsou nastaveny metody, které jsou spouštěny buď přes eventy (viz v-on) nebo voláním v kódu. Za zmínku ještě stojí this.$emit('selected', item); což vyvolá událost selected, kterou si můžeme nechat zpracovat vně komponenty – je to podobný koncept, jaký používáme v Nette. Tím tedy můžeme komponentu použít na více místech s různými zdroji a s různým zpracováním výsledku.

Pamatujete si ještě na životní cyklus presenteru v Nette? Tak Vue to má taky . Dají se tedy použít hooky, kterými jsme si navěsili událost, jenž nám zavře našeptávač, pokud uživatel klikne mimo něj.

3/3 css

<style scoped>
    .autocomplete-result .is-active,
    .autocomplete-result li:hover {
        background-color: #379d2f;
        color: white;
    }
</style>

Tady už jen zmíním, že pokud máme nastaveno style scoped, je css pouze lokální pro danou komponentu a nebude nám rušit ostatní styly.

Propojení Vue komponenty s Nette

Fajn, Vue komponentu máme, jak jí ale propojíme s Nette?

Vytvoříme si klasickou Nette komponentu a v Latte si vyvoláme Vue komponentu. Jednoduché, že?

Jednoduchá ukázka Nette komponenty

class ProductsAutocomplete extends Control
{

	/**
	 * @var ProductService
	 */
	private $productService;

	public $onSelect = [];


	public function __construct(ProductService $productService)
	{
		$this->productService = $productService;
	}

	public function render()
	{
		$this->template->handleParameter = $this->getParameterId('productId');
		$this->template->setFile(__DIR__ . '/ProductsAutocomplete.latte');
		$this->template->render();
	}

	public function handleSelect($productId)
	{
		$product = $this->productService->get($productId);
		$this->onSelect($product);
	}
}

Protože chceme používat autocomplete komponentu na více místech, musíme jí předat informace, kde má hledat. To uděláme jednoduše nastavením atributů na daném elementu – viz props výše.

ProductsAutocomplete.latte

<div id="autocomplete">
    <autocomplete
		inputname="nameOrCode"
		source="/api/v1/products"
		placeholder="Najít produkt"
 		choice="name"
		ref="autocomplete"
	></autocomplete>
</div>

Jakmile si v našeptávači vybereme položku, měli bychom výsledek zpracovat v Nette (PHP). Např. prostřednictvím signálů Nette komponenty

-<div id="autocomplete">
+<div id="autocomplete"  data-handle-link="{link select!}" data-handle-param="{$handleParameter}">
    <autocomplete
		inputname="nameOrCode"
		source="/api/v1/products"
		placeholder="Najít produkt"
 		choice="name"
+		@selected="onSelect"
		ref="autocomplete"
	></autocomplete>
</div>

Takže nejprve Vue předáme informaci o adrese signálu nazvaného select a komponentním názvu jeho parametru, abychom si po výběru z vyhledávače mohli tento signál přes javascript zavolat. Dále pak nastavíme listener onSelect, který se má spustit při výběru položky (viz. this.$emit('selected', item); v komponentě)

Instance Vue

autocomplete.js

import Vue from 'vue';
import axios from 'axios';
import autocomplete from './components/autocomplete';

//for Tracy Debug Bar
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

var vm = new Vue({
    data: function () {
        return {
            handle: null,
            param: null,
        }
    },
    components: {
		//our component
        autocomplete
    },
    methods: {
        onSelect(item) {
            //is emitted from child component and call nette handleAdd
            axios.get(this.handle + '&' + this.param + '=' + item.id)
                .then( (response) => {
                    if (response.data.snippets) {
                        this.updates(response.data.snippets);
                    }
                })

        },
        updates(snippets) {
            this.$refs.autocomplete.searchquery = '';

            Object.keys(snippets).forEach((id) => {
                const el = document.getElementById(id);
                if (el) {
                    this.updateSnippet(el, snippets[id]);
                }
            });

 			//maybe reinit nette.ajax.js for new ajax call
            //$.nette.load();
        },
        updateSnippet(el, content) {
            el.innerHTML = content;
        }
    },
    mounted: function () {
        // `this` points to the vm instance
        this.handle = this.$el.getAttribute('data-handle-link');
        this.param = this.$el.getAttribute('data-handle-param');
    }
}).$mount('#autocomplete');

Jak je z výše uvedeného kódu patrné, zaregistrujeme do Vue instance komponentu autocomplete a při vyvolání onSelect odešleme přes axios signál do Nette. Protože axios nezná snippety z Nette, musíme si překreslení snippetů zpracovat sami. Je samozřejmě možné použít i ajaxové knihovny pro Nette (nette.ajax.js, naja.js), ale vzhledem k tomu, že všechny mé Vue komponenty používají axios, zůstal jsem u něj.

Pokud se nemýlím, ve Vue.js 2 nelze standardně na rodičovském elementu instance přečíst atributy. Musíme si tedy pomoci obezličkou v podobě napojení na mounted – viz životní cyklus a nastavit si je do dat sami.

Webpack

A na úplný závěr si to necháme zpracovat webpackem. Tady je kousek z mého nastavení Encore

var Encore = require('@symfony/webpack-encore');
const path = require('path');

Encore
	// directory where compiled assets will be stored
    .setOutputPath('www/assets/admin')
    // public path used by the web server to access the output path
    .setPublicPath('/assets/admin')

    .addEntry('admin', [
        path.resolve(__dirname, 'front_dev/src/vue/autocomplete.js'),
    ])

    .addAliases({
        'nette-forms': path.resolve(__dirname, 'vendor/nette/forms/src/assets/netteForms.js')
    })
    .enableBuildNotifications()
    .enableSingleRuntimeChunk()
    .splitEntryChunks()
    .enableSourceMaps(!Encore.isProduction())
    // enables hashed filenames (e.g. app.abc123.css)
    .enableVersioning(Encore.isProduction())

    .enableSassLoader()
    .enablePostCssLoader()
    .enableVueLoader()

Důležité je nastavení .enableVueLoader(), bez toho by nám webpack vyhodil chybu o nemožnosti zpracovat vue soubory. Nezapomeňme také zadat cestu ke všem instancím vue komponent, které potřebujeme zpracovat.

Encore
    .addEntry('admin', [
        path.resolve(__dirname, 'front_dev/src/vue/autocomplete.js'),
		path.resolve(__dirname, 'front_dev/src/vue/xxx.js'),
    ])

Nyní už tedy máme funkční našeptávač, který můžeme používat v různých příležitostech (hledání produktů, kategorií, atd). Každá komponenta může být zpracována různými signály podle svého. Tím pádem máme nejen znovupoužitelné komponenty Nette, ale i Vue :)

Komentáře (RSS)

  1. Nebyl by k tomuto článku GIT respositar?

    před 5 měsíci · replied [2] petak23
  2. #1 Pilnik Ja by som sa tiež prihováral za nejaký ukážkový GIT, lebo mi nie je celkom jasné, čo kde uložiť. Vďaka

    před 4 měsíci

Chcete-li odeslat komentář, přihlaste se