Expandable rows don't work inside <q-table> wrapper component - vuejs2

I created a BaseTable component based on q-table:
BaseTable.vue
<template>
<q-table
v-bind="$props"
v-on="$listeners"
>
<!-- Cycling through slots -->
<template v-for="(_, name) in $slots">
<template :slot="name">
<slot :name="name" />
</template>
</template>
<!-- Cycling through scoped slots -->
<template
v-for="(_, name) in $scopedSlots"
#[name]="data"
>
<slot
:name="name"
v-bind="data"
/>
</template>
</q-table>
</template>
<script>
import { QTable } from 'quasar'
export default {
extends: QTable,
props: {
/* Setting defaults */
dense: {
default: true
}
},
created () {
/* TODO: watch out!
We are directly mutating the prop, not a copy */
this.columns.forEach(col => {
col.align = col.align ?? 'left'
})
}
}
</script>
Then I created a specific Table component based on BaseTable:
Table.vue
<template>
<BaseTable
class="fixed-header"
:style="{ 'height': height }"
v-bind="$props"
v-on="$listeners"
>
<!-- Cycling through scoped slots -->
<template
v-for="(_, name) in $scopedSlots"
#[name]="data"
>
<slot
:name="name"
v-bind="data"
/>
</template>
<!-- code I'll show you later -->
</BaseTable>
</template>
<script>
import BaseTable from 'components/base/BaseTable.vue'
export default {
extends: BaseTable,
props: {
/* Custom props */
height: String
},
components: {
BaseTable
}
}
</script>
<style lang="sass">
.fixed-header
.q-table__top,
.q-table__bottom,
thead tr:first-child th
background-color: white
thead tr th
position: sticky
z-index: 1
thead tr:first-child th
top: 0
&.q-table--loading thead tr:last-child th
top: 28.5px
</style>
Now I'm trying to implement what is written here, but it doesn't work:
Table.vue
<template>
<BaseTable
class="fixed-header"
:style="{ 'height': height }"
v-bind="$props"
v-on="$listeners"
>
<!-- Cycling through scoped slots -->
<template
v-for="(_, name) in $scopedSlots"
#[name]="data"
>
<slot
:name="name"
v-bind="data"
/>
</template>
<template #header="props">
<q-tr :props="props">
<q-th auto-width />
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.label }}
</q-th>
</q-tr>
</template>
<template #body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
:icon="props.expand ? 'expand_less' : 'expand_more'"
size="md"
padding="0"
flat
round
#click="props.expand = !props.expand"
/>
</q-td>
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.value }}
</q-td>
</q-tr>
<q-tr
v-show="props.expand"
:props="props"
>
<q-td colspan="100%">
<div class="text-left">
This is expand slot for row above: {{ props.row.name }}.
</div>
</q-td>
</q-tr>
</template>
</BaseTable>
</template>
<script>
import BaseTable from 'components/base/BaseTable.vue'
export default {
extends: BaseTable,
props: {
/* Custom props */
height: String
},
components: {
BaseTable
}
}
</script>
<style lang="sass">
.fixed-header
.q-table__top,
.q-table__bottom,
thead tr:first-child th
background-color: white
thead tr th
position: sticky
z-index: 1
thead tr:first-child th
top: 0
&.q-table--loading thead tr:last-child th
top: 28.5px
</style>
Desserts.vue
<template>
<Table
:data="data"
:columns="columns"
:rows-per-page-options="[0]"
row-key="name"
height="600px"
/>
</template>
data and columns are taken from Quasar example. What's wrong with my code?

Related

Vue 3: How to use the render function to render different components that themselves render different components with slots, events, etc?

The title is basically my question. But the following code should make clearer what my goal is.
I have a version of this:
// AppItem.vue
<script>
import { h } from 'vue'
import { AppItem1 } from './item-1';
import { AppItem2 } from './item-2';
import { AppItem3 } from './item-3';
const components = {
AppItem1,
AppItem2,
AppItem3,
};
export default {
props: {
level: Number
},
render() {
return h(
components[`AppItem${this.level}`],
{
...this.$attrs,
...this.$props,
class: this.$attrs.class + ` AppItem--${this.level}`,
},
this.$slots
)
}
}
</script>
// AppItem1.vue
<template>
<AppBlock class="AppItem--1">
<slot name="header">
#1 - <slot name="header"></slot>
</slot>
<slot></slot>
</AppBlock>
</template>
// AppItem2.vue
<template>
<AppBlock class="AppItem--2">
<template #header>
#2 - <slot name="header"></slot>
</template>
<slot></slot>
</AppBlock>
</template>
// AppBlock.vue
<template>
<div class="AppBlock">
<div class="AppBlock__header">
<slot name="header"></slot>
</div>
<div class="AppBlock__body">
<slot></slot>
</div>
</div>
</template>
And my goal would be to use <AppItem> like...
<AppItem level="1">
<template #header>
Animal
</template>
<AppItem level="2">
<template #header>
Gorilla
</template>
<p>The gorilla is an animal...</p>
</AppItem>
<AppItem level="2">
<template #header>
Chimpanzee
</template>
<p>The Chimpanzee is an animal...</p>
</AppItem>
</AppItem>
...and have it render like...
<div class="AppBlock AppItem AppItem--1">
<div class="AppBlock__header">
Animal
</div>
<div class="AppBlock__body">
<div class="AppBlock AppItem AppItem--2">
<div class="AppBlock__header">
Gorilla
</div>
<div class="AppBlock__body">
<p>The gorilla is an animal...</p>
</div>
</div>
<div class="AppBlock AppItem AppItem--2">
<div class="AppBlock__header">
Chimpanzee
</div>
<div class="AppBlock__body">
<p>The Chimpanzee is an animal...</p>
</div>
</div>
</div>
</div>
Why does it not work? What I'm I misunderstaning?
The right way to get the AppItem--N component by name is to use the resolveComponent() function:
const appItem = resolveComponent(`AppItem${props.level}`);
I also fixed a couple of other small problems and had to rewrite the <setup script> to the setup() function. Now it works.
Playground
const { createApp, h, resolveComponent } = Vue;
const AppBlock = { template: '#appblock' }
const AppItem1 = { components: { AppBlock }, template: '#appitem1' }
const AppItem2 = { components: { AppBlock }, template: '#appitem2' }
const AppItem = {
components: {
AppItem1, AppItem2, AppBlock
},
props: {
level: Number
},
setup(props, { attrs, slots, emit, expose } ) {
const appItem = resolveComponent(`AppItem${props.level}`);
return () =>
h(appItem, {
...attrs,
...props,
class: (attrs.class ? attrs.class : '') + " AppItem--" + props.level,
}, slots);
}
}
const App = { components: { AppItem } }
const app = createApp(App)
app.mount('#app')
#app { line-height: 1; }
[v-cloak] { display: none; }
<div id="app">
<app-item :level="1">
<template #header>
<h4>Animal</h4>
</template>
<app-item :level="2">
<template #header>
Gorilla
</template>
<p>The gorilla is an animal...</p>
</app-item>
<app-item :level="2">
<template #header>
Chimpanzee
</template>
<p>The Chimpanzee is an animal...</p>
</app-item>
</app-item>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<script type="text/x-template" id="appitem1">
<app-block class="AppItem">
<slot name="header">
#1 - <slot name="header"></slot>
</slot>
<slot></slot>
</app-block>
</script>
<script type="text/x-template" id="appitem2">
<app-block class="AppItem">
<template #header>
#2 - <slot name="header"></slot>
</template>
<slot></slot>
</app-block>
</script>
<script type="text/x-template" id="appblock">
<div class="AppBlock">
<div class="AppBlock__header">
<slot name="header"></slot>
</div>
<div class="AppBlock__body">
<slot></slot>
</div>
</div>
</script>

Which is the best way to extend a Vuetify component?

I searched a lot and this seems to be the best way.
CustomVTextField.vue
<template>
<v-text-field
v-bind="$props"
v-on="$listeners"
>
<!-- Cycling through slots -->
<template v-for="(_, name) in $slots">
<template :slot="name">
<slot :name="name"></slot>
</template>
</template>
<!-- Cycling through scoped slots -->
<template
v-for="(_, name) in $scopedSlots"
#[name]="data"
>
<slot
:name="name"
v-bind="data"
></slot>
</template>
</v-text-field>
</template>
<script>
import { VTextField } from 'vuetify/lib'
export default {
extends: VTextField,
props: {
/* Setting defaults */
outlined: {
default: true
},
dense: {
default: true
}
}
}
</script>
Let's use it now.
<template>
<v-container fluid>
<CustomVTextField
v-model="firstName"
placeholder="Insert first name"
hint="I'm an hint"
#click="handleClick"
>
<template #label>
<b>First Name</b>
</template>
</CustomVTextField>
</v-container>
</template>
<script>
import CustomVTextField from '../components/CustomVTextField.vue'
export default {
components: {
CustomVTextField
},
data () {
return {
firstName: null
}
},
watch: {
firstName () {
console.log('first name:', this.firstName)
}
},
methods: {
handleClick () {
console.log('click handled')
}
}
}
</script>
This is the output.
As you can see v-model, props, slots and listeners are usable. Let's do the same thing with another component.
CustomVChipGroup.vue
<template>
<v-chip-group
v-bind="$props"
v-on="$listeners"
>
<!-- Cycling through slots -->
<template v-for="(_, name) in $slots">
<template :slot="name">
<slot :name="name"></slot>
</template>
</template>
<!-- Cycling through scoped slots -->
<template
v-for="(_, name) in $scopedSlots"
#[name]="data"
>
<slot
:name="name"
v-bind="data"
></slot>
</template>
</v-chip-group>
</template>
<script>
import { VChipGroup } from 'vuetify/lib'
export default {
extends: VChipGroup,
props: {
/* Setting defaults */
activeClass: {
default: 'primary--text'
}
}
}
</script>
Let's use it now.
<template>
<v-container fluid>
<CustomVChipGroup
v-model="selectedAnimal"
mandatory
#change="handleChange"
>
<v-chip value="DOG">
Dog
</v-chip>
<v-chip value="CAT">
Cat
</v-chip>
</CustomVChipGroup>
</v-container>
</template>
<script>
import CustomVChipGroup from '../components/CustomVChipGroup.vue'
export default {
components: {
CustomVChipGroup
},
data () {
return {
selectedAnimal: null
}
},
watch: {
selectedAnimal () {
console.log('selected animal:', this.selectedAnimal)
}
},
methods: {
handleChange () {
console.log('change handled')
}
}
}
</script>
This is the output.
The question should be clear now: which is the best way to extend a Vuetify component without occurring in these sort of errors?
Edit #1
#KaelWatts-Deuchar pointed out the same thing here (comments section), so I followed his solution.
CustomVTextField.vue
<template>
<v-text-field
v-bind="$attrs"
v-on="$listeners"
outlined
dense
>
<!-- Cycling through slots -->
<template v-for="(_, name) in $slots">
<template :slot="name">
<slot :name="name"></slot>
</template>
</template>
<!-- Cycling through scoped slots -->
<template
v-for="(_, name) in $scopedSlots"
#[name]="data"
>
<slot
:name="name"
v-bind="data"
></slot>
</template>
</v-text-field>
</template>
Everything works the same, except...
<template>
<v-container fluid>
<CustomVTextField
v-model="firstName"
placeholder="Insert first name"
hint="I'm an hint"
:outlined="false"
:dense="false"
#click="handleClick"
>
<template #label>
<b>First Name</b>
</template>
</CustomVTextField>
</v-container>
</template>
As you can see I'm not able to change the component defaults.
CustomVChipGroup.vue
<template>
<v-chip-group
v-bind="$attrs"
v-on="$listeners"
active-class="primary--text"
>
<!-- Cycling through slots -->
<template v-for="(_, name) in $slots">
<template :slot="name">
<slot :name="name"></slot>
</template>
</template>
<!-- Cycling through scoped slots -->
<template
v-for="(_, name) in $scopedSlots"
#[name]="data"
>
<slot
:name="name"
v-bind="data"
></slot>
</template>
</v-chip-group>
</template>
Console errors are solved, but watcher on selection is no more triggered.
<template>
<v-container fluid>
<CustomVChipGroup
v-model="selectedAnimal"
mandatory
#change="handleChange"
>
<v-chip value="DOG">
Dog
</v-chip>
<v-chip value="CAT">
Cat
</v-chip>
</CustomVChipGroup>
</v-container>
</template>
<script>
import CustomVChipGroup from '../components/CustomVChipGroup.vue'
export default {
components: {
CustomVChipGroup
},
data () {
return {
selectedAnimal: null
}
},
watch: {
/* BROKEN */
selectedAnimal () {
console.log('selected animal:', this.selectedAnimal)
}
},
methods: {
handleChange () {
console.log('change handled')
}
}
}
</script>
Edit #2
To solve both issues I came up with this solution.
CustomVChipGroup.vue
<template>
<v-chip-group
v-bind="computedAttrs"
v-on="$listeners"
>
<!-- Cycling through slots -->
<template v-for="(_, name) in $slots">
<template :slot="name">
<slot :name="name"></slot>
</template>
</template>
<!-- Cycling through scoped slots -->
<template
v-for="(_, name) in $scopedSlots"
#[name]="data"
>
<slot
:name="name"
v-bind="data"
></slot>
</template>
</v-chip-group>
</template>
<script>
export default {
model: {
event: 'change'
},
computed: {
computedAttrs () {
return {
'active-class': 'primary--text',
...this.$attrs
}
}
}
}
</script>
Unfortunately changing defaults is only possible if used attr and computedAttrs are both kebab-case or camelCase. I don't know if this is the best solution, but for now goes well.

Qsplitter output value

I have this Qsplitter:
<div class="filter-container">
<q-splitter v-model="splitterModel">
<template v-slot:before>
<div>
<q-splitter v-model="splitterModel2">
<template v-slot:before>
<Buttons/>
</template>
<template v-slot:after>
<Buttons/>
</template>
</q-splitter>
</div>
</template>
<template v-slot:after>
<div>
<q-splitter v-model="splitterModel3">
<template v-slot:before>
<Buttons/>
</template>
<template v-slot:after>
<Buttons/>
</template>
</q-splitter>
</div>
</template>
</q-splitter>
</div>
setup() {
const splitterModel = ref(50);
const splitterModel2 = ref(50);
const splitterModel3 = ref(50);
...
return {
splitterModel,
splitterModel2,
splitterModel3,
...
Is there any way to save the splitterModel value on mouse drag or when the splitterModel value is changed? I want to use the values later on the app.
Option 1
This can be achieved by using the splitter event #update:model-value seen in the docs here
Example
<div class="filter-container">
<q-splitter v-model="splitterModel" #update:model-value="handleUpdate">
<template v-slot:before>
<div>
<q-splitter v-model="splitterModel2">
<template v-slot:before>
<Buttons/>
</template>
<template v-slot:after>
<Buttons/>
</template>
</q-splitter>
</div>
</template>
<template v-slot:after>
<div>
<q-splitter v-model="splitterModel3">
<template v-slot:before>
<Buttons/>
</template>
<template v-slot:after>
<Buttons/>
</template>
</q-splitter>
</div>
</template>
</q-splitter>
</div>
setup() {
const splitterModel = ref(50);
const splitterModel2 = ref(50);
const splitterModel3 = ref(50);
function handleUpdate(value) {
// Do what you need to do with value here
console.log(value);
}
...
return {
splitterModel,
splitterModel2,
splitterModel3,
handleUpdate,
...
Option 2
Can also just set up a watcher as explained here

How to make available all slots and events with a different name

I created an ExpansionPanel component based on Vuetify <v-expansion-panel>. The idea is to have a component completely identical to the Vuetify one, plus a couple of useful features (loading, items counter, etc).
ExpansionPanel
<template>
<v-expansion-panel
v-bind="$props"
v-on="$listeners"
>
<template v-for="(_, name) in $slots">
<template :slot="name">
<slot :name="name"></slot>
</template>
</template>
<template
v-for="(_, name) in $scopedSlots"
#[name]="data"
>
<slot
:name="name"
v-bind="data"
></slot>
</template>
<!-- TODO: $listeners -->
<v-expansion-panel-header v-bind="headerProps">
<!-- TODO: slots -->
<v-row
align="center"
no-gutters
>
<span>{{ heading }}</span>
<v-spacer></v-spacer>
<v-chip
v-if="items !== undefined && !loading"
class="mr-4"
color="primary"
label
small
>
{{ $tc('component.expansionPanel.item', items) }}
</v-chip>
</v-row>
</v-expansion-panel-header>
<v-progress-linear
v-if="loading"
indeterminate
></v-progress-linear>
<!-- TODO: $listeners -->
<v-expansion-panel-content v-bind="contentProps">
<!-- TODO: slots -->
<v-row
v-if="message"
justify="center"
>
<v-col cols="auto">
{{ message }}
</v-col>
</v-row>
<slot
v-else
name="content"
></slot>
</v-expansion-panel-content>
</v-expansion-panel>
</template>
<script>
import { VExpansionPanel } from 'vuetify/lib'
export default {
extends: VExpansionPanel,
props: {
/* Custom props */
heading: String,
items: Number,
loading: Boolean,
headerProps: Object,
contentProps: Object
},
computed: {
message () {
return this.loading
? 'Loading items...'
: this.items !== undefined && !this.items
? 'No data available'
: null
}
}
}
</script>
As you can see I'm only able to pass props to <v-expansion-panel-header> and <v-expansion-panel-content> sub components. I need a way to also use their slots and listeners without conflicting with the <v-expansion-panel> ones, maybe a way to rename them before making them available.

How to make v-skeleton-loader inside v-for in vuetify?

I am trying to show a v-skeleton-loader in Vuetify. I have used v-if and v-else. If the image is not loaded, then it should show the skeleton loader. Otherwise, it should should show the image. This is my code:
<template>
<v-col v-for="option in options" :key="option.id" cols="6">
<v-lazy :options="{ threshold: 0.5 }" min-height="130">
<v-hover v-slot="{ hover }">
<v-card id="options_card" link width="160">
<v-sheet v-if="!images" class="px-3 pt-3 pb-3">
<v-skeleton-loader max-width="300" type="image"></v-skeleton-loader>
</v-sheet>
<v-img
v-else
id="thumbnail"
width="100%"
height="130"
:src="option.thumbnail"
></v-img>
</v-card>
</v-hover>
</v-lazy>
</v-col>
</template>
<script>
export default {
data() {
return {
images: false,
}
},
mounted() {
this.images = true
},
}
</script>
But the v-skeleton-loader is not seen on the screen.
VImage has a placeholder slot that would be used for customizing the loader component to be shown while the image is loading:
<v-img>
<template v-slot:placeholder>
<v-sheet>
<v-skeleton-loader />
</v-sheet>
</template>
</v-img>
demo
<v-img>
<template v-slot:placeholder>
<v-sheet>
<v-skeleton-loader />
</v-sheet>
</template>
</v-img>