Vue 3 custom checkbox components bound to array of selected values - javascript

I've been trying to create a simple component with a styled checkbox and a corresponding label. The values (strings) of all selected checkboxes should be stored in an array. This works well with plain html checkboxes:
<template>
<div>
<div class="mt-6">
<div>
<input type="checkbox" value="EVO" v-model="status" /> <label for="EVO">EVO</label>
</div>
<div>
<input type="checkbox" value="Solist" v-model="status" /> <label for="Solist">Solist</label>
</div>
<div>
<input type="checkbox" value="SPL" v-model="status" /> <label for="SPL">SPL</label>
</div>
</div>
<div class="mt-3">{{status}}</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
let status = ref([]);
</script>
It results in the following, desired situation:
Now if I replace those checkboxes with my custom checkbox component, I can't get it to work. If I check a box, it's emitted value seems to replace the status array instead of it being added to or removed from it, resulting in the following:
So all checkboxes are checked by default for some reason, when I click on one of them they all get unchecked and the status value goes to false and clicking any of the checkboxes again will check them all and make status true.
Now I get that returning whether the box is checked or not in the emit returns a true or false value, but I don't get how Vue does this with native checkboxes and how to implement this behaviour with my component.
Here's the code of my checkbox component:
<template>
<div class="mt-1 relative">
<input
type="checkbox"
:id="id ?? null"
:name="name"
:value="value"
:checked="modelValue ?? false"
class="bg-gray-200 text-gold-500 rounded border-0 w-5 h-5 mr-2 focus:ring-2 focus:ring-gold-500"
#input="updateValue"
/>
{{ label }}
</div>
</template>
<script setup>
const props = defineProps({
id: String,
label: String,
name: String,
value: String,
errors: Object,
modelValue: Boolean,
})
const emit = defineEmits(['update:modelValue'])
const updateValue = function(event) {
emit('update:modelValue', event.target.checked)
}
</script>
And the parent component only uses a different template:
<template>
<div>
<div class="mt-6">
<Checkbox v-model="status" value="EVO" label="EVO" name="status" />
<Checkbox v-model="status" value="Solist" label="Solist" name="status" />
<Checkbox v-model="status" value="SPL" label="SPL" name="status" />
</div>
<div class="mt-3">{{status}}</div>
</div>
</template>
I've tried to look at this answer from StevenSiebert, but it uses an object and I want to replicate the original Vue behaviour with native checkboxes.
I've also referred the official Vue docs on v-model, but can't see why this would work different with native checkboxes than with components.

Your v-model is the same for every checkbox, maybe like following snippet:
const { ref } = Vue
const app = Vue.createApp({
setup() {
const status = ref([{label: 'EVO', status: false}, {label: 'Solist', status: false}, {label: 'SPL', status: false}])
return {
status
}
},
})
app.component('Checkbox', {
template: `
<div class="mt-1 relative">
<input
type="checkbox"
:id="id ?? null"
:name="name"
:value="value"
:checked="modelValue ?? false"
class="bg-gray-200 text-gold-500 rounded border-0 w-5 h-5 mr-2 focus:ring-2 focus:ring-gold-500"
#input="updateValue"
/>
{{ label }}
</div>
`,
props:{
id: String,
label: String,
name: String,
value: String,
errors: Object,
modelValue: Boolean,
},
setup(props, {emit}) {
const updateValue = function(event) {
emit('update:modelValue', event.target.checked)
}
return {
updateValue
}
}
})
app.mount('#demo')
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.18/tailwind.min.css" integrity="sha512-JfKMGsgDXi8aKUrNctVLIZO1k1iMC80jsnMBLHIJk8104g/8WTaoYFNXWxFGV859NY6CMshjktRFklrcWJmt3g==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<div>
<div class="mt-6" v-for="box in status">
<Checkbox v-model="box.status" :value="box.label" :label="box.label" name="status"></Checkbox>
</div>
<div class="mt-3">{{status}}</div>
</div>
</div>

I can pass an array as the v-model to the Checkbox component and mark is as checked if the value is within that array. When the checkbox gets toggles, I add or remove the value to/from the array depending if it's already in there.
Parent component:
<template>
<div>
<div class="mt-6">
<Checkbox v-for="box in ['EVO', 'Solist', 'SPL']" v-model="status" :value="box" :label="box" name="status" />
</div>
<div class="mt-3">{{status}}</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
let status = ref([]);
</script>
Checkbox component:
<template>
<div class="mt-1 relative">
<input
type="checkbox"
:id="id ?? null"
:name="name"
:value="value"
:checked="modelValue.includes(value)"
class="bg-gray-200 text-gold-500 rounded border-0 w-5 h-5 mr-2 focus:ring-2 focus:ring-gold-500"
#input="updateValue"
/>
{{ label }}
</div>
</template>
<script setup>
const props = defineProps({
id: String,
label: String,
name: String,
value: String,
errors: Object,
modelValue: Boolean,
})
const emit = defineEmits(['update:modelValue'])
const updateValue = function(event) {
const arr = props.modelValue;
if(arr.includes(props.value)) { // Remove if present
arr.splice(arr.indexOf(props.value), 1)
}
else { // Add if not present
arr.push(props.value)
}
emit('update:modelValue', arr)
}
</script>
Or to accomodate for booleans as well (like native checkboxes):
<template>
<!-- ... -->
<input
type="checkbox"
:value="value"
:checked="isChecked"
#input="updateValue"
<!-- ... -->
/>
<!-- ... -->
</template>
<script setup>
// ...
const isChecked = computed(() => {
if(props.modelValue instanceof Array) {
return props.modelValue.includes(props.value)
}
return props.modelValue;
})
const updateValue = function(event) {
let model = props.modelValue;
if(model instanceof Array) {
if(isChecked()) {
model.splice(model.indexOf(props.value), 1)
}
else {
model.push(props.value)
}
}
else {
model = !model
}
emit('update:modelValue', model)
}

Related

Using t translation on props in Vue with vue i18n

I would like to translate the titles that are passed to my component via props. However I assume because my strings are being passed via props they are not being translated like the rest of my code. Below you will find my current 2 components that I am working with:
Parent Component:
`<setting-section
:title="$t('Random Text 1')"
:description="$t('Random Text 2')"
>`
In the Child:
`<template>
<div class="flex grid w-full">
<div class="divider mr-4 mt-5" />
<div class="header col-2">
<div class="title text-primary">{{ title }}</div>
<div class="description text-xs">{{ description }}</div>
</div>
<div class="flex col-10" v-if="!isLoading">
<slot />
</div>
<div class="flex col-10" v-else>
<Skeleton height="5rem" />
</div>
</div>
</template>
<script>
export default {
name: 'Menu',
props: {
title: {
type: String,
default: '',
},
description: {
type: String,
default: '',
},
},
};
</script>`
How ever if I do add the below variation it obviously wont work.
`<template>
<div class="flex grid w-full">
<div class="header col-2">
<div class="title text-primary">{{ $t('title')}} </div>
<div class="description text-xs">{{ description }}</div>
</div>
</div>
</template>`
Your both the solutions should work as we always configured VueI18n at global level. Hence, translation literals always accessible from any nested component.
Live Demo as per both the use cases :
Vue.component('child-one', {
props: ['childmsg'],
template: '<p>{{ childmsg }}</p>'
});
Vue.component('child-two', {
template: `<p>{{ $t('message') }}</p>`
});
Vue.use(VueI18n);
const i18n = new VueI18n({
locale: 'ja',
fallbackLocale: 'en',
messages: {
"ja": {
"message": "こんにちは、世界"
},
"en": {
"message": "Hello World"
}
}
});
new Vue({
el: "#app",
i18n,
data() {
return {
locale: "ja"
}
},
watch: {
locale(newLocale) {
this.$i18n.locale = newLocale;
}
}
});
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.8/dist/vue.min.js"></script>
<script src="https://unpkg.com/vue-i18n#8.8.2/dist/vue-i18n.min.js"></script>
<main id="app">
<div>
<label><input type="radio" value="ja" v-model="locale" />Japanese</label>
<label><input type="radio" value="en" v-model="locale" />English</label>
</div>
<h3>Passing translated string from parent</h3>
<child-one :childmsg="$t('message')"></child-one>
<h3>Transaltions happens in child component itself</h3>
<child-two></child-two>
</main>

How do I get v-model data from a child component Vue 2?

I made a custom input component and want to get data from it in the parent component. At first I used the component as it was written in the guide:
Input.vue
<input
:value="value"
#input="$emit('input', $event.target.value)"
class="w-full border-[1px] bg-transparent p-4 lg:text-sm placeholder:text-[#97999B]"
:placeholder="placeholder"
:class="statusStyles"
/>
MyComponent.vue
<Input
placeholder="Phone number"
type="text"
v-model="phone"
/>
Everything worked, but I broke this code into components and another wrapper appeared, it looks like this:
Form.vue
<OrderFormInfo
v-if="step === 'info'"
:name="name"
:apart="apart"
:city="city"
:phone="phone"
:postal="postal"
:region="region"
:address="address"
#next-step="handleNext"
/>
OrderInfo.vue
<Input
placeholder="phone number"
type="text"
v-model="phone"
/>
<Input
placeholder="recipient name"
type="text"
v-model="name"
/>
Input.vue
<template>
<div class="w-full space-y-[10px]">
<input
:value="value"
#input="$emit('input', $event.target.value)"
class="w-full border-[1px] bg-transparent p-4 lg:text-sm placeholder:text-[#97999B]"
:placeholder="placeholder"
:class="statusStyles"
/>
<p v-if="errorStatus" class="text-red-500">{{ errors[0] }}</p>
</div>
</template>
<script>
export default {
props: {
errors: Array,
sucess: Boolean,
value: String,
errorStatus: Boolean,
placeholder: String,
},
computed: {
statusStyles() {
if (this.errorStatus) {
return "border-red-500 text-red-500";
}
if (!this.errorStatus && this.value.length > 3) {
return "bg-white border-black text-black";
}
return "text-black border-[#97999B]";
},
},
};
</script>
How do I get the data from OrderInfo.vue in Form.vue? I've tried passing data through props, but the vue gives an error that you can't do that. I don't understand how to use v-model with nested components
You can watch the props by using a watcher function in your parent (Form.vue) component inside the mounted hook.
You have to just attach a ref to your top most child component. For ex :
<order-form-info :name="name" :phone="phone" ref="orderFormComponent"></order-form-info>
Live Demo :
Vue.component('orderFormInfo', {
props: ['name', 'phone'],
template: `<div>
<input-field
placeholder="phone number"
type="text"
v-model="phone"/>
<input-field
placeholder="recipient name"
type="text"
v-model="name"/>
</div>`
});
Vue.component('inputField', {
props: ['value', 'placeholder'],
template: `<input
:value="value"
#input="$emit('input', $event.target.value)"
:placeholder="placeholder"
/>`
});
var app = new Vue({
el: '#form',
data: {
name: 'Alpha',
phone: '1111111111'
},
mounted() {
this.$watch(
"$refs.orderFormComponent.phone", (newVal, oldVal) => {
console.log(newVal, oldVal)
}
);
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="form">
<order-form-info :name="name" :phone="phone" ref="orderFormComponent"></order-form-info>
</div>
I was able to solve it this way, I found an article on the Internet. Here are the changes:
OrderFormInfo.vue
<Input
placeholder="Phone number"
type="text"
:error-status="false"
:errors="[]"
:value="phone"
#input="$emit('input-phone', $event)"
/>
OrderForm.vue
<OrderFormInfo.
v-if="step === 'info'"
:name="name"
#input-name="name = $event"
:phone="phone"
#input-phone="phone = $event"
#next-step="handleNext"
/>
There are a few ways to do this. For example-
Using Event Bus (to emit event when data updates.)
Using Vuex (to update in state and access it anywhere)
The above two solutions required some installation. The easiest way would be to use this.$root.$emit to emit an event to any component without using any global variable or bus or props.
Here is a working demo. Try changing the phone and name values from the input field (OrderFormInfo.vue), and you should see the changes in Form.vue (parent component).
Vue.component('orderFormInfo', {
props: ['prop_name', 'prop_phone'],
data() {
return {
name: this.prop_name,
phone: this.prop_phone,
}
},
watch: {
name(newVal, oldVal) {
this.$root.$emit('onNameUpdate', newVal);
},
phone(newVal, oldVal) {
this.$root.$emit('onPhoneUpdate', newVal);
}
},
template: `<div>
<b>OrderFormInfo.vue</b><br>
<input-field
placeholder="phone number"
type="text"
v-model="phone"/>
<input-field
placeholder="recipient name"
type="text"
v-model="name"/>
</div>`
});
Vue.component('inputField', {
props: ['value', 'placeholder'],
template: `<input
:value="value"
#input="$emit('input', $event.target.value)"
:placeholder="placeholder"
/>`
});
var app = new Vue({
el: '#app',
data() {
return {
name: 'My Name',
phone: '9090909090'
}
},
created() {
this.$root.$on('onNameUpdate', (name) => {
this.name = name;
});
this.$root.$on('onPhoneUpdate', (phone) => {
this.phone = phone;
})
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<b>Form.vue</b><br>
Name - {{ name }} <br>
Phone - {{ phone }} <br><br>
<order-form-info :prop_name="name" :prop_phone="phone"></order-form-info>
</div>
Important-
When you pass props from Form.vue to OrderFormInfo.vue, do not try to mutate that prop directly. First, copy the props to the data variable and mutate those. Read the reason here.

How to pass data between components in react?

I have been trying to display the form data submitted but the map is throwing an error.
I have two components
NameForm.js
Here is the form input, handlechange and handlesubmit methods are done
function Nameform() {
const [form, setForm] = useState({firstname: "", lastname: ""});
const handleChange = (e) => {
setForm({
...form,
[e.target.id]: (e.target.value),
});
};
const handleSubmit = (e) => {
e.preventDefault();
console.log("hello from handle submit", form );
}
return (
<section>
<div className='card pa-30'>
<form onSubmit={ handleSubmit }>
<div className='layout-column mb-15'>
<label htmlFor='name' className='mb-3'>First Name</label>
<input
type='text'
id='firstname'
placeholder='Enter Your First Name'
data-testid='nameInput'
value={form.firstname}
onChange={handleChange}
/>
</div>
<div className='layout-column mb-15'>
<label htmlFor='name' className='mb-3'>First Name</label>
<input
type='text'
id='firstname'
placeholder='Enter Your First Name'
data-testid='nameInput'
value={form.firstname}
onChange={handleChange}
/>
</div>
<div className='layout-row justify-content-end'>
<button
type='submit'
className='mx-0'
data-testid='addButton'
>
Add Name
</button>
</div>
</form>
</div>
</section>
)
}
export default Nameform
NameList.js
I want to pass the data in handleSubmit in NameForm.js to NameList.js. But the data is not displayed.
function NameList({form}) {
return (
<section>
{form.map(displayName => {
return (
<ul
className='styled w-100 pl-0'
>
<li
className='flex slide-up-fade-in justify-content-between'
>
<div className='layout-column w-40'>
<h3 className='my-3'>{displayName.firstname}</h3>
<p className='my-0'{displayName.lastname}></p>
</div>
</li>
</ul>
)
})}
</section>
)
}
export default NameList;
App.js
In App.js, I want to display both the form and the data.
import { Nameform, Namelist } from './components'
function App() {
return (
<div>
<div className='layout-row justify-content-center mt-100'>
<div className='w-30 mr-75'>
<Nameform />
</div>
<div className='layout-column w-30'>
<NameList />
</div>
</div>
</div>
)
}
export default App;
Thank you for your help!
Pass the data you want to share between parent and children via props (which stands for properties).
In the parent class, when rendering <NameForm> and <ListForm> add the data like that:
//if you want to share count and name for example:
<NameForm
count={this.state.count}
name={this.state.name}
/>
You can add as many props as you want. Furthermore, you can pass a function and its argument using arrow functions:
<NameForm
aFunction={() => this.myFunction( /* anArgument */ )}
/>
To access props in a child class dynamically wherever you need them:
{this.props.count}
{this.props.name}
{this.props.aFucntion}
You can get rid of this.props using a technique called object destructing:
render(
const {count, name, aFunction} = this.props;
//now you can use {count} and {name} and {aFunction} without this.props
);
There are some bugs in your code, first form is an object not an array, so you can't map it, you need to use form.firstname and form.lastname, Also you set both input ids equal firstname you need to modify it, Also you need to move the form state and handleChange function to the App component.
This is a working code of your example.
https://codesandbox.io/s/upbeat-forest-328bon
You can save the state in the parent component and pass it as props to the child components like so.
Here we make use of an outer state called submittedForm to display only the submitted values. The inner form state is being used for handling the values before submitting.
// App.js
function App() {
const [submittedForm, setSubmittedForm] = useState({
firstname: "",
lastname: "",
});
return (
<div>
<div className="layout-row justify-content-center mt-100">
<div className="w-30 mr-75">
<NameForm setSubmittedForm={setSubmittedForm} />
</div>
<div className="layout-column w-30">
<NameList form={submittedForm} />
</div>
</div>
</div>
);
}
export default App;
// NameForm.js
function NameForm({ setSubmittedForm }) {
const [form, setForm] = useState({
firstname: "",
lastname: "",
});
const handleChange = (e) => {
// setActive(true);
setForm({
...form,
[e.target.id]: e.target.value,
});
};
const handleSubmit = (e) => {
e.preventDefault();
setSubmittedForm(form);
};
return (
<section>
<div className="card pa-30">
<form onSubmit={handleSubmit}>
<div className="layout-column mb-15">
<label htmlFor="name" className="mb-3">
First Name
</label>
<input
type="text"
id="firstname"
placeholder="Enter Your First Name"
data-testid="nameInput"
value={form.firstname}
onChange={handleChange}
/>
</div>
<div className="layout-column mb-15">
<label htmlFor="name" className="mb-3">
Last Name
</label>
<input
type="text"
id="lastname"
placeholder="Enter Your Last Name"
data-testid="nameInput"
value={form.lastname}
onChange={handleChange}
/>
</div>
<div className="layout-row justify-content-end">
<button type="submit" className="mx-0" data-testid="addButton">
Add Name
</button>
</div>
</form>
</div>
</section>
);
}
export default NameForm;
// NameList.js
function NameList({ form }) {
return (
<section>
<ul className="styled w-100 pl-0">
<li className="flex slide-up-fade-in justify-content-between">
<div className="layout-column w-40">
<h3 className="my-3">{form.firstname}</h3>
<p className="my-0">{form.lastname}</p>
</div>
</li>
</ul>
</section>
);
}
export default NameList;

Vue 3/Quasar: Handling opening and closing of modals

I have two modals:
One is a Sign Up modal which takes in information of an existing user. Another modal which allows an existing user to login.
The only way to get to the Login modal is through the Signup modal.
But what I would like to do is, if the use wants to open the Login, I would like to close the Sign up modal first.
Right now that seems impossible with my setup. With the code below (nextTick), it does not work and closes both modals... despite there being a unique v-model for each modal.
Sign up Modal
<template>
<AVModal
:title="$t('signup.create_an_account')"
:button-text="$t('signup.button_text')"
classes="hide-icon q-mt-sm"
modal-style="width: 350px"
v-model="modal"
>
<q-form
#submit="signUp(user)"
class="row column fitq-gutter-md q-gutter-md"
>
<q-input
outlined
v-model="signup_user.username"
:label="$t('signup.name')"
>
<template v-slot:append>
<q-icon name="person" color="grey" />
</template>
</q-input>
<q-input
outlined
v-model="signup_user.email"
type="email"
:label="$t('signup.email')"
>
<template v-slot:append>
<q-icon name="email" color="grey" />
</template>
</q-input>
<q-input
outlined
v-model="signup_user.password"
type="password"
:label="$t('signup.password')"
>
<template v-slot:append>
<q-icon name="lock" color="grey" />
</template>
</q-input>
<q-checkbox
v-model="signup_user.privacy_policy"
color="secondary"
:label="$t('signup.privacy_policy')"
/>
<q-checkbox
v-model="signup_user.newsletter"
color="secondary"
:label="$t('signup.newsletter')"
/>
<q-btn color="primary" class="q-py-sm" type="submit">{{
$t("signup.get_started")
}}</q-btn>
</q-form>
<div class="row q-my-sm q-mt-md fill">
<AVLoginModal #on-open="handleOpen" />
</div>
<!-- <AVSeperator text="or" /> -->
<!-- <AVSocialMediaButtons class="q-mt-md" /> -->
</AVModal>
</template>
<script setup>
import { ref, nextTick } from "vue";
import AVModal from "../atoms/AVModal.vue";
// import AVSeperator from "../atoms/AVSeperator.vue";
// import AVSocialMediaButtons from "../atoms/AVSocialMediaButtons.vue";
import AVLoginModal from "./LoginModal.vue";
import { useAuth } from "../../composables/useAuth";
const modal = ref(false);
const handleOpen = () => {
nextTick(() => (modal.value = false));
};
const { signUp, signup_user } = useAuth();
</script>
Login Modal:
<template>
<AVModal
:title="$t('login.welcome_back')"
:button-text="$t('signup.login_instead')"
classes="hide-icon q-mt-sm fit"
color="blue"
modal-style="width: 350px"
v-model="modal"
#on-open="emit('on-open')"
>
<q-form
#submit="login(login_user)"
class="row column fitq-gutter-md q-gutter-md"
>
<q-input
outlined
v-model="login_user.email"
type="email"
:label="$t('signup.email')"
>
<template v-slot:append>
<q-icon name="email" color="grey" />
</template>
</q-input>
<q-input
outlined
v-model="login_user.password"
type="password"
:label="$t('signup.password')"
>
<template v-slot:append>
<q-icon name="lock" color="grey" />
</template>
</q-input>
<q-btn color="primary" class="q-py-sm" type="submit">{{
$t("login.login")
}}</q-btn>
</q-form>
<!-- <AVSeperator text="or" class="q-my-md" /> -->
<!-- <AVSocialMediaButtons class="q-mt-md" /> -->
</AVModal>
</template>
<script setup>
import { ref, defineEmits } from "vue";
import { useAuth } from "../../composables/useAuth";
import AVModal from "../atoms/AVModal.vue";
// import AVSeperator from "../atoms/AVSeperator.vue";
// import AVSocialMediaButtons from "../atoms/AVSocialMediaButtons.vue";
const modal = ref(false);
const emit = defineEmits(["on-open"]);
const { login_user, login } = useAuth();
</script>
Maybe my design is bad? Is there a more conventional way of creating modals in Quasar?
Here is my reuseable Modal component:
<template>
<q-btn
:color="color"
:align="align"
flat
#click="openModal"
:icon="icon"
:class="[classes]"
:style="{ width }"
>{{ buttonText }}</q-btn
>
<q-dialog v-model="modal" :persistent="persistent">
<q-card :style="modalStyle">
<q-card-section>
<div class="row justify-between">
<div>
<div class="text-h6">{{ title }}</div>
<div class="text-subtitle2">
<slot name="subtitle" />
</div>
</div>
<slot name="top-right" />
</div>
</q-card-section>
<q-card-section class="q-pt-none">
<slot />
</q-card-section>
<q-card-actions align="right" class="bg-white text-teal">
<slot name="actions" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup>
import { computed, defineProps, defineEmits } from "vue";
const props = defineProps({
icon: {
type: String,
default: null,
},
color: {
type: String,
default: "",
},
title: {
type: String,
default: "",
},
buttonText: {
type: String,
default: "Open Modal",
},
modalStyle: {
type: String,
default: "width: 900px; max-width: 80vw",
},
width: {
type: String,
default: "",
},
align: {
type: String,
default: "around",
},
classes: {
type: String,
default: "",
},
persistent: {
type: Boolean,
default: false,
},
modelValue: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["on-open", "update:modelValue"]);
const modal = computed({
get: () => props.modelValue,
set: (val) => emit("update:modelValue", val),
});
const openModal = () => {
modal.value = true;
emit("on-open");
};
</script>
<style lang="scss">
.hide-icon {
i.q-icon {
display: none;
}
}
</style>
You can use the Dialog plugin with a custom component. By doing that, you can not just correctly open nested dialogs, but also simplify the prop/event management. Instead of a re-usable modal(AVModal.vue), you would create a re-usable card/container. Then, use that card in your Dialog plugin custom components.
Here is what closing the signup modal would look like in both approaches:
// Signup Modal
// Current way to close the modal (which doesn't correctly work)
modal.value = false; // v-model="modal"
// Custom dialog component way
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
onDialogCancel();
Instead of a button and a q-dialog inside the component, you would have the custom dialog component in a separate file, then when the button is clicked, invoke the Dialog plugin like this:
Dialog.create({
component: LoginModal,
})

#click within v-for triggers all #click events

When a single #click is triggered from within the v-for the display property in all of the objects within the array is updated, rather than just by index. I only want that element that is click to receive the event and update the display property, not all of them.
<script>
import TransitionExpand from '#/transitions/transition-expand'
import ResourceCard from '#/components/resource-card'
export default {
components: {
TransitionExpand,
ResourceCard
},
data () {
return {
}
},
methods: {
toggle (evt, i) {
this.status[i].display = !this.status[i].display
}
},
async asyncData ({ app }) {
const { data } = await app.$axios.get('training_modules')
const status = Array(data.trainingModules.length).fill({ display: false })
return {
modules: data.trainingModules,
status
}
}
}
</script>
<template>
<div>
<div class="container px-4 mx-auto mt-16 lg:mt-32">
<div class="flex flex-wrap mb-20">
<h1 class="w-full lg:w-1/2 mb-6">Training Modules</h1>
<p class="w-full lg:w-1/2 leading-7 text-abbey"></p>
</div>
<div
v-for="(item, i) in modules"
:key="item.id"
class="mb-12"
>
<div class="flex items-center">
<h2>{{ item.title }}</h2>
<img #click="toggle($event, i)" class="ml-auto cursor-pointer" src="#/assets/images/icons/plus.svg" alt="open">
</div>
<TransitionExpand v-show="status[i].display">
<div class="">
<p class="mt-6 mb-12">{{ item.description }}</p>
<div class="flex flex-wrap -m-3">
<ResourceCard
v-for="resource in item.resources"
:key="resource.id"
:resource="resource"
/>
</div>
</div>
</TransitionExpand>
</div>
</div>
<BaseFooter />
</div>
</template>
Problem is in const status = Array(data.trainingModules.length).fill({ display: false }) code which is filing all array items with the same object which is observable hence change in display property will be applied to all elements in the array as status[0] === status[1].
Use map instead and it will work as expected:
const status = data.trainingModules.map(() => ({ display: false }));

Categories

Resources