VueJS - The select list is not updated after a data change - vue.js

I'm starting to use Vuejs and I'm having difficulty to do something that I think simple.
To summarize, I have a list of filters that contains all the information about the filters (name, items available for selection, witch item selected and if the filter is selected).
In my page, I have a table that lists all the filters selected and drop-down list with the rest of filters not selected.
To be clean, I have two methods in the computed section:
filtersSelected (return all the filter with "isSelected" is true)
filtersNoSelected (return all the filter with "isSelected" is false)
Vue.component('filter-item', {
props: ['filter'],
template:
'<tr>' +
'<th scope="row">{{ filter.id }}</th>' +
'<td>' +
'{{ filter.name }}' +
'</td>' +
'<td>' +
'<button type="button" class="btn btn-sm btn-info">></button>' +
'</td>' +
'<td>' +
'<button type="button" class="btn btn-sm btn-danger" v-on:click="$emit(\'remove\', filter)">X</button>' +
'</td>' +
'</tr>'
})
Vue.component('filter-panel', {
props: ['allfilters'],
data: function () {
return {
filters: this.allfilters
}
},
methods: {
removeFilter: function (filter) {
filter.isSelected = false;
}
},
template:
'<table class="table table-sm">' +
'<thead>' +
'<tr>' +
'<th scope="col">#</th>' +
'<th scope="col">Name</th>' +
'<th scope="col">Update</th>' +
'<th scope="col">Delete</th>' +
'</tr>' +
'</thead>' +
'<tbody>' +
'<filter-item v-for="filter in filters" v-bind:key="filter.id" v-bind:filter="filter" #remove="removeFilter">' +
'</filter-item>' +
'</tbody>' +
'</table>'
})
Vue.component('filter-select', {
props: ['allfilters'],
data: function () {
return {
selectedItem: 0,
filters: this.allfilters
}
},
methods: {
},
template:
'<div><select class="selectpicker" data-live-search="true" v-model="selectedItem" v-on:change="$emit(\'selected\', selectedItem)"> ' +
'<option disabled value="0" selected>Select</option>' +
'<option v-for="filter in filters" v-bind:key="filter.id" v-bind:value="filter.id">{{ filter.name }}' +
'</option>' +
'</select>' +
'<span>{{selectedItem}}</span></div>'
})
var app = new Vue({
el: '#app',
data: {
filters:
[
{
id: 1,
name: "Country",
type: "List",
isSelected: false,
listItem: [
{
value: "1",
text: "France"
},
{
value: "2",
text: "United States"
},
{
value: "3",
text: "China"
}
],
selectedItem: ["1", "3"]
},
{
id: 2,
name: "Product category",
type: "List",
isSelected: false,
listItem: [
{
value: "1",
text: "Food"
},
{
value: "2",
text: "Drink"
},
{
value: "3",
text: "Home"
}
],
selectedItem: ["1"]
}
],
filterSelected: 2
},
methods:
{
AddFilter: function (filterId) {
var filter = this.filters.find(function (f) { return f.id === filterId });
filter.isSelected = true;
$('#FilterModal').modal('hide');
}
},
computed:
{
filtersSelected: function () {
var list = this.filters.filter(function (f) { return f.isSelected });
return list;
},
filtersNoSelected: function () {
var list = this.filters.filter(function (f) { return !f.isSelected });
return list;
}
}
})
<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>
<filter-panel v-bind:allfilters="filtersSelected"></filter-panel>
<filter-select v-bind:allfilters="filtersNoSelected" v-on:selected="filterSelected = $event"></filter-select>
<button type="button" class="btn btn-primary" v-on:click="AddFilter(filterSelected)">Add the filter</button>
So the logic is simple, when we select a filter and click on the add button, the method will put "isSelected" to true. If we click on delete, the method will put "isSelected" to false. And after, the computed methods will share to my components the filtered list. But it's doesn't work.
I checked the logic step by step in the browser, we pass by the computed methods, the computed methods send a different list each time, but i have no change in my page.
I tried another solution, share all the list to my components and add "v-if" or "v-show" on and but it worked only for the table, not for the drop-down list. beside, I don't like this solution.
With all the posts I read on this forum, I have not found a solution to my problem.
(use key in v-for or use reactivity)
Because I'm in the learning phase, I want to know what is the good way to do it. I want to use the best practice !
Thank you in advance for your helps !

I have ran into similar type of issue. This may be helpful in your case also. When you use arrays or objects in data and if you change the data inside that array/object then Vue cannot recognise it.
Vue can identify if you push some data in the array and re-render the component, but it cannot do that if some nested data changes.
We use lodash.cloneDeep() in our project to overcome this issue.
The mutations should be performed on new copy, can you try something like this:
AddFilter: function (filterId) {
var index = this.filters.findIndex(function (f) { return f.id === filterId });
let newFilters = _.cloneDeep(this.filters);
newFilters[index].isSelected = true;
this.filters = newFilters;
$('#FilterModal').modal('hide');
}
This concept is similar to state in react, that you should not mutate
the state directly(its similar not exactly the same)
You can read about it in Official docs, which suggests that
The object must be plain

Related

VueJS - Interpolate a string within a string

In VueJS, is there a way to interpolate a string within a string, either in the template or in the script? For example, I want the following to display 1 + 1 = 2 instead of 1 + 1 = {{ 1 + 1 }}.
<template>
{{ myVar }}
</template>
<script>
export default {
data() {
"myVar": "1 + 1 = {{ 1 + 1 }}"
}
}
</script>
Edit: to better illustrate why I would need this, here's what my actual data looks like:
section: 0,
sections: [
{
inputs: {
user: {
first_name: {
label: "First Name",
type: "text",
val: ""
},
...
},
...
},
questions: [
...
"Nice to meet you, {{ this.section.inputs.user.first_name.val }}. Are you ...",
...
]
},
...
],
this.section.inputs.user.first_name.val will be defined by the user. While I could rebuild the question properties as computed properties, I would rather keep the existing data structure in tact.
I found the solution I was looking for from https://forum.vuejs.org/t/evaluate-string-as-vuejs-on-vuejs2-x/20392/2, which provides a working example on JsFiddle: https://jsfiddle.net/cpfarher/97tLLq07/3/
<template>
<div id="vue">
<div>
{{parse(string)}}
</div>
</div>
</template>
<script>
new Vue({
el:'#vue',
data:{
greeting:'Hello',
name:'Vue',
string:'{{greeting+1}} {{name}}! {{1 + 1}}'
},
methods:{
evalInContext(string){
try{
return eval('this.'+string)
} catch(error) {
try {
return eval(string)
} catch(errorWithoutThis) {
console.warn('Error en script: ' + string, errorWithoutThis)
return null
}
}
},
parse(string){
return string.replace(/{{.*?}}/g, match => {
var expression = match.slice(2, -2)
return this.evalInContext(expression)
})
}
}
})
</script>

How can I make a reusable "CheckAll" checkbox solution in Vue2

I am trying to create a reusable "Check All" solution for displaying a list of objects retrieved from an API.
I really like the get/set methods of computed properties that I use in this example here, https://codepen.io/anon/pen/aLeLOZ but I find that rewriting the same function over and over again and maintaining a seperate checkbox state list is tedious.
index.html
<div id="app">
<input type="checkbox" v-model="selectAll1"> Check All
<div v-for="person in list1">
<input type="checkbox" v-model="checkbox" :value="person.id">
<span>{{ person.name }}</span>
</div>
<hr/>
<input type="checkbox" v-model="selectAll2"> Check All
<div v-for="person in list2">
<input type="checkbox" v-model="checkbox2" :value="person.id">
<span>{{ person.name }}</span>
</div>
</div>
main.js
new Vue({
el: '#app',
data () {
return {
list1: [
{ id: 1, name: 'Jenna1'},
{ id: 2, name: 'Jenna2'},
{ id: 3, name: 'Jenna3'},
{ id: 4, name: 'Jenna4'}
],
list2: [
{ id: 1, name: 'Mary1'},
{ id: 2, name: 'Mary2'},
{ id: 3, name: 'Mary3'},
{ id: 4, name: 'Mary4'}
],
checkbox: [],
checkbox2: []
}
},
computed: {
selectAll1: {
get: function () {
return this.list1 ? this.checkbox.length === this.list1.length : false
},
set: function (value) {
let selected = []
if (value) {
this.list1.forEach(function (bf) {
selected.push(bf.id)
})
}
this.checkbox = selected
}
},
selectAll2: {
get: function () {
return this.list2 ? this.checkbox2.length === this.list2.length : false
},
set: function (value) {
let selected = []
if (value) {
this.list2.forEach(function (bf) {
selected.push(bf.id)
})
}
this.checkbox2 = selected
}
},
}
});
How can I make a resuable selectAll() function that will work in this example that can be included as often as needed?
Is it possible to make a class that can maintain the check box state for each list and still function as a computed property to make use of the v-model directive?
It's not the same at all, but a method based solution would be
methods: {
selectUs: function(){
if (this.checkbox.length <= this.list1.length) {
this.checkbox = Array.from(Array(this.list1.length + 1).keys())
} else {
this.checkbox = []
}
}
}
with #click="selectUs" instead of v-model="selectAll1"
(you could keep the get part of your computed properties to keep track of whether all are selected, and then use if (selectAll1) { etc } in the method)

LocalStorage in Vue App with multiple inputs

i dont know if this is possible - but i am working on a Vue app with multiple input fields that posts to the same list - and i need this to be stored somehow, so when you refresh the site, the outputs from the input fields are saved.
This means both the taskList and subTaskList array should be saved ( i know i've only worked on taskList ).
The example i posted here saves the data fine, however if you refresh, it will post all the data in all the components, can this be fixed so it will only be in the right components?
const STORAGE_KEY = 'madplan-storage'
Vue.component('list-component', {
data: function() {
return {
newTask: "",
taskList: [],
newSubTask: "",
subTaskList: [],
};
},
created() {
this.taskList = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
},
template:
'<div>' +
'<section class="prefetch">' +
'<input v-if="showInput" class="input typeahead" type="text" placeholder="Tilføj ret til madplanen" v-model="newTask" v-on:keyup.enter="addTask">' +
'</section>' +
'<details open v-for="task in taskList" v-bind:key="task.text" class="sub-list-item">' +
'<summary>{{ task.text }}<i class="fa fa-times" aria-hidden="true" v-on:click="removeTask(task)"></i>' + '</summary>' +
'<input class="subInput" type="text" placeholder="Tilføj til indøbsseddel" v-model="newSubTask" v-on:keyup.enter="addSubTask">' +
'</details>' +
'</div>',
computed: {
showInput: function() {
return !this.taskList.length
},
},
methods: {
//addTasks
//
addTask: function() {
var task = this.newTask.trim();
if (task) {
this.taskList.push({
text: task
});
this.newTask = "";
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.taskList));
}
},
addSubTask: function() {
var task = this.newSubTask.trim();
if (task) {
this.subTaskList.push({
text: task
});
this.newSubTask = "";
this.$emit('addedtask', task);
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.subTaskList));
}
},
//removeTasks
//
removeTask: function(task) {
var index = this.taskList.indexOf(task);
this.taskList.splice(index, 1);
},
},
});
new Vue({
el: "#madplan",
data: {
newTask: "",
taskList: [],
newSubTask: "",
subTaskList: [],
},
methods: {
acknowledgeAddedTask: function(cls, task) {
this.$data.subTaskList.push({
text: task,
class: "list-item " + cls
})
},
acknowledgeRemovedTask: function(task) {
this.$data.subTaskList = this.$data.subTaskList.filter(it => it.text != task.text)
},
removeSubTask: function(task) {
var index = this.subTaskList.indexOf(task);
this.subTaskList.splice(index, 1);
},
}
});
<section id="madplan" class="section-wrapper">
<section class="check-list">
<div id="mandag" class="dayWrapper">
<h1>Day One</h1>
<list-component
class="mandag"
v-on:addedtask='task => acknowledgeAddedTask("mandag", task)'
></list-component>
</div>
<div id="tirsdag" class="dayWrapper">
<h1>Day Two</h1>
<list-component
class="tirsdag"
v-on:addedtask='task => acknowledgeAddedTask("tirsdag", task)'
></list-component>
</div>
<ul id="indkobsseddel">
<h2>Shopping List</h2>
<li v-for="task in subTaskList" v-bind:key="task.text" :class="task.class">{{ task.text }}<i class="fa fa-times" aria-hidden="true" v-on:click="removeSubTask(task)"></i></li>
</ul>
</section>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://unpkg.com/vue/dist/vue.js" charset="utf-8"></script>
To be clear, i will come with an example:
As it is now, if i post "Foo" and "Bar" to the "Day One" component and refresh the page, it will then post "Foo" and "Bar" to both "Day One" and "Day Two".
Essentially, i would like to be able to post for an example "Foo" to "Day One", "Bar" to "Day Two" and from there post "Hello World" to the Shopping List, and it would all be saved in the right places, instead of posting everything everywhere.
BTW: I'm a scrub at backend work.
To persist global state, you may use the plugin vue-persistent-state like this:
import persistentStorage from 'vue-persistent-storage';
const initialState = {
newTask: "",
taskList: [],
newSubTask: "",
subTaskList: [],
};
Vue.use(persistentStorage, initialState);
Now newTask, taskList, newSubTask and subTaskList is available as data in all components and Vue instances. Any changes will be stored in localStorage, and you can use this.taskList etc. as you would in a vanilla Vue app.
Your list component now becomes:
Vue.component('list-component', {
// data removed
// created removed
template:
'<div>' +
'<section class="prefetch">' +
'<input v-if="showInput" class="input typeahead" type="text" placeholder="Tilføj ret til madplanen" v-model="newTask" v-on:keyup.enter="addTask">' +
'</section>' +
'<details open v-for="task in taskList" v-bind:key="task.text" class="sub-list-item">' +
'<summary>{{ task.text }}<i class="fa fa-times" aria-hidden="true" v-on:click="removeTask(task)"></i>' + '</summary>' +
'<input class="subInput" type="text" placeholder="Tilføj til indøbsseddel" v-model="newSubTask" v-on:keyup.enter="addSubTask">' +
'</details>' +
'</div>',
computed: {
showInput: function() {
return !this.taskList.length
},
},
methods: {
//addTasks
//
addTask: function() {
var task = this.newTask.trim();
if (task) {
this.taskList.push({
text: task
});
this.newTask = "";
// localStorage.setItem not needed
}
},
addSubTask: function() {
var task = this.newSubTask.trim();
if (task) {
this.subTaskList.push({
text: task
});
this.newSubTask = "";
// $emit not needed, state is global and shared
// localStorage.setItem not needed
}
},
//removeTasks
//
removeTask: function(task) {
var index = this.taskList.indexOf(task);
this.taskList.splice(index, 1);
},
},
});
If you want to understand how this works, the code is pretty simple. It basically
adds a mixin to make initialState available in all Vue instances, and
watches for changes and stores them.
Disclaimer: I'm the author of vue-persistent-state.

How to delete a dynamically generated form based on the click of the delete button with respect to its ID in vuejs2

I am creating an application using Quasar and VueJS. I am able to generate a dynamic form on click of the add button, but not able to delete any of the newly generated form based on the click of the delete button.Find the code below:
<template>
<div v-for="h in htmlList">
<div v-for="r in h" >
<div v-html="r" v-on:click="useRemoveFromProject(1)" v-bind:id="r.id">
</div>
</div>
</div>
</template>
<script>
/*
* Root component
*/
import Vue from 'vue'
export default {
name: 'q-app',
data () {
return {
flag: 0,
htmlList: [],
select: 'fb',
select1: 'fb1',
multipleSelect: ['goog', 'twtr'],
usersInProject: [],
selectOptions: [
{
label: 'Google',
value: 'goog'
},
{
label: 'Select',
value: 'fb'
},
{
label: 'Twitter',
value: 'twtr'
},
{
label: 'Apple Inc.',
value: 'appl'
},
{
label: 'Oracle',
value: 'ora'
}
],
selectOptions1: [
{
label: 'Integer',
value: 'goog1'
},
{
label: 'Float',
value: 'fb1'
},
{
label: 'String',
value: 'twtr1'
}
]
}
},
methods: {
useRemoveFromProject: function (id) {
console.log('hi....')
Vue.delete(this.htmlList, id)
},
identifyMe: function (event) {
alert('hi - ' + event.target.id)
},
process: function () {
this.flag += 1
let temp = []
temp.push('<div class="card" id="a_' + this.flag + '"> <div class="card-content content-center "> <large id="l4">Expression LHS:</large> <input><br> <large id="l5">Operators:</large> <q-select type="radio" v-model="this.select" :options="this.selectOptions"></q-select><br><large id="l4">Expression RHS:</large> <input><br><large id="l5">Data type:</large> <q-select type="radio" v-model="select1" :options="selectOptions1"></q-select><br></div><button class="cordova-hide circular red " style="margin-bottom:5px; margin-right:30px;" v-on:click="userRemoveFromProject(i)"><i>delete</i></button><input value="click" type="button"> </div>')
let ids = ['a_' + this.flag]
console.log(ids)
this.htmlList.push(temp)
}
}
}
</script>
After looking to your code i noticed that you have some errors:
Call function useRemoveFromProject without the 'r' of 'user'
Call userRemoveFromProject when clicking on the element and not only the delete button
Call userRemoveFromProject(i) with a 'i' variable, but what is 'i' ?
Why using a double v-for? The first level is enough.
I propose to you a working example on a fiddle. Please let me know if it's useful for you (and mark it as resolve if it's the case).
EDIT: for Vue.js 2 https://jsfiddle.net/86216oko/

Vue.js permanently add filter to component

I'm wanted to force the running of a filter on a component everytime it's used. I know I can add a filter to a component in it's markup, but in this instance the filter is to be considered "required", or "core" functionality of the component.
For example, the below component and filter can be used like: <my-component v-model="amount | custom-currency" name="my-field"></my-component>
What I'm ultimately wanting to achieve is the same behavior, but with the markup only looking like: <my-component v-model="amount" name="my-field"></my-component>
The examples are based on the currency filter example outlined here: http://vuejs.org/guide/custom-filter.html#Two-way_Filters
Any help or suggestions greatly appreciated
Component and filter for reference:
var CurrencyComponent = Vue.extend({
props: {
name: {
type: String,
required: true
}
},
filter: 'customCurrency',
template: '<input type="text" name="{{ name }}" >'
});
Vue.filter('customCurrency', {
read: function(val, symbol) {
if (typeof val !== 'undefined') {
return symbol + val.toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
return symbol + '0';
},
write: function(val, oldVal) {
var number = +val.replace(/[^\d.]/g, '');
return isNaN(number) ? 0 : parseFloat(number.toFixed(2))
}
});
EDIT:
In reference to Dewey's answer:
var CurrencyComponent = Vue.extend({
props: {
name: {
type: String,
required: true
}
},
computed: {
'hehe2': function() {
return this.$eval('hehe | custom-currency');
}
},
template: '<input type="text" name="{{ name }}" >'
});
You can use computed properties.
Make 2 variables: one variable as input, and the other as the filtered input. Something like:
HTML:
<div id="app">
<input v-model="asInput" />
<h1>{{ asOutput }}</h1>
</div>
JS:
new Vue({
el: '#app',
data: {
asInput: ''
},
computed: {
'asOutput': function() {
return this.$eval('asInput | yourCustomFilter');
}
}
});
See the working example here: https://jsfiddle.net/7ae9t9wv/
Hope this helps