Quasar q-drawer behaves inconsistently - javascript

I've had to switch from Vue.js 2 with Vuetify to Vue.js 3 with Quasar (as Vuetify does not support Vue.js 3 officially yet). I am using q-drawer and its mini property, where I can switch between the mini state and the normal expanded state by clicking a button. I have two problems here:
When I'm starting in the mini state, expanding and returning back to mini state changes the layout in the q-drawer, so a horizontal scrollbar is shown, even though there is none at the beginning. An illustration of this behaviour can be seen in the following pictures:
No horizontal scrollbar before the first expansion:
A horizontal scrollbar after expanding and switching to mini again
I tried using the the q-mini-drawer-hide class on the q-item-section that should not be visible when in the mini mode, however, the content still has a different width after expanding and minifying again than it had before.
Question:
Is there a way to prevent showing this horizontal scrollbar, while also leaving the functionality of the q-scroll-area for vertical scrolling? Do I have something wrong in the code, which makes the content behave in an unexpected way?
My second problem is with the behavior of long texts in the q-drawer. When I reload the page with the drawer in its expanded state, the long text is broken into two or more lines. This is a desired behaviour. However, once I minify and expand it again, the text is no longer on multiple lines, but on a single line going outside the width of the q-drawer, which has a set width. Illustrations following:
Long text on multiple lines when reloading with expanded q-drawer
After minifying and expanding the q-drawer, the long text is on a single line going outside of the q-drawer component set width
Question:
How to tell the q-drawer component (or some child component) to always break the text into multiple lines? Or to use ellipsis on the text?
The example code to reproduce the problem:
<template>
<q-layout view="hHh Lpr fff">
<q-header elevated class="bg-primary text-white">
<q-toolbar class="bg-black">
<q-btn flat #click="toggleSidebar" round dense icon="mdi-menu" />
<q-toolbar-title>
A title
</q-toolbar-title>
<q-space></q-space>
<q-btn flat round dense icon="mdi-dots-vertical"></q-btn>
</q-toolbar>
</q-header>
<q-drawer show-if-above v-model="sidebar_open" side="left" elevated
:mini="!sidebar_expanded"
bordered
:width="240"
class="bg-grey-3">
<q-scroll-area class="fit">
<q-list>
<q-item clickable v-ripple>
<q-item-section avatar>
<q-icon name="mdi-view-dashboard" />
</q-item-section>
<q-item-section class="q-mini-drawer-hide">Some text</q-item-section>
</q-item>
<q-item clickable v-ripple>
<q-item-section avatar>
<q-icon name="mdi-cube" />
</q-item-section>
<q-item-section class="q-mini-drawer-hide">Some other text</q-item-section>
</q-item>
<q-item clickable v-ripple>
<q-item-section avatar>
<q-icon name="mdi-clipboard-text" />
</q-item-section>
<q-item-section class="q-mini-drawer-hide">Some other other text</q-item-section>
</q-item>
<q-item clickable v-ripple>
<q-item-section avatar>
<q-icon name="mdi-cog" />
</q-item-section>
<q-item-section class="q-mini-drawer-hide">Some very very very very long text that should break</q-item-section>
</q-item>
</q-list>
</q-scroll-area>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script>
import { useStore } from 'vuex'
import { defineComponent, ref, computed } from 'vue'
export default defineComponent({
setup() {
//------------------------------------------------------------------------------------------------------------------
// Basic setup
//------------------------------------------------------------------------------------------------------------------
const $store = useStore()
const sidebar_open = ref(true)
//------------------------------------------------------------------------------------------------------------------
// Sidebar expansion functionality
//------------------------------------------------------------------------------------------------------------------
/**
* The expansion state of the sidebar (true = expanded, false = mini)
*/
const sidebar_expanded = computed({
// For some reason, this needs to use the name of the store ('global' in this case) even with the namespacing disabled
get() { return $store.state.global.sidebar_expanded },
/** #param { boolean } value */
set(value) { $store.dispatch('changeSidebarState', { expanded: value }) }
})
/**
* Sets the sidebar expansion state to mini
*/
const collapseSidebar = () => {
sidebar_expanded.value = false
}
/**
* Sets the sidebar expansion state to expanded
*/
const expandSidebar = () => {
sidebar_expanded.value = true
}
/**
* Toggles the sidebar expansion state
*/
const toggleSidebar = () => {
if (sidebar_expanded.value) {
collapseSidebar()
} else {
expandSidebar()
}
}
return {
sidebar_open,
// Sidebar expansion functionality
sidebar_expanded,
toggleSidebar,
}
}
})
</script>
<style scoped>
</style>
Note: I am using mdi-v5 icons, as the default material-design Quasar icons behaved strangely.

This issue can be fix with css to force break lines
.q-item__section { white-space: break-spaces; }
or otherwise to keep one line
.q-item__section { white-space: nowrap; }

Related

TailwindCSS overflow-auto class not working with React Lists

I am experiencing an issue where the overflow properties of Tailwind are not working with react lists. I am wondering if this is just a simple mistake on my part or if there is a work around that I need to do.
Image to It Not Working
import CoinSummary from './CoinSummary'
const Holdings = ({ coins }) => {
return (
<div className='overflow-auto p-4'>
<h2 className='text-2xl text-center font-bold mt-4'>Holdings</h2>
{coins &&
coins.map((coin, index) => {
return <CoinSummary key={index} coin={coin} />
})}
</div>
)
}
export default Holdings
I want the list of coins to stay inside of the Holdings component and if it overflows, to have a scroll bar instead. However, as you can see in the photo, it doesn't seem to be working like I expected it to.
I was able to figure it out on my own. It was because I did not specify the height to be h-full

How to toggle Vuetify Carousel component right/left arrows on and off

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);
},
},

How to pass particular color to parent component in Vue.js?

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';
}
}

Dynamically integrate Vuetify v-stepper with Vue router

I would like to integrate vuetify's v-stepper with vue router. My requirements are as follows:
Each step has its own route (e.g. /myform/step1, /myform/step2, /myform/step3, etc)
Each step is component on its own which is dynamically loaded (lazy-load).
Each step is dynamically created (e.g. via a loop).
This is more of a 'what is the best approach' kind-of-question. I've tried multiple solutions but none fit my requirements.
I've tried creating nested routes and placing a router-view in v-stepper-content. Example below. The issue I faced here was that it's impossible to synchroniously update position (see v-stepper element) and the route. So you'll always see the route updating before the step is updated.
<v-stepper v-model="position" vertical>
<template v-for="(item, index) in steps">
<v-stepper-step :complete="position > index + 1" :step="index + 1">
<h2>
{{item.title}}
</h2>
</v-stepper-step>
<v-stepper-content :step="index+1">
<router-view></router-view>
</v-stepper-content>
</template>
</v-stepper>
Another solution I tried is loading the components async/dynamically directly (so without router). However, then I lose the beautiful ability to navigate through my v-stepper using the browser's back and next buttons.
In my experience, the biggest pitfall is that (contrary to e.g. v-tab), is that every step has to have its own v-stepper-content. If I were to do this with tabs, I would just create one tab-item and update the view. I can't do that with v-stepper, because it wouldn't continue to the next 'step'.
Would anyone have a creative approach?
I was able to achieve this by doing the following:
<v-stepper-step #click="goToRoute('step1')">
with
goToRoute(name) {
this.$router.push({'name': name})
}
You should be able to do this:
<v-stepper-step #click="$router.push({'name': name})">
As an additional answer to #tmfmaynard, in order to align the correct highlighted stepper with your current route after a page refresh, here is the code.
<v-stepper v-model="e1" alt-labels non-linear>
<v-stepper-header class="elevation-0">
<v-stepper-step
step="1"
class="caption"
editable
#click="$router.push({name: 'name'}).catch(err => {})"
>
</v-stepper-step>
</v-stepper-header>
<v-stepper-items>
<v-stepper-content step="1">
<router-view />
</v-stepper-content>
</v-stepper-items>
</v-stepper>
<script>
export default {
data () {
return {
e1: 1
}
},
created() {
this.getStepper()
},
methods: {
getStepper() {
const path = this.$route.path.split('/')
if(path[path.length-1].toLowerCase() === 'your-router-path') {
this.e1 = 1
// this.e1 = 1 = <v-stepper-step step="1" />
// this.e1 = 2 = <v-stepper-step step="2" />
// and so on.
}
}
}
}

How to control order of rendering in vue.js for sibling component

I have following kind of code:
<div>
<compA />
<compB />
</div>
How do I make sure that first compA is rendered only after it compB is rendered.
Why I want is I have some dependency on few elements of compA, and style of compB depends on presence of those elements.
Why in details:
I have some complex UI design, where one box will become fixed when you scroll. SO It will not go above the screen when you scroll, it will be fixed once you start scrolling and it start touching the header. So I am using jquery-visible to find if a div with a particular id is visible on the screen, if it is not visible, I change the style and make that box fixed. Following code should give the idea what I am doing:
methods: {
onScroll () {
if ($('#divId').visible(false, false, 'vertical')) { // This is div from the compA, so I want to make sure it is rendered first and it is visible
this.isFixed = false
} else {
this.isFixed = true
}
}
},
mounted () {
window.addEventListener('scroll', this.onScroll() }
},
destroyed () {
window.removeEventListener('scroll', this.onScroll)
}
I dont want to make those in same component as one reason is it dont make sense as the nature of these components, and other I use compA at many places, while compB is specific to only one page. Also layout of these does not allow me to make compB child of compA as suggested in comments.
Any suggestions are welcome.
An option with events:
<!-- Parent -->
<div>
<comp-a #rendered="rendered = true"></comp-a>
<component :is="compB"></component>
</div>
<script>
// import ...
export default {
components: { CompA, CompB },
watch: {
rendered: function (val) {
if (val) this.compB = 'comp-b';
}
},
data() {
return {
rendered: false,
compB: null
}
}
}
</script>
<!-- Component B -->
<script>
export default {
mounted() {
this.$emit('rendered');
}
}
</script>
After going through the edit I realised that the dependency is not data driven but event driven (onscroll). I have tried something and looks like it works (the setTimeout in the code is for demonstration).
My implementation is slightly different from that of Jonatas.
<div id="app">
RenderSwitch: {{ renderSwitch }} // for demonstration
<template v-if='renderSwitch'>
<comp-a></comp-a>
</template>
<comp-b #rendered='renderSwitchSet'></comp-b>
</div>
When the component-B is rendered it emits an event, which just sets a data property in the parent of both component-A and component-B.
The surrounding <template> tags are there to reduce additional markup for a v-if.
The moment renderSwitch is set to true. component-a gets created.

Categories