Computed properties in vue not reflected in select list - vue.js

I have a vue component that is meant to create a select list with all available options. The save method puts the saved value into a vuex store. The available fields are generated using a computed property on the component that calls the vuex getter for the list.
In the component, there's a v-for with a v-if that checks that the select item isn't already being used by another component (by looking at a mapped property on the list item object).
Testing this, everything seems to be working as expected, the vuex store gets the list, it accepts the update, and once a save is called, the destination field is marked as mapped and that mapped property is visible in the vuex debug panel.
However, the other select lists on the page don't get updated to reflect the (now shorter) list of available options.
Once the select item is selected in another instance of the component, I'd expect the other components to drop that select option- but it appears the v-if is not re-evaluated after the initial load of the component?
Sorry, here's the basic component:
<template>
<div class="row">
<div class="col-4">
{{ item.source_id }}
</div>
<div class="col-8">
<select v-model="destination" class="form-control form-control-sm">
<option v-for="dest in destinationFields" v-if="shouldShow(dest)" v-bind:value="dest.id">{{ dest.id }} - {{ dest.label }} ({{ dest.dataType }})</option>
</select>
</div>
</div>
</template>
<script>
export default {
props: ['item'],
data() {
return {
destination: ''
}
},
methods: {
shouldShow: function(dest) {
if (this.hideDestination && (!dest.hasOwnProperty('mapped') || dest.id === this.destination)) {
return true
} else if (!this.hideDestination) {
return true
}
return false
}
},
computed: {
destinationFields: function() {
return this.$store.getters.visibleDestination
},
hideDestination: function() {
return this.$store.getters.hideMappedDestinations // boolean
}
}
}

I think a better approach would be to already filter the data inside of your computed function as follows:
computed: {
destinationFields: function() {
return this.$store.getters.visibleDestination()
.filter(dest => !dest.hasOwnProperty('mapped') || dest.id === this.destination)
},
hideDestination: function() {
return this.$store.getters.hideMappedDestinations // boolean
}
}
You would also have to change your template to:
<select v-model="destination" class="form-control form-control-sm">
<option v-for="dest in destinationFields" v-bind:value="dest.id">{{ dest.id }} - {{ dest.label }} ({{ dest.dataType }})</option>
</select>

Related

VueJS Using #click on <option> elements and #change on <select> element

I have a simple function that manipulates a data between true and false. I use it to make my div hidden and visible like a toggle. I also have a with three element. I found out that I cannot use #click for <option> elements, I need to use #change for my <select>.
But in this way, whenever an is selected, the function is being triggered and my data toggles between true and false. Here is my <select> element;
<select #change="isDisabled">
<option>Please select a security type</option>
<option>No Security</option>
<option>Personal</option>
<option>Enterprise</option>
</select>
IsDisabled function takes a variable and change its values between true and false so my div becomes hidden and visible as follows;
<div v-if="noSecurity">something</div>
But here is the thing, I only want to trigger the function when the user select the "No Security" option. Now it's being triggered whenever I select an option, so it turned out to be some kind of a toggle. But I want to hide the div when I select the "No Security" option and show the div if something different is selected. What should I do?
I've made a CodeSandbox where you could see the result :
https://codesandbox.io/s/magical-meitner-63eno?file=/src/App.vue
But here is the explanation:
<template>
<section>
<select #change="isDisabled">
<option>Please select a security type</option>
<option>No Security</option>
<option>Personal</option>
<option>Enterprise</option>
</select>
<div v-if="noSecurity">You Choose no security, that's dangerous !</div>
</section>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
name: "App",
data() {
return {
noSecurity: false,
};
},
methods: {
isDisabled(e) {
console.log("e", e.target.value);
if (e.target.value === "No Security") {
// do your change
return (this.noSecurity = !this.noSecurity);
}
// to allow reset if another option is selected
if (this.noSecurity) {
return this.noSecurity = false;
}
},
},
};
</script>
Basically when you use the #change handler, your function will receive an event, in this event you can catch the target value with event.target.value.
Doing so, you do a condition if the value is equal to No Security (so the selected item), you change your state, if it's not No Security, you do nothing, or you do something else you would like to do.
Appart from that, I advice you to change your method name isDisabled to a global convention name like handleChange, or onChange.
Pass id values in your option so when you get the select event you're clear that No security or whatver the name you would like to change will be the same.
Because if one day you change No security to another name, you have to update all your conditions in your app. Try to avoid conditions with strings values like this if you can.
<option value="1">No Security</option> // :value="securityType.Id" for example if coming from your database
<option value="2">Personal</option>
<option value="3">Enterprise</option>
then in your function it will be
if (e.target.value === noSecurityId) {
// do your change
this.noSecurity = !this.noSecurity;
}
//...
There's no need for the additional noSecurity variable. Create your select with v-model to track the selected value. Give each option a value attribute.
<select v-model="selected">
<option value="">Please select a security type</option>
<option value="none">No Security</option>
<option value="personal">Personal</option>
<option value="enterprise">Enterprise</option>
</select>
Check that value:
<div v-if="selected === 'none'">something</div>
You can still use the noSecurity check if you prefer by creating a computed:
computed: {
noSecurity() {
return this.selected === 'none';
}
}
Here's a demo showing both:
new Vue({
el: "#app",
data() {
return {
selected: ''
}
},
computed: {
noSecurity() {
return this.selected === 'none';
}
},
methods: {},
created() {}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<select v-model="selected">
<option value="">Please select a security type</option>
<option value="none">No Security</option>
<option value="personal">Personal</option>
<option value="enterprise">Enterprise</option>
</select>
<div v-if="selected === 'none'">something</div>
<div v-if="noSecurity">something</div>
</div>
Is using v-model instead of using a method is option for you? If it is, please try the following:
HTML:
<div id="hello-vue" class="demo">
<select v-model="security">
<option>Please select a security type</option>
<option>No Security</option>
<option>Personal</option>
<option>Enterprise</option>
</select>
<div v-if="security=='No Security'">something</div>
</div>
JS:
const HelloVueApp = {
data() {
return {
security: undefined
}
}
}

Vue JS Using local variable without defining in data() function

I am a beginner to Vue JS. I have to use a variable inside a component whose value changes often.
So when I declare and define it under data() the following warn is coming in Chrome console
Since when there is a change in data() variables automatically Vue framework calls render function.
Is there any way to declare and use a variable other than declaring it in data() method ??
<template>
<ul>
<div v-for="(list,index) in itemlist" :key="index">
<div v-if="!isFirstCharSame(list.label)" >{{ firstChar }} </div>
<li>
<span>{{ list.label }}</span>
</li>
</div>
</ul>
</template>
<script>
export default {
data() {
return {
itemlist: [
{"label":"Alpha"},
{"label":"Beta"},
{"label":"Charlie"},
{"label":"Delta"}],
firstChar:"$"
}
},
methods : {
isFirstCharSame: function(str) {
if(str.startsWith(this.firstChar)) {
return true;
}
this.firstChar = str.charAt(0);
return false;
}
}
}
</script>
Expected output should be like this
Inside Group A It should display all the elements starting with A
Below we will render using a computed property to make sure its sorted alphabetically and then render your first char. Though You should be using grouping imo.
<template>
<ul>
<div v-for="(list, index) in sortedlist" :key="`people_${index}`">
<div v-if="!isFirstCharSame(list.label)" >{{ firstChar }} </div>
<li>
<span>{{ list.label }}</span>
</li>
</div>
</ul>
</template>
<script>
export default {
data() {
return {
itemlist: [
{"label":"Alpha"},
{"label":"Beta"},
{"label":"Charlie"},
{"label":"Delta"},
],
firstChar: '',
};
},
methods: {
isFirstCharSame(char) {
if (str.startsWith(this.firstChar)) {
return true;
}
this.firstChar = str.charAt(0);
return false;
},
},
computed: {
sortedList() {
return this.itemList.sort((a, b) => {
if (a.label > b.label) {
return 1;
}
if (b.label > a.label) {
return -1;
}
return 0;
});
},
},
};
</script>
And yes, You can update your data any time you wish and the component will do a re render to reflect it.
You can declare variables in your component within your methods or inside computed properties, etc., but they won't be reachable from the template or the rest of the code nor they would be reactive.
The only way for them to be reactive and reachable from the higher scope is adding the data property to the component in the following way:
data: function () {
return {
foo: 'bar'
}
},
or
data () {
return {
foo: 'bar'
}
},
Besides this, the reason of your error is that you are mutating the state of your variables inside the render. When this happens, Vue re-renders the template because the values have mutated and calls again to the function and voilà: there you have an infinite loop.
You should probably check the function you are calling and try to replace the changing variables from the data property with local variables that take their data from the actual data variables.

How to search within nested objects

I have done my research trying to figure out how to achieve what I am describing below, however I had no luck.
In my Algolia index, some records have nested objects.
For example, title and subtitle attributes are of the following format:
title:
{
"en": "English title",
"gr": "Greek title"
}
I would like to execute queries only for a specific subset (in our example "en" or "gr") of these attributes, withoute "exposing" any facet in the UI — language selection would ideally be done “automatically” based on a variable (lang) passed to the Vue component with props. I am using Laravel Scout package with default Vue implementation, as described in documentation here.
My InstantSearch implementation is pretty simple, I am not defining anything specific regarding queries and searchable attributes, I am currently using all the default functionality of Algolia.
<template>
<ais-instant-search
:search-client="searchClient"
index-name="posts_index"
>
<div class="search-box">
<ais-search-box placeholder="Search posts..."></ais-search-box>
</div>
<ais-hits>
<template
slot="item"
slot-scope="{ item }"
>
<div class="list-image">
<img :src="'/images/' + item.image" />
</div>
<div class="list-text">
<h2">
{{ item.title }}
</h2>
<h3>
{{ item.subtitle }}
</h3>
</div>
</template>
</ais-hits>
</ais-instant-search>
</template>
<script>
import algoliasearch from 'algoliasearch/lite';
export default {
data() {
return {
searchClient: algoliasearch(
process.env.ALGOLIA_APP_ID,
process.env.ALGOLIA_SEARCH
),
route: route,
};
},
props: ['lang'],
computed: {
computedItem() {
// computed_item = this.item;
}
}
};
</script>
I would like to somehow pass an option to query “title.en” and “subtitle.en” when variable lang is set to “en”. All this, without the user having to select “title.en” or “subtitle.en” in the UI.
Update
Maybe computed properties is the path to go, however I cannot find how to reference search results/hits attributes (eg item.title) within computed property. It is the code I have commented out.
I think, you can use computed property. Just transform current item according to the current language variable.
new Vue({
template: "<div>{{ computedItem.title }}</div>",
data: {
langFromCookie: "en",
item: {
title: {
en: "Hello",
ru: "Привет"
}
}
},
computed: {
computedItem() {
const item = JSON.parse(JSON.stringify(this.item));
for (value in item) {
if (typeof item[value] === "object" && Object.keys(item[value]).includes(this.langFromCookie))
item[value] = item[value][this.langFromCookie];
}
return item;
}
}
}).$mount("#app")
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
If lang variable is available via props, you can check that inside list-text class and return {{title.en}} or {{title.gr}} accordingly by passing a dynamic lang value title[lang] like below
...
<div class="list-text">
<h2>
{{ item.title[lang] }}
</h2>
<h3>
{{ item.subtitle[lang] }}
</h3>
</div>
If you want to make a request according to lang prop when component mounts ,then you can make a request inside mounted() method then query like below
mounted() {
axios.get(`/getSomethingWithLang/:${this.item.title[this.lang]}`)
...
}

Is it possible to detect if a change event was triggered by a click on a Vue select element?

I have a <select>-element that has a data property bound to it using v-model in Vue.
Sometimes I want to change that value dynamically. I also have an event-listener attached to this element which is triggered on the change-event. See code example:
<template>
<div class="mySelector">
<select id="testSelect" v-model="mySelectModel"
#change="onChange($event)">
<template v-for="(item, index) in someList">
<option :class="['btn', 'btn-default', 'removing-button']" :value="index">{{item.name}}</option>
</template>
</select>
</div>
</template>
<script>
export default {
data() {
return {
mySelectModel: null
}
},
props: {
},
methods: {
customChange: function() {
this.mySelectModel = ... // some value we from somewhere else that is set dynamically on some condiftion
},
onChange: function (event) {
if (!event) return;
// DO SOMETHING THAT WE ONLY WANT TO DO ON A REAL CLICK
}
},
}
</script>
The problem I have is that when I change the data value mySelectModel dynamically, like in the customChange-method, the change event is also called, triggering the method onChange. I only want to do stuff in that method if it was really triggered by a real click, not when it was changed dynamically.
I can not find a way to distinguish between those cases when the change-event is triggered by a click or when it is just changed for some other reason. Any suggestions?
See vue-js-selected-doesnt-triggering-change-event-select-option, it appears that select does not trigger #change when v-model is updated by JS (only when the selected value is changed by user).
A directive can add the functionality
Vue.directive('binding-change', {
update: function (el, binding, vnode) {
const model = vnode.data.directives.find(d => d.name === 'model')
if (model) {
binding.value(model.value)
}
}
})
use like
<select id="testSelect"
v-binding-change="onChange"
v-model="mySelectModel"
#change="onChange($event)">
Not sure about the parameter to onChange - I'll give it a test.
Similar to this suggested solution, you can make a settable computed that you v-model in your widget:
The get function simply returns the data item
The set function does whatever you want a change in the widget to do, in addition to setting the data item
Other code can change the data item directly and will not execute the set code of the computed.
new Vue({
el: '#app',
data: {
values: ['one','two','three'],
selectedItem: 'two'
},
computed: {
wrappedSelectedItem: {
get() { return this.selectedItem; },
set(value) {
console.log("Changed in widget");
this.selectedItem = value;
}
}
},
methods: {
changeToThree() {
console.log("Stealth change!");
this.selectedItem = 'three';
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<select v-model="wrappedSelectedItem">
<option v-for="value in values" :value="value">{{value}}</option>
</select>
<button #click="changeToThree">Set to three</button>
</div>

VueJs reactivity with parent component property object

I'm having difficulty to get parent component's property object, with dynamically populated properties to make the values available inside of the same component.
A bit hard to explain, so please have a look at the example below:
Parent Component
<script>
export default {
data() {
return {
fields: {},
}
}
}
</script>
Child Component
<template>
<select
#change="update()"
v-model="field"
>
<option
v-for="option in options"
:value="option.value"
>
{{ option.name }}
</option>
</select>
</template>
<script>
export default {
props: {
initialOptions: {
type: Array,
required: true
}
},
data() {
return {
field: '',
options: this.initialOptions
}
},
mounted() {
if (
(this.field === undefined || this.field === '') &&
this.options.length > 0
) {
this.field = this.options[0].value;
}
this.update();
},
methods: {
update() {
this.$emit('input', this.field);
}
}
}
</script>
DOM
<parent-component inline-template>
<div>
<child-component>
:initial-options="[{..}, {..}]"
v-model="fields.type_id"
></child-component>
</div>
<div :class="{ dn : fields.type_id == 2 }">
// ...
</div>
</parent-component>
Using Vue console I can see that fields object gets all of the child component models with their associated values as they emit input when they are mounted, however for some strange reason the :class="{ dn : fields.type_id == 2 }" does not append the class dn when the selection changes to 2. Dom doesn't seem to reflect the changes that are synced between parent and child components.
Any help on how to make it work?
Here is what I was trying to get at in comments. Vue cannot detect changes to properties that are added dynamically to an object unless you add them using $set. Your fields object does not have a type_id property, but it gets added because you are using v-model="fields.type_id". As such, Vue does not know when it changes.
Here, I have added it and the color of the text changes as you would expect.
console.clear()
Vue.component("child-component", {
template: `
<select
#change="update()"
v-model="field"
>
<option
v-for="option in options"
:value="option.value"
>
{{ option.name }}
</option>
</select>
`,
props: {
initialOptions: {
type: Array,
required: true
}
},
data() {
return {
field: '',
options: this.initialOptions
}
},
mounted() {
if (
(this.field === undefined || this.field === '') &&
this.options.length > 0
) {
this.field = this.options[0].value;
}
this.update();
},
methods: {
update() {
this.$emit('input', this.field);
}
}
})
new Vue({
el: "#app",
data: {
fields: {
type_id: null
}
}
})
.dn {
color: red;
}
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<div id="app">
<div>
<child-component :initial-options="[{name: 'test', value: 1}, {name: 'test2', value: 2}]" v-model="fields.type_id"></child-component>
</div>
<div :class="{ dn : fields.type_id == 2 }">
Stuff
</div>
</div>
It looks like you are trying to make a re-usable component.
I would ask myself what the value of a re-usable component is when the parent component has to handle more than half of the effort. The component might be better named...
<DifficultToUseSelect/>.
Essentially, you are creating a component that provides, all by itself, all of the following HTML...
<select></select>
Everything else is managed by the parent component.
It would probably be more useful to do any of the following...
Encapsulate often needed options in a specific select component, as in
StateAbbrevsSelect v-model="state"
Pass the name of a data model to a select component. The component would then load and manage its own data via the model.
Pass the URL of a web service to the component, which it then calls to load its options.
Again, the main point I am trying to convey here is that making a re-usable component where more than half of the effort is handled by the parent component is really not very re-usable.