I want to dynamically control the visibility of the (<) and (>) arrows in the Vuetify carousel component.
For example, so that the final right arrow on the last item disappears, or so that I can use internal buttons or other interactivity within the carousel-item content to replace the buttons dynamically. (I know the continuous prop can do the simple end case).
The documentation for the next-icon and prev-icon prop is bool or string and the default says $next.
Name next-icon
Type boolean | string
Default $next
Description Icon used for the "next" button if show-arrows is true
I can make the icon button disappear by setting it to false, but true doesn't make it reappear.
I'm guessing the string value is the icon name (like md-arrow-right?) but the documentation doesn't say what the default is, and that doesn't work. I'm guessing that "off" is setting the prop to false and "on" is restoring it to the icon name.
I also don't understand what $next means, and this isn't explained in the page. It errors if you use that as a value. Everything else seems to evaluate to false.
I'm guessing it's something like this:
<template>
<v-carousel v-model="stepNo" :show-arrows="show.arrows" :next-icon="show.nextArrow" height="auto" light>
<!-- ... -->
</template>
<script>
export default {
data: () => {
return {
stepNo: 0,
show: {
arrows: true,
nextArrow: "md-arrow-right",
},
}
},
watch: {
stepNo: function(newStep, oldStep) {
// some logic here, for example
this.nextArrow = (newStep === 4) ? "md-arrow-right" : false;
},
},
//...
}
</script>
UPDATE
One of my mistakes was md-arrow-right should be mdi-arrow-right (missing the i), or actually mdi-chevron-right as noted by tony19. So I can now set it to a literal icon OK.
But setting it to $next or $prev still doesn't work - it displays either nothing, and empty circle, or a $ sign which is actually the word $next. And this seems to "break" the binding and setting it to a literal icon after this, fails until reloading the page.
<i aria-hidden="true" class="v-icon notranslate material-icons theme--light" style="font-size: 36px;">$next</i>
I think that you can achieve the behavior you wanted without relying on documentation if it doesn't provide what you need.
Just inspect the left and right arrow of the carousel component and get the DOM Node by selector.
Then you are ready to do what you want with the elements.
For exemple:
const nextButton = document.querySelector('.v-window__next button');
const prevButton = document.querySelector('.v-window__prev button');
(Maybe instead of document you can use the $el inside your component)
Now you can do whatever you want with your elements.
To show/hide dynamically:
nextButton.style.display = 'None'; // Hide
nextButton.style.display = 'Block'; // Show
To navigate:
nextButton.click(); // Go next.
prevButton.click(); // Go prev.
Vue is just JavaScript at the end, no magic ;)
BTW, you could try this directly in the browser console on the link you provided for the carousel.
The icon visibility should be restored when setting it to $next (as seen in demo code snippet below).
About $next...
For all icons in the framework, Vuetify uses v-icon to render the icon specified by name. Icon names are mapped to an iconset (default is Material Design Icons). The mapped icon names are identified by the $ prefix, and remapped during icon rendering.
For instance, the mdi preset maps $prev to mdi-chevron-left and $next to mdi-chevron-right; and the fa (Font Awesome) preset maps $prev to fas fa-chevron-left and $next to fas fa-chevron-right.
Literal icon names (without the $ prefix) could also be explicitly used. For example, you could specify mdi-arrow-expand-right instead of $next in v-icon.
new Vue({
el: '#app',
vuetify: new Vuetify(),
data () {
return {
nextIcon: '$next',
prevIcon: '$prev',
nextIconEnabled: true,
prevIconEnabled: true,
colors: [
'indigo',
'warning',
'pink darken-2',
'red lighten-1',
'deep-purple accent-4',
],
slides: [
'First',
'Second',
'Third',
'Fourth',
'Fifth',
],
}
},
watch: {
nextIconEnabled(nextIconEnabled) {
if (nextIconEnabled) {
this.nextIcon = this._lastNextIcon
} else {
this._lastNextIcon = this.nextIcon
this.nextIcon = false
}
},
prevIconEnabled(prevIconEnabled) {
if (prevIconEnabled) {
this.prevIcon = this._lastPrevIcon
} else {
this._lastPrevIcon = this.prevIcon
this.prevIcon = false
}
}
}
})
.controls {
display: flex;
flex-direction: column;
}
<script src="https://unpkg.com/vue#2.6.11/dist/vue.min.js"></script>
<script src="https://unpkg.com/vuetify#2.2.8/dist/vuetify.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/#mdi/font#4.x/css/materialdesignicons.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons">
<link rel="stylesheet" href="https://unpkg.com/vuetify#2.2.8/dist/vuetify.min.css">
<div id="app">
<v-app id="inspire">
<div class="controls">
<label>Toggle next-icon
<input type="checkbox" v-model="nextIconEnabled">
</label>
<label>next-icon:
<input v-model="nextIcon" placeholder="icon name"/>
</label>
<label>Toggle prev-icon
<input type="checkbox" v-model="prevIconEnabled">
</label>
<label>prev-icon:
<input v-model="prevIcon" placeholder="icon name"/>
</label>
</div>
<v-carousel
height="400"
hide-delimiter-background
:prev-icon="prevIcon"
:next-icon="nextIcon"
>
<v-carousel-item
v-for="(slide, i) in slides"
:key="i"
>
<v-sheet
:color="colors[i]"
height="100%"
>
<v-row
class="fill-height"
align="center"
justify="center"
>
<div class="display-3">{{ slide }} Slide</div>
</v-row>
</v-sheet>
</v-carousel-item>
</v-carousel>
</v-app>
</div>
A simple typo in the icon name:
nextArrow: "md-arrow-right",
should be
nextArrow: "mdi-arrow-right",
I keep making this mistake because I get the icon names by searching https://materialdesignicons.com/ where the icon names do not have the mdi- prefix and I so often get it wrong when manually adding md- for just material design.
There some ways to have more control over the carousel component
To programaticaly control if the arrows will be showed, you can delegate this to a variable
continuous=false will do the job hiding the arrows on the begining/end of the elements list
And to determine wich element will be active, you can use v-model
<v-carousel
:show-arrows=arrows
:progress=false
:continuous=false
v-model="item"
hide-delimiter-background
>
<v-carousel-item
v-for="n in 15"
:key="n"
>
<v-card>
{{item}}
<v-btn
text
#click="nextItem"
>
Next Item
</v-btn>
<v-btn
text
#click="showHideArrows"
>
showHideArrows
</v-btn>
</v-card>
</v-carousel-item>
</v-carousel>
nextItem(): will change the current active item
showHideArrows(): will toggle the arrow's state
data: () => ({
arrows: false,
item: 0,
}),
methods: {
nextItem() {
console.log('next');
this.item += 1;
},
showHideArrows() {
this.arrows = !this.arrows;
console.log(this.arrows);
},
},
Related
I'm trying to create a Tabs component in Vue 3 similar to this question here.
<tabs>
<tab title="one">content</tab>
<tab title="two" v-if="show">content</tab> <!-- this fails -->
<tab :title="t" v-for="t in ['three', 'four']">{{t}}</tab> <!-- also fails -->
<tab title="five">content</tab>
</tabs>
Unfortunately the proposed solution does not work when the Tabs inside are dynamic, i.e. if there is a v-if on the Tab or when the Tabs are rendered using a v-for loop - it fails.
I've created a Codesandbox for it here because it contains .vue files:
https://codesandbox.io/s/sleepy-mountain-wg0bi?file=%2Fsrc%2FApp.vue
I've tried using onBeforeUpdate like onBeforeMount, but that does not work either. Actually, it does insert new tabs, but the order of tabs is changed.
The biggest hurdle seems to be that there seems to be no way to get/set child data from parent in Vue 3. (like $children in Vue 2.x). Someone suggested to use this.$.subtree.children but then it was strongly advised against (and didn't help me anyway I tried).
Can anyone tell me how to make the Tab inside Tabs reactive and update on v-if, etc?
This looks like a problem with using the item index as the v-for loop's key.
The first issue is you've applied v-for's key on a child element when it should be on the parent (on the <li> in this case).
<li v-for="(tab, i) in tabs">
<a :key="i"> โ
</a>
</li>
Also, if the v-for backing array can have its items rearranged (or middle items removed), don't use the item index as the key because the index wouldn't provide a consistently unique value. For instance, if item 2 of 3 were removed from the list, the third item would be shifted up into index 1, taking on the key that was previously used by the removed item. Since no keys in the list have changed, Vue reuses the existing virtual DOM nodes as an optimization, and no rerendering occurs.
A good key to select in your case is the tab's title value, as that is always unique per tab in your example. Here's your new Tab.vue with the index replaced with a title prop:
// Tab.vue
export default {
props: ["title"], ๐
setup(props) {
const isActive = ref(false)
const tabs = inject("TabsProvider")
watch(
() => tabs.selectedIndex,
() => {
isActive.value = props.title === tabs.selectedIndex
} ๐
)
onBeforeMount(() => {
isActive.value = props.title === tabs.selectedIndex
}) ๐
return { isActive }
},
}
Then, update your Tabs.vue template to use the tab's title instead of i:
<li class="nav-item" v-for="tab in tabs" :key="tab.props.title">
<a ๐
#click.prevent="selectedIndex = tab.props.title"
class="nav-link" ๐
:class="tab.props.title === selectedIndex && 'active'"
href="#" ๐
>
{{ tab.props.title }}
</a>
</li>
demo
This solution was posted by #anteriovieira in Vuejs forum and looks like the correct way to do it. The missing piece of puzzle was getCurrentInstance available during setup
The full working code can be found here:
https://codesandbox.io/s/vue-3-tabs-ob1it
I'm adding it here for reference of anyone coming here from Google looking for the same.
Since access to slots is available as $slots in the template (see Vue documentation), you could also do the following:
// Tabs component
<template>
<div v-if="$slots && $slots.default && $slots.default()[0]" class="tabs-container">
<button
v-for="(tab, index) in getTabs($slots.default()[0].children)"
:key="index"
:class="{ active: modelValue === index }"
#click="$emit('update:model-value', index)"
>
<span>
{{ tab.props.title }}
</span>
</button>
</div>
<slot></slot>
</template>
<script setup>
defineProps({ modelValue: Number })
defineEmits(['update:model-value'])
const getTabs = tabs => {
if (Array.isArray(tabs)) {
return tabs.filter(tab => tab.type.name === 'Tab')
} else {
return []
}
</script>
<style>
...
</style>
And the Tab component could be something like:
// Tab component
<template>
<div v-show="active">
<slot></slot>
</div>
</template>
<script>
export default { name: 'Tab' }
</script>
<script setup>
defineProps({
active: Boolean,
title: String
})
</script>
The implementation should look similar to the following (considering an array of objects, one for each section, with a title and a component):
...
<tabs v-model="active">
<tab
v-for="(section, index) in sections"
:key="index"
:title="section.title"
:active="index === active"
>
<component
:is="section.component"
></component>
</app-tab>
</app-tabs>
...
<script setup>
import { ref } from 'vue'
const active = ref(0)
</script>
I'm trying to create a custom play/pause button for my audio items which I did like so...
<div v-for="(post, p) in post_list">
<!-- ... -->
<!-- ... -->
<!-- ... -->
<v-avatar v-if="!is_played" color="#663399" size="42" class="mx-2"
#click="playMe('custom-wave-aud-'+p)"> <!-- is_played = true -->
<v-icon dark> mdi-play </v-icon>
</v-avatar>
<v-avatar v-if="is_played" color="#663399" size="42" class="mx-2"
#click="pauseMe('custom-wave-aud-'+p)"> <!-- is_played = false-->
<v-icon dark> mdi-pause </v-icon>
</v-avatar>
</div>
The above code toggles from pause icon to play icon on click. The problem is that, it affects all play/pause button for all items since the is_played is assigned for all the items.
What I want is that is_played should only affect one item and should only be exclusive to the item that I toggle play/pause button.
EDITED: Here's the play/pause function
playMe(c, index) {
document.getElementsByClassName(c)[0].play();
this.is_played = true;
},
pauseMe(c, index) {
document.getElementsByClassName(c)[0].pause();
this.is_played = false;
},
As explained in comments, you have to reference the index of the current post in is_played. Otherwise all the buttons will change when is_played changes. Here's some sample code which should do that:
<div v-for="(post, p) in post_list" :key="p">
<v-avatar v-if="is_played !== p"
color="#663399"
size="42"
class="mx-2"
#click="playMe('custom-wave-aud-' + p, p)">
<v-icon dark>mdi-play</v-icon>
</v-avatar>
<v-avatar v-if="is_played === p"
color="#663399"
size="42"
class="mx-2"
#click="pauseMe('custom-wave-aud-' + p, p)">
<v-icon dark>mdi-pause</v-icon>
</v-avatar>
</div>
data: () => ({
is_played: null
}),
methods: {
playMe(className, index) {
if (this.is_played !== null) {
// pause the currently playing one first
this.pauseMe(className, this.is_played);
}
this.is_played = index;
document.getElementsByClassName(className)[0].play();
},
pauseMe(className, index) {
this.is_played = null;
document.getElementsByClassName(className)[0].pause();
}
}
Note: the above code assumes you don't want more than one of the wavs to play at one time. If you do want to allow playing multiple at the same time, you'll have to change is_playing to an array and change the condition to check if current index is contained in the array. Obviously, you'll want to add/remove the index to the array when playing/pausing.
If you need more help, please provide a runnable minimal reproducible example.
I have a small container of text. What I'm trying to do is If the text length is large, collapse the div, then have a button that says "...show more", once pressed expands the div. pressed again collapses the div.
Thatโs fine and works.
I have an issue at the moment. The div is initially set to collapse=true. The โ...show moreโ button is displayed.
The thing I want to change is, if the text content is not long, it will not be collapsed, the show more button will not be displayed.
Template
<v-card v-show="showAccount" class="mb-4">
<v-card-title class="title-container align-start">
<div class="title-data" :class="{collapsed: isElementOverflown}" ref="title-data">
<h1 class="title mb-2"><router-link :to="{name: 'profile', params: {account: account.account}}">{{ account.account }}</router-link></h1>
<router-link v-if="isActiveUserAccount" :to="{name: 'account-image', params: {account: account.account}}">
<v-avatar color="#c35219" size="56" class="mr-4 mb-2">
<img v-if="accountMedia" :src="accountMedia" :alt="account.account" />
<span v-else class="white--text headline">{{ account.account[0].toUpperCase() }}</span>
</v-avatar>
</router-link>
<template v-else>
<v-avatar color="#c35219" size="56" class="mr-4 mb-2">
<img v-if="accountMedia" :src="accountMedia" :alt="account.account" />
<span v-else class="white--text headline">{{ account.account[0].toUpperCase() }}</span>
</v-avatar>
</template>
<div class="caption my-0" ref="bio">
<nl2br v-if="account.about" tag="p" :text="account.about"></nl2br>
</div>
</div>
<button v-if="showButton" type="button" style="font-size:small; margin: auto; margin-right: 5%" #click="toggleHeight">
{{showMoreTextLabel}}
</button>
</v-card-title>
JS
mounted() {
// elements have been created, so the `ref` will return an element.
// but the elements have not necessarily been inserted into the DOM yet.
// you can use $nextTick() to wait for that to have happened.
// this is espeically necessary if you want to to get dimensions or position of that element.
this.$nextTick(() => {
console.log("refs", this.$refs); // logs correct this.$refs
console.log("$refs.title-data", this.$refs["title-data"]); //undefined
let el = this.$refs["title-data"];
if (el.offsetHeight < el.scrollHeight || el.offsetWidth < el.scrollWidth) {
this.isElementOverflown = true;
this.showButton = true;
}
})
},
toggleHeight() {
if (this.$refs && 'title-data' in this.$refs) {
this.$refs['title-data'].classList.toggle('collapsed');
this.$refs['title-data'].classList.contains('collapsed')
? this.showMoreTextLabel = "...show more"
: this.showMoreTextLabel = "...show less";
}
},
In mounted Iโm getting an error that
this.$refs[โtitle-dataโ] is undefined but
This.$refs is there and it shows the correct refs. Iโm not sure why.
Thank you for any help!
You can create a computed property that checks if the length of your text exceeds a given number.
computed: {
isTextLengthLongEnough() {
if(el.offsetHeight > 150) {
this.showButton = true;
}
}
Then you can check in your template with a v-if if that computed property is true or false and then display the button or not.
Unfortunately, I couldn't get either of the above answers to work. el was coming up as undefined, So if someone could explain to me based on the code I have in the question how to get el, that would be great.
I did a work around not ideal, but it works where I have code in updated, So I'm going with that for now. Thanks very much for everyone's help
Here is the code I used
updated() {
if ('title-data' in this.$refs) {
const el = this.$refs['title-data']
const heightDiff = Boolean(el.scrollHeight - el.offsetHeight > ALLOWED_HEIGHT_VARIANCE)
if (heightDiff) {
this.showButton = heightDiff
el.className += ' read-more'
}
}
},
There are three key components in this case: Card (wrapper), Panels (parents for Sections, inside Card) and Sections (children for Panels, inside Panel each). Therefore, there is one Card and inside Card the same number of Panels and Sections (as I've said Sections are inside their own Panels).
The code looks as following:
1) Card:
<panel :collapsible="true" v-for="(item, key) in docSections" :key="key" :title="setSectionTitl(item)" :colorTitle="color">
<template slot="body">
<component
:document="document"
:sectionData="document.Sections[item]"
:is="getSection(item)"
:options="sectionOptions"
#setColor="setColor"
/>
</template>
</panel>
color: ''
methods: {
setColor (color) {
this.color = color
}
As you can see we use dynamic component feature that can contain several Sections inside Panels.
2) Section(s):
mounted () {
// setColor
if (!this.sectionData.permitStart || !this.sectionData.permitFinish) {
this.$emit('setColor', 'red')
} else {
this.$emit('setColor', 'black')
}
}
3) Panel(s):
<h6
:style="{ color: setTitleColor }"
v-if="title"
:title="title">
{{ title }}
</h6>
props: {
title: String,
colorTitle: {
type: String,
default: ''
}
...
}
computed: {
setTitleColor () {
if (this.colorTitle) {
if (this.colorTitle === 'red') {
return this.colorTitle
} else {
return 'black'
}
}
}
How does it look like:
How do look components like in DevTools:
The Card is wrapper and inside it there are several Sections inside Panels (collapsible) each. I need to paint the Section's that is Panel's titles (because titles are in Panels) under some conditions. Those conditions are not in all Sections. If conditions are in Sections, paint it in red, if not keep default color.
The issue is all Panel's titles are red, but I haven't emitted from all Sections.
How to solve the issue that is paint in red only Panel's titles that contain Sections where I've emitted from?
You probably realized the issue already: Your event sets the color property of the Card component. The card component passes this one color property to all of its Panel children.
If you want to have an independent color property in each of your Panels, you have to
either move the logic to your Panel and set the color there, or
offer each child Panel in your Card it's own color value. You could achieve that by creating an array of colors and pass the right color (by index) to your Panes.
However, there is no need to make the detour through your Section at all to pass the color to your Panel. Just move the logic to your Card:
<panel :collapsible="true" v-for="(item, key) in docSections" :key="key" :title="setSectionTitl(item)" :colorTitle="getColor(item)">
<template slot="body">
<component
:document="document"
:sectionData="document.Sections[item]"
:is="getSection(item)"
:options="sectionOptions"
#setColor="setColor"
/>
</template>
</panel>
methods: {
getColor(item) {
const permitFinish = this.document.Sections[item].permitFinish;
const permitStart = this.document.Sections[item].permitStart;
return permitStart && permitFinish ? 'black' : 'red';
}
}
How do you toggle a class in vue.js for list rendered elements? This question is an extension on this well answered question. I want to be able to toggle each element individually as well as toggle them all. I have attempted
a solution with the below code but it feels fragile and doesn't seem to work.
A different solution would be to use a single variable to toggle all elements and then each element has a local variable that can be toggled on and off but no idea how to implement that..
// html element
<button v-on:click="toggleAll"></button>
<div v-for="(item, i) in dynamicItems" :key=i
v-bind:class="{ active: showItem }"
v-on:click="showItem[i] = !showItem[i]">
</div>
//in vue.js app
//dynamicItems and showItem will be populated based on API response
data: {
dynamicItems: [],
showItem: boolean[] = [],
showAll: boolean = false;
},
methods: {
toggleAll(){
this.showAll = !this.showAll;
this.showItem.forEach(item => item = this.showAll);
}
}
Here is the small example to acheive you want. This is just a alternative not exact copy of your code.
var app = new Vue({
el:'#app',
data: {
dynamicItems: [
{id:1,name:'Niklesh',selected:false},
{id:2,name:'Raut',selected:false}
],
selectedAll:false,
},
methods: {
toggleAll(){
for(let i in this.dynamicItems){
this.dynamicItems[i].selected = this.selectedAll;
}
}
}
});
.active{
color:blue;
font-size:20px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.9/vue.js"></script>
<div id="app">
<template>
<input type="checkbox" v-model="selectedAll" #change="toggleAll"> Toggle All
<div v-for="(item, i) in dynamicItems">
<div :class='{active:item.selected}'><input type="checkbox" v-model="item.selected">Id : {{item.id}}, Name: {{item.name}}</div>
</div>
{{dynamicItems}}
</template>
</div>
I think all you need to do is this
v-bind:class="{ active: showItem || showAll }"
and remove the last line from toggleAll
You also need to use Vue.set when updating array values, as array elements aren't reactive.