"Cannot read property 'props' of undefined" while mounting functional Vue component - javascript

Case:
I am trying dynamically mount functional vue component inside custom directive
(I need to show special button, when user hovers on some elements on the page). I do it in this way, dynamically, because I have thousands of elements on the page, so I try to avoid excess components and try to mount them on the fly, while hovering.
Directive code:
import AIcon from './ui/AIcon';
let AIconComponent = Vue.extend(AIcon);
editable: {
bind: (element, binding, vNode) => {
element.addEventListener('mouseover', () => {
element.style.position = 'relative';
let editButtonWrp = htmlToElement(`<div class="btn-wrp"><span class="btn"></span></div>`);
element.prepend(editButtonWrp);
new AIconComponent({
el: editButtonWrp.querySelector('.btn'),
parent: vNode.context,
props: {
size: 'xs', icon: 'edit',
},
});
});
}
}
Button functional component code:
export default {
functional: true,
props: {
icon: {
type: String,
default: '',
},
size: {
type: String,
default: 'md',
}
},
render(createElement, { props }) {
let iconHref = `/dist/img/sprite.svg#${props.icon}`;
return createElement('svg', {
class: { 'a-icon': true, [`-${props.icon}`]: true },
}, [
createElement('use', {
attrs: {
'xlink:href': iconHref,
},
}),
]);
},
}
But on the page I get this error:
TypeError: Cannot read property 'props' of undefined
at Proxy.render (AIcon.js?27d5:19)
As I understand, when I call new AIconComponent({ ... }) in directive, it passes undefind context to render(createElement, { props }) ... but why? How I can resolve this case?

Related

How make dynamic breadcrumbs in vue.js?

I would like to have a dynamic breadcrumbs based on where I clicked on a category but I get an error that says my variable is undefined: TypeError: Cannot read properties of undefined (reading 'homeMenu'). Yet in my getHomeCategory function, the console.log of homeCategory displays Perma'Thèque. I don't understand how to do it, thanks
Here is the code :
<script>
export default {
props: {
},
data: () => ({
homeMenu: "",
breadcrumbs: [
{
text: 'Accueil',
disabled: false,
href: '/',
},
{
text: this.homeMenu,
disabled: false,
href: "/" + this.homeMenu,
},
],
}),
computed: {
...mapGetters({
console: () => console,
homeCategory: 'home/getCategory',
})
},
methods: {
getHomeCategory () {
if (this.homeCategory === "Perma'Thèque") {
console.log(this.homeCategory)
return this.homeMenu = "permatheque"
} else {
return this.homeMenu = "null"
}
},
},
mounted() {
if (this.plantActive) this.loading = false;
this.getHomeCategory()
}
}
</script>
data() is declared here as an arrow function, so this refers to the outer scope, not the Vue component instance, but even as a regular function here, this.homeMenu won't yet exist.
It seems that you actually want breadcrumbs to be reactive to homeMenu, so you should move breadcrumbs to a computed prop:
export default {
data: () => ({
homeMenu: '',
}),
computed: {
breadcrumbs() {
return [
{
text: 'Accueil',
disabled: false,
href: '/',
},
{
text: this.homeMenu,
disabled: false,
href: '/' + this.homeMenu,
},
]
}
}
}
demo
I used Element Plus UI and my mind was blown. Check out my blog and the codes. Element Plus is a free library that is great for Vue JS. They have very nice UI for breadcrumb and can be implemented with v-for loop.
https://medium.com/#samchowdhury/create-a-breadcrumb-with-vue-js-and-element-plus-ui-f3e2fde50a3e

Testing component - Error in mounted hook: "TypeError: Cannot read property 'dispatch' of undefined"

I am trying to write a simple test for my vue component. Since the vue component makes an async call on mount and updates the vuex store, dispatch is called during mount, which breaks my existing unit tests. Any idea how to overcome this? Since I am mocking table data, I don't need the mounted() function to be called when running the tests.
MyTable.spec.js
const wrapper = shallowMount(MyTable, {
propsData: {
tableData: [
{
"product_id":10826345236,
"name":"T-Shirt"
}
],
columns: ['product_id', 'name'],
headings: ['Product ID', 'Name'],
actionType: 'loadProducts'
}
});
...
MyTable.vue
...
data() {
return {
options: {
...
}
};
},
methods: {
getHeadings() {
let headings = {};
this.columns.map((key, i) => headings[key] = this.headings[i]);
return headings;
},
setColumnClasses() {
let classes = {};
this.columns.map((key) => classes[key] = key);
return classes;
},
loadRecords(actionType) {
this.$store.dispatch(actionType);
}
},
props: {
tableData: {
type: Array,
required: true
},
columns: {
type: Array,
required: true
},
actionType: {
type: String,
required: true
},
headings: {
type: Array,
required: true
},
...
},
mounted() {
this.loadRecords(this.actionType);
}
You are getting this error message because Vue (when mounted) is expecting that the this.$store is defined, and while it might be within your application, you are not importing it, nor are you mocking it.
Here is your test function code you provided:
const wrapper = shallowMount(MyTable, {
propsData: {
tableData: [
{
"product_id":10826345236,
"name":"T-Shirt"
}
],
columns: ['product_id', 'name'],
headings: ['Product ID', 'Name'],
actionType: 'loadProducts'
}
});
Here is what you need to add:
import store from '../path/to/store.js';
import { createLocalVue, shallowMount } from '#vue/test-utils';
// You will want to create a local Vue instance for testing purposes: https://vue-test-utils.vuejs.org/api/#createlocalvue
const localVue = createLocalVue();
// This tells the local Vue instance to use Vuex for the store mechanism.
localVue.use(Vuex);
const wrapper = shallowMount(MyTable, {
localVue, // Bind the local Vue instance when we shallow-mount the component.
store, // Bind the store so all of the methods are available when invoked.
propsData: {
tableData: [
{
"product_id":10826345236,
"name":"T-Shirt"
}
],
columns: ['product_id', 'name'],
headings: ['Product ID', 'Name'],
actionType: 'loadProducts'
}
});

Dynamically add navigation items in Vue CoreUI

In my project, I want to add some Ajax loaded menu items to my CoreUI sidebar in Vue. I already found a working solution, but it's kind of hacky and might have timing issues. Therefore I want to ask you, if there is a proper or at least better solution.
I also found this question from a few days ago, but it doesn't have an answer yet.
// main.js
new Vue({
el: '#app',
router,
icons,
template: '<App/>',
components: {
App
},
data: {
clientConfiguration: null
},
created: async function () {
let svcResult = await this.$http.get('Picking/ViewerSettings');
this.clientConfiguration = svcResult.data;
this.$children[0].$children[0].$children[0].$data.nav[0]._children[0].items =
svcResult.data.map(vc => ({
name: vc.name,
to: 'test/' + vc.name,
icon: 'cil-spreadsheet'
}));
}
})
// _nav.js
export default [
{
_name: 'CSidebarNav',
_children: [
{
_name: 'CSidebarNavDropdown',
name: 'Lists',
to: '/test',
icon: 'cil-list-numbered',
items: []
},
// ...
]
}
]
The _nav.js file is just an example of data structure that can be rendered by CRenderFunction component docs
The idea behind CRenderFunction is that you can render components from the Array/Object.
In your case, you have two options:
generate CRenderFunction object on backend,
generate CRenderFunction object on frontend by computed properties, based on data you got from the backend
Here is the example of the second approach:
in template
<CRenderFunction flat :content-to-render="navItems"/>
in script:
//example array that you receive from backend
const menuItems = [
{
name: 'first item',
to: '/first',
icon: 'cil-user'
},
{
name: 'second item',
to: '/second'
},
{
name: 'third item',
to: '/third'
}
]
export default {
computed: {
navItems () {
return [
{
_name: 'CSidebarNav',
_children: this.sidebarNavChildren
}
]
},
sidebarNavChildren () {
return menuItems.map(menuItem => {
return {
_name: 'CSidebarNavItem',
name: menuItem.name,
to: menuItem.to,
icon: menuItem.icon || 'cil-spreadsheet'
}
})
}
}
}
navItems computed property result:
[{"_name":"CSidebarNav","_children": [
{"_name":"CSidebarNavItem","name":"first item","to":"/first","icon":"cil-user"},
{"_name":"CSidebarNavItem","name":"second item","to":"/second","icon":"cil-spreadsheet"},
{"_name":"CSidebarNavItem","name":"third item","to":"/third","icon":"cil-spreadsheet"}
]
}]

dat.GUI and Complex Variables

I'm fairly new to dat.GUI and I'm having problems getting it to work with a more complex variable.
I have the following:
let complexVariable= {
topLayer:
{
deeperLayer:
{
deepestLayer: 20,
//Other stuff
}
}
}
let gui = new dat.GUI();
gui.add(complexVariable, "topLayer.deeperLayer.deepestLayer", 10, 40);
This gives me the following error:
Uncaught Error: Object "[object Object]" has no property "topLayer.deeperLayer.deepestLayer"
Any help here would be appreciated.
It currently doesn't seem possible by looking at the source code. They use bracket notation with the single property you pass in on the object.
function add(gui, object, property, params) {
if (object[property] === undefined) {
throw new Error(`Object "${object}" has no property "${property}"`);
}
...
So what you are telling dat.GUI to do is find a top level property "topLayer.deeperLayer.deepestLayer" and that obviously does not exist on your object. It seems like more code would have to be written to support nested properties.
dat.gui would have to do something like if (object[property1][property2][...] === undefined) or in your case - complexVariable["topLayer"]["deeperLayer"]["deepestLayer"];
It's obvious that question is ~4 years old. But I was searching for the same and I found your question before realizing that it's possible. Below is E2E sample in Angular.
import { AfterViewInit, Component, ViewChild, ViewContainerRef } from '#angular/core';
import { GUI, GUIController, GUIParams } from 'dat.gui';
export type EditorMetadata = Array<IEditorFolder>;
export interface IDatState {
state: EditorMetadata;
instance: GUI;
metadata: EditorMetadata;
}
export interface IEditorFolder {
title: string;
property?: string;
tooltip: string;
elements?: Array<IEditorElement>;
folders?: Array<IEditorFolder>;
folderRef?: GUI;
}
export interface IEditorElement {
title: string;
tooltip: string;
options?: any;
elementRef?: GUIController;
target?: Object;
}
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements AfterViewInit {
#ViewChild('container', { read: ViewContainerRef, static: true })
container: ViewContainerRef;
constructor() { }
ngAfterViewInit(): void {
this.composeEditor(this.container.element.nativeElement, options, foldersMetadata);
}
composeEditor(container: HTMLElement, editorOptions: GUIParams, editorMetadata: EditorMetadata): IDatState {
const
addFolder = (folder: IEditorFolder, parent: GUI): GUI => {
const
{ title, tooltip } = folder,
folderController = parent.addFolder(title);
folderController.domElement.setAttribute('title', tooltip);
return folderController;
},
addElement = (folder: GUI, element: IEditorElement, target: Object): GUIController => {
const
{ title, options, tooltip } = element,
elmController = folder
.add(target, title, options)
.name(title)
.onChange((value) => {
console.log(model);
});
elmController.domElement.setAttribute('title', tooltip);
return elmController;
},
createDat = (options: GUIParams, metadata: EditorMetadata): IDatState => {
const
state: IDatState = {
instance: new GUI(options),
state: JSON.parse(JSON.stringify(metadata)), // Don't touch original metadata, instead clone it.
metadata: metadata, // Return original metadata
};
return state;
},
process = (folders: EditorMetadata, parent: GUI, model: Object): void => {
folders.forEach(folder => {
const target: Object = folder.property ? model[folder.property] : model; // Root level or object nested prop?
folder.folderRef = addFolder(folder, parent);
folder.elements && folder.elements.forEach(element => element.elementRef = addElement(folder.folderRef, element, target));
folder.folders && process(folder.folders, folder.folderRef, target);
});
},
{ state, instance, metadata } = createDat(editorOptions, editorMetadata);
process(state, instance, model);
container.appendChild(instance.domElement);
return { state, instance, metadata };
}
}
const options: GUIParams = {
hideable: false,
autoPlace: false,
closeOnTop: false,
width: 250,
name: 'DAT Sample'
};
const model = {
rtl: true,
renderer: 'geras',
theme: 'dark',
toolbox: 'foundation',
toolboxPosition: 'left',
debug: {
alert: false
}
};
const foldersMetadata: EditorMetadata = [
{
title: 'Options',
tooltip: 'Options AA',
elements: [
{
title: 'rtl',
tooltip: 'RTL',
},
{
title: 'renderer',
tooltip: 'Renderer',
options: ['geras', 'thrasos', 'zelos']
},
{
title: 'theme',
tooltip: 'Theme',
options: ['classic', 'dark', 'deuteranopia', 'tritanopia', 'highcontrast']
},
{
title: 'toolbox',
tooltip: 'Toolbox',
options: ['foundation', 'platform', 'invoice']
},
{
title: 'toolboxPosition',
tooltip: 'Toolbox Position',
options: ['left', 'right', 'top', 'bottom']
},
],
folders: [
{
title: 'Debug',
property: 'debug',
tooltip: 'Debug mode',
elements: [
{
title: 'alert',
tooltip: 'Alert Tooltip',
},
]
}
]
},
];

$watch do not handling mutations in VueJS Component prop

I'm using Vue.js v1.0.24. In component Vue.component I have a prop events and handling this using:
Vue.component('my-component', {
template: '<div id="my-component"></div>',
props: {
events: {
type: Array,
required: true
},
event: {
type: Object,
required: true
}
},
ready: function() {
// ...
this.$watch('events', function (value, mutation) {
if (mutation) {
console.log(
mutation.method,
mutation.args,
mutation.result,
mutation.inserted,
mutation.removed)
}
})
},
//...
}
Whenever I push something in events, the $watch triggers, but the result is:
undefined undefined undefined undefined undefined
Does anyone know what is wrong?

Categories