Which is the best way to extend a Vuetify component? - vuejs2

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.

Related

Vue.js child component to populate the slots of a dynamic component inside parent component

How can I render the header slot to the table component from the child component? This code is not working:
Table component
<template>
<div>
<slot name="header"></slot>
</div>
</template>
Parent component
<template>
<div>
<component :is="currentComponent" > </component>
</div>
</template>
<script>
export default {
data() {
return {
currentComponent: 'Table'
};
},
};
</script>
Child component
<template>
<div>
<parent-component>
<template slot="header">
<h1>test</h1>
</template>
</parent-component>
</div>
</template>
Tried the code above but it dosent work
Expecting to render title from child component
Solution:
Table component
<template>
<div>
<slot name="header"></slot>
</div>
</template>
Parent component
**<template>
<div>
<component :is="currentComponent" >
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData || {}" />
</template>
</component>
</div>
</template>**
<script>
export default {
data() {
return {
currentComponent: 'Table'
};
},
};
</script>
Child component
<template>
<div>
<parent-component>
<template slot="header">
<h1>test</h1>
</template>
</parent-component>
</div>
</template>

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>

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.

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

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?

Vuetify: Adaptive v-date-picker width v-dialog and v-menu

I want to do adaptive v-date-picker, i.e when phone page width then date picker open in v-dialog, and when desktop then data picker open in v-menu.
It's my try:
<template>
<div>
<template v-if="$vuetify.breakpoint.xsOnly">
<v-dialog
ref="dialog"
v-model="modal"
persistent
width="290px"
>
<template v-slot:activator="{ on }">
<slot name="input" ref="input" v-on="on"/>
</template>
<slot name="picker" ref="picker"/>
</v-dialog>
</template>
<template v-else>
<v-menu
ref="menu"
v-model="menu"
:close-on-content-click="false"
transition="scale-transition"
offset-y
min-width="290px"
>
<template v-slot:activator="{ on }">
<slot name="input" ref="input" v-on="on"/>
</template>
<slot name="picker" ref="picker"/>
</v-menu>
</template>
</div>
</template>
<script>
export default {
name: "v-date",
data() {
return {
menu: false,
modal: false,
}
},
methods: {
close() {
this.menu = false;
this.modal = false;
}
}
}
</script>
But v-on doesn't work. I try :listeners="on", it doesn't work too...
For example use component:
<v-date>
<template v-slot:input>
<v-text-field
label="Дедлайн"
v-model="data.deadline"
readonly
/>
</template>
<template v-slot:picker>
<v-date-picker v-model="data.deadline" no-title scrollable>
<v-spacer></v-spacer>
<v-btn text color="primary" #click="$refs.deadline.close()">ОК</v-btn>
</v-date-picker>
</template>
</v-date>
Thanks to KrasnokutskiyEA for the idea.
Work version:
<template>
<div>
<template v-if="$vuetify.breakpoint.xsOnly">
<v-dialog
ref="dialog"
v-model="modal"
persistent
width="290px"
>
<template v-slot:activator="{ on }">
<slot name="input" ref="input" :on="on"/>
</template>
<slot name="picker" ref="picker"/>
</v-dialog>
</template>
<template v-else>
<v-menu
ref="menu"
v-model="menu"
:close-on-content-click="false"
transition="scale-transition"
offset-y
min-width="290px"
>
<template v-slot:activator="{ on }">
<slot name="input" ref="input" :on="on"/>
</template>
<slot name="picker" ref="picker"/>
</v-menu>
</template>
</div>
</template>
<script>
export default {
name: "v-date",
data() {
return {
menu: false,
modal: false,
}
},
methods: {
close() {
this.menu = false;
this.modal = false;
}
}
}
</script>
Use:
<v-date>
<template v-slot:input="{ on }">
<v-text-field
label="Дедлайн"
v-model="data.deadline"
readonly
v-on="on"
/>
</template>
<template v-slot:picker>
<v-date-picker v-model="data.deadline" no-title scrollable>
<v-spacer></v-spacer>
<v-btn text color="primary" #click="$refs.deadline.close()">ОК</v-btn>
</v-date-picker>
</template>
</v-date>