Vue.js Dynamic Component - Template not showing components data - vue.js

I'm trying to build a quiz-game with VueJs and up until now everything worked out smoothly, but now that I'm started using dynamic components I'm running into issues with displaying the data.
I have a start component (Start View) that I want to be replaced by the actual Quiz component ("In Progress") when the user clicks on the start button. This works smoothly. But then, in the second components template, the data referenced with {{ self.foo }} does not show up anymore, without any error message.
The way I implemented is the following:
startComponent:
startComponent = {
template: '#start-component',
data: function () {
return {
QuizStore: QuizStore.data
}
},
methods: {
startQuiz: function () {
this.QuizStore.currentComponent = 'quiz-component';
}
}
}
};
And the template:
<script type="x-template" id="start-component">
<div>
<button v-on:click="startQuiz()">
<span>Start Quiz</span>
</button>
</div>
</script>
Note: I'm using x-templates since it somehow makes the most sense with the rest of the application being Python/Flask. But everything is wrapped in {% raw %} so the brackets are not the issue.
Quiz Component:
quizComponent = {
template: '#quiz-component',
data: function () {
return {
QuizStore: QuizStore.data,
question: 'foo',
}
};
And the template:
<script type="x-template" id="quiz-component">
<div>
<p>{{ self.question }}</p>
</div>
</script>
And as you might have seen I'm using a QuizStore that stores all the states.
The store:
const QuizStore = {
data: {
currentComponent: 'start-component',
}
};
In the main .html I'm implementing the dynamic component as follows:
<div id="app">
<component :is="QuizStore.currentComponent"></component>
</div>
So what works:
The Start screen with the button shows up.
When I click on the Start Button, the quizComponent shows up as expected.
What does not work:
The {{ self.question }} data in the QuizComponent template does not show up. And it does not throw an error message.
it also does not work with {{ question }}.
What I don't understand:
If I first render the quizComponent with setting QuizStore.currentComponent = 'startComponent', the data shows up neatly.
If I switch back to <quiz-component></quiz-component> (rather than the dynamic components), it works as well.
So it seems to be the issue that this. does not refer to currently active dynamic component - so I guess here is the mistake? But then again I don't understand why there is no error message...
I can't figure out what the issue is here - anyone?

You may have some issues with your parent component not knowing about its child components, and your construct for QuizStore has a data layer that you don't account for when you set currentComponent.
const startComponent = {
template: '#start-component',
data: function() {
return {
QuizStore: QuizStore.data
}
},
methods: {
startQuiz: function() {
this.QuizStore.currentComponent = 'quiz-component';
}
}
};
const QuizStore = {
data: {
currentComponent: 'start-component',
}
};
new Vue({
el: '#app',
data: {
QuizStore
},
components: {
quizComponent: {
template: '#quiz-component',
data: function() {
return {
QuizStore: QuizStore.data,
question: 'foo'
}
}
},
startComponent
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<script type="x-template" id="start-component">
<div>
<button v-on:click="startQuiz()">
<span>Start Quiz</span>
</button>
</div>
</script>
<script type="x-template" id="quiz-component">
<div>
<p>{{ question }}</p>
</div>
</script>
<div id="app">
<component :is="QuizStore.data.currentComponent"></component>
</div>

The following worked in the end:
I just wrapped <component :is="QuizStore.currentComponent"></component> in a parent component ("index-component") instead of putting it directly in the main html file:
<div id="app">
<index-component></index-component>
</div>
And within the index-component:
<script type="x-template" id="index-component">
<div>
<component :is="QuizStore.currentComponent"></component>
</div>
</script>
Maybe this would have been the right way all along, or maybe not, but it works now :) Thanks a lot Roy for your help!

Related

VueJS: Not printing data returned in method

I'm successfully getting data into the console. When I try to print that data to the page by calling the method in double moustache braces it doesn't appear on screen. All other data in template appears just fine.
Template:
<template>
<div>
<div v-for="data in imageData" :key="data.id">
<div class="card">
<img :src="data.source" :alt="data.caption" class="card-img" />
<div class="text-box">
<p>{{ moment(data.timestamp.toDate()).format("MMM Do YYYY") }}</p>
<p>{{ data.caption }}</p>
// The Geocoding method is the problem
<p>{{reverseGeocode(data.location.df, data.location.wf)}}</p>
</div>
</div>
</div>
</div>
</template>
Method:
methods: {
reverseGeocode: (lat, long) => {
fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${long}&key=API_KEY&result_type=locality`
).then((res) =>
res.json().then((data) => {
console.log(data.results[0].formatted_address); // works fine
return data.results[0].formatted_address;
})
);
},
},
Here's the image data I'm getting in props
Your problem is a common problem when you start making requests in JavaScript.
The date requests are asynchronous so the method cannot return a value after the execution of the method has finished.
Imagine the following call stack:
Start method.
Throw fetch. <- Asynchronous
Finish method.
Fetch ends.
You are trying to do a return in step 4 and it should be in 3.
To solve this you should use async with await. You could also solve it by making a component and passing the data (this is my favorite since you are using vue).
Component parent
<template>
<div>
<component-card v-for="data in imageData" :key="data.id" :dataItem="data">
</component-card>
</div>
</template>
Child component
<template>
<div class="card">
<img :src="dataItem.source" :alt="dataItem.caption" class="card-img" />
<div class="text-box">
<p>{{ moment(dataItem.timestamp.toDate()).format("MMM Do YYYY") }}</p>
<p>{{ dataItem.caption }}</p>
<p>{{formattedAddress}}</p>
</div>
</div>
</template>
<script>
export default {
props: {
dataItem: {
type: {},
default: () => ({})
}
},
data() {
return {
formattedAddress: ""
};
},
created() {
this.reverseGeocode(this.dataItem.location.df, dataItem.location.wf)
},
methods: {
reverseGeocode(lat, long) {
fetch(
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${long}&key=API_KEY&result_type=locality`
).then(res =>
res.json().then(data => {
console.log(data.results[0].formatted_address); // works fine
this.formattedAddress = data.results[0].formatted_address;
})
);
}
}
};
</script>
I have not tried it, surely some things are missing but the template should be that.
The above I think is correct as well, but I would push for async
async reverseGeocode(lat, long) {
const response = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${long}&key=API_KEY&result_type=locality`
);
const data = response.json();
return data.results[0].formatted_address;
}
You should change your approach to the following:
Do all requests in the created() lifecycle method and store the results in a data attribute then iterate over the data attribute. The created() lifecycle method executes before the DOM is mounted so all data fetching APIs should be called there. FYR: https://v2.vuejs.org/v2/guide/instance.html
Please also refer to Vue.js - Which component lifecycle should be used for fetching data?

How to include a local JS file into Vue template

I want to import a JS file to be run along with a template in browser. I tried this, but it didn't work because I need everything loaded before my script can run.
Let me show you the problematic vue file:
<template>
<div id="canvaspage">
<canvas id="canvas"></canvas>
<div id="buttonlist">
<h5>Select your action:</h5>
<div class="col">
<button id="btn1">JS file custom action 1</button>
<button id="btn2">JS file custom action 2</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CanvasPage'
}
</script>
...
See that canvas and buttons on template? I want to interact with it using pure JS.
Here is an example of what the JS file is trying to do:
let canvas = document.getElementById('canvas')
let button1 = document.getElementById('btn1')
let button2 = document.getElementById('btn2')
canvas.addEventListener('click', () => {
console.log('Canvas clicked')
})
button1.addEventListener('click', () => {
console.log('Button 1 clicked')
})
button2.addEventListener('click', () => {
console.log('Button 2 clicked')
})
If I try the solution linked above, what happens is that 'canvas', 'button1' and 'button2' are all null, because JS cannot find them. How can I make it work on Vue?
I don't see a reason- in this example- why you want to do anything in external js file, why not just interact with dom the vue way - I mean, proper way? Vue can destroy or replace your element with any v-if or rerender action. You can always link to your elements with this.$refs if you want to interact with DOM directly which is lots better than querySelector thingy. But anyway, here's a dummy example:
// external js file - ./extfile.js
export function canvasClick(...args) {
console.log('canvas clicked with: ', args);
}
export function button1Click(...args) {
console.log('button1 clicked with: ', args);
}
export function button2Click(...args) {
console.log('button2 clicked with: ', args);
}
// vue component
<template>
<div id="canvaspage">
<canvas id="canvas" #click="canvasAction"></canvas>
<div id="buttonlist">
<h5>Select your action:</h5>
<div class="col">
<button id="btn1" #click.prevent="button1Action">JS file custom action 1</button>
<button id="btn2" #click.prevent="button2Action">JS file custom action 2</button>
</div>
</div>
</div>
</template>
<script>
import { canvasClick, button1Click, button2Click } from './extfile';
export default {
name: 'CanvasPage',
methods: {
canvasAction(event) { canvasClick(event, this) },
button1Action(event) { button1Click(event, this) },
button2Action(event) { button2Click(event, this) },
}
}
</script>
Objects managed by Vue are create/destroyed according to Vue' lifecycle. This means that any external code you use to query vue-managed elements should be somewhat coupled to Vue's lifecycle.
This means that, ideally, you should use Vue itself to add the behaviour you want. You should, for instance, add this new function you want into a Vue component. This guarantees a simpler design.
Alternative: If the Vue components are from third-parties, perhaps from another team which you can't count on, you could hook those event listeners to the document and check the target's id attribute instead of hooking the event listeners directly to the canvas element (which may be destroyed by Vue and the hooks lost).
document.body.addEventListener('click', (event) => {
switch (event.target.id) {
case 'canvas':
console.log('Canvas clicked');
break;
case 'btn1':
console.log('Button 1 clicked');
break;
case 'btn2':
console.log('Button 2 clicked');
break;
}
}, true);
This code makes it very obvious that if you have more than one element in the DOM with those IDs, all of them will trigger the code.
Demo:
const CanvasComponent = Vue.component('canvas-component', {
template: `#canvas-component`,
});
const BlankComponent = Vue.component('blank-component', {
template: `<div><h3>Now click back to canvas and see that the listeners still work.</h3></div>`,
});
var router = new VueRouter({
routes: [{
path: '/',
component: {template: '<div>Click one link above</div>'}
},{
path: '/blank',
component: BlankComponent,
name: 'blank'
},
{
path: '/canvas',
component: CanvasComponent,
name: 'canvas'
}
]
});
var app = new Vue({
el: '#app',
router: router,
template: `
<div>
<router-link :to="{name: 'canvas'}">canvas</router-link> |
<router-link :to="{name: 'blank'}">blank</router-link>
<router-view></router-view>
</div>
`
});
document.body.addEventListener('click', (event) => {
switch (event.target.id) {
case 'canvas':
console.log('Canvas clicked');
break;
case 'btn1':
console.log('Button 1 clicked');
break;
case 'btn2':
console.log('Button 2 clicked');
break;
}
}, true);
<script src="//unpkg.com/vue#2.6.9/dist/vue.min.js"></script>
<script src="//unpkg.com/vue-router#3.1.3/dist/vue-router.min.js"></script>
<div id="app">
<canvas-component></canvas-component>
</div>
<template id="canvas-component">
<div id="canvaspage">
<canvas id="canvas"></canvas>
<div id="buttonlist">
<h5>Select your action:</h5>
<div class="col">
<button id="btn1">JS file custom action 1</button>
<button id="btn2">JS file custom action 2</button>
</div>
</div>
</div>
</template>

Prop passed to child component is undefined in created method

I am using Vue.js 2.
I have a problem with passing value to the child component as a prop. I am trying to pass card to card-component.
In card-component I can access the prop in the Card goes here {{card}} section.
However when I try to access it in created or mounted methods it's undefined.
Parent:
<template>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<card-component :card="place.card"></card-component>
</div>
</div>
</div>
</template>
<script>
import CostComponent from './CostComponent';
import CardComponent from './CardComponent';
export default {
components: {
CostComponent, CardComponent
},
props: ['id'],
data() {
return {
place: []
}
},
created() {
axios.get('/api/places/' + this.id)
.then(response => this.place = response.data);
}
}
</script>
Child:
<template>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<ul class="list-unstyled">
Card goes here {{card}}
</ul>
</div>
</div>
</div>
</template>
<script>
import CardItemComponent from './CardItemComponent';
export default {
components: {
CardItemComponent
},
props: ['card'],
created() {
console.log(this.card); // undefined
},
mounted() {
console.log(this.card); // undefined
},
}
</script>
I did a lot of googling but none of the solutions I found have fixed my issue.
This is purely a timing issue. Here's what happens...
Your parent component is created. At this time it has an empty array assigned to place (this is also a problem but I'll get to that later). An async request is started
Your parent component creates a CardComponent instance via its template
<card-component :card="place.card"></card-component>
at this stage, place is still an empty array, therefore place.card is undefined
3. The CardComponent created hook runs, logging undefined
4. The CardComponent is mounted and its mounted hook runs (same logging result as created)
5. Your parent component is mounted
6. At some point after this, the async request resolves and changes place from an empty array to an object, presumably with a card property.
7. The new card property is passed down into your CardComponent and it reactively updates the displayed {{ card }} value in its template.
If you want to catch when the card prop data changes, you can use the beforeUpdate hook
beforeUpdate () {
console.log(this.card)
}
Demo
Vue.component('CardComponent', {
template: '<pre>card = {{ card }}</pre>',
props: ['card'],
created () {
console.log('created:', this.card)
},
mounted () {
console.log('mounted:', this.card)
},
beforeUpdate () {
console.log('beforeUpdate:', this.card)
}
})
new Vue({
el: '#app',
data: {
place: {}
},
created () {
setTimeout(() => {
this.place = { card: 'Ace of Spades' }
}, 2000)
}
})
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app">
<card-component :card="place.card" />
</div>
See https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram
If place is meant to be an object, you should not be initialising it as an array. Also, if your CardComponent relies on data being present, you may want to conditionally render it.
For example
data () {
return { place: null }
}
and
<card-component v-if="place" :card="place.card"></card-component>
then CardComponent will only be created and mounted after place has data.
Make sure you have props: true in the router file. It is a simple solution but many of us forget this.
{
path: '/path-to',
name: 'Name To',
component: Component,
props: true
}

Vue - Change in the state does not re render the template?

I'm trying to understand the basics of Vue and so far what I understand is every time any of the states in the data property changes, the template or the component should re render. Here is the code snippet I'm working with.
index.html
<div id="app">
<h3>Generator</h3>
<div>
Input:
<input #input="onInput"/>
</div>
<div>
Output:
{{test()}}
</div>
</div>
main.js
new Vue({
el:'#app',
data: {
textInput: ''
},
methods: {
onInput(event){
this.textInput = event.target.value
},
test(){
console.log("Test running")
}
}
})
What I expected to happen?
Since I'm updating the textInput data property with every keystroke, I thought that since the template would re render itself, I would see the Test running message in the console every time I hit a key and since the page would re render every time, I would see the input field as blank.
What currently happens
I see the test function run only once when I run the code.
I don't see a blank input field with every key stroke
The DOM does not depend on textInput, so changes to it do not cause a re-render. If the render function uses the variable, you will get a re-render when the variable changes.
new Vue({
el:'#app',
data: {
textInput: ''
},
methods: {
onInput(event){
this.textInput = event.target.value;
},
test(){
console.log(this.textInput);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h3>Generator</h3>
<div>
Input:
<input #input="onInput"/>
</div>
<div>
Output:
{{textInput.length}}
{{test()}}
</div>
</div>

How to add/remove class on body tag when open/close modal in vuejs

I have a modal in one of my pages and I want to add a class “active” on body when I open the modal, so I can make the body overflow hidden (no scroll).
Is there a way to toogle a class on the body tag when I click from one component? I can't figure it out...
I use routes
<template>
<div id="app">
<Header />
<router-view/>
<Footer />
</div>
</template>
Thx in advance
The correct way of doing this in Vue is to communicate between components, in this case it might not be a simple parent/child communication, so you might want to create an Event Bus.
By using this approach the modal's code is has minimum effects on the rest of your application, it only dispatches events that you can subscribe to from any other component.
Note: In this case you won't add the class on your body tag (because you can't mount Vue on body), but you may just add it to your root div to have a similar result.
const eventBus = new Vue();
Vue.component('modal', {
props: ['isOpen'],
template: `
<div class="modal" v-if="isOpen">This is a modal</div>
`,
});
Vue.component('wrapper', {
template: `
<div>
<modal :isOpen="isModalOpen"></modal>
<button #click="toggleModal">toggle modal</button>
</div>
`,
data() {
return {
isModalOpen: false,
}
},
methods: {
toggleModal() {
this.isModalOpen = !this.isModalOpen;
eventBus.$emit('toggleModal', this.isModalOpen);
}
}
});
new Vue({
el: "#app",
data: {
active: false,
},
created() {
eventBus.$on('toggleModal', (isModalOpen) => {
this.active = isModalOpen;
});
},
})
.active {
background: grey;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app" :class="{active}">
<wrapper></wrapper>
</div>
This should help
document.body.className += 'active'