Dynamically integrate Vuetify v-stepper with Vue router - javascript

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.
}
}
}
}

Related

Quasar q-drawer behaves inconsistently

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

Using vanilla js append, vue.js loses refrence

Ill simplify the example. Basically i have multiple widgets on one page and i tought it would be a good practice not to copy all the widgets into v-dialog but use the refrence and apenned them into the dialog and back to the grid when needed. The problem is when i append my html into dialog and I try to run this.$refs vue loses track of infinite-loading componente... this.$refs does not contain ref="infinitiveLoading". If some1 can explain and maybe reccommend better practice.. thx
<div>
<div id="item_containerTest" ref="item_containerTest">
<span>Hello world</span>
<infinite-loading
ref="infinitiveLoading"
v-show="items.length !== 0 || this.loading"
#infinite="infiniteHandler"
>
<div slot="no-more"></div>
</infinite-loading>
</div>
<v-dialog v-model="scheduleDialog" id="dialog" ref="dialog"> </v-dialog>
</div>
//ignore itemID and columnID, i need them so i can append item back to the grid after dialog closes
openFullScreenDialog(itemId, columnId, title){
itemContainer = document.getElementById(`item_container${title}`);
dialog = document.getElementById("dialog");
dialog.append(itemContainer);
}
As Lucero said in the comments, you shouldn't manipulate the DOM with the classic javascript API. The breaks the shadow DOM of Vuejs (i.e. a runtime copy of the current DOM state in memory) and thus, its reactivity and components references.
If you have a content that have to be wrapped in different container depending on a prop for example, you can use slots for this :)
<template>
<div>
<div v-if="dialog">
<slot>
</div>
<v-dialog v-else v-model="openDialog">
<slot>
</v-dialog>
</div>
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
export default Vue.extends({
name: 'GridOrDialog',
props: {
dialog: { type: Boolean, default: false }
},
data() {
return {
openDialog: true,
}
}
})
</script>
This way, you just have to declare the content once, and it will be on a v-dialog if you sets the prop dialog to true.
<GridOrDialog dialog="isDialogMode">
<span>Hello world</span>
<infinite-loading
ref="infinitiveLoading"
v-show="items.length !== 0 || this.loading"
#infinite="infiniteHandler"
>
<div slot="no-more"></div>
</infinite-loading>
</GridOrDialog>

VueJS: Can't update progress loader during rendering

I am having a Vue app which is loading up to 15 different pivot tables with a lot of data.
The entire rendering time takes about 15 seconds.
Within this time period I really would like to display a progress bar up 100 %.
But unfortunately i can't update the progress bar during the rendering.
It is somehow blocked.
Which recommendations do you have to solve this issue?
The code looks like:
<template
v-for="n in (numberOfPivotTables - 1)"
>
<!-- Pivot table #1: spot count / ad count -->
<mdb-col
sm="12"
md="12"
lg="6"
xl="6"
:key="n"
>
<div class="pt-5 mx-3 pb-5 pr-5">
<div style="height:350px">
<h5>{{customPivotOptions[n]['reducerKey']}}</h5>
<channel-filters
:hour-options="filters.hour[n]"
:adtype-options="filters.adType[n]"
:length-options="filters.spotLength[n]"
:creative-options="filters.motive[n]"
:weekday-options="filters.weekday[n]"
:flight-options="filters.flightId[n]"
:month-options="filters.month[n]"
:product-options="filters.product[n]"
:station-options="filters.station[n]"
:pivot-table-id="String(n)"
#getValue="getSelectValue"
>
</channel-filters>
<mdb-row>
<mdb-col
cols="12"
sm="2"
>
<mdb-select selectAll search
#getValue="setReducerKey"
:options="customPivotOptions[n].reducerOptions"
label="Value"
:arrayId="String(n)"
modelName="reducerKey"
:visibleOptions="10"
placeholder="Choose your Value" />
</mdb-col>
</mdb-row>
</div>
<pivot-table
:headline="customPivotOptions[n]['reducerKey']"
:data="pivotData[n].data"
:pivot-table-id="String(n)"
:fields="fields[n]"
:available-field-keys="customPivotOptions[n].availableFieldKeys"
:row-field-keys="customPivotOptions[n].rowFieldKeys"
:col-field-keys="customPivotOptions[n].colFieldKeys"
:default-show-settings="defaultShowSettings"
:reducer="getReducerKey"
#getValues="getPivotValues"
:sum-row="true"
:sum-column="true"
:sum-column-custom="{}"
>
</pivot-table>
</div>
</mdb-col>
<div v-if="n==7" v-bind:key="n" class="w-100"></div>
</template>
Vue rendering is JS execution. When JS is executing, browser is not rendering (updating screen) so app looks frozen.
The way around it is simply do not render everything at the same time. requestAnimationFrame API can be very useful here. The code below is based on the 9 Performance Secrets Revealed talk by with Guillaume Chau (Vue core team member) (GitHub repo with code)
1st we create a reusable mixin (factory function returning the mixin):
defer.js
export default function (count = 10) {
// #vue/component
return {
data () {
return {
displayPriority: 0,
}
},
mounted () {
this.runDisplayPriority()
},
methods: {
runDisplayPriority () {
const step = () => {
requestAnimationFrame(() => {
this.displayPriority++
if (this.displayPriority < count) {
step()
}
})
}
step()
},
defer (priority) {
return this.displayPriority >= priority
},
},
}
}
This mixin adds displayPriority counter to your component. It starts at 0 and is increased every time browser is ready to render new frame (repaint the screen) until it reaches count (passed as an argument to the factory function - default 10).
Important part is defer() function. It simply compares it's argument with displayPriority and returns true/false. For example defer(2) returns false when the browser repaints the screen for the 1st time (after component was rendered) and true for the rest of the page lifetime.
So the component with v-if="defer(2)" will be rendered (for the 1st time) before the component with v-if="defer(3)" etc.
Now it is easy to "split" the rendering of the parts of the component into multiple stages:
<template>
<div>
<template v-for="n in (numberOfPivotTables - 1)">
<heavyComponent v-if="defer(n)" :key="n" />
</template>
</div>
</template>
<script>
import Defer from 'defer'
export default {
mixins: [
Defer(), // pass number of steps as an argument. Default is 10
],
}
</script>

How to access $children in Vue 3 for creating a Tabs component?

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>

How to make vuetify v-stepper header as completed in dynamically?

Currently, I am working on <v-stepper> on vuetify js, and there are 12 steps that I've created. Every step there are set of tasks of contents available when you will click on each step, and accordingly step the contents will be changed by on click. I wanted the <v-stepper> step will show complete(or tick icon) if the set of tasks has completed on that particular step. Let's say if Step 1 is completed then step 1 icon should show a green tick icon. Now contents are populated on each step and I have the completed steps data but couldn't able to show complete-icon if the step is completed.
Below is my code:
<v-stepper-header>
<template v-for="n in steps">
<v-stepper-step :key="`${n}-step`" complete="completed" :step="n" :editable="editable" >
Week {{ n }}
</v-stepper-step>
<v-divider v-if="n !== steps" :key="n"></v-divider>
</template>
</v-stepper-header>
completed method:
e1(e1){
this.SET_STATUS_TO_DEFAULT(false)
// alert here
console.log(e1)
if(this.completedWeeksList.includes(e1)){
this.completed = true
this.editable = false
}else{
this.completed = false
this.editable = true
}
}
Please let me know if you need more elaboration.
The question isn't clear on wether you're trying to change the complete icon, or just trying to change the color to show the step as completed.
Either way complete is a prop, so you must pass it in as :complete...
<v-stepper-header>
<template v-for="n in steps">
<v-stepper-step :key="`${n}-step`" :complete="completed" :step="n" :editable="editable" >
Week {{ n }}
</v-stepper-step>
<v-divider v-if="n !== steps" :key="n"></v-divider>
</template>
</v-stepper-header>
Demo: https://codeply.com/p/kNjSEzu15W
the problem with the <v-stepper> in vutify is that, the <v-divider> must be a sibling not a child of each individual step, so in this case you need to use the <template> element to loop through the list of items by printing 2 siblings or more for each iteration and the most important thing to note here is all children of <template> which are siblings must have the same :key binding attribute but not the same value when you use it for any v-for directive
<v-stepper v-if="studyReportsList.length > 1">
<v-stepper-header>
<template v-for="(item, key) in studyReportsList">
<!-- all siblings must have the same :key or v-bind:key-->
<v-stepper-step :key="key+1" :step="key+1" editable>
{{ item.name }}
</v-stepper-step>
<v-divider v-if="key !== studyReportsList.length - 1" :key="key"></v-divider>
</template>
</v-stepper-header>
</v-stepper>

Categories