Vue.JS value tied on input having the focus - vue.js

Is there a way to change a value in the model when an input gets/loses focus?
The use case here is a search input that shows results as you type, these should only show when the focus is on the search box.
Here's what I have so far:
<input type="search" v-model="query">
<div class="results-as-you-type" v-if="magic_flag"> ... </div>
And then,
new Vue({
el: '#search_wrapper',
data: {
query: '',
magic_flag: false
}
});
The idea here is that magic_flag should turn to true when the search box has focus. I could do this manually (using jQuery, for example), but I want a pure Vue.JS solution.

Apparently, this is as simple as doing a bit of code on event handlers.
<input
type="search"
v-model="query"
#focus="magic_flag = true"
#blur="magic_flag = false"
/>
<div class="results-as-you-type" v-if="magic_flag"> ... </div>

Another way to handle something like this in a more complex scenario might be to allow the form to track which field is currently active, and then use a watcher.
I will show a quick sample:
<input
v-model="user.foo"
type="text"
name="foo"
#focus="currentlyActiveField = 'foo'"
>
<input
ref="bar"
v-model="user.bar"
type="text"
name="bar"
#focus="currentlyActiveField = 'bar'"
>
...
data() {
return {
currentlyActiveField: '',
user: {
foo: '',
bar: '',
},
};
},
watch: {
user: {
deep: true,
handler(user) {
if ((this.currentlyActiveField === 'foo') && (user.foo.length === 4)) {
// the field is focused and some condition is met
this.$refs.bar.focus();
}
},
},
},
In my sample here, if the currently-active field is foo and the value is 4 characters long, then the next field bar will automatically be focused. This type of logic is useful when dealing with forms that have things like credit card number, credit card expiry, and credit card security code inputs. The UX can be improved in this way.
I hope this could stimulate your creativity. Watchers are handy because they allow you to listen for changes to your data model and act according to your custom needs at the time the watcher is triggered.
In my example, you can see that each input is named, and the component knows which input is currently focused because it is tracking the currentlyActiveField.
The watcher I have shown is a bit more complex in that it is a "deep" watcher, which means it is capable of watching Objects and Arrays. Without deep: true, the watcher would only be triggered if user was reassigned, but we don't want that. We are watching the keys foo and bar on user.
Behind the scenes, deep: true is adding observers to all keys on this.user. Without deep enabled, Vue reasonably does not incur the cost of maintaining every key reactively.
A simple watcher would be like this:
watch: {
user() {
console.log('this.user changed');
},
},
Note: If you discover that where I have handler(user) {, you could have handler(oldValue, newValue) { but you notice that both show the same value, it's because both are a reference to the same user object. Read more here: https://github.com/vuejs/vue/issues/2164
Edit: to avoid deep watching, it's been a while, but I think you can actually watch a key like this:
watch: {
'user.foo'() {
console.log('user foo changed');
},
},
But if that doesn't work, you can also definitely make a computed prop and then watch that:
computed: {
userFoo() {
return this.user.foo;
},
},
watch: {
userFoo() {
console.log('user foo changed');
},
},
I added those extra two examples so we could quickly note that deep watching will consume more resources because it triggers more often. I personally avoid deep watching in favour of more precise watching, whenever reasonable.
However, in this example with the user object, if all keys correspond to inputs, then it is reasonable to deep watch. That is to say it might be.

You can use a flat by determinate a special CSS class, for example this a simple snippet:
var vm = new Vue({
el: '#app',
data: {
content: 'click to change content',
flat_input_active: false
},
methods: {
onFocus: function(event) {
event.target.select();
this.flat_input_active = true;
},
onBlur: function(event) {
this.flat_input_active = false;
}
},
computed: {
clazz: function() {
var clzz = 'control-form';
if (this.flat_input_active == false) {
clzz += ' only-text';
}
return clzz;
}
}
});
#app {
background: #EEE;
}
input.only-text { /* special css class */
border: none;
background: none;
}
<!-- libraries -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<!-- html template -->
<div id='app'>
<h1>
<input v-model='content' :class='clazz'
#focus="onFocus($event)"
#blur="onBlur"/>
</h1>
<div>
Good luck

You might also want to activate the search when the user mouses over the input - #mouseover=...
Another approach to this kind of functionality is that the filter input is always active, even when the mouse is in the result list. Typing any letters modifies the filter input without changing focus. Many implementations actually show the filter input box only after a letter or number is typed.
Look into #event.capture.

Related

Vue class components dynamically add component depending on answer from backend

So from the backend I get a array of objects that look kind of like this
ItemsToAdd
{
Page: MemberPage
Feature: Search
Text: "Something to explain said feature"
}
So i match these values to enums in the frontend and then on for example the memberpage i do this check
private get itemsForPageFeatures(): ItemsToAdd[] {
return this.items.filter(
(f) =>
f.page== Pages.MemberPage &&
f.feature != null
);
}
What we get from the backend will change a lot over time and is only the same for weeks at most. So I would like to avoid to have to add the components in the template as it will become dead code fast and will become a huge thing to have to just go around and delete dead code. So preferably i would like to add it using a function and then for example for the search feature i would have a ref on the parent like
<SearchBox :ref="Features.Search" />
and in code just add elements where the ItemsToAdd objects Feature property match the ref
is this possible in Vue? things like appendChild and so on doesn't work in Vue but that is the closest thing i can think of to kind of what I want. This function would basically just loop through the itemsForPageFeatures and add the features belonging to the page it is run on.
For another example how the template looks
<template>
<div class="container-fluid mt-3">
<div
class="d-flex flex-row justify-content-between flex-wrap align-items-center"
>
<div class="d-align-self-end">
<SearchBox :ref="Features.Search" />
</div>
</div>
<MessagesFilter
:ref="Features.MessagesFilter"
/>
<DataChart
:ref="Features.DataChart"
/>
So say we got an answer from backend where it contains an object that has a feature property DataChart and another one with Search so now i would want components to be added under the DataChart component and the SearchBox component but not the messagesFilter one as we didnt get that from the backend. But then next week we change in backend so we no longer want to display the Search feature component under searchbox. so we only get the object with DataChart so then it should only render the DataChart one. So the solution would have to work without having to make changes to the frontend everytime we change what we want to display as the backend will only be database configs that dont require releases.
Closest i can come up with is this function that does not work for Vue as appendChild doesnt work there but to help with kind of what i imagine. So the component to be generated is known and will always be the same type of component. It is where it is to be placed that is the dynamic part.
private showTextBoxes() {
this.itemsForPageFeatures.forEach((element) => {
let el = this.$createElement(NewMinorFeatureTextBox, {
props: {
item: element,
},
});
var ref = `${element.feature}`
this.$refs.ref.appendChild(el);
});
}
You can use dynamic components for it. use it like this:
<component v-for="item in itemsForPageFeatures" :is="getComponent(item.Feature)" :key="item.Feature"/>
also inside your script:
export default {
data() {
return {
items: [
{
Page: "MemberPage",
Feature: "Search",
Text: "Something to explain said feature"
}
]
};
},
computed: {
itemsForPageFeatures() {
return this.items.filter(
f =>
f.Page === "MemberPage" &&
f.Feature != null
);
}
},
methods: {
getComponent(feature) {
switch (feature) {
case "Search":
return "search-box";
default:
return "";
}
}
}
};

How can I implement v-model.number on my own in VueJS?

I have a text field component for numeric inputs. Basically I'm just wrapping v-text-field but in preparation for implementing it myself. It looks like this.
<template>
<v-text-field v-model.number = "content" />
</template>
<script>
export default {
name: 'NumericTextField',
props: [ 'value' ],
computed: {
content: {
get () { return this.value },
set (v) { this.$emit('input', f) },
},
}
}
</script>
This has generated user feedback that it's annoying when the text field has the string "10.2" in it and then backspace over the '2', then decimal place is automatically delete. I would like to change this behavior so that "10." remains in the text field. I'd also like to understand this from first principles since I'm relatively new to Vue.
So I tried this as a first past, and it's the most instructive of the things I've tried.
<template>
<v-text-field v-model="content" />
</template>
<script>
export default {
name: 'NumericTextField',
props: [ 'value' ],
computed: {
content: {
get () { return this.value },
set (v) {
console.log(v)
try {
const f = parseFloat(v)
console.log(f)
this.$emit('input', f)
} catch (err) {
console.log(err)
}
},
},
}
}
</script>
I read that v-model.number is based on parseFloat so I figured something like this must be happening. So it does fix the issue where the decimal place is automatically deleted. But... it doesn't even auto delete extra letters. So if I were to type "10.2A" the 'A' remains even though I see a console log with "10.2" printed out. Furthermore, there's an even worse misfeature. When I move to the start of the string and change it to "B10.2" it's immediately replaced with "NaN".
So I'd love to know a bunch of things. Why is the body of the text body immediately reactive when I change to a NaN but not immediately reactive when I type "10.2A"? Relatedly, how did I inadvertently get rid of the auto delete decimal place? I haven't even gotten to that part yet. So I'm misunderstanding data flow in Vue.
Lastly, how can I most simply provide a text box that's going to evaluate to a number for putting into my data model but not have the annoying auto delete of decimal places? The existing functionality doesn't auto delete trailing letters so I'm guessing the auto delete of decimal places was a deliberate feature that my users don't like.
I'm not 100% sure of any of this, but consider how v-model works on components. It basically is doing this:
<v-text-field
v-bind:value="content"
v-on:input="content = $event.target.value"
/>
And consider how the .number modifier works. It runs the input through parseFloat, but if parseFloat doesn't work, it leaves it as is.
So with that understanding, I would expect the following:
When you type in "10.2" and then hit backspace, "10." would be emitted via the input event, parseFloat("10.") would transform it to 10, v-on:input="content = $event.target.value" would assign it to content, and v-bind:value="content" would cause the input to display "10". So then, this is the expected behavior.
When you type in "10.2" and then hit "A", "10.2A" would be emitted via the input event, parseFloat("10.2A") would transform it to 10.2, v-on:input="content = $event.target.value" would assign it to content, and v-bind:value="content" would cause the input to display "10.2". It looks like it's failing at that very last step of causing the input to display "10.2", because the state of content is correctly being set to 10.2. If you use <input type="text" v-model.number="content" /> instead of <v-text-field v-model.number="content" />, once you blur, the text field successfully gets updated to "10.2". So it seems that the reason why <v-text-field> doesn't is due to how Vuetify is handling the v-bind:value="content" part.
When you type in "10.2" and then enter "B", in the beginning, "B10.2" would be emitted via the input event, parseFloat("B10.2") would return NaN, and thus the .number modifier would leave it as is, v-on:input="content = $event.target.value" would assign "B10.2" to content, and v-bind:value="content" would cause the input to display "B10.2". I agree that it doesn't seem right for parseFloat("10.2A") to return 10.2 but parseFloat("B10.2") to return "B10.2".
Lastly, how can I most simply provide a text box that's going to evaluate to a number for putting into my data model but not have the annoying auto delete of decimal places?
Given that the default behavior is weird, I think you're going to have to write your own custom logic for transforming the user's input. Eg. so that "10.2A" and "B10.2" both get transformed to 10.2 (or are left as is), and so that decimals are handled like you want. Something like this (CodePen):
<template>
<div id="app">
<input
v-bind:value="content"
v-on:input="handleInputEvent($event)"
/>
<p>{{ content }}</p>
</div>
</template>
<script>
export default {
data() {
return {
content: 0,
};
},
methods: {
handleInputEvent(e) {
this.content = this.transform(e.target.value);
setTimeout(() => this.$forceUpdate(), 500);
},
transform(val) {
val = this.trimLeadingChars(val);
val = this.trimTrailingChars(val);
// continue your custom logic here
return val;
},
trimLeadingChars(val) {
if (!val) {
return "";
}
for (let i = 0; i < val.length; i++) {
if (!isNaN(val[i])) {
return val.slice(i);
}
}
return val;
},
trimTrailingChars(val) {
if (!val) {
return "";
}
for (let i = val.length - 1; i >= 0; i--) {
if (!isNaN(Number(val[i]))) {
return val.slice(0,i+1);
}
}
return val;
},
},
};
</script>
The $forceUpdate seems to be necessary if you want the input field to actually change. However, it only seems to work on <input>, not <v-text-field>. Which is consistent with what we saw in the second bullet point. You can customize your <input> to make it appear and behave like <v-text-field> though.
I put it inside of a setTimeout so the user sees "I tried to type this but it got deleted" rather than "I'm typing characters but they're not appearing" because the former does a better job of indicating "What you tried to type is invalid".
Alternatively, you may want to do the transform on the blur event rather than as they type.

Do I need to check for inequality between old and new values in watchers?

Currently, a lot of my watcher code looks like this:
watch: {
// whenever question changes, this function will run
question: function (newQuestion, oldQuestion) {
if (newQuestion != oldQuestion) {
// do something
}
}
},
My question is if I need newQuestion != oldQuestion or does Vue not trigger the watcher if newQuestion = oldQuestion. If the latter is true, then what kind of inequalities does Vue check for? Just = or deep equality (e.g. all checking if elements of array are same even if arrays aren't the same) as well?
To answer your question, the watcher does not trigger if they are equal. To check for deep equal you could structure your watcher like so (only needed when dealing with non scalars).
question: {
handler: () => {},
deep: true
}
Hope that helps.
try it yourself:
new Vue({
el:"#app",
data:_=>({x:"", log:""}),
watch:{
x(nw,od){
this.log += `<hr/>old: ${od} new: ${nw} x: ${this.x}`
}
}
})
<script src="https://unpkg.com/vue#2.5.21/dist/vue.js"></script>
<div id="app">
<input v-model="x"/>
<div v-html="log"></div>
</div>

Form number input does not work with keys

<template v-for="(paint, index) in paints">
<input type="number" v-bind:min="1" v-model.number="paint.qty">
</template>
-
var paintListApp = new Vue({
delimiters: ['${', '}'],
el: '#paintListApp',
data: {
paints: paints
},
methods: {
addToSet: function(sku, name, image) {
// method triggered when item is clicked - sends data to event bus
this.$eventHub.$emit('addToSelectedPaints', sku, name, image)
}
}
});
var paintWidget = new Vue({
el: '#paintWidget',
delimiters: ['${', '}'],
data: {
paints: []
},
created() {
// data picked up - processed by 'addToSelectedPaints'
this.$eventHub.$on('addToSelectedPaints', this.addToSelectedPaints);
},
methods: {
addToSelectedPaints: function (sku, name, image) {
var skuIndex = _.findIndex(this.paints, function (o) { return o.sku === sku; });
if (skuIndex !== -1) {
this.paints[skuIndex].qty = this.paints[skuIndex].qty + 1;
} else {
this.paints.push({
sku: sku,
name: name,
image: image,
qty: 1
});
}
}
}
});
Trying to get min values to work on number inputs. The min is respected by the browser number plus / minus controls - however, when using the keyboard, the min attribute appears to be ignored. I've tried all sorts of things from adding a method triggered by keyup etc and testing the value, through to watchers.
Keyup gets messy as when deleting, it automatically added a 1... making it difficult to type numbers above 19... (eg, you backspace to enter 2, but - it inserts a 1).
I just need to get native browser input min attribute working with keyboard input.
** Edit **
<input type="number" v-model="paint.qty" #change="paint.qty = paint.qty < 1 ? 1 : paint.qty">
Sort of solves the issue, albeit at the expense of the min attribute. Hooking into the #change event. If input is less than 1, switch it for 1. It also doesn't update until the input has lost focus - not locking the ui up. So not exactly the way I wanted it to work - but the result is the same.
** edit **
I've adapted Richard Matsens answer (the accepted one) to use an input and timeout... this behaves a bit more like the Chrome and Firefox native implementation.
<input type="number" min="1" v-model.number="paint.qty" #input="handleUpdate($event, index)">
and in the handleUpdate method:
...handleUpdate(event, index) {
var updater;
clearTimeout(updater);
this.currentIndex = index;
var paints = this.paints;
var max = this.max;
updater = setTimeout(function() {
if(event.target.value < event.target.min) {
paints[index].qty = parseInt(event.target.min);
}
if(event.target.value > max){
console.log(max);
paints[index].qty = parseInt(max);
}
}, 1000);
}...
clearing the timeout to prevent the updater bit firing too many times -
bouncing / mashing etc...
From this Validate input type number with range min/max
Most browsers “ignore” (it’s their default behavior) min and max, so that the user can freely edit the input field and type a number that’s not in the range 1-5.
From this How to detect changes in nested data, can use an #input on the control and a method() to handle the check.
Works for min="0", but say min="1" may be problematic if the user wants to type in "11".
Changed to blur event to handle above caveat.
methods: {
handleUpdate(event, index) {
if(event.target.value < event.target.min) {
this.paints[index].qty = event.target.min;
}
}
},
also add #blur() to the input
<div >
<input v-for="(paint, index) in paints"
#blur="handleUpdate($event, index)"
type="number" min="2" v-model.number="paint.qty">
</div>
For completeness, you may also want to add a validation message so that the user knows why the input value is being changed.

Twitter typeahead.js not working in Vue component

I'm trying to use Twitter's typeahead.js in a Vue component, but although I have it set up correctly as tested out outside any Vue component, when used within a component, no suggestions appear, and no errors are written to the console. It is simply as if it is not there. This is my typeahead setup code:
var codes = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('code'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
prefetch: contextPath + "/product/codes"
});
$('.typeahead').typeahead({
hint: true,
highlight: true,
minLength: 3
},
{
name: 'codes',
display: 'code',
source: codes,
templates: {
suggestion: (data)=> {
return '<div><strong>' + data.code + '</strong> - ' + data.name + '</div>';
}
}
});
I use it with this form input:
<form>
<input id="item" ref="ttinput" autocomplete="off" placeholder="Enter code" name="item" type="text" class="typeahead"/>
</form>
As mentioned, if I move this to a div outside Vue.js control, and put the Javascript in a document ready block, it works just fine, a properly formatted set of suggestions appears as soon as 3 characters are input in the field. If, however, I put the Javascript in the mounted() for the component (or alternatively in a watch, I've tried both), no typeahead functionality kicks in (i.e., nothing happens after typing in 3 characters), although the Bloodhound prefetch call is made. For the life of me I can't see what the difference is.
Any suggestions as to where to look would be appreciated.
LATER: I've managed to get it to appear by putting the typeahead initialization code in the updated event (instead of mounted or watch). It must have been some problem with the DOM not being in the right state. I have some formatting issues but at least I can move on now.
The correct place to initialize Twitter Typeahead/Bloodhound is in the mounted() hook since thats when the DOM is completely built. (Ref)
Find below the relevant snippet: (Source: https://digitalfortress.tech/js/using-twitter-typeahead-with-vuejs/)
mounted() {
// configure datasource for the suggestions (i.e. Bloodhound)
this.suggestions = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('title'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
identify: item => item.id,
remote: {
url: http://example.com/search + '/%QUERY',
wildcard: '%QUERY'
}
});
// get the input element and init typeahead on it
let inputEl = $('.globalSearchInput input');
inputEl.typeahead(
{
minLength: 1,
highlight: true,
},
{
name: 'suggestions',
source: this.suggestions,
limit: 5,
display: item => item.title,
templates: {
suggestion: data => `${data.title}`;
}
}
);
}
You can also find a working example: https://gospelmusic.io/
and a Reference Tutorial to integrate twitter typeahead with your VueJS app.