Vue: change component prop from parent component on click, radio buttons - javascript

I have two components: Toggle.vue which is basically a button and a TestToggle.vue which has two Toggle components inside. I want to be able for the toggle elements to serve as a radio button of sorts: only one can be selected at a time.
It is supposed to look like this (only one button is active at a time):
However I can select two buttons:
which isn't right.
Toggle.vue:
<template>
<div class="rounded-full m-5 w-40
flex justify-center
p-2 cursor-pointer"
:class = "status ? 'bg-green-700
hover:bg-green-600' :
'bg-red-700
hover:bg-red-600'"
v-on:click="status = true">
<p>{{text}} : {{status}}</p>
</div>
</template>
<script>
export default {
props: {
text: {
type: String,
default: ''
},
status: {
type: Boolean,
default: false
}
}
}
</script>
TestToggle.vue:
<template>
<div>
<p>Active: {{activeTab}}</p>
<Toggle v-on:click = "activeTab = 1"
text="Toggle 1 "/>
<Toggle v-on:click = "activeTab = 2"
text = "Toggle 2"/>
</div>
</template>
<script>
import Toggle from '../test/Toggle.vue';
export default {
components: {Toggle},
data: function () {
return {
activeTab: 1
}
},
methods: {
}
}
</script>
I think I need to set status = false from TestToggle to Toggle when another Toggle is clicked? How do I do that? Or should I do it completely differently?
Another problem is that I can't update activeTab data property inside TestToggle component: it always shows 1...
EDIT:
I tried this code (as suggested in the answer), but it just doesn't work: the buttons don't react to clicks:
Toggle.vue:
<template>
<div class="rounded-full m-5 w-40
flex justify-center
p-2 cursor-pointer"
:class = "status ? 'bg-green-700 hover:bg-green-600' :
'bg-red-700 hover:bg-red-600'">
<p>{{text}} : {{status}}</p>
</div>
</template>
<script>
export default {
props: {
text: {
type: String,
default: ''
},
status: {
type: Boolean,
default: false
}
}
}
</script>
TestToggle.vue:
<template>
<div>
<p>Active: {{activeTab}}</p>
<Toggle v-on:click = "activeTab = 1"
text="Toggle 1 "
v-bind:status="activeTab === 1"/>
<Toggle v-on:click = "activeTab = 2"
text = "Toggle 2"
v-bind:status="activeTab === 2"/>
</div>
</template>
<script>
import Toggle from '.././toggle-so/Toggle.vue';
export default {
components: {Toggle},
data: function () {
return {
activeTab: 1
}
},
methods: {
}
}
</script>

In Toggle.vue, status is declared as a prop, so you should not modify it:
<template>
<div class="rounded-full m-5 w-40
flex justify-center
p-2 cursor-pointer"
:class = "status ? 'bg-green-700
hover:bg-green-600' :
'bg-red-700
hover:bg-red-600'"
<p>{{text}} : {{status}}</p>
</div>
</template>
but pass it to Toggle.vue from TestToggle.vue:
<template>
<div>
<p>Active: {{activeTab}}</p>
<Toggle v-on:click.native = "activeTab = 1"
text="Toggle 1 "
v-bind:status="activeTab === 1"/>
<Toggle v-on:click.native = "activeTab = 2"
text = "Toggle 2"
v-bind:status="activeTab === 2"/>
</div>
</template>
If you change the status in Toggle.vue, you make it independent of every other Toggle, but if you want a radio button behavior, each status is dependent of other statuses. That's why you need to manage if from the parent component.
You also need to use the native event modifier to listen to the div click of the children.
I made a simple JSFiddle to show a working example.

Related

How to load tab items dynamically in Vuejs?

AppsTab.vue
<script>
export default {
props: {
tabList: {
type: Array,
required: true,
},
variant: {
type: String,
required: false,
default: () => "vertical",
},
},
data() {
return {
activeTab: 1,
};
},
};
</script>
<template>
<div
:class="{
'flex space-x-4': variant === 'horizontal',
}"
>
<ul
class="list-none bg-blue-900 bg-opacity-30 p-1.5 rounded-lg text-center overflow-auto whitespace-nowrap"
:class="{
'flex items-center mb-6': variant === 'vertical',
}"
>
<li
v-for="(tab, index) in tabList"
:key="index"
class="w-full px-4 py-1.5 rounded-lg"
:class="{
'text-blue-600 bg-white shadow-xl': index + 1 === activeTab,
'text-white': index + 1 !== activeTab,
}"
>
<label
:for="`${_uid}${index}`"
v-text="tab"
class="cursor-pointer block"
/>
<input
:id="`${_uid}${index}`"
type="radio"
:name="`${_uid}-tab`"
:value="index + 1"
v-model="activeTab"
class="hidden"
/>
</li>
</ul>
<template v-for="(tab, index) in tabList">
<div
:key="index"
v-if="index + 1 === activeTab"
class="flex-grow bg-white rounded-lg shadow-xl p-4"
>
<!-- <slot :name="`tabPanel-${index + 1}`" /> --> want to get data using loop inside tab
</div>
</template>
</div>
</template>
App.vue
< script >
import AppTabs from "./components/AppTabs";
export default {
components: {
AppTabs,
},
data() {
return {
tabList: ["Tab 1", "Tab 2", "Tab 3", "Tab 4"],
};
},
}; <
/script>
<template>
<div class="bg-gradient-to-r from-blue-300 to-blue-500 min-h-screen">
<app-tabs
class="w-11/12 lg:w-10/12 mx-auto"
:tabList="tabList"
variant="horizontal"
>
{{value}} // want to bind data inside tab
</app-tabs>
</div>
</template>
Expected Output
I am working on vertical tabs. Where the functionality is working fine. Here is the complete working code with static mock data https://codesandbox.io/s/vue-js-tabs-forked-we2cx?file=/src/App.vue
Now i want to create some mockdata inside of my data like 'tabList' and then, i want to display data dynamically when user clicked on tabs(including content -->tabs)
How to remove static data, which is inside slots and then only use data dynamically
To start with that, i am not sure, Where to start the looping to display data inside tabs dynamically with mock data?
You can set dynamic slot name using :slot="slotName" where slotName is a dynamic value
This can be achieved using a v-for aswell like below.
<template v-for="content in contentList" :slot="content.slot">
{{ content.content }}
</template>
Where contentList is your array something like below.
contentList: [
{ id: 1, slot: "tabPanel-1", content: "Content 1" },
{ id: 2, slot: "tabPanel-2", content: "Content 2" },
{ id: 3, slot: "tabPanel-3", content: "Content 3" },
{ id: 4, slot: "tabPanel-4", content: "Content 4" },
]
Working Fiddle

VueJS 3 custom Checkbox not changing UI when clicked

I'm trying to create a custom checkbox with Vue 3 and the composition API following this example, but even when I can see on devtools that all my props and bound data are passing from the parent component to the child component the checkbox UI won't change when the checkbox is checked:
Parent Component:
<template>
<div class="flex">
<div class="flex">
<span>Detected Language</span>
<BaseCheckBox
v-model:checked="translationOn"
fieldId="translate"
label="Translate"
class="ml-4"
/>
</div>
</div>
</template>
<script>
import BaseCheckBox from './BaseCheckBox.vue'
import { ref } from 'vue'
export default {
setup() {
const translationOn = ref(false)
return {
translationOn,
}
},
components: { BaseCheckBox },
}
</script>
Child Component:
<template>
<div class="flex">
<input
#input="(event) => $emit('update:checked', event.target.checked)"
type="checkbox"
:checked="checked"
:id="fieldId"
class="mr-2 hidden"
/>
<label
:for="fieldId"
class="flex flex-row items-center cursor-pointer select-none"
>
<i
class="fa mr-1"
:class="{
'fa-check-square text-blue-600': checked,
'fa-square border-2 border-gray-700 text-white': !checked,
}"
></i>
{{ label }}
</label>
</div>
</template>
<script>
export default {
props: {
label: String,
fieldId: {
type: String,
required: true,
},
checked: {
type: Boolean,
},
},
}
</script>
Whenever I click the checkbox I can see that the "translationOn" property on the parent change its value and the "checked" prop on the children change its value too but the font-awesome classes that are supposed to switch depending on that value don't:
<i
class="fa mr-1"
:class="{
'fa-check-square text-blue-600': checked,
'fa-square border-2 border-gray-700 text-white': !checked,
}"
></i>
The strange thing (at least for me) is that when I manually change the value in the code in this line of the parent component:
const translationOn = ref(true)
From "true" to "false" or viceversa it works but not when I click on the checkbox, even when I can see all the values behaving accordingly.
Will really appreciate any help! Thanks!
So found the answer to this problem here
For some reason the font-awesome classes are not reactive hence ignore the vue directive to conditional render the html. Find the answer (basically wrap the <i> tag within a <span> tag) on the link.

Vue Remove loop rendered component from DOM

I have image upload form for images to my website. When the user clicks on input images, he can choose multiple images. After selecting images, images are previewed and the user can select some meta info(Category, Type) about the image.
upload.vue
<template>
<div>
<div>
//Universal category select. this selection will apply to all comp.
<v-select placeholder="Select Category"
class="mt-2 md:w-1/2"
:options="category"
v-model="parentDesignCategory"
/>
<v-select
placeholder="Select Type"
class="mt-2 md:w-1/2"
:options="type"
v-model="parentDesignType"
/>
</div>
<input
type="file"
accept="image/*"
name="images"
#change="uploadImage"
id="images"
multiple
/>
<div class="flex flex-wrap">
<div class="md:w-1/2" v-for="(file, index) in files" :key="index">
<transition name="fade">
<AdminFileUpload
:file="file"
:type="type"
:category="category"
:parentDesignType="parentDesignType"
:parentDesignCategory="parentDesignCategory"
#delete-row="deleteThisRow(index)"
/>
</transition>
</div>
</div>
</div>
</template>
<script>
export default {
name: "admin",
// middleware: "auth",
data: function() {
return {
files: [],
parentDesignType: null,
parentDesignCategory: null,
type: ["1", "2", "3"],
category: ["a","b","c"
]
};
},
components: {},
methods: {
uploadImage(event) {
let file = event.target.files;
for (let i = 0; i < file.length; i++) {
this.files.push(file[i]);
}
},
deleteThisRow: function(index) {
this.files.splice(index, 1);
}
}
};
</script>
<style scoped>
.fade-enter-active {
transition: opacity 1.5s;
}
.fade-leave-active {
opacity: 0;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>
And if all image falls in one category than a user can select one category from this page and all component follows this category.
fileUpload.vue Component
<template>
<div>
<div class="m-4">
<form
#submit.prevent="uploadImage"
class="flex flex-wrap w-full shadow-lg border border-black"
action="/upload"
>
<div class="w-full md:w-1/2 p-2">
<div class="relative pb-1/1">
<img :src="imageSrc" class="w-full absolute h-full" />
</div>
</div>
<div class="flex flex-col w-full md:w-1/2 p-2">
<v-select
placeholder="Select Category"
class="mt-2"
:options="category"
v-model="designCategory"
></v-select>
<v-select
placeholder="Select Type"
class="mt-2"
:options="type"
v-model="designType"
></v-select>
<input
placeholder="likes"
class="w-full text-black border-2 mt-2 p-3 rounded-lg focus:outline-none focus:shadow-outline"
type="number"
v-model="designLikes"
/>
<button
#click="removeSelf"
class="uppercase h-12 text-lg font-bold tracking-wide bg-primary text-gray-100 mt-2 p-3 rounded-lg w-full cursor-pointer"
type="button"
>
Cancel
</button>
<button
type="submit"
class="uppercase mt-2 h-16 text-xl font-bold tracking-wide bg-accent text-gray-100 p-3 rounded-lg w-full transition duration-300 hover:opacity-80 cursor-pointer"
>
Upload
</button>
</div>
</form>
</div>
</div>
</template>
<script>
import "vue-select/dist/vue-select.css";
export default {
name: "fileUpload",
middleware: "auth",
props: [
"file",
"type",
"category",
"parentDesignCategory",
"parentDesignType"
],
data() {
return {
designCategory: this.parentDesignCategory,
designType: this.parentDesignType,
designLikes: null
};
},
computed: {
imageSrc: function() {
return URL.createObjectURL(this.file);
}
},
created() {},
methods: {
async uploadImage() {
let formData = new FormData();
const config = {
headers: {
"content-type": "multipart/form-data"
}
};
formData.append("likes", this.designLikes);
formData.append("image", this.file);
formData.append("category", this.designCategory);
formData.append("type", this.designType);
await this.$axios
.post("upload", formData, config)
.then(response => {
this.progress = 0;
this.showToast("Photo Uploaded.", "success");
// Delete coomponent when upload complete
this.$emit("delete-row");
})
.catch(error => {
});
},
removeSelf: function() {
this.$emit("delete-row");
});
}
}
};
</script>
Now my first and main problem is when the user removes the component from the dom, it removes the component but the Selected category/type stays in the same position.
Suppose I chose 4 images. I set 2nd image category as "a". When I remove 1st image. 1st image gets removed and 2nd image comes at 1st place but the category selected "a" remains on position 2.
Now 2nd problem is if I chose the category for the universal component in the parent page before selecting images it applies to all components. but after selecting images, Universal select doesn't work.
3rd problem is transition doesn't work on any component.
Simple answer is - you have to set an unique ID. Here is how you can solve that:
Changings in the template:
First of all you need to set an id instead of using your index - setting an id makes it unique and that is what we need. So set your :key to file.id (we will create it in the script) and pass your file with deleteThisRow to your methods. Done!
<div class="md:w-1/2" v-for="file in files" :key="file.id">
//and change your index here to file here we will reference on the unique file we will create with the unique id we will set
#delete-row="deleteThisRow(file)"
Changings in the script: Set your id = null in data() - that your created id will not be undefined. After that go to your methods and set your id = i - now it's unique and could not be change anymore like your index could. Last thing you should do is to map over your files array and get the correct index which should be deleted with indexOf.
//in your data
data() {
return {
id: null,
}
},
//in your methods
methods: {
uploadImage(event) {
let file = event.target.files;
for (let i = 0; i < file.length; i++) {
this.files.push({image:file[i], id : i}); //here you set your id to an unique number! (could be this.id you have to try)
}
},
deleteThisRow: function(file) {
var indexDelete = this.files.map(x => {
return x.id;
}).indexOf(file.id);
this.files.splice(indexDelete, 1);
}
}
After all you have to pass your file.id to your child with following code:
<child :uniqueID="file.id"
:file="file.image">
and reference on this in your child with props
Hopefully I understood your question correct - than that should work out for your problem - please let me know if this works for you !
Additional Info: Please change all index-values to file.id - than everything is really unique.

Vue v-if not hiding mobile header

I have this project where I try to add a mobile view menu. This menu is displayed by clicked a button. The button I have created switches a boolean on and off. But when the value of the boolean changes the v:if on the menu doesn't hide it. It keeps on showing.
This is my menu item inside the template:
<template>
<div>
<div> ... main menu ... </div>
<div :v-if="menuOpened" class="bg-purple-primary h-10 z-20">
<p>Hello World</p>
</div>
</div>
</template>
<script>
export default {
name: 'Header',
data () {
return {
menuOpened: false
}
},
methods: {
switchMenuState () {
this.menuOpened = !this.menuOpened
console.log(this.menuOpened)
}
}
}
</script>
You do not need that colon before v-if directive
<template>
<div>
<button #click="switchMenuState()">Switch</button>
<div v-if="menuOpened" class="bg-purple-primary h-10 z-20">
<p>Hello World</p>
</div>
</div>
</template>
<script>
export default {
name: "Header",
data() {
return {
menuOpened: false,
};
},
methods: {
switchMenuState() {
this.menuOpened = !this.menuOpened;
},
},
};
</script>

How to change the :class name on the view file from a component in Vue?

I currently have two active themes using Tailwind light-theme and dark-theme but I can't make it work within an external button component, just with the code and function inside the view.
This is just an example where the function inside the view is acceptable but I have a "real world" case where I need it working from the component, changing the class between dark/light mode.
Here is my "Home.vue" file importing ButtonA and ButtomB:
<template>
<div :class="theme" class="bg-background-primary">
<h1 class="text-4xl text-typo-primary">Test title</h1>
<!-- Inside Home.vue - WORKING -->
<button class="border border-gray-400 bg-blue-500 hover:bg-blue-700 text-white p-2 rounded" #click="toggleThemeOne()">Toggle Dark/Light</button>
<!-- Component with function outside - WORKING -->
<ButtonA msg="From component" #click.native="toggleThemeTwo()" />
<ButtonB msg="Full from component" />
</div>
</template>
<script>
// # is an alias to /src
import ButtonA from '#/components/ButtonA.vue'
import ButtonB from '#/components/ButtonB.vue'
export default {
name: 'Home',
components: {
ButtonA,
ButtonB
},
data() {
return {
theme: 'theme-light',
}
},
methods: {
toggleThemeOne() {
this.theme = this.theme === 'theme-light' ? 'theme-dark' : 'theme-light'
localStorage.setItem('theme', this.theme)
console.log('toggleThemeOne working');
console.log(this.theme)
},
toggleThemeTwo() {
this.theme = this.theme === 'theme-light' ? 'theme-dark' : 'theme-light'
localStorage.setItem('theme', this.theme)
console.log('toggleThemeTwo working');
console.log(this.theme)
},
}
}
</script>
Home.vue has a working button that's changing the theme
ButtonA
It has the HTML only and the function applied on the component
<template>
<div>
<button class="border border-gray-400 bg-blue-500 hover:bg-blue-700 text-white p-2 rounded"> {{ msg }} </button>
</div>
</template>
<script>
export default {
name: "ButtonComp",
props: [
'msg'
]
}
</script>
ButtonB
<template>
<div>
<button
class="border border-gray-400 bg-blue-500 hover:bg-blue-700 text-white p-2 rounded"
#click="toggleThemeTree()"
> {{ msg }} </button>
</div>
</template>
<script>
export default {
name: "ButtonComp",
props: [
'msg'
],
methods: {
toggleThemeTree() {
this.theme = this.theme === 'theme-light' ? 'theme-dark' : 'theme-light'
localStorage.setItem('theme', this.theme)
console.log('toggleThemeTree working');
console.log(this.theme)
},
},
}
</script>
This is the one that's not working. the function should change the :class on Home.vue but I only get the values on the console and the :class isn't working.
I did try with $emit and computed property before but It didn't work.
You should pass theme to ButtonB component in Home.vue:
<ButtonB msg="Full from component" :theme.sync="theme" />
Then in ButtonB component, emit the value back to parent on click:
<script>
export default {
name: "ButtonComp",
props: [
'msg',
'theme'
],
methods: {
toggleThemeTree() {
let theme = this.theme === 'theme-light' ? 'theme-dark' : 'theme-light' // Do not change this.theme directly
localStorage.setItem('theme', theme)
this.$emit('update:theme', theme)
},
},
}
</script>

Categories