Vuejs - show autocomplete suggestions while user digit - javascript

I have this js fiddle code. I want to create a suggestions autocomplete using vuejs. At the moment I've achived only in part the scope, I have a problem with the suggestions. They will be placed under the user input chars and it's not exactly what I was expecting, I want to do something similar to the autocompleto of a smartphone keyboard where the suggested words will be displayed while the user digit a word. Can anyone help me?
<div id="app">
<textarea id="input" v-model="input" #input="predictWord()"></textarea>
<span id="suggestion" ref="suggestion"></span>
</div>
#app {
.input {
position: relative;
}
#suggestion {
position: absolute;
left: 0;
}
}
Vue prototype code
new Vue({
el: "#app",
data() {
return {
input: null,
t9: null,
words: []
}
},
mounted() {
this.init();
},
methods: {
init() {
axios({
method: 'GET',
url: 'https://raw.githubusercontent.com/napolux/paroleitaliane/master/paroleitaliane/660000_parole_italiane.txt'
}).then( (res) => {
this.words = res.data.split('\n');
this.t9 = Predictionary.instance();
this.t9.addWords(this.words);
});
},
predictWord() {
let suggestion;
this.countChars();
suggestion = this.t9.predict(this.input);
this.$refs.suggestion.innerText = suggestion[0];
},
countChars() {
console.log(this.input.length);
}
}
});

I created a working snippet: simplified it a bit, added a loading state (as the dictionary is quite large), updated the resulting output, so it's not dependent on any DOM element.
new Vue({
el: "#app",
data() {
return {
loading: false,
input: null,
t9: null,
suggestion: [],
}
},
mounted() {
this.init();
},
methods: {
async init() {
this.loading = true
try {
const {
data = ''
} = await axios({
method: 'GET',
url: 'https://raw.githubusercontent.com/napolux/paroleitaliane/master/paroleitaliane/660000_parole_italiane.txt'
})
this.t9 = Predictionary.instance();
const a = data.split('\n').filter(e => e)
this.t9.addWords(a)
} catch (err) {
console.error(err)
} finally {
this.loading = false
}
},
predictWord: _.debounce(function() {
this.suggestion = this.input ? this.t9.predict(this.input) : [];
}, 300),
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios#0.21.1/dist/axios.min.js"></script>
<script src="https://unpkg.com/predictionary/dist/predictionary.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.21/lodash.min.js"></script>
<div id="app">
<textarea id="input" v-model="input" #input="predictWord" :disabled="loading"></textarea><br />
<span id="suggestion">{{ suggestion.join(', ') }}</span>
</div>
Also added a debounce function, so the prediction doesn't have to run so many times - 300ms is a reasonable delay in my experience.

Related

Vue component get and set conversion with imask.js

I am trying to use iMask.js to change 'yyyy-mm-dd' to 'dd/mm/yyyy' with my component however when I am setting the value I think it is taking the value before the iMask has finished. I think using maskee.updateValue() would work but don't know how to access maskee from my component.
I am also not sure if I should be using a directive to do this.
Vue.component("inputx", {
template: `
<div>
<input v-mask="" v-model="comp_date"></input>
</div>
`,
props: {
value: { type: String }
},
computed: {
comp_date: {
get: function () {
return this.value.split("-").reverse().join("/");
},
set: function (val) {
const iso = val.split("/").reverse().join("-");
this.$emit("input", iso);
}
}
},
directives: {
mask: {
bind(el, binding) {
var maskee = IMask(el, {
mask: "00/00/0000",
overwrite: true,
});
}
}
}
});
var app = new Vue({
el: "#app",
data: {
date: "2020-12-30"
}
});
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.12"></script>
<script src="https://unpkg.com/imask"></script>
<div id="app">
<inputx v-model="date"></inputx>
Date: {{date}}
</div>
The easiest way you can achieve this is by installing the external functionality on the mounted hook of your Vue component, instead of using a directive.
In this way you can store the 'maskee' object on your component's data object to later access it from the setter method.
Inside the setter method you can then call the 'updateValue' method as you hinted. Then, you can extract the processed value just by accessing the '_value' prop of the 'maskee' object.
Here is a working example:
Vue.component("inputx", {
template: `
<div>
<input ref="input" v-model="comp_date"></input>
</div>
`,
data: {
maskee: false,
},
props: {
value: { type: String },
},
computed: {
comp_date: {
get: function () {
return this.value.split("-").reverse().join("/");
},
set: function () {
this.maskee.updateValue()
const iso = this.maskee._value.split("/").reverse().join("-");
this.$emit("input", iso);
}
}
},
mounted(){
console.log('mounted');
const el = this.$refs.input;
this.maskee = IMask(el, {
mask: "00/00/0000",
overwrite: true,
});
console.log('maskee created');
}
});
var app = new Vue({
el: "#app",
data: {
date: "2020-12-30"
}
});
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.12"></script>
<script src="https://unpkg.com/imask"></script>
<div id="app">
<inputx v-model="date"></inputx>
Date: {{date}}
</div>

V-model is not listening to value change for an input (vuejs)

I have an object property which could listen to the user input or could be changed by the view.
With the snipped below :
if I typed something the value of my input is updated and widget.Title.Name is updated.
if I click on the button "External Update", the property widget.Title.Name is updated but not the value in my field above.
Expected result : value of editable text need to be updated at the same time when widget.Title.Name change.
I don't understand why there are not updated, if I inspect my property in vue inspector, all my fields (widget.Title.Name and Value) are correctly updated, but the html is not updated.
Vue.component('editable-text', {
template: '#editable-text-template',
props: {
value: {
type: String,
default: '',
},
contenteditable: {
type: Boolean,
default: true,
},
},
computed: {
listeners() {
return { ...this.$listeners, input: this.onInput };
},
},
mounted() {
this.$refs["editable-text"].innerText = this.value;
},
methods: {
onInput(e) {
this.$emit('input', e.target.innerText);
}
}
});
var vm = new Vue({
el: '#app',
data: {
widget: {
Title: {
Name: ''
}
}
},
async created() {
this.widget.Title.Name = "toto"
},
methods: {
externalChange: function () {
this.widget.Title.Name = "changed title";
},
}
})
button{
height:50px;
width:100px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<editable-text v-model="widget.Title.Name"></editable-text>
<template>Name : {{widget.Title.Name}}</template>
<br>
<br>
<button v-on:click="externalChange">External update</button>
</div>
<template id="editable-text-template">
<p ref="editable-text" v-bind:contenteditable="contenteditable"
v-on="listeners">
</p>
</template>
I searched a lot of subject about similar issues but they had reactivity problem, I think I have a specific problem with input. Have you any idea of what's going on ? I tried to add a listener to change event but it was not triggered on widget.Title.Name change.
To anwser to this problem, you need to do 3 differents things.
Add watch property with the same name as your prop (here value)
Add debounce function from Lodash to limit the number of request
Add a function to get back the cursor (caret position) at the good position when the user is typing
For the third point : when you change the value of widget.Title.Name, the component will re-render, and the caret position will be reinitialize to 0, at the beginning of your input. So, you need to re-update it at the last position or you will just write from right to left.
I have updated the snippet above with my final solution.
I hope this will help other people coming here.
Vue.component('editable-text', {
template: '#editable-text-template',
props: {
value: {
type: String,
default: '',
},
contenteditable: {
type: Boolean,
default: true,
},
},
//Added watch value to watch external change <-> enter here by user input or when component or vue change the watched property
watch: {
value: function (newVal, oldVal) { // watch it
// _.debounce is a function provided by lodash to limit how
// often a particularly expensive operation can be run.
// In this case, we want to limit how often we update the dom
// we are waiting for the user finishing typing his text
const debouncedFunction = _.debounce(() => {
this.UpdateDOMValue();
}, 1000); //here your declare your function
debouncedFunction(); //here you call it
//not you can also add a third argument to your debounced function to wait for user to finish typing, but I don't really now how it works and I didn't used it.
}
},
computed: {
listeners() {
return { ...this.$listeners, input: this.onInput };
},
},
mounted() {
this.$refs["editable-text"].innerText = this.value;
},
methods: {
onInput(e) {
this.$emit('input', e.target.innerText);
},
UpdateDOMValue: function () {
// Get caret position
if (window.getSelection().rangeCount == 0) {
//this changed is made by our request and not by the user, we
//don't have to move the cursor
this.$refs["editable-text"].innerText = this.value;
} else {
let selection = window.getSelection();
let index = selection.getRangeAt(0).startOffset;
//with this line all the input will be remplaced, so the cursor of the input will go to the
//beginning... and you will write right to left....
this.$refs["editable-text"].innerText = this.value;
//so we need this line to get back the cursor at the least position
setCaretPosition(this.$refs["editable-text"], index);
}
}
}
});
var vm = new Vue({
el: '#app',
data: {
widget: {
Title: {
Name: ''
}
}
},
async created() {
this.widget.Title.Name = "toto"
},
methods: {
externalChange: function () {
this.widget.Title.Name = "changed title";
},
}
})
/**
* Set caret position in a div (cursor position)
* Tested in contenteditable div
* ##param el : js selector to your element
* ##param caretPos : index : exemple 5
*/
function setCaretPosition(el, caretPos) {
var range = document.createRange();
var sel = window.getSelection();
if (caretPos > el.childNodes[0].length) {
range.setStart(el.childNodes[0], el.childNodes[0].length);
}
else
{
range.setStart(el.childNodes[0], caretPos);
}
range.collapse(true);
sel.removeAllRanges();
}
button{
height:50px;
width:100px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<editable-text v-model="widget.Title.Name"></editable-text>
<template>Name : {{widget.Title.Name}}</template>
<br>
<br>
<button v-on:click="externalChange">External update</button>
</div>
<template id="editable-text-template">
<p ref="editable-text" v-bind:contenteditable="contenteditable"
v-on="listeners">
</p>
</template>
you can use $root.$children[0]
Vue.component('editable-text', {
template: '#editable-text-template',
props: {
value: {
type: String,
default: '',
},
contenteditable: {
type: Boolean,
default: true,
},
},
computed: {
listeners() {
return {...this.$listeners, input: this.onInput
};
},
},
mounted() {
this.$refs["editable-text"].innerText = this.value;
},
methods: {
onInput(e) {
this.$emit('input', e.target.innerText);
}
}
});
var vm = new Vue({
el: '#app',
data: {
widget: {
Title: {
Name: ''
}
}
},
async created() {
this.widget.Title.Name = "toto"
},
methods: {
externalChange: function(e) {
this.widget.Title.Name = "changed title";
this.$root.$children[0].$refs["editable-text"].innerText = "changed title";
},
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="app">
<editable-text v-model="widget.Title.Name"></editable-text>
<template>Name : {{widget.Title.Name}}</template>
<br>
<br>
<button v-on:click="externalChange">External update</button>
</div>
<template id="editable-text-template">
<p ref="editable-text" v-bind:contenteditable="contenteditable" v-on="listeners">
</p>
</template>
or use Passing props to root instances
Vue.component('editable-text', {
template: '#editable-text-template',
props: {
value: {
type: String,
default: '',
},
contenteditable: {
type: Boolean,
default: true,
},
},
computed: {
listeners() {
return {...this.$listeners, input: this.onInput
};
},
},
mounted() {
this.$refs["editable-text"].innerText = this.value;
this.$root.$on("titleUpdated",(e)=>{
this.$refs["editable-text"].innerText = e;
})
},
methods: {
onInput(e) {
this.$emit('input', e.target.innerText);
}
}
});
var vm = new Vue({
el: '#app',
data: {
widget: {
Title: {
Name: ''
}
}
},
async created() {
this.widget.Title.Name = "toto"
},
methods: {
externalChange: function(e) {
this.widget.Title.Name = "changed title";
this.$root.$emit("titleUpdated", this.widget.Title.Name);
},
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="app">
<editable-text v-model="widget.Title.Name"></editable-text>
<template>Name : {{widget.Title.Name}}</template>
<br>
<br>
<button v-on:click="externalChange">External update</button>
</div>
<template id="editable-text-template">
<p ref="editable-text" v-bind:contenteditable="contenteditable" v-on="listeners">
</p>
</template>

Vue js. Data fields not binding

I have the following definition for the Vue element:
new Vue({
el: "#app",
data: {
latitude1: 'a',
name: 'aa'
},
mounted() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(position => {
this.latitude1 = position.coords.latitude;
})
} else {
this.latitude1 = "WTF??"
// this doesn't work either:
// this.$nextTick(() => { this.latitude1 = "WTF??" })
}
},
methods: {
// button works... WTF?!?
doIt() {
this.latitude1 = "WTF??"
}
}
});
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<div>{{ latitude1 }}: {{ name }}</div>
<button #click="doIt">Do it</button>
</div>
I can see the location data being populated. The alert displays the latitude but the 2 way binding for the data field latitude1 is not working.
I have tried storing the object state using this and that also did not work.
My html is as follows:
<div class="form-group" id="app">
<p>
{{latitude1}}
</p>
</div>
One of the things to do inside the Vue.js is to use the defined methods for reactive properties changes.
Here is a code I've provided for it:
function error(err) {
console.warn(`ERROR(${err.code}): ${err.message}`);
}
var options = {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
};
new Vue({
el: "#app",
data: {
latitude1: 'a',
name: 'aa'
},
mounted: function() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(position => {
console.log(position.coords.latitude);
Vue.set(this, 'latitude1', position.coords.latitude);
}, error, options)
}
}
});
I also set error handler and options for the navigator query. For following the results please check the console.

Vue.js global event not working

I've got
<component-one></component-one>
<component-two></component-two>
<component-three></component-three>
Component two contains component three.
Currently I emit an event in <component-one> that has to be caught in <component-three>.
In <component-one> I fire the event like this:
this.$bus.$emit('setSecondBanner', finalBanner);
Then in <component-three> I catch it like this:
mounted() {
this.$bus.$on('setSecondBanner', (banner) => {
alert('Caught');
this.banner = banner;
});
},
But the event is never caught!
I define the bus like this (in my core.js):
let eventBus = new Vue();
Object.defineProperties(Vue.prototype, {
$bus: {
get: () => { return eventBus; }
}
});
What could be wrong here? When I check vue-dev-tools I can see that the event has fired!
This is the working example for vue1.
Object.defineProperty(Vue.prototype, '$bus', {
get() {
return this.$root.bus;
}
});
Vue.component('third', {
template: `<div> Third : {{ data }} </div>`,
props:['data']
});
Vue.component('second', {
template: `<div>Second component <third :data="data"></third></div>`,
ready() {
this.$bus.$on('setSecondBanner', (event) => {
this.data = event.data;
});
},
data() {
return {
data: 'Defautl value in second'
}
}
});
Vue.component('first', {
template: `<div>{{ data }}</div>`,
ready() {
setInterval(() => {
this.$bus.$emit('setSecondBanner', {
data: 'Bus sending some data : '+new Date(),
});
}, 1000);
},
data() {
return {
data: 'Defautl value in first'
}
}
});
var bus = new Vue({});
new Vue({
el: '#app',
data: {
bus: bus
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.28/vue.js"></script>
<div id="app">
<second></second>
<first></first>
</div>
Have you tried registering the listener in created instead of mounted?
Also, why define the bus with defineProperties and not simply:
Vue.prototype.$bus = new Vue();

Vue.js component model update

Im absolutely new in Vue framework and I need create reusable component with live BTC/LTC/XRP price
For live prices Im using Bitstamp websockets API. Here is example usage with jQuery - run this snippet, is really live.
var bitstamp = new Pusher('de504dc5763aeef9ff52')
var channel = bitstamp.subscribe('live_trades')
channel.bind('trade', function (lastTrade) {
$('p').text(lastTrade.price)
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pusher/4.1.0/pusher.min.js"></script>
<h3>BTC/USD price</h3>
<p>loading...</p>
As you can see, its really simple. But, I need to use Vue.js component. So I created this, and its also fully functional:
var bitstamp = new Pusher('de504dc5763aeef9ff52')
Vue.component('live-price', {
template: '<div>{{price}}</div>',
data: function () {
return {
price: 'loading...'
}
},
created: function () {
this.update(this)
},
methods: {
update: function (current) {
var pair = current.$attrs.pair === 'btcusd'
? 'live_trades'
: 'live_trades_' + current.$attrs.pair
var channel = bitstamp.subscribe(pair)
channel.bind('trade', function (lastTrade) {
current.price = lastTrade.price
})
}
}
})
new Vue({
el: '.prices'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/pusher/4.1.0/pusher.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.1/vue.min.js"></script>
<section class="prices">
<live-price pair="btcusd"></live-price>
<live-price pair="ltcusd"></live-price>
<live-price pair="xrpusd"></live-price>
</section>
But, there is big BUT. Am I using Vue right way? WHERE IS IDEAL PLACE to run Pusher? In "created" or "mounted" method? In "computed"? In "watch"? Or where? Am i doing it right? I really dont known, I started with Vue ... today :(
Looks pretty good for your first day using Vue! I would just make a few changes.
The component is reaching out and using a global, bitstamp. Generally with components, you want them to be independent, and not reaching out of themselves to get values. To that end, declare the socket as a property that can be passed in to the component.
Likewise, the pair is passed in as a property, but you do not declare it and instead, use current.$attrs.pair to get the pair. But that's not very declarative and makes it harder for anyone else to use the component. Moreover, by making it a property, you can reference it using this.pair.
When using something like a socket, you should always remember to clean up when you are done using it. In the code below, I added the unsubscribe method to do so. beforeDestroy is a typical lifecycle hook to handle these kinds of things.
Computed properties are useful for calculating values that are derived from your components data: the channel you are subscribing to is a computed property. You don't really need to do this, but its generally good practice.
A Vue can only bind to a single DOM element. You are using a class .prices which works in this case because there is only one element with that class, but could be misleading down the road.
Finally, created is an excellent place to initiate your subscription.
console.clear()
var bitstamp = new Pusher('de504dc5763aeef9ff52')
Vue.component('live-price', {
props:["pair", "socket"],
template: '<div>{{price}}</div>',
data() {
return {
price: 'loading...',
subscription: null
}
},
created() {
this.subscribe()
},
beforeDestroy(){
this.unsubscribe()
},
computed:{
channel(){
if (this.pair === 'btcusd')
return 'live_trades'
else
return 'live_trades_' + this.pair
}
},
methods: {
onTrade(lastTrade){
this.price = lastTrade.price
},
subscribe() {
this.subscription = this.socket.subscribe(this.channel)
this.subscription.bind('trade', this.onTrade)
},
unsubscribe(){
this.subscription.unbind('trade', this.onTrade)
this.socket.unsubscribe(this.channel)
}
}
})
new Vue({
el: '#prices',
data:{
socket: bitstamp
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/pusher/4.1.0/pusher.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.1/vue.min.js"></script>
<section id="prices">
<live-price pair="btcusd" :socket="bitstamp"></live-price>
<live-price pair="ltcusd" :socket="bitstamp"></live-price>
<live-price pair="xrpusd" :socket="bitstamp"></live-price>
</section>
Rewrited - is it ok now?
var config = {
key: 'de504dc5763aeef9ff52'
}
var store = new Vuex.Store({
state: {
pusher: null
},
mutations: {
initPusher (state, payload) {
state.pusher = new Pusher(payload.key)
}
}
})
var livePrice = {
template: '#live-price',
props: ['pair'],
data () {
return {
price: 'loading...',
subscription: null
}
},
computed: {
channel () {
return this.pair === 'btcusd'
? 'live_trades'
: 'live_trades_' + this.pair
}
},
methods: {
onTrade (lastTrade) {
this.price = lastTrade.price
},
subscribe () {
this.subscription = this.$store.state.pusher.subscribe(this.channel)
this.subscription.bind('trade', this.onTrade)
},
unsubscribe () {
this.subscription.unbind('trade', this.onTrade)
this.$store.state.pusher.unsubscribe(this.channel)
}
},
created () {
this.subscribe()
},
beforeDestroy () {
this.unsubscribe()
}
}
new Vue({
el: '#prices',
store,
components: {
'live-price': livePrice
},
created () {
store.commit({
type: 'initPusher',
key: config.key
})
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/pusher/4.1.0/pusher.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.1/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/2.3.1/vuex.min.js"></script>
<section id="prices">
<live-price pair="btcusd"></live-price>
<live-price pair="ltcusd"></live-price>
<live-price pair="xrpusd"></live-price>
</section>
<template id="live-price">
<div>
{{price}}
</div>
</template>

Categories

Resources