Vue + Vue-Paginate: Array will not refresh once empty - vue.js

I am using vue-paginate in my app and I've noticed that once my array is empty, refreshing its value to an array with contents does not display.
<paginate
name="recipes"
:list="recipes"
:per="16"
class="p-0"
>
<transition-group name="zoom">
<div v-for="recipe in paginated('recipes')" :key="recipe.id">
<recipe class=""
:recipe="recipe"
:ref="recipe.id"
></recipe>
</div>
</transition-group>
</paginate>
This is how things get displayed, and my recipe array changes depending on a search. If I type in "b" into my search, results for banana, and bbq would show. If I typed "ba" the result for bbq is removed, and once I backspace the search to "b" it would re-appear as expected.
If I type "bx" every result is removed and when I backspace the search to "b", no results re-appear.
Any idea why this might happen?
UPDATE
When I inspect the component in chrome I see:
currentPage:-1
pageItemsCount:"-15-0 of 222"
Even though the list prop is:
list:Array[222]

Paginate needs a key in order to know when to re-render after the collection it's looking at reaches a length of zero. If you add a key to the paginate element, things should function as expected.
<paginate
name="recipes"
:list="recipes"
:per="16"
class="p-0"
:key="recipes ? recipes.length : 0" // You need some key that will update when the filtered result updates
>
See "Filtering the paginated list" is not working on vue-paginate node for a slightly more in depth answer.

I found a hacky workaround that fixed it for my app. First, I added a ref to my <paginate></paginate> component ref="paginator". Then I created a computed property:
emptyArray () {
return store.state.recipes.length == 0
}
then I created a watcher that looks for a change from length == 0 to length != 0:
watch: {
emptyArray: function(newVal, oldVal) {
if ( newVal === false && oldVal === true ) {
setTimeout(() => {
if (this.$refs.paginator) {
this.$refs.paginator.goToPage(page)
}
}, 100)
}
}
}
The timeout was necessary otherwise the component thought there was no page 1.

Using :key in the element has certain bugs. It will not work properly if you have multiple search on the table. In that case input will lose focus by typing single character. Here is the better alternative:
computed:{
searchFilter() {
if(this.search){
//Your Search condition
}
}
},
watch:{
searchFilter(newVal,oldVal){
if ( newVal.length !==0 && oldVal.length ===0 ) {
setTimeout(() => {
if (this.$refs.paginator) {
this.$refs.paginator[0].goToPage(1)
}
}, 50)
}
}
},

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.

I need help in calling watcher in Vuejs2 when looping the objects in HTML

I am adding one object when clicking on button and displaying the same in HTML. User can able to select the drop down values in options (string or number). Based on the input, need to disable or enable the next text input field. Here is my HTML code,
<table>
<tr><button #click="add_new_input()">Add </button></tr>
<tr v-for="(key, index) in NewArr" v-bind:key=value>
<td>
<multiselect
v-model="key.name"
:options="NameList"
selectLabel='select'
#input="userInput(value)"
></multiselect>
</td>
<td class="modify-td-padding__multi">
<input type="text"
v-model="key.value"
:disabled="isNumber"
class="input-increase-height">
</td>
</tr>
</table>
if we change the key.name dropdown, it will call one function userInput() using #input. passing value will be either "string" or "number". Vue Mehods is below,
userInput: function (value) {
this.getInputType(value);
},
getInputType: function (value) {
if(value === "string") {
this.isNumber = false;
} else {
this.isNumber = true;
}
},
add_new_input: function () {
let vm = this;
vm.NewArr.push({
name: '',
value: '',
});
vm.$set(vm.NewArr, vm.name, vm.value);
}
add_new_input will add new object to NewArr, getInputType function will check the value is "string" or "number". If it is "string", text field should be disabled else enabled.
My issue is, if there are two rows, and if i am selecting key.name for 2nd row, it is affecting the first row input field also(key.name for both rows getting enabled or disabled). I need to make change only the specific text field. So, all the text fields becoming disabled even it is "number".
This is my first project in VueJS. Thanks a lot if anyone helps me on this. Thanks in advance.
You need to manage isNumber per key, so not just
data() {return {isNumber: false}}
But:
#input="userInput(key.name, value)"
:disabled="isNumber[key.name]"
data(){ return { isNumber: {} }}
...
onUserInput: function (key, value) {
this.setIsNumber(key, value);
},
setIsNumber: function (key, value) {
this.$set(this.isNumber, key, value !== "string");
},

Vue + Vuex: input value not set by store if no store change

<template>
<input
#input="formatValue"
type="text"
:value="formattedValue"
/>
</template>
<script type="text/javascript">
import {formatPhoneNumber} from '~/utils/string';
export default {
computed: {
formattedValue: function(){
return formatPhoneNumber(this.value)
},
},
methods: {
formatValue(e) {
this.$emit('input', formatPhoneNumber(e.target.value))
}
},
props: ['value']
}
</script>
As long as the formatPhoneNumber(value) produces a different value, every thing works fine, but once the max length is reached (Since formatPhoneNumber('xx xx xx xx xx whatever') == 'xx xx xx xx xx'), the emitted value is the same as the current store one.
It is totally fine, except that as a consequence, state is not mutated and component is not re-rendered, hence formattedValue() is not called.
So I end up with xx xx xx xx xx in the store, but the input displays xx xx xx xx xx whatever as local input value varies from the store one.
How can I avoid this unexpected behavior? Moving formatPhoneNumber() to the store would not solve my issue since it would still prevent mutation, and only using formatPhoneNumber() in formattedValue() would make me end up with an un-formatted value in the store which is not what I want either.
How come Vue's input with dynamic value set still manages a local state?
To achieve what you want (I think), you could change your formatValue method to
formatValue(e) {
this.$emit('input', e.target.value = formatPhoneNumber(e.target.value));
}
So that it sets the input to the formatted phone number value. One way or another you're going to be overriding what the input produces so you might as well do it on the input event.
I would use a v-model instead of a v-value since that would give me full control over what I want to display in the input field.
In this way, you can format the input value, and then set it back in the model. It would look something like this:
<template>
<input #input="formatValue" type="text" v-model="inputModel">
</template>
<script type="text/javascript">
export default {
data() {
return {
inputModel: this.value
};
},
methods: {
formatValue() {
this.inputModel = formatPhoneNumber(this.inputModel);
this.$emit("input", this.inputModel);
}
},
props: ["value"]
};
</script>
Here's a working example I created to test this.
I think the easiest approach is a simple one-line modification to the parent's #input event, that clears the prop value before it updates it.
You still only need to emit the one value, but before working with the emitted value, clear the prop.
I've provided a snippet below (but note the additional differences in the snippet):
Instead of specifying the input field value, I opted to use v-model to bind it to a computed property that has a get and set method. This allowed me to use different logic when accessing vs modifying the data (quite handy in many situations).
By separating this logic, I was able to move the functionality from inside the input event to the set method, and eliminate the input event entirely.
new Vue({
el: "#app",
// props: ['valueProp'],
data: {
valueProp: "" //simulate prop data
},
computed: {
// --Value input element is binded to--
inputValue:{
get(){ //when getting the value, return the prop
return this.valueProp;
},
set(val){ //when the value is set, emit value
this.formatValue(val);
}
}
},
methods: {
// --Emit the value to the parent--
formatValue(val) {
this.parentFunction(this.formatPhoneNumber(val)); //simulate emitting the value
// this.$emit('input', formatPhoneNumber(val));
},
// --Simulate parent receiving emit event--
parentFunction(emittedValue){
console.log("emitted:" + emittedValue);
this.valueProp = null; //first clear it (updates the input field)
this.valueProp = emittedValue; //then assign it the emitted value
},
// --Simulate your format method--
// THIS LOGIC CAN BE IGNORED. It is just a quick implementation of a naive formatter.
// The "important" thing is it limits the length, to demonstrate exceeding the limit doesn't get reflected in the input field
formatPhoneNumber(val){
var phoneSpaces = [2,4,6,8]; //specify space formatting (space locations)
var maxLength = 10; //specify the max length
val = val.replace(/ /g,''); //remove existing formatting
if(val.length > maxLength) //limits the length to the max length
val = val.substring(0, maxLength);
// for the number of desired spaces, check each space location (working backwards) ... if value is longer than space location and space location is not a space ... add a space at the location.
for(var i = phoneSpaces.length-1; i >= 0; i--){
if(val.length > phoneSpaces[i] && val[phoneSpaces[i]] != " "){
val = val.substring(0, phoneSpaces[i]) + " " + val.substring(phoneSpaces[i], val.length);
}
}
return val
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input type="text" v-model="inputValue"/>
<label style="float: right;">
Prop Value: <span>{{valueProp}}</span>
</label>
<br>
<label >format (xx xx xx xx xx)</label>
</div>

Using Vue, how can I remove elements from the screen after they have been deleted?

I am learning Vue, and I have a list of todo items that has a checkbox that I am able to mark as complete. Everything in my application is working.
When I check the checkbox, I am adding items to the completedItems array. When unchecked, I am removing items. I am able to check the array length and it is also correct.
I have a button that I can click that will remove all items marked as complete from my list.
The overarching logic is working fine. The status of being marked as complete is working, and the actual record is getting deleted as expected.
However, I am unable to remove the item from the actual view. I am not sure what I am doing wrong -- incorrectly updating my completedItems array or something. The items that I delete will only disappear after a full page refresh.
Here is what I am doing:
<task v-for="item in list.items">...</task>
...
data() {
return {
completedItems: [],
}
},
props: ['list'],
...
axios.delete(...)
.then((response) => {
if (response.status === 204) {
this.completedItems = this.completedItems.filter(i => i !== item);
} else {
console.error('Error: could not remove item(s).', response);
}).catch((error) => {
alert(error);
});
Thank you for any suggestions!
EDIT
Here is how I am checking for a match now, and it is coming across correctly, the element in the array still isn't getting removed from the page.
this.completedItems = this.completedItems.filter(i => i.id !== item.data.id);
// i.id = 123
// item.data.id = 123
You should avoid manipulating props directly, since props are supplied by the parent component and can be changed without notice. I would do something like this:
data(){
return{
completedItems[],
localList: this.list
}
}
Then, manipulate and bind the localList array instead of the prop, this should give you what you are looking for.