StencilJS - Issue with nested child components and a checkbox not being updated - stenciljs

UPDATE: Solved by separating rendering into different functions and passing it the MenuItem:
renderItem(item: TreeMenuItem) {
console.log('render item:', item.label);
return (
<div>
<li>
{item.children ? <button onClick={this.toggleDisplayChildren}>{this.displayChildren ? 'v' : '>'}</button> : ''}
{this.showCheckbox ? <input type="checkbox" checked={item.selected} onClick={() => this.selectItem(item)} /> : ''}
{item.label}
</li>
{this.renderChildren(item)}
</div>
);
}
renderChildren(item: TreeMenuItem) {
return item.children && this.displayChildren
? item.children.map(child => {
child.parent = item;
return this.renderItem(child);
})
: '';
}
render() {
return <Host>{this.renderItem(this.item)}</Host>;
}
I'm having some difficulties figuring this out. I have an object with nested children and I am getting it rendered correctly. My issue lies in that I'm looking to add functionality so that when I select a parent, it will automatically extend all children elements as well(and grandchildren, etc). I am currently only able to do this for immediate children, but not any children of the children, etc.
I apologize for the verbose code. I tried to trim it down as much as possible while still showcasing the relevant parts. I am hoping to be able to achieve this without having a displayChild boolean property in the object itself, but rather use:
#Prop({ mutable: true }) displayChildren = false;
toggleDisplayChildren = () => {
this.displayChildren = !this.displayChildren;
};
My issue is that when I try to do it this way(in a forEach loop), the this keyword always refers to the parent calling the children, not the individual children.
TL;DR: When displayChildren = true for a parent element, I would like for every child element of that parent that also has children under it to also displayChildren = true.
Side note: I'm also having an issue where the checkbox is not updating automatically when item.selected is being set. It only updates when displayChildren is changed.
selectItem() {
this.item.selected = !this.item.selected;
this.isParent() ? this.selectParent() : this.selectChild();
this.item = { ...this.item };
}
private selectParent() {
if (!this.isParent()) return;
if (this.item.indeterminate) this.item.indeterminate = false;
if (!this.displayChildren) this.displayChildren = true;
console.log('Selecting parent.');
if (this.item.children.length === 1) {
this.item.children[0].selected = this.item.selected;
console.log('Only one child, so setting same as parent.');
} else {
this.item.children.map(child => {
child.selected = true;
console.log(`${child.label} selected: ${child.selected}`);
if (child.children) {
console.log(`${child.label} has children, so do them too.`);
}
});
}
}
items: TreeMenuItem[] = [
{
label: 'Plants',
value: 'plants',
children: [
{
label: 'Mono-Cotyledons',
value: 'monocotyledons',
children: [
{
label: 'Coconut',
value: 'coconut',
selected: false,
indeterminate: false,
disabled: false,
},
{ label: 'Banana', value: 'banana', selected: false, indeterminate: false, disabled: false },
],
selected: false,
indeterminate: false,
disabled: false,
},
{
label: 'Di-Cotyledons',
value: 'dicotyledons',
children: [{ label: 'Mango', value: 'mango', selected: false, indeterminate: false, disabled: false }],
selected: false,
indeterminate: false,
disabled: false,
},
],
selected: false,
indeterminate: false,
disabled: false,
},
];
I am rendering it as such:
render() {
return (
<div>
<li>
{this.item.children ? <button onClick={this.toggleDisplayChildren}>{this.displayChildren ? 'v' : '>'}</button> : ''}
{this.showCheckbox ? <input type="checkbox" checked={this.item.selected} onClick={() => this.selectItem()} style={{ color: 'red' }} /> : ''}
{this.item.label}
</li>
{this.item.children && this.displayChildren
? this.item.children.map(child => {
child.parent = this.item;
return <menu-item item={child} class="child-item"></menu-item>;
})
: ''}
</div>
);
}

Related

Cannot read properties of undefined on formatter, but data is showing fine

I have a bootstrap table that shows a list of appliances. I am importing my data with Axios and for this specific table I am outputting data from two database tables, so I have one object which is called applianceReferences which stores another object called activeAppliances.
Not sure if it is relevant for this question, but just so you know.
Before talking about the problem, let me just post the whole code and below I will talk about the section that is giving me issues.
<template>
<b-container class="my-2">
<b-card v-if="showTable" class="ml-4 mr-4">
<b-table
search-placeholder="search"
:filter-included-fields="fields.map(f => f.key)"
include-filter
:items="applianceReferences"
:fields="fields"
/>
</b-card>
</b-container>
</template>
<script>
import {applianceService} from "#/services/appliance";
import CommonCollapsible from "#/components/common/CommonCollapsible";
import moment from 'moment';
export default {
components: { CommonCollapsible, CommonTable },
props: {
ownerId: String,
ownerType: String,
showDocuments: Boolean,
goToAppliances: "",
importAppliances: ""
},
data() {
return {
applianceReferences: [],
showTable: true
}
},
computed: {
fields() {
return [
{
key: 'referenceName',
label: this.$t('referenceName'),
sortable: true
},
{
key: 'activeAppliance.type',
label: this.$t('type'),
sortable: true,
},
{
key: 'activeAppliance.brandName',
label: this.$t('brand'),
sortable: true
},
{
key: 'activeAppliance.purchaseDate',
label: this.$t('purchaseDate'),
sortable: true,
template: {type: 'date', format: 'L'}
},
{
key: 'activeAppliance.warrantyDuration',
label: this.$t('warrantyDuration'),
sortable: true,
formatter: (warrantyDuration, applianceId, appliance) =>
this.$n(warrantyDuration) + ' ' +
this.$t(appliance.activeAppliance.warrantyDurationType ?
`model.appliance.warrantyDurationTypes.${appliance.activeAppliance.warrantyDurationType}` :
''
).toLocaleLowerCase(this.$i18n.locale),
sortByFormatted: (warrantyDuration, applianceId, appliance) =>
appliance.activeAppliance.warrantyDurationType === 'YEARS' ? warrantyDuration * 12 : warrantyDuration
},
{
key: 'activeAppliance.purchaseAmount',
label: this.$t('amount'),
sortable: true,
template: {
type: 'number', format: {minimumFractionDigits: '2', maximumFractionDigits: '2'},
foot: sum
}
},
{
key: 'actions',
template: {
type: 'actions',
head: [
{
text: 'overviewOfAppliances',
icon: 'fas fa-fw fa-arrow-right',
action: this.createAppliance
},
{
icon: 'fas fa-fw fa-file-excel',
action: this.importAppliance,
tooltip: this.$t('importAppliances'),
}
],
cell: [
{
icon: 'fa-trash',
variant: 'outline-danger',
action: this.remove
},
]
}
}
]
},
},
methods: {
load() {
Object.assign(this.$data, this.$options.data.apply(this));
this.applianceReferences = null;
applianceService.listApplianceReferences(this.ownerId).then(({data: applianceReferences}) => {
this.applianceReferences = applianceReferences;
this.applianceReferences.forEach( reference => {
applianceService.listAppliances(reference.id).then(result => {
this.$set(reference, 'appliances', result.data);
this.$set(reference, 'activeAppliance', result.data.find(appliance => appliance.active))
this.loaded = true
})
})
}).catch(error => {
console.error(error);
})
},
createAppliance(){
this.goToAppliances()
},
importAppliance(){
this.importAppliances()
},
},
watch: {
ownerId: {
immediate: true,
handler: 'load'
}
},
}
</script>
Okay, so the error occurs in this specific property:
{
key: 'activeAppliance.warrantyDuration',
label: this.$t('warrantyDuration'),
sortable: true,
formatter: (warrantyDuration, applianceId, appliance) =>
this.$n(warrantyDuration) + ' ' +
this.$t(appliance.activeAppliance.warrantyDurationType ?
`model.appliance.warrantyDurationTypes.${appliance.activeAppliance.warrantyDurationType}` :
''
).toLocaleLowerCase(this.$i18n.locale),
sortByFormatted: (warrantyDuration, applianceId, appliance) =>
appliance.activeAppliance.warrantyDurationType === 'YEARS' ? warrantyDuration * 12 : warrantyDuration
},
What I am basically doing here is combining two values from the object: warrantyDuration and warrantyDurationType and putting them in one single row in my bootstrap table.
The problem is that this is giving me an error: Cannot read properties of undefined (reading 'warrantyDurationType'
Yet the data actually outputs normally.
So what exactly does it want me to do?
I tried wrapping a v-if around the table to make sure that the application checks if the data exist before outputting it, but this does not solve the issue.
<div v-if="applianceReferences && applianceReferences.activeAppliance">
<b-card v-if="showTable" class="ml-4 mr-4">
<common-table
search-placeholder="search"
:filter-included-fields="fields.map(f => f.key)"
include-filter
:items="applianceReferences"
:fields="fields"
/>
</b-card>
</div>
Last, just to give you a full overview, my array looks like this:
Any ideas?

Dynamic index of array

I;m new on Vuejs and I'm currently working with composition API so I have an array like this:
const tabs = ref([
{
id: 1,
pdf: 'name1',
...
},
{
id: 2,
pdf: 'name2',
...
},
{
id: 3,
pdf: 'name3',
...
},
])
Then I have a div like this:
<div
v-for="tab in tabs"
:key="tab.name"
:href="tab.href"
class="px-12 pt-8 flex flex-col"
:class="[tab.current || 'hidden']"
#click="changeTab(tab)"
>
<div v-if="pdf != ''">
<div class="pt-4 font-bold underline">
<a :href="pdfSrc" target="_blank">See PDF</a>
</div>
</div>
</div>
And then I use computed to get current href value as:
props: {
tabs: {
type: Array as PropType<Array<any>>,
required: true,
},
},
computed: {
pdfSrc(): string {
return `/img/modal/pdf/${encodeURIComponent(this.tabs[0].pdf)}.pdf`
},
}
As you can see I always use tabs[0] so pdf value is always value name1 and I want to get depending of the selected tab
The tab method:
setup(props) {
const changeTab = (selectedTab: { id: number }) => {
props.tabs?.map((t) => {
t.id === selectedTab.id ? (t.current = true) : (t.current = false)
})
}
return {
changeTab,
}
},
How can I change static index 0 to dynamic one depending on the current tab?
I would suggest creating a new variable for tracking the selected tab.
const selectedTabId = ref(0);
Similar to tabs, this could be passed down in array and the value updated in changeTab function.
props: {
tabs: {
type: Array as PropType<Array<any>>,
required: true,
},
selectedTabId: {
type: Number
}
},
setup(props) {
const changeTab = (selectedTab: { id: number }) => {
selectedTabId = selectedTab.id
props.tabs?.map((t) => {
t.id === selectedTab.id ? (t.current = true) : (t.current = false)
})
}
return {
changeTab,
}
},
Finally in the computed use selectedTabId
computed: {
pdfSrc(): string {
return `/img/modal/pdf/${encodeURIComponent(this.tabs[this.selectedTabId].pdf)}.pdf`
},
}

Make computations in a child component after axios requests in a parent component

Here is my problem : I have a parent component Edit that makes several axios requests, the results of these requests are passed down to a child component (Menu in my example).
<template>
<div>
<p>
Order Id : {{ this.$route.params.id }}
</p>
<head-fields :menu="menu"></head-fields>
<div>
<b-tabs content-class="mt-3">
<b-tab title="Menu" active>
<Menu :menuItems="items" :nutritionalValues="nutritionalValues"></Menu>
</b-tab>
<b-tab title="Specifications">
<specifications :specifications="specifications">
</specifications>
</b-tab>
<b-tab title="Redundancies"><p>I'm the redundancies tab!</p></b-tab>
</b-tabs>
</div>
</div>
</template>
<script>
import HeadFields from "./HeadFields";
import Menu from "./Menu";
import Specifications from "./Specifications";
export default {
name: "Edit",
components: {HeadFields, Menu, Specifications},
data(){
return{
menu: {},
loaded: false,
items: {},
nutritionalValues: {},
specifications: {},
error:{}
}
},
created(){
this.find(this.$route.params.id);
},
methods:{
find(id){
axios.get('/menusV2/'+id)
.then(response => {
this.loading = false;
this.menu = response.data[0];
this.fetchMenu(this.menu.orderId);
this.fetchSpecifications(this.menu.orderId);
});
return this.menu;
},
fetchMenu(orderId){
// console.log(orderId);
axios
.get('/menusV2/'+orderId+'/menu')
.then(response => {
this.loading = false;
this.items = response.data.items;
this.nutritionalValues = response.data.nutritionalValues;
})
.catch(error => {
this.loading = false;
this.error = error.response.data.message || error.message;
})
},
fetchSpecifications(orderId){
axios
.get('/menusV2/'+orderId+'/specifications')
.then(response => {
this.loading = false;
this.specifications = response.data;
// this.checkSpecifications();
})
.catch(error => {
this.loading = false;
// this.error = error.response.data.message || error.message;
})
}
}
}
</script>
The data is passed down to the child component "Menu" as a prop :
<template>
<div class="panel panel-default">
<b-table
striped hover
:items="menuItems"
:fields="fields"
:primary-key="menuItems.pivotId"
>
</b-table>
</div>
</template>
<script>
export default {
name: "Menu",
props: ['menuItems', 'nutritionalValues'],
data() {
return {
loading: true,
perPage: ['10', '25', '50'],
rowSelected: true,
fields: [
{key: "meal", label: "Meal", sortable: true},
{key: "category", label: "Category", sortable: true},
{key: "itemName", label: "Name", sortable: true},
{key: "noOfServing", label: "Serving", sortable: true},
{key: "weight", label: "Weight", sortable: true},
{key: "calories", label: "Calories", sortable: true},
{key: "carbs", label: "Carbs", sortable: true},
{key: "proteins", label: "Proteins", sortable: true},
{key: "fats", label: "Fats", sortable: true},
]
}
},
mounted(){
this.checkSpecifications();
},
methods:{
searchIngredientSpecification(itemId, itemName, specifications){
//Checking of the ingredients name
for (var i=0; i < specifications.length; i++) {
if (specifications[i].itemName === itemName) {
console.log("Specification ! "+itemName);
}
}
//Checking of the nutritional properties
var ingredientNutritionalProperties = {};
axios
.get('/menusV2/'+itemId+'/ingredient/nutritionalProperties')
.then(response => {
ingredientNutritionalProperties = response.data;
});
console.log("Ingredient : " + itemName);
console.log(ingredientNutritionalProperties);
},
searchDishSpecification(itemId, itemName, specifications){
//Checking of the ingredients name
for (var i=0; i < specifications.length; i++) {
if (specifications[i].itemName === itemName) {
console.log("Specification ! "+itemName);
}
}
//Checking of the nutritional properties
var dishNutritionalProperties = {};
axios
.get('/menusV2/'+itemId+'/dish/nutritionalProperties')
.then(response => {
dishNutritionalProperties = response.data;
});
console.log("Dish : " + itemName);
console.log(dishNutritionalProperties);
var ingredientsDish = {};
var ingredientsNutritionalProperties = {};
axios
.get('/menusV2/'+itemId+'/getIngredients')
.then(response => {
ingredientsDish = response.data.ingredients;
ingredientsNutritionalProperties = response.data.nutritionalProperties;
});
console.log("Dish : " + itemName);
console.log(ingredientsDish);
console.log(ingredientsNutritionalProperties);
},
checkSpecifications(){
console.log("Check Specifications launched !");
console.log(this.menuItems);
var items = this.menuItems;
items.forEach(
element => {
switch(element.type){
case 'Ingredient':
this.searchIngredientSpecification(element.itemId,element.itemName,this.specifications);
break;
case 'Dish':
this.searchDishSpecification(element.itemId,element.itemName,this.specifications);
break;
}
}
);
},
}
}
</script>
The problem I have is around the methods in the child component that are fired before the menuItems prop is filled with data from the axios request.
I think that a possible fix to this problem would be to use computed properties or watchers but I don't really know if it will help me..
Here is the error that is thrown :
Thanks for your help !
You are getting the error because when the checkSpecifications method is run on mount, this.menuItems is not an array and forEach must only be used on arrays.
One option is to add a watcher to menuItems and only once it has been filled with a value (making sure it's an array) then run the checkSpecifications method.
Alternatively, you could define menuItems as an array and provide a default value.
props: {
menuItems: {
type: Array,
default: []
},
nutritionalValues: {
type: Array,
default: []
}
It's always good practice to define the type of your props.

Vue: Accessing prop to set class?

I am using a pagination library ( https://github.com/arnedesmedt/vue-ads-pagination ) and the VueAdsPageButton has a hidden prop on it called active that is a boolean value depending on whether or not the button is active. I am trying to set the id based on whether or not the active prop is true so I can style it accordingly. I tried:
v-bind:id="{ selected: active} but I get the warning that active is referenced in the render but doesn't exist. Not sure what I am doing wrong here.
This is my code below:
<VueAdsPagination
:total-items="totalOrdersNumber ? totalOrdersNumber : 0"
:page="page"
:loading="loading"
:items-per-page="10"
:max-visible-pages="10"
#page-change="pageChange"
#range-change="rangeChange"
>
<template
slot="buttons"
slot-scope="props"
>
<VueAdsPageButton
v-for="(button, key) in props.buttons"
v-bind:id="{ selected: active}"
:key="key"
:page="page"
v-bind="button"
#page-change="page = button.page;"
/>
</template>
</VueAdsPagination>
EDIT:
here is the component code from the library for VueAdsPageButton
<template>
<button
:class="buttonClasses"
:disabled="disabled"
:title="title"
#click="pageChange"
>
<i
v-if="loading"
class="fa fa-spinner fa-spin"
/>
<span
v-else
v-html="html"
/>
</button>
</template>
<script>
export default {
name: 'VueAdsPageButton',
props: {
page: {
type: [
Number,
String,
],
required: true,
},
active: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
html: {
type: String,
required: true,
},
title: {
type: String,
default: '',
},
loading: {
type: Boolean,
default: false,
},
disableStyling: {
type: Boolean,
default: false,
},
},
computed: {
buttonClasses () {
if (this.disableStyling) {
return {};
}
return {
'focus:vue-ads-outline-none': true,
'vue-ads-ml-1': true,
'vue-ads-leading-normal': true,
'vue-ads-w-6': true,
'vue-ads-bg-teal-500': this.active,
'vue-ads-text-white': this.active,
'vue-ads-cursor-default': this.active || this.disabled,
'vue-ads-bg-gray-200': this.disabled && this.page !== '...',
'vue-ads-text-gray': this.disabled && this.page !== '...',
'hover:vue-ads-bg-gray-100': !this.active && !this.disabled,
};
},
},
methods: {
pageChange () {
if (
this.page === '...' ||
this.disabled ||
this.active
) {
return;
}
this.$emit('page-change');
},
},
};
</script>
You can bind custom id or class on active button like this:
<vue-ads-page-button
v-for="(button, key) in props.buttons"
:key="key"
v-bind="button"
:id="button.active ? 'some-id' : null"
:class="{'some-class': button.active}"
#page-change="page = button.page"
#range-change="start = button.start; end = button.end"
/>
Here is also JSFiddle from library documentation where you can also see this - Link

Vue.js - Computed property is not triggered when data is changing

I'm using vue-js and element-ui running on dev-server coming from vue-cli with the webpack template.
I'm trying to debounce the search value of a filterable input. In other words, I would like to debounce the :filter-method and getting the parameters query to do an ajax call
Here is the fiddle example https://jsfiddle.net/ffeohmk4/
In that example there is no debounce yet.
Problem
The function getFinalList is never triggered. I would assume since it is a computed propertyit should be triggered each time this.searchValue changes.
var Main = {
data() {
return {
searchValue : '',
filteredOptions : [],
options: [{
value: 'A',
label: 'A'
}, {
value: 'B',
label: 'B'
}, {
value: 'C',
label: 'C'
}, {
value: 'D',
label: 'D'
}, {
value: 'E',
label: 'E'
}],
value8: ''
}
},
computed : {
getFinalList () {
alert('getFinalList is called');
this.filteredOptions = this.options.filter(option => {
return option.value.toLowerCase().indexOf(this.searchValue.toLowerCase()) > -1;
})
}
},
methods : {
setSearchInput (query) {this.searchValue = query}
},
created () {
this.filteredOptions = this.options;
}
}
var Ctor = Vue.extend(Main)
new Ctor().$mount('#app')
Here is a working another example https://jsfiddle.net/ffeohmk4/2/
getFinalList () {
return this.filteredOptions = this.options.filter(option => {
return option.value.toLowerCase().indexOf(this.searchValue.toLowerCase()) > -1;
})
}
<el-select v-model="searchValue" filterable placeholder="Select" :filter-method="setSearchInput">
<el-option v-for="item in getFinalList" :key="item.value" :label="item.label" :value="item.value">
</el-option>