VueJS cannot render a dynamically generated component - vue.js

I'm struggling to make this work. I'm developing a VueJS small app, served using ParcelJS.
In a view called DisplayProgram, I'm trying to create multiple instances of a specific component ModuleListView, with network fetched data.
When I try to display the components, I get this error : [Vue warn]: Failed to mount component: template or render function not defined.
Here are the two views I'm using :
DisplayProgram.vue
<template lang="pug">
div.grix.xs4
div.col-xs4.row-xs5.row-md8.vself-stretch.vcenter
transition(:name="getSlideSide()" mode="out-in")
component(:is="getModuleListView()")
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import ModuleListView from './ModuleListView.vue';
#Component({
components: {
'module-list': ModuleListView
}
})
export default class DisplayProgram extends Vue {
#Prop() program: any;
activeModuleList!: {name: string, number: number, modules: Array<any>};
activeModuleListNumber: number = 0;
previousModuleListNumber: number = 0;
moduleLists : Array<{name: string, number: number, modules: Array<any>}> = [];
listViews: Array<ModuleListView> = [];
async created() {
this.moduleLists = this.getModuleLists(this.program);
this.activeModuleList = this.moduleLists[0];
this.emitModuleListChanged(this.activeModuleList.number);
this.instanciateListViews();
}
instanciateListViews() {
this.moduleLists.forEach(moduleList => {
let moduleListView: ModuleListView = new ModuleListView(moduleList);
Vue.set(this.$options.components, moduleList.number, moduleListView);
this.listViews.push(moduleListView);
});
}
getModuleListView() {
return this.listViews[this.activeModuleListNumber];
}
getSlideSide() {
if (this.previousModuleListNumber > this.activeModuleListNumber) {
return "slide-right";
}
return "slide-left";
}
getProgramProgress() : {actualNumber: number, totalNumber: number} {
return {actualNumber: this.activeModuleListNumber + 1 , totalNumber: this.moduleLists.length};
}
getModuleLists(program: any) : Array<{name: string, number: number, modules: Array<any>}> {
// Getting data and generating the javascript objects
return moduleLists;
}
displayModuleList(index: number) {
this.emitModuleListChanged(index);
}
emitModuleListChanged(moduleListNumber: number) {
this.previousModuleListNumber = this.activeModuleList.number;
this.activeModuleList = this.moduleLists[moduleListNumber];
this.$emit("module-list-changed", {moduleList : this.activeModuleList, total : this.moduleLists.length});
this.activeModuleListNumber = moduleListNumber;
}
}
</script>
<style scoped>
</style>
ModuleListView.vue
<template lang="pug">
div
h2.txt-center.responsive {{moduleList.name}}
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component({
components: {
}
})
export default class ModuleListView extends Vue {
#Prop() moduleList!: {name: string, number: number, modules: Array<any>};
constructor(moduleList: any) {
super();
this.moduleList = moduleList;
}
async created() {
console.log("modulelistview is created");
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.responsive {
font-size:3vw;
}
</style>
Do you have a solution for this, folks ? Thank you for your help !

Related

Vue 3 does not update getter

I am in the process of updating vue2 to vue3 but encounter this problem.
I have a service called TService
// T.ts
class T {
public obj = { value: false };
constructor() {
setInterval(() => {
this.obj.value = !this.obj.value;
}, 1000);
}
}
const t = new T();
export { t as TService };
The service is very simple, it update it's obj value every 1 second.
Now come to the fun part
On vue2, I can do this:
<template>
<div> {{ test }} </div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { TService } from './T;
#Component
export default class HelloWorld extends Vue {
public obj = TService.obj;
get test() {
return this.obj.value;
}
}
</script>
The test value updated on screen every 1sec and works as expected.
However, when I changed to vue3 with the below code, it does not work any more
<template>
<div>{{ test }}</div>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { TService } from './T';
#Options({})
export default class HelloWorld extends Vue {
public obj = TService.obj;
get test() {
return this.obj.value;
}
}
</script>
Not sure what is going on and appreciate if anyone can fix my code.
I am using latest vue 3.1.5 and vue-class-component 8.0.0-rc.1
You probably should make it reactive so Vue knows its value can be updated, see here
The anwser can be found in this post:
Changes made to an object created outside of Vue component are not detected by Vue 3
Basically I will need to wrap reactive around my object in my service
import { reactive } from 'vue';
// T.ts
class T {
public obj = reactive({ value: false });
constructor() {
setInterval(() => {
this.obj.value = !this.obj.value;
}, 1000);
}
}
const t = new T();
export { t as TService };

Is that the right way to set an id in Vue.js component?

I am trying to integrate Phaser 3 with Vue.js 2.
My goal is to create a Vue.js component associated with a game canvas.
My initial solution was:
<template>
<div :id="id">
</div>
</template>
<script>
import Phaser from 'phaser'
export default {
data () {
return {
id: null,
game: null
}
},
mounted () {
this.id = 'game' + this._uid
var config = {
parent: this.id,
type: Phaser.CANVAS
}
this.game = new Phaser.Game(config)
....
}
}
</script>
This code attach the game canvas to my template. But to my surprise it only worked 'sometimes'.
I figured out, after hours of debugging, that my div element in the DOM wasn't updated with the id when I was instantiating my new Game.
So I came up with the solution of instantiating the id in the beforeMount () method as follow:
<template>
<div :id="id">
</div>
</template>
<script>
import Phaser from 'phaser'
export default {
data () {
return {
id: null,
game: null
}
},
beforeMount () {
this.id = 'game' + this._uid
},
mounted () {
var config = {
parent: this.id,
type: Phaser.CANVAS
}
this.game = new Phaser.Game(config)
....
}
}
</script>
It is working, but I would like to know if there is a more simple and elegant solution ?
One better solution for integrating Phaser.Game into the application is directly passing the config the HTML element, a configuration supported by Phaser.Game.
To get a reference to a HTML element in vue, you can use refs, these are basically id's, but local to the component itself, so there is not risk in creating conflicts.
<template>
<div ref="myDiv">
</div>
</template>
<script>
import Phaser from 'phaser'
export default {
data () {
return {
game: null
}
},
mounted () {
var config = {
parent: this.$refs.myDiv,
type: Phaser.CANVAS
}
this.game = new Phaser.Game(config)
....
}
}
</script>
Vue3 sample:
<script setup>
import { ref,onMounted } from 'vue';
import Phaser from 'phaser'
const myDiv = ref(null)
let canvasWidth = 750;
let canvasHeight = 1450;
onMounted(() => {
const config = {
type: Phaser.AUTO,
parent: popWrap.value,
width: canvasWidth,
height: canvasHeight,
scene: {
preload: preload,
create: create,
update: update
}
};
const game = new Phaser.Game(config);
})
</script>
<template>
<div ref="myDiv">
</div>
</template>

Call method from another component in Vue.js

How do I call the method from another component in this scenario? I would like to load additional pice of data from the API once the button is clicked in the component 1 to the component 2.
Thanks
Here are my two components in the seperate files:
compbutton.vue
<template>
<div>
<a href v-on:click="buttonClicked">Change Name</a>
</div>
</template>
<script>
export default {
name: 'compbutton',
methods: {
buttonClicked: function () {
//call changeName here
}
}
}
</script>
compname.vue
<template>
<div>{{name}}</div>
</template>
<script>
export default {
name: 'compname',
data: function () {
return {
name: 'John'
}
},
methods: {
changeName: function () {
this.name = 'Ben'
}
}
}
</script>
You can name the component and then $ref to the method from another componenent.
compbutton.vue
<template>
<div>
<a href v-on:click="buttonClicked">Change Name</a>
</div>
</template>
<script>
export default {
name: "compbutton",
methods: {
buttonClicked: function() {
//call changeName here
this.$root.$refs.compname_component.changeName();
}
}
};
</script>
compname.vue
<template>
<div>{{name}}</div>
</template>
<script>
export default {
name: "compname",
data: function() {
return {
name: "John"
};
},
methods: {
changeName: function() {
this.name = "Ben";
}
},
created() {
// set componenent name
this.$root.$refs.compname_component = this;
}
};
</script>
Alternative answer: you can pass the function you want the child to invoke as a prop from the parent component. Using your example:
compbutton.vue
<template>
<div>
<a href v-on:click="buttonClicked">Change Name</a>
</div>
</template>
<script>
export default {
name: 'compbutton',
props: {
clickHandler: {
type: Function,
default() {
return function () {};
}
}
},
methods: {
buttonClicked: function () {
this.clickHandler(); // invoke func passed via prop
}
}
}
</script>
compname.vue
<template>
<div>{{name}}</div>
<compbutton :click-handler="changeName"></compbutton>
</template>
<script>
export default {
name: 'compname',
data: function () {
return {
name: 'John'
}
},
methods: {
changeName: function () {
this.name = 'Ben'
}
}
}
</script>
Note, in your example, it doesn't appear where you want the 'compbutton' component to be rendered, so in the template for compname.vue, thats been added as well.
You can use a service as a go-between. Usually, services are used to share data but in javascript functions can be treated like data also.
The service code is trivial, just add a stub for the function changeName
changeName.service.js
export default {
changeName: function () {}
}
To have services injected into the components, you need to include vue-injector in the project.
npm install --save vue-inject
or
yarn add vue-inject
and have a register of services,
injector-register.js
import injector from 'vue-inject';
import ChangeNameService from '#/services/changeName.service'
injector.service('changeNameService', function () {
return ChangeNameService
});
then in main.js (or main file may be called index.js), a section to initialize the injector.
import injector from 'vue-inject';
require('#/services/injector-register');
Vue.use(injector);
Finally, add the service to the component dependencies array, and use the service
compname.vue
<script>
export default {
dependencies : ['changeNameService'],
created() {
// Set the service stub function to point to this one
this.changeNameService.changeName = this.changeName;
},
...
compbutton.vue
<script>
export default {
dependencies : ['changeNameService'],
name: 'compbutton',
methods: {
buttonClicked: function () {
this.changeNameService.changeName();
}
}
...
Add a # to the button href to stop page reloads
Change Name
See the whole thing in CodeSandbox

Why this.$listeners is undefined in Vue JS?

Vue.js version: 2.4.2
Below component always print this.$listeners as undefined.
module.exports = {
template: `<h1>My Component</h1>`,
mounted() {
alert(this.$listeners);
}
}
I register the component and put it inside a parent component.
Can someone tell me why?
You have to understand what $listeners are.
this.$listeners will be populated once there are components that listen to events that your components is emitting.
let's assume 2 components:
child.vue - emits an event each time something is written to input field.
<template>
<input #input="emitEvent">
</input>
</template>
<script>
export default {
methods: {
emitEvent() {
this.$emit('important-event')
console.log(this.$listeners)
}
}
}
</script>
parent.vue - listen to the events from child component.
<template>
<div class="foo">
<child #important-event="doSomething"></child>
</div>
</template>
<script>
import child from './child.vue'
export default {
data() {
return {
newcomment: {
post_id: 'this is default value'
}
}
},
components: { child },
methods: {
doSomething() {
// do something
}
}
}
</script>
With this setup, when you type something to the input field, this object should be written to the console:
{
`important-event`: function () { // some native vue.js code}
}
I added the following alias to my webpack.config.js file and this resolved the issue for me:-
resolve: {
alias: {
'vue$': path.resolve(__dirname, 'node_modules/vue/dist/vue.js')
}
},

How to address the data of a component from within that component?

In a standalone Vue.js script I can mix functions and Vue data:
var vm = new Vue ({
(...)
data: {
number: 0
}
(...)
})
function return100 () {
return 100
}
vm.number = return100()
I therefore have a Vue instance (vm) which data is directly addressable via vm.<a data variable>)
How does such an addressing works in a component, since no instance of Vue is explicitly instantiated?
// the component file
<template>
(...)
</template>
<script>
function return100 () {
return 100
}
export default {
data: function () {
return {
number: 0
}
}
}
// here I would like to set number in data to what return100()
// will return
??? = return100()
</script>
You can achieve the target by using code like this.
<template>
<div>{{ name }}</div>
</template>
<script>
const vm = {
data() {
return {
name: 'hello'
};
}
};
// here you can modify the vm object
(function() {
vm.data = function() {
return {
name: 'world'
};
}
})();
export { vm as default };
</script>
But I really don't suggest you to modify data in this way and I think it could be considered as an anti-pattern in Vuejs.
In almost all the use cases I met, things could be done by using Vue's lifecycle.
For example, I prefer to write code with the style showed below.
<template>
<div>{{ name }}</div>
</template>
<script>
export default {
data() {
return {
name: 'hello'
};
},
mounted() {
// name will be changed when this instance mounted into HTML element
const vm = this;
(function() {
vm.name = 'world';
})();
}
};
</script>