Hide-selected in multiple components sharing the same items prop - javascript

So I basically have multiple v-select that all share the same :items prop. Now if an item is selected from one of the v-select, I want to hide it from all v-select so we can't choose it again. Inverse logic when the item is free again.
Is that possible?
<div v-for="(filter, index) in group.filterMeta" :key="index">
<v-select
v-model="filter.tag"
:items="availableTags"
:label="i18n('select.tag.label')"
>
</v-select>
</div>
#Component
export default class ManageGroupDialog extends Vue {
...
/** Available tags */
public availableTags = ['resource', 'resource_type', 'host', 'technology']
...
public group: Group = {
...
filterMeta: [
{
tag: '',
value: '',
operator: ''
}
]
...
}
}

If you're working with multiple components looks like you need to start using Vuex. This way you can have a global state, and use the same items array in all your v-selects over your components.

Related

Vue 3 - V-Model Confusion

I am teaching myself vue 3. I have read article after article on v-model and each time I think I understand how it works I get confused again.
My goal: I built a custom dropdown component. I need the ability to control the value of this dropdown from the parent. When the dropdown changes I want to let the parent know the new value and the index.
Child component.vue
<div>
<select
:value="modelValue"
#input="$emit('update:modelValue', $event.target.value)"
>
<option v-for="option in options" :key="option">
{{ option }}
</option>
</select>
</div>
</template>
<script>
export default {
props: ["options", "modelValue"],
emits: ["update:modelValue"],
methods: {
selected() {
//??????
//want to emit this to the parent
let selectedIndex = this.$event.target.selectedIndex + 1
//this.$emit(value, selectedIndex)
},
},
};
</script>
parent.vue
<template>
<my-drop-down :options="options" v-model="selectedOption" />
</template>
<script>
import myDropDown from "./components/base_dropdown.vue";
export default {
name: "App",
data: () => ({
selectedOption: "2 Line",
selectedIndex: 0,
options: ["1 Line", "2 Line", "3 Line"],
}),
components: {
myDropDown,
},
methods: {
//How can I call this when the select value changes??
onSelectChange(selected, index) {
console.log(`Parent L3rd Change, name: ${selected}, index: ${index} `);
},
},
};
</script>
The two way binding is working correctly. I can control the value of the dropdown from either the child or the parent. But how do I call the onSelectChange method in my child component
Also, and this is may be a dumb question but...
v-model="selectedOption" is the same as writing :value="modelValue" #input="$emit('update:modelValue', $event.target.value)"
so why is the parent written like this <my-drop-down :v-model="selectedOption" />
and the child written like this <select :value="modelValue" #input="$emit('update:modelValue', $event.target.value)">
and not simply
<select :v-model="selectedOption />
If you want to call a method inside your parent component when the "select value changes", It is better to call it inside a Vue watch like the codes below:
Parent component:
<template>
<my-drop-down :options="options" v-model="selectedOption" />
</template>
<script>
import myDropDown from "../components/baseDropdown.vue";
export default {
name: "parentModel",
data: () => ({
selectedOption: "2 Line",
// selectedIndex: 0,
options: ["1 Line", "2 Line", "3 Line"],
}),
components: {
myDropDown,
},
computed: {
/* It is better to use computed property for "selectedIndex", because it is related to "selectedOption" and changes accordingly. */
selectedIndex: function () {
return this.options.indexOf(this.selectedOption)
}
},
watch: {
selectedOption(newSelect, oldSelect) {
this.onSelectChange(this.selectedOption, this.selectedIndex)
}
},
methods: {
//How can I call this when the select value changes??
onSelectChange(selected, index) {
console.log(`Parent L3rd Change, name: ${selected}, index: ${index} `);
},
},
}
</script>
<style scoped>
</style>
Child component:
<template>
<div>
<select
:value="modelValue"
#change="$emit('update:modelValue', $event.target.value)"
>
<!-- You can use v-model also here. But it only changes the value of "modelValue" and does not emit anything to parent component. -->
<!-- <select v-model="modelValue">-->
<option v-for="option in options" :key="option">
{{ option }}
</option>
</select>
</div>
</template>
<script>
export default {
name: "baseDropdown",
props: ["options", "modelValue"],
emits: ["update:modelValue"],
/* --------------------------------- */
/* You don't need this method, because "$emit('update:modelValue', $event.target.value)" that is used in "select" tag itself is enough to emit data to the parent component. */
/* --------------------------------- */
// methods: {
// selected() {
//
// //??????
// //want to emit this to the parent
// let selectedIndex = this.$event.target.selectedIndex + 1
// //this.$emit(value, selectedIndex)
// },
// },
}
</script>
<style scoped>
</style>
And about your second part of the question:
v-model="selectedOption" is the same as writing :value="modelValue" #input="$emit('update:modelValue', $event.target.value)"
In my opinion it is not a true statement for two reasons:
Reason one: according to Vue docs :
v-model="selectedOption" is the same as writing :value="selectedOption"
#input="event => selectedOption = event.target.value"
you can't see any $emit in the above statement. But in your case you want to emit data to the parent component.
Reason two: again according to Vue docs it is better to use change as an event for <select> tag.
You look to be needing a watcher in your parent component, one that watches for changes to the selectedOption property, and then uses the new value to get the index from the options array and adds one to it, and uses the new value to set the selectedIndex property.
Per the Vuejs API section on Watchers:
Computed properties allow us to declaratively compute derived values. However, there are cases where we need to perform "side effects" in reaction to state changes - for example, mutating the DOM, or changing another piece of state based on the result of an async operation.
With Options API, we can use the watch option to trigger a function whenever a reactive property changes.
So, for your code, it might look something like:
watch: {
selectedOption(newValue, oldValue) {
console.log('In watcher. New value: ' + newValue)
// get the index of the String in the array and use it to set
// the selectedIndex:
this.selectedIndex = this.options.findIndex(i => i === newValue) + 1;
console.log('selectedIndex: ' + this.selectedIndex)
}
},
As for your question's "second part",
why is the parent written like this <my-drop-down :v-model="selectedOption" />
and the child written like this <select :value="modelValue" #input="$emit('update:modelValue', $event.target.value)">
and not simply <select :v-model="selectedOption />
It is probably best to ask that as a separate question, in order to maintain question specificity as required by the site, but as I see it, selectedOption is not acting as a model for the select tag, and in fact selectedOption isn't even a property of the child component, nor should it be.

How to reference a child component from the parent in Vue

I'm trying to figure out the Vue-way of referencing children from the parent handler.
Parent
<div>
<MyDropDown ref="dd0" #dd-global-click="ddClicked"></MyDropDown>
<MyDropDown ref="dd1" #dd-global-click="ddClicked"></MyDropDown>
<MyDropDown ref="dd2" #dd-global-click="ddClicked"></MyDropDown>
</div>
export default {
methods: {
ddClicked: function(id) {
console.log("I need to have MyDropDown id here")
}
}
}
Child
<template>
<h1>dropdown</h1>
<Button #click="bclick"></Button>
</template>
export default {
methods: {
bclick: function() {
this.$emit('dd-global-click')
}
}
}
In the parent component I need to see which dropdown was clicked.
What I've tried so far
I tried to set "ref" attribute in the parent. But I can't refer to this prop within the child component. Is there a way to do it? There is nothing like this.ref or this.$ref property.
I tried to use $event.targetElement in the parent, but it looks like I'm mixing Real DOM and Vue Components together. $event.targetElement is a DOM like . So in the parent I have to go over the tree until I find my dropdown. It is ugly I guess.
I set an additional :id property for the dropdown making it the copy of the 'ref' property. In the blick and I called this.$emit('dd-global-click', this.id). Later in the parent I check this.$refs[id]. I kind of works, but I'm not really content with it, because I have to mirror attributes.
Using the _uid property didn't work out either. On top of that, I think, that since it starts with an underscore it is not a recommended way to go.
It seems like a very basic task, so there must be a simplier way to achieve this.
If this custom dropdown element is the top level one (the root element) in the component, you could access the native DOM attributes (like id, class, etc) via this.$el, once it's mounted.
Vue.component('MyDropdown', {
template: '#my-dropdown',
props: {
items: Array
},
methods: {
changed() {
this.$emit('dd-global-click', this.$el.id);
}
}
})
new Vue({
el: '#app',
data: () => ({
items: [
{
id: 'dropdown-1',
options: ['abc', 'def', 'ghi']
},
{
id: 'dropdown-2',
options: ['jkl', 'lmn', 'opq']
},
{
id: 'dropdown-3',
options: ['rst', 'uvw', 'xyz']
}
]
}),
methods: {
ddClicked(id) {
console.log(`Clicked ID: ${id}`);
}
}
})
Vue.config.devtools = false;
Vue.config.productionTip = false;
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.11"></script>
<div id="app">
<my-dropdown
v-for="item of items" :key="item.id"
:id="item.id"
:items="item.options"
#dd-global-click="ddClicked">
</my-dropdown>
</div>
<script id="my-dropdown" type="text/x-template">
<select #input="changed">
<option v-for="item of items" :key="item" :value="item">
{{item}}
</option>
</select>
</script>

Element Ui component is not rerendering on vue component prop change

I have a parent component and multiple child components, which use the same prop. This prop is an array of keys for a dropdown menu in element.js.
When the children render the first time, they contain no data. However, once the keys from arrive using vuefire the children get the dropdown menu items. However, the element dropdown menu is not rerendered as it should have been.
However using the vue dev tools, I can see that the dropdown menu entries have been passed down as a key. When vue does a hot reload, because of a file change, the keys will load.
Once the entries are loaded, I can select the entry and everything works as expected.
I also had the same results using the vuetify dropdown and the HTML dropdown. Both have the same issue.
parent
<template>
<div class="setup">
<h1>Setup</h1>
<div class="selectIngredients" v-for="number in 6">
<setupSelection :bottle="number" :ingredients="options" />
</div>
</div>
</template>
<script>
import {db} from "#/firebaseConfig"
import setupSelection from '#/components/setupSelection';
export default {
components: {
setupSelection,
},
firestore: {
options: db.collection('ingredients'),
},
};
</script>
child
<template>
<div class="ingredientSelector">
<h3>Select for Pump <span>{{bottle}}</span></h3>
<el-select v-model="selected" clearable placeholder="Select" >
<el-option
v-for="ingredient in ingredients"
v-bind:key="ingredient.text"
v-bind:label="ingredient.text"
v-bind:value="ingredient">
</el-option>
</el-select>
<!-- <v-select
v-model="selected"
:items="ingredients"
label="Select a favorite activity or create a new one"
></v-select> -->
<!-- <select v-model="selected" v-for="ingredient in ingredients">
<option :value="ingredient.value">{{ingredient.text}}</option>
</select> -->
</div>
</template>
<script>
import {db} from "#/firebaseConfig";
export default {
props: {
ingredients: { required: true },
bottle: { type: Number, required: true },
},
data() {
return {
selected: ''
}
},
},
};
</script>
I expected the dropdown menu to update once the client received them.
Thank you!
I haven't used Vuefire myself but I read the following in the documentation:
Make sure to create any property added to firestore in data as well
https://github.com/vuejs/vuefire/tree/master/packages/vuefire#firestore-option
Similar advice is given here:
https://vuefire.vuejs.org/vuefire/binding-subscriptions.html#declarative-binding
In your example you don't have options in the parent's data. This would, presumably, leave it non-reactive, leading to the symptoms you describe.
Use a data property for your items, and set them after the options are loaded.
data() {
return {
options: []
}
},
created() {
db.collection('ingredients').then(data=> this.options = data}
}
The promise returned from db.collection('ingredients') is not reactive.
Even better approach would be to set options: null, and show a loading indicator until it is an array.

Vue JS - Problem with computed property not updating

I am quite new with VueJS and I have been having trouble lately with some computed properties which do not update as I would like. I've done quite some research on Stack Overflow, Vue documentation and other ressources but i haven't found any solution yet.
The "app" is basic. I've got a parent component (Laundry) which has 3 child components (LaundryMachine). The idea is to have for each machine a button which displays its availability and updates the latter when clicked on.
In order to store the availability of all machines, I have a data in the parent component (availabilities) which is an array of booleans. Each element corresponds to a machine's availability.
When I click on the button, I know the array availibities updates correctly thanks to the console.log. However, for each machine, the computed property "available" does not update is I would want it to and I have no clue why.
Here is the code
Parent component:
<div id="machines">
<laundry-machine
name="AA"
v-bind:machineNum="0"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
<laundry-machine
name="BB"
v-bind:machineNum="1"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
<laundry-machine
name="CC"
v-bind:machineNum="2"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
</div>
</div>
</template>
<script>
import LaundryMachine from './LaundryMachine.vue';
export default {
name: 'Laundry',
components: {
'laundry-machine': LaundryMachine
},
data: function() {
return {
availabilities: [true, true, true]
};
},
methods: {
editAvailabilities(index) {
this.availabilities[index] = !this.availabilities[index];
console.log(this.availabilities);
}
}
};
</script>
Child component:
<template>
<div class="about">
<h2>{{ name }}</h2>
<img src="../assets/washing_machine.png" /><br />
<v-btn color="primary" v-on:click="changeAvailability">
{{ this.availability }}</v-btn>
</div>
</template>
<script>
export default {
name: 'LaundryMachine',
props: {
name: String,
machineNum: Number,
availableArray: Array
},
methods: {
changeAvailability: function(event) {
this.$emit('change-avlb', this.machineNum);
console.log(this.availableArray);
console.log('available' + this.available);
}
},
computed: {
available: function() {
return this.availableArray[this.machineNum];
},
availability: function() {
if (this.available) {
return 'disponible';
} else {
return 'indisponible';
}
}
}
};
</script>
Anyway, thanks in advance !
Your problem comes not from the computed properties in the children, rather from the editAvailabilities method in the parent.
The problem is this line in particular:
this.availabilities[index] = !this.availabilities[index];
As you can read here, Vue has problems tracking changes when you modify an array by index.
Instead, you should do:
this.$set(this.availabilities, index, !this.availabilities[index]);
To switch the value at that index and let Vue track that change.

Vue.js checkbox component multiple instances

I have a list of filters using checkboxes. I'm trying to make each checkbox it's own components. So I loop through my list of filters adding a checkbox component for each filter. The Vue.js documentation says that if I have multiple checkboxes that use the same model that array will get updated with the value of the checkboxes. I see that working if the group of checkboxes is part of the parent component. But if I make the checkbox a component and add each checkbox component in a loop then the model doesn't update as expected.
How can I have a checkbox component that updates an array on the parent? I know I can do this with emitting an event for a method on the component that updates the array but the Vue documentation makes it seems like the framework does this for you.
Here is a code sample I've been playing around with https://www.webpackbin.com/bins/-KwGZ5eSofU5IojAbqU3
Here is a working version.
<template>
<div class="filter-wrapper">
<input type="checkbox" v-model="internalValue" :value="value">
<label>{{label}}</label>
</div>
</template>
<script>
export default {
props: ['checked','value', 'label'],
model: {
prop: "checked"
},
computed:{
internalValue: {
get(){return this.checked},
set(v){this.$emit("input", v) }
}
}
}
</script>
Updated bin.
The answer given by #Bert is right. I just want to complete the picture with the list of components and how thay are integrated. As this is a useful pattern.
Also including Select All functionality
ListItem.vue
<template>
<div class="item">
<input type="checkbox" v-model="internalChecked" :value="item.id" />
... other stuff
</div>
</template>
<script>
export default {
// Through this we get the initial state (or if the parent changes the state)
props: ['value'],
computed:{
internalChecked: {
get() { return this.value; },
// We let the parent know if it is checked or not, by sending the ID
set(selectedId) { this.$emit("input", selectedId) }
}
}
}
</script>
List.vue
<template>
<div class="list">
<label><input type="checkbox" v-model="checkedAll" /> All</label>
<list-item
v-for="item in items"
v-bind:key="item.id"
v-bind:item="item"
v-model="checked"
</list-item>
... other stuff
</div>
</template>
<script>
import ListItem from './ListItem';
export default {
data: function() {
return: {
// The list of items we need to do operation on
items: [],
// The list of IDs of checked items
areChecked: []
}
},
computed: {
// Boolean for checked all items functionality
checkedAll: {
get: function() {
return this.items.length === this.areChecked.length;
},
set: function(value) {
if (value) {
// We've checked the All checkbox
// Starting with an empty list
this.areChecked = [];
// Adding all the items IDs
this.invoices.forEach(item => { this.areChecked.push(item.id); });
} else {
// We've unchecked the All checkbox
this.areChecked = [];
}
}
}
},
components: {
ListItem
}
}
</script>
Once boxes get checked we have in checked the list of IDS [1, 5] which we can use to do operation on the items with those IDs

Categories

Resources