Vue.js in Nette
Integrating Vue.js into Nette is not complicated, and as we will soon see, it can be done in just a few minutes. I will show you how to create your own Vue component for autocomplete search with an example.
If you have an SPA, Nette is likely managing the API with which Vue exchanges information, making both applications practically independent. However, I wanted to use Vue on a traditional website in conjunction with Latte templates in components, which requires some integration.
We will start by getting webpack up and running on our project. By the way, if you don't like the classic webpack configuration as much as I do, you can use Symfony Encore, which is much more familiar to PHP developers, in my opinion. Here is the integration of Encore into Nette that I used.
Vue Component
If webpack is already building assets, it's time to create a Vue SFC (Single File Component).
An SFC is a component (e.g., autocomplete.vue
) that can be
divided into three parts. One is for the HTML template, the second is for
JavaScript, and the third is for CSS styles. Although they belong to a single
file, I will describe each part separately for better clarity.
1/3 template
<template>
<div class="autocomplete">
<form autocomplete="off"><!--required to disable Google Chrome autofill-->
<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>
First, we defined the component's template. The interesting part of this
code is the usage of :
which is shorthand for v-bind, ensuring reactivity by
connecting with data and props. If you're curious about the @
part,
it's shorthand for v-on, which
attaches event listeners to the element and triggers the corresponding method
when the event occurs, as shown further.
Remember, Vue must have only one 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>
First, we imported axios, a library for HTTP communication (ajax). Then, we prepared the props, which are the parameters passed to the component from the outside upon initialization. We also set up the data that are reflected in the template (note: data must be returned as a function to prevent interference between multiple components).
Finally, we defined the methods that are triggered either through events (see
v-on) or by calling them in the code. Noteworthy is
this.$emit('selected', item);
, which triggers the selected event,
allowing external handling of this event, similar to concepts used in Nette.
This allows the component to be reused in various places with different sources
and result processing.
Remember the presenter lifecycle in Nette? Well, Vue has one too. We can use hooks, like the one we used to close the autocomplete when the user clicks outside it.
3/3 css
<style scoped>
.autocomplete-result .is-active,
.autocomplete-result li:hover {
background-color: #379d2f;
color: white;
}
</style>
Note that with style scoped
, the CSS is only local to the
component and won't interfere with other styles.
Connecting the Vue Component with Nette
Great, we have our Vue component. But how do we connect it with Nette?
We create a standard Nette component and call the Vue component in a Latte template. Simple, right?
Simple Example of a Nette Component
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);
}
}
Since we want to use the autocomplete component in multiple places, we need to provide it with information on where to search. We do this by setting attributes on the element – see props above.
ProductsAutocomplete.latte
<div id="autocomplete">
<autocomplete
inputname="nameOrCode"
source="/api/v1/products"
placeholder="Find product"
choice="name"
ref="autocomplete"
></autocomplete>
</div>
When we select an item in the autocomplete, we should process the result in Nette (PHP), for example, via signals Nette components
-<div id="autocomplete">
+<div id="autocomplete" data-handle-link="{link select!}" data-handle-param="{$handleParameter}">
<autocomplete
inputname="nameOrCode"
source="/api/v1/products"
placeholder="Find product"
choice="name"
@selected="onSelect"
ref="autocomplete"
></autocomplete>
</div>
So, we first pass information about the signal address named select and the
component parameter name to Vue, so that after selecting from the search, we can
call this signal via JavaScript. We then set a listener onSelect
to
trigger upon item selection (see this.$emit('selected', item);
in
the component).
Vue Instance
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');
As seen in the above code, we register the autocomplete component into the Vue instance and upon triggering onSelect, we send a signal to Nette via axios. Since axios doesn't understand Nette's snippets, we need to handle snippet redrawing ourselves. It's also possible to use AJAX libraries for Nette (nette.ajax.js, naja.js), but since all my Vue components use axios, I stuck with it.
If I'm not mistaken, in Vue.js 2 it's not possible to read attributes on
the parent element of the instance by default. So, we use a workaround by
hooking into mounted
– see the lifecycle – and set them into
data ourselves.
Webpack
Finally, we process everything with webpack. Here is a piece of my Encore setup:
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()
The key setting is .enableVueLoader()
, without which webpack
would throw an error about not being able to process Vue files. Also, don't
forget to specify the path to all instances of Vue components that need
processing.
Encore
.addEntry('admin', [
path.resolve(__dirname, 'front_dev/src/vue/autocomplete.js'),
path.resolve(__dirname, 'front_dev/src/vue/xxx.js'),
])
Now we have a functional autocomplete that we can use in various situations (searching for products, categories, etc.). Each component can be processed by different signals accordingly. This way, we have not only reusable Nette components but also Vue components.
Sign in to submit a comment