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