Edit : Solved, see answer.
So i'm new to vue.js and quasar so it is probably a rookie mistake about vue lifecycle hooks and vue reactivity.
I want to resize an element based on the browser's size. The height of this element is calculated using the height of other elements. I use $refs to get the other element's height and I capture the onResize() event launched by a quasar component.
This event is launched once when the page is loading, and my element's sizes are all 0 because I guess they are not rendered in the DOM yet. I have a method to refresh my calculated height, which I call when "onResize" event is captured, and also at the "mounted()" vue.js hook.
My problem is :
the first onResize() calls the method but all elements have 0px in height.
mounted() calls the method again and here all elements have their height calculated. The results are good, but it does not show on the display, see screenshot #1 : resize events and sizes logged in the console, note that the size is calculated twice, once on onResize() and once on mounted() . the one on mounted() has the good value but it does not show in the DOM.
after I resize the window once, then everything is ok and i don't have any problem anymore. (screenshots #2 (window mode) and #3 (fullscreen again))
My question is : why the height is not update in the DOM when mounted() hook is called even if it is calculated correctly ? (everything is in the same .vue file)
My code :
My problem is with the height of the div that has the "tableRow" ref
<template>
<q-page>
<div class="row" :style="'height: '+pageSize.height*0.95+'px;'">
<div class="col-6 q-pa-lg">
<div class="row" ref="actionsRow">
<div class="col-6 q-mb-sm">
<q-search hide-underline v-model="filter" />
</div>
<div class="col-6">
</div>
</div>
<div class="row" ref="tableHeaderRow">
<q-table class="col-12" :selection="selectionMode" :selected.sync="selectedRows" :data="templateTableData" :columns="templateColumns"
row-key="slug" :pagination.sync="pagination" dense hide-bottom>
<q-tr slot="body" slot-scope="props" :props="props">
</q-tr>
</q-table>
</div>
<div class="row" ref="tableRow" :style="'height: '+tableHeight+'px;'">
<q-scroll-area style="height: 100%" class="col-12 q-mt-sm shadow-3">
<q-table :selection="selectionMode" :selected.sync="selectedRows" :data="templateTableData" :columns="templateColumns" row-key="slug"
:filter="filter" :pagination.sync="pagination" dense hide-bottom hide-header>
<q-tr slot="body" slot-scope="props" :props="props" #click.native="onRowClick(props.row)" class="cursor-pointer">
<q-td auto-width>
<q-checkbox color="primary" v-model="props.selected" />
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</q-table>
</q-scroll-area>
</div>
</div>
<router-view class="col-6 q-pa-lg">
</router-view>
</div>
<q-window-resize-observable #resize="onResize" />
</q-page>
</template>
Script:
var data = []
for (var i = 1; i <= 100; i++) {
data.push({
id: i,
name: 'Template ' + i,
slug: 'template' + i,
active: true
})
}
import { mapActions, mapGetters } from 'vuex'
export default {
data: () => ({
pageSize: {
height: 0,
width: 0
},
tableHeight: 0,
templateColumns: [
{
name: 'templateName',
required: true,
label: 'Name',
align: 'left',
field: 'name',
sortable: true
},
{
name: 'templateSlug',
label: 'Slug',
align: 'left',
field: 'slug',
sortable: true
},
{
name: 'templateActive',
label: 'Active',
align: 'left',
field: 'active',
sortable: true,
sort: (a, b) => {
if ((a && b) || (!a && !b)) {
return 0
} else if (a) {
return 1
} else {
return -1
}
}
}
],
selectionMode: 'multiple',
selectedRows: [],
pagination: {
sortBy: null, // String, column "name" property value
descending: false,
page: 1,
rowsPerPage: 0 // current rows per page being displayed
},
templateTableData: data,
filter: ''
}),
computed: {
...mapGetters('appUtils', [
'getPageTitle',
'allConst'
])
},
methods: {
...mapActions('appUtils', [
'setPageTitle',
'deletePageTitle'
]),
onResize (size) {
this.pageSize.height = size.height - this.getPageTitle.height
this.resizeTable()
console.log('ON RESIZE EVENT:')
console.log('tableHeaderRow:'+
this.$refs.tableHeaderRow.clientHeight)
console.log('actionsRow:' + this.$refs.actionsRow.clientHeight)
console.log('table:' + this.tableHeight)
},
onRowClick (row) {
this.$router.push('/templates/' + row.slug)
},
resizeTable () {
this.tableHeight = this.pageSize.height - this.$refs.actionsRow.clientHeight -
this.$refs.tableHeaderRow.clientHeight - this.getPageTitle.height -
this.allConst.templatePageHeaderMargins
}
},
mounted () {
console.log('MOUNT TEMPLATES')
this.setPageTitle({ text: 'Manage templates', height: this.allConst.titleHeight })
this.resizeTable()
console.log('tableHeaderRow:' + this.$refs.tableHeaderRow.clientHeight)
console.log('actionsRow:' + this.$refs.actionsRow.clientHeight)
console.log('table:' + this.tableHeight)
},
destroyed () {
console.log('DESTROY TEMPLATES')
this.deletePageTitle()
}
}
Another one of my variables (titleSize) was 0, like the other sizes, during the first onResize() event, and I did not take that into account to correct it during the mounted() Hook.
Related
I have created a reusable input component with label, but i want label to hide(if hidden it should not take a space something like display none in css) on some place and label should be visible on some places
here is my code of the input component
<template>
<div>
<label for="" :label="label" class="mb-1 select-label">{{label}}</label> //hide or visible depending on requirement
<div class="custom-select" :tabindex="tabindex" #blur="open = false">
<div class="selected" :class="{ open: open }" #click="open = !open">
{{ selected }}
</div>
<div class="items" :class="{ selectHide: !open }">
<div
v-for="(option, i) of options"
:key="i"
#click="
selected = option;
open = false;
$emit('input', option);
"
class="border-bottom px-3"
>
{{ option }}
</div>
</div>
</div>
</div>
</template>
here is the code of my script
<script>
export default {
props: {
label: {
type: String,
required: false,
default: ''
},
options: {
type: Array,
required: true,
},
default: {
type: String,
required: false,
default: null,
},
tabindex: {
type: Number,
required: false,
default: 0,
},
},
data() {
return {
selected: this.default
? this.default
: this.options.length > 0
? this.options[0]
: null,
open: false,
};
},
mounted() {
this.$emit("input", this.selected);
},
}
</script>
You can use v-if directive to conditionally render the label element based on the props value.
As v-if will actually destroy and recreate elements when the conditional is toggled. Hence, all the classes/attributes applied to the element will also destroy.
Demo :
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
showMessage: false
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<p v-if="showMessage">{{ message }}</p>
</div>
If you will run above code snippet and open the developer console. You will see that <p> element will not be there as it has been removed from the DOM.
console screenshot :
You can just add a prop to control label visibility
script:
props: {
showLabel: {
type: Boolean,
default: false
}
}
template:
<label v-show="showLabel" />
parent component:
<MyCustomInput :show-label="false" />
<MyCustomInput :show-label="true" />
I have a hierarchical list component where child items have checkboxes. Checkbox actions(check/uncheck) must keep the parent component in sync with the checkbox's changed state. I cannot figure out how to achieve this using v-bind.sync recursively. My code is as below:
Menu.vue
This component holds the hierarchical list. (Only relevant code included)
HierarchicalCheckboxList is the component that displays the hierarchical list
Property 'value' holds the check/uncheck value (true/false)
Property 'children' contains the child list items
How do I define the .sync attribute on HierarchicalCheckboxList and with what parameter?
<template>
<div>
<HierarchicalCheckboxList
v-for="link in links"
#checked="primaryCheckChanged"
:key="link.id"
v-bind="link">
</HierarchicalCheckboxList>
</div>
</template>
<script>
import HierarchicalCheckboxList from 'components/HierarchicalCheckboxList'
data () {
return {
links: [{
id: 1,
title: 'Home',
caption: 'Feeds, Dashboard & more',
icon: 'account_box',
level: 0,
children: [{
id: 2,
title: 'Feeds',
icon: 'feeds',value: true,
level: 1,
children: [{
id: '3',
title: 'Dashboard',
icon: 'settings',
value: true,
level: 1
}]
}]
}]
}
},
methods: {
primaryCheckChanged (d) {
// A child's checked state is propogated till here
console.log(d)
}
}
</script>
HierarchicalCheckboxList.vue
This component calls itself recursively:
<template>
<div>
<div v-if="children != undefined && children.length == 0">
<!--/admin/user/user-->
<q-item clickable v-ripple :inset-level="level" :to="goto">
<q-item-section>
{{title}}
</q-item-section>
</q-item>
</div>
<div v-else>
<div v-if="children != undefined && children.length > 0">
<!-- {{children}} -->
<q-expansion-item
expand-separator
:icon="icon"
:label="title"
:caption="caption"
:header-inset-level="level"
default-closed>
<template v-slot:header>
<q-item-section>
{{ title }}
</q-item-section>
<q-item-section side>
<div class="row items-center">
<q-btn icon="add" dense flat color="secondary"></q-btn>
</div>
</q-item-section>
</template>
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
#checked="primaryCheckChanged"
v-bind="child">
</HierarchicalCheckboxList>
</q-expansion-item>
</div>
<!-- to="/admin/user/user" -->
<div v-else>
<q-item clickable v-ripple :inset-level="level">
<q-item-section>
<q-checkbox :label="title" v-model="selection" />
</q-item-section>
</q-item>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'HierarchicalCheckboxList',
props: {
id: { type: String, required: true },
title: { type: String, required: false },
caption: { type: String, default: '' },
icon: { type: String, default: '' },
value: { type: Boolean, default: false },
level: { type: Number, default: 0 },
children: { type: Array }
},
data () {
return {
localValue: this.$props.value
}
},
computed: {
selection: {
get: function () {
return this.localValue
},
set: function (newvalue) {
this.localValue = newvalue
this.$emit('checked', this.localValue)
// or this.$emit('checked', {id: this.$props.id, value: this.localValue })
}
}
},
methods: {
primaryCheckChanged (d) {
this.$emit('checked', d)
}
}
}
</script>
What works so far
As a work-around I am able to get the checkbox state emitted with $emit('checked'), which I use to send it to the next process. But the parent's state is not updated until I refresh it back from the database.
How do I update the parent component's state using v-bind.sync recursively?
Appreciate any help!!
UI
Figured out how to do it after I broke the code down from the whole 2000 line code to a separate 'trial-n-error' code of 20 lines and then things became simple and clear.
Menu.vue
A few changes in the parent component in the HierarchicalCheckboxList declaration:
Note the sync property
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="link.value"
v-bind="child">
</HierarchicalCheckboxList>
HierarchicalCheckboxList.vue
Change the same line of code in the child component (as its recursive)
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="child.value"
v-bind="child">
</HierarchicalCheckboxList>
And in the computed set property, emit as below:
this.$emit('update:u', this.localValue)
That's it - parent n children components now stay in snyc.
Trying to get a 'displayImages' array as a computed property. Using a default 'selected' property = 0.
this.selected changes accordingly on mouseover and click events.
When trying to get the computed 'displayImages' it says:
"this.variations[this.selected] is undefined."
I'm using an api to get my product data and images.
<template>
<div id="product-page">
<v-card width="100%" class="product-card">
<div class="image-carousel">
<v-carousel height="100%" continuos hide-delimiters>
<v-carousel-item
v-for="(image, i) in displayImages"
:key="i"
:src="image"
>
</v-carousel-item>
</v-carousel>
</div>
<div class="details">
<h2>{{ this.title }}<br />Price: ${{ this.price }}</h2>
<p>{{ this.details }}</p>
<ul style="list-style: none; padding: 0">
<li
style="border: 1px solid red; width: auto"
v-for="(color, index) in variations"
:key="index"
#mouseover="updateProduct(index)"
#click="updateProduct(index)"
>
{{ color.color }}
</li>
</ul>
<div class="buttons">
<v-btn outlined rounded
>ADD TO CART<v-icon right>mdi-cart-plus</v-icon></v-btn
>
<router-link to="/shop">
<v-btn text outlined rounded> BACK TO SHOP</v-btn>
</router-link>
</div>
</div>
</v-card>
</div>
</template>
<script>
export default {
name: "Product",
props: ["APIurl"],
data: () => ({
title: "",
details: "",
price: "",
variations: [],
selected: 0,
}),
created() {
fetch(this.APIurl + "/products/" + this.$route.params.id)
.then((response) => response.json())
.then((data) => {
//console.log(data);
this.title = data.title;
this.details = data.details.toLowerCase();
this.price = data.price;
data.variations.forEach((element) => {
let imagesArray = element.photos.map(
(image) => this.APIurl + image.url
);
this.variations.push({
color: element.title,
images: imagesArray,
qty: element.qty,
productId: element.productId,
});
});
});
},
computed: {
displayImages() {
return this.variations[this.selected].images;
},
},
methods: {
updateProduct: function (index) {
this.selected = index;
console.log(index);
}
},
};
</script>
To properly expand on my comment, the reason why you are running into an error is because when the computed is being accessed in the template, this.variations is an empty array. It is only being populated asynchronously, so chances are, it is empty when VueJS attempts to use it when rendering the virtual DOM.
For that reason, accessing an item within it by index (given as this.selected) will return undefined. Therefore, attempting to access a property called images in the undefined object will return an error.
To fix this problem, all you need is to introduce a guard clause in your computed as such:
computed: {
displayImages() {
const variation = this.variations[this.selected];
// GUARD: If variation is falsy, return empty array
if (!variation) {
return [];
}
return variation.images;
},
}
Bonus tip: if you one day would consider using TypeScript, you can even simplify it as such... but that's a discussion for another day ;) for now, optional chaining and the nullish coalescing operator is only supported by bleeding edge versions of evergreen browsers.
computed: {
displayImages() {
return this.variations[this.selected]?.images ?? [];
},
}
For avoid this kind of error, you must to use the safe navigation property.
Remember, it's useful just when the app is loading.
Try something like that:
<script>
export default {
name: 'Product',
computed: {
displayImages() {
if (this.variations[this.selected]) {
return this.variations[this.selected].images;
}
return [];
},
},
};
</script>
I have a number input inside a checkbox label, as shown in the screenshot above. When I click the input's plus/minus buttons to change the number, it also changes the checkbox's checked-value as an unintended side effect. How do I prevent the side effect?
<template>
<el-checkbox-group v-model="auditFinding" #change="checkAuditFinding" style="display:flex;flex-direction: column;">
<el-checkbox v-for="item in auditFindings" :key="item.value" :label="item.label">
<el-input-number v-if="item.value !== 'N/A'" v-model="item.num" :disabled="item.disabled" :min="0" :max="99" size="small" />
{{ item.value }}
</el-checkbox>
</el-checkbox-group>
</template>
<script>
export default {
//...
methods: {
checkAuditFinding(val) {
const t = val.toString()
this.auditFindings.map(item => {
if (val.indexOf(item.value) > -1) {
item.disabled = false
} else {
item.disabled = true
}
})
},
}
}
</script>
No. this is incorrect nest for your goal.
clicking on any nested element also fires click event on parent.
All you can do is keep checkbox and number as siblings. not inherited.
<el-checkbox-group v-model="auditFinding" style="display:flex;flex-direction: column;">
<div v-for="item in auditFindings">
<el-checkbox #change="checkAuditFinding" :key="item.value" :label="item.label" />
<el-input-number v-if="item.value !== 'N/A'" v-model="item.num" :disabled="item.disabled" :min="0" :max="99" size="small" />
{{ item.value }}
</div>
</el-checkbox-group>
You could stop the click-event propagation from the el-input-number element by using the #click.native.prevent event modifiers.
.native binds a handler for a native DOM event (click in this case). The caveat to this modifier is it depends on the implementation of el-nput-number (the root element must always emit click event).
.prevent invokes Event.preventDefault to effectively cancel the click-event, preventing it from reaching the parent checkbox.
new Vue({
el: '#app',
data() {
return {
auditFinding: false,
auditFindings: [
{ value: 11, label: 'label A', disabled: false, num: 1 },
{ value: 22, label: 'label B', disabled: false, num: 2 },
{ value: 33, label: 'label C', disabled: false, num: 3 },
]
}
},
methods: {
checkAuditFinding(e) {
console.log('checkAuditFinding', e)
},
}
})
<script src="https://unpkg.com/vue#2.6.11/dist/vue.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-ui#2.13.0/lib/theme-chalk/index.css">
<script src="https://unpkg.com/element-ui#2.13.0/lib/index.js"></script>
<div id="app">
<el-checkbox-group v-model="auditFinding" #change="checkAuditFinding" style="display:flex;flex-direction: column;">
<el-checkbox v-for="item in auditFindings" :key="item.value" :label="item.label">
<el-input-number #click.native.prevent
v-if="item.value !== 'N/A'"
v-model="item.num"
:disabled="item.disabled"
:min="0"
:max="99"
size="small"
label="item.label" />
{{ item.value }}
</el-checkbox>
</el-checkbox-group>
</div>
I have an expanding data table in my parent component and a child component inside the expanded row with a button. I would like to change the background color of the associated row when I click the button inside the child component. I'm not sure how to target the row to add the css class on event.
ScanGrid(parent):
<template>
<v-flex v-if="items.length === 0">
<ScanAdd #selectBatch="showScan" />
</v-flex>
<v-card v-else class="ma-5">
<v-card-text>
<v-layout align-center>
<v-data-table
:headers="headers"
:items="items"
item-key="StorageName"
show-expand
single-expand
:expanded="expanded"
hide-default-footer
#click:row="clickedRow"
>
<template
#isDeleted="deleteRow"
v-if="groupBy === 'barCode'"
v-slot:expanded-item="{ item }"
>
<td :colspan="12">
<ScanGridCode :item="item" />
</td>
</template>
<template v-else v-slot:expanded-item="{ item }">
<td :colspan="12">
<ScanGridDef :item="item" />
</td>
</template>
</v-data-table>
</v-layout>
</v-card-text>
</v-card>
</template>
<script>
import { API } from "#/api";
import ScanAdd from "./ScanAdd";
import ScanGridCode from "./ScanGridCode";
import ScanGridDef from "./ScanGridDef";
export default {
name: "ScanGrid",
props: {
items: {
type: Array,
required: true
}
},
components: {
ScanGridCode,
ScanGridDef,
ScanAdd
},
methods: {
deleteRow(value) {
this.isDeleted = value;
},
showScan(value) {
this.selectedId = value;
this.addScanBatch(value);
this.$emit("processingBatch", true);
this.processingBatch = true;
},
async addScanBatch(Id) {
const selectedItems = await API.getPhysicalInventoryBatch(Id);
if (selectedItems.data.Id === this.selectedId) {
this.items = selectedItems.data.Locations;
}
},
clickedRow(value) {
if (
this.expanded.length &&
this.expanded[0].StorageName == value.StorageName
) {
this.expanded = [];
} else {
this.expanded = [];
this.expanded.push(value);
}
}
},
data: () => ({
isDeleted: false,
groupBy: "barCode",
expanded: [],
items: [],
toDelete: "",
totalResults: 0,
loading: true,
headers: [
{
text: "Localisation",
sortable: true,
value: "StorageName",
class: "large-column font-weight"
},
{
text: "Paquets scannés",
sortable: true,
value: "ScannedProduct",
class: "large-column font-weight"
},
{
text: "Paquets entrants",
sortable: true,
value: "Incoming",
class: "large-column font-weight"
},
{
text: "Paquets sortants",
sortable: true,
value: "Outgoing",
class: "large-column font-weight"
},
{
text: "Paquets inconnus",
sortable: true,
value: "Unknown",
class: "large-column font-weight"
}
]
})
};
</script>
ScanGridCode(child):
<template>
<div class="codeContainer">
<div class="cancelLocation">
<v-flex class="justify-center">
<v-btn class="ma-5" large color="lowerCase" tile #click="deleteLocation"
>Annuler le dépôt de cette localisation</v-btn
>
</v-flex>
</div>
</div>
</template>
<script>
export default {
name: "ScanGridCode",
props: {
item: {
type: Object,
required: true
}
},
methods: {
deleteLocation() {
this.item.IsDeleted = true;
this.$emit("IsDeleted", true);
}
},
data: () => ({
IsDeleted: false,
groupBy: 0,
headersGroupCode: [
{
text: "Code barre",
sortable: true,
value: "SerialNumber",
class: "large-column font-weight-light"
},
{
text: "De",
sortable: true,
value: "FromLocation",
class: "large-column font-weight-light"
},
{
text: "Vers",
sortable: true,
value: "ToLocation",
class: "large-column font-weight-light"
}
]
})
};
</script>
I use Vuetify 2.1.7 and Vue 2.6.10. When I click on the button I call deleteLocation function. I assume I need to $emit a value to my parent but after that I don't know how to target the tr to change its style.
Since you're using Vuex, I would suggest using some variable such as store.state.selectedRow to keep track of whether or not a row has been selected (or in cases where there are more than one row, which row has been selected). Then you can have a computed property myProperty = this.$store.state.selectedRow in your Vue component which will automatically reflect the single source of truth, and your conditional class can be bound to this myProperty. This means you don't need to worry about emitting on events.
The approach to emitting the event is what should be done. So I am assuming you will emit from deleteLocation function.
Since you need a custom styling on rows you need to add the items slot and add your logic there
<template v-slot:item="{ item, select}">
<tr :class="key === coloredRow ? 'custom-highlight-row' : ''">
<td :colspan="12">
<ScanGridCode #changeColor="changeColor(key)" :item="item" />
</td>
//add this method to your script element
changeColor(idx) {
this.coloredRow = idx;
}