Qsplitter output value - vue.js

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

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.

How to specify when a Vue Component has extra functionality?

I have a table component, and this table component has the ability to select attributes, however I only want this ability to be available when I need it to be, aka not every time it's rendered. How do I do this?
Functionality snippet from Component.vue to only be active when in a certain file ie Component2.vue
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<b-table-column field="columnValue" v-slot="props2" class="attr-column">
<b-table :bordered="false" class="attr-table" :striped="true" :data="props2.row.columnValues">
<b-table-column field="columnName" v-slot="itemProps">
<SelectableAttribute :attr-name="props2.row.fieldClass" :attr-id="itemProps.row.id" :model-id="itemProps.row.id" model-name="NewParticipant">
{{ itemProps.row.value }}
</SelectableAttribute>
</b-table-column>
</b-table>
</b-table-column>
</b-table>
Component.vue
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template>
<b-table :striped="striped" :bordered="false" :data="participants" detailed class="participant-table" :row-class="() => 'participant-row'">
<b-table-column field="primaryAlias" :label="t('participant.table.primary_alias')" v-slot="props">
<template v-if="props.row.primaryAlias">
<SelectableAttribute attr-name="Alias" :attr-id="props.row.primaryAlias.id" :model-id="props.row.id" model-name="NewParticipant">
{{ props.row.primaryAlias.value }}
</SelectableAttribute>
</template>
<template v-else>-</template>
</b-table-column>
<b-table-column field="primaryEmail" :label="t('participant.table.primary_email')" v-slot="props">
<template v-if="props.row.primaryEmail">
<SelectableAttribute attr-name="Email" :attr-id="props.row.primaryEmail.id" :model-id="props.row.id" model-name="NewParticipant">
{{ props.row.primaryEmail.value }}
</SelectableAttribute>
</template>
<template v-else>-</template>
</b-table-column>
<b-table-column field="primaryAddress" :label="t('participant.table.primary_address')" v-slot="props">
<template v-if="props.row.primaryAddress">
<SelectableAttribute attr-name="Address" :attr-id="props.row.primaryAddress.id" :model-id="props.row.id" model-name="NewParticipant">
{{ props.row.primaryAddress.value }}
</SelectableAttribute>
</template>
<template v-else>-</template>
</b-table-column>
*<b-table-column field="primaryPhone" :label="t('participant.table.primary_phone')" v-slot="props">
<template v-if="props.row.primaryPhone">
<SelectableAttribute attr-name="Phone" :attr-id="props.row.primaryPhone.id" :model-id="props.row.id" model-name="NewParticipant">
{{ props.row.primaryPhone.value }}
</SelectableAttribute>
</template>
<template v-else>-</template>
</b-table-column>*
<b-table-column v-slot="props" cell-class="cell-action">
<slot v-bind="props.row">
</slot>
</b-table-column>
<template slot="detail" slot-scope="props">
<b-table class="attrs-detail-container" :data="tableDataToDataValueCells(props.row)" cell-class="with-bottom-border">
<b-table-column field="columnName" v-slot="props">
<b>{{ props.row.columnName }}</b>
</b-table-column>
<b-table-column field="columnValue" v-slot="props2" class="attr-column">
<b-table :bordered="false" class="attr-table" :striped="true" :data="props2.row.columnValues">
<b-table-column field="columnName" v-slot="itemProps">
<SelectableAttribute
:attr-name="props2.row.fieldClass"
:attr-id="itemProps.row.id"
:model-id="itemProps.row.id"
model-name="NewParticipant"
>
{{ itemProps.row.value }}
</SelectableAttribute>
</b-table-column>
</b-table>
</b-table-column>
</b-table>
</template>
</b-table>
</template>
You can achieve this by using props that you pass while calling that component. That prop could be anything you need. Here is a small example with simple true/false prop:
// Set the prop in your called component, in this case a boolean
props: {
myBoolean: Boolean
}
// Passing the prop from your parent to the component, where it has to be a property, in this case called myBooleanFromParent
<my-component :myBoolean="myBooleanFromParent"></my-component>
// In your component your template changes according to the passed prop from your parent
<template>
<div>
<div v-if="myBoolean">
If myBoolean is true
</div>
<div v-else>
Else, so if myBoolean is false
</div>
</div>
</template>
This is, as stated, a small and simple example. You can pass any kind of data with props. It could also be a object full of data to handle multiple conditions.

How to create a wrapper component in vuejs

I am trying to create a wrapper component around an existing component (q-table from the quasar-framework).
<!-- MyTable.vue -->
<template>
<q-table :title="title" ...>
<template v-slot:top="props">
...
</template>
<template v-slot:body="props">
<q-tr :props="props">
...
</q-tr>
</template>
</q-table>
</template>
<template v-slot:body="props"> and <template v-slot:top="props"> are slots of the q-table component.
How to overwrite these slots in the App.vue:
<!-- App.vue -->
<template>
<div>
<my-table>
???
</my-table>
</div>
</template>

Passing events between Vue parent and child

I am trying to emit an event from a child and listen for it on the parent, but I am always getting errors like Invalid handler for event "new-tab": got undefined.
My app:
<div class="ibox" id="app">
<div class="ibox-content">
<h2>Table</h2>
<div class="details">
<table-tabs
v-on:new-tab="newTab"
v-on:close-tab="closeTab"
ref="tableTabs"
name="table-tabs"
></table-tabs>
</div>
</div>
</div>
TableTabs component:
<template>
<div>
<b-card no-body>
<b-tabs card>
<b-tab active>
<template slot="title">
<i class="fa fa-tablet-alt"></i> Orders
</template>
<orders-table
name="orders-table"
></orders-table>
</b-tab>
<b-tab v-for="order in tabs" :key="i">
<template slot="title">
<div>{{ order.name }}</div>
<b-button type="button" class="close float-right" aria-label="Close" #click="closeTab(order.id)">
<span aria-hidden="true">×</span>
</b-button>
</template>
<items-table
name="items-table"
:api-url="'/api/items/' + order.id"
></items-table>
</b-tab>
</b-tabs>
</b-card>
</div>
</template>
<script>
export default {
name: 'table-tabs',
data() {
return {
tabs: [],
}
},
methods: {
closeTab(id) {
for (let i = 0; i < this.tabs.length; i++) {
if (this.tabs[i].id === id) {
this.tabs.splice(i, 1);
}
}
},
newTab(item) {
this.tabs.push(item);
}
}
}
</script>
OrdersTable component:
<template>
<div>
<vuetable ref="vuetable"
:api-url="apiUrl"
:fields="fields"
pagination-path="pagination"
#vuetable:pagination-data="onPaginationData"
>
<div slot="name-slot" slot-scope="{ rowData }">
<a href="#" class="float-left" #click="newTab(rowData.order)">
{{ rowData.order.name }}
<span class="fa fa-search-plus"></span>
</a>
</div>
</vuetable>
<div class="row">
<div class="col-md-6">
<vuetable-pagination-info
ref="paginationInfo"
></vuetable-pagination-info>
</div>
<div class="col-md-6">
<vuetable-pagination-bootstrap
ref="pagination"
class="pull-right"
#vuetable-pagination:change-page="onChangePage"
></vuetable-pagination-bootstrap>
</div>
</div>
</div>
</template>
<script>
import Vuetable from 'vuetable-2/src/components/Vuetable';
import VuetablePaginationBootstrap from '../VuetablePaginationBootstrap';
import VuetablePaginationInfo from 'vuetable-2/src/components/VuetablePaginationInfo';
import TableFieldDef from './table-field-def';
export default {
name: 'orders-table',
components: {
Vuetable,
VuetablePaginationBootstrap,
VuetablePaginationInfo
},
data() {
return {
apiUrl: '/api/orders?include=items',
fields: TableFieldDef,
}
},
methods: {
newTab(order) {
this.$emit('new-tab', order);
}
}
}
</script>
Why does this.$emit('new-tab', order) always result in the handler newTab being undefined.
Vue 2.5.21
The parent in this case is the TableTabs component. If the parent needs to listen to events emitted by a child component then it needs to add the event listener to the child component, which is the OrdersTable component in this case. So instead of this ..
<table-tabs
v-on:new-tab="newTab"
v-on:close-tab="closeTab"
ref="tableTabs"
name="table-tabs"
></table-tabs>
You should do this (inside the TableTabs component) ..
<orders-table
name="orders-table"
v-on:new-tab="newTab"
></orders-table>