vue.js - close modal when update - vue.js

Here is my architecture :
Vue.component('center-view', {
props: ["center"],
template: `
<ul style="border: 1px black solid">
<li>Center : {{ center.name }}</li>
<li v-for="p in center.people">
{{ p }}
<button>Affect this user to another center (modal)</button>
</li>
</ul>`
});
new Vue({
el: "#root",
data: {
centers: []
},
created: function () {
this.refreshData();
},
methods: {
refreshData: function() {
// (actually, an ajax request here)
this.centers = [{
name: "Paris",
people: ["Seb", "Eric"]
},
{
name: "NY",
people: ["Edmond", "Nath"]
}
];
}
}
});
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="root">
<center-view
:center="center"
v-for="center in centers"
></center-view>
</div>
The <button> will be replace by a modal.
Here what I would like:
By default the modal's state is "hidden" and only a button is shown
When the user click on that button, the modal's state becomes
"visible" and the entire modal is shown.
The user can affect the people to another center and validate by clicking a button (in the modal), this button emits an event caught by
root.
The root component make an ajax request to the server to update the affectations.
4.a If this request is succesfull, the root component apply a "refreshData" and the modal is closed
4.b Else, the modal is not shown and an error message is shown in this modal.
I'm stuck at the steps 4.a/b. : the only solution I see is to put the state of all modals in the root component. It seems a bit uggly to me (the root component doesn't care about the modal is visible or not).

Related

Single File Component with slot gets unexpectedly re-rendered on parent event

I am not very confortable with Vue slots yet so maybe I'm using them wrongly. I have 2 Single File Components that are defined the following way :
HelloWorld.vue :
<template>
<div>
<div
v-for="item in items"
:key="item"
#mouseover="highlighted = item"
:class="{ highlighted: highlighted === item }"
>
{{ item }}
<Info>
<img
src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png"
/>
</Info>
</div>
</div>
</template>
<script>
import Info from "./Info.vue";
export default {
name: "HelloWorld",
components: {
Info,
},
data: () => ({
highlighted: null,
items: [1, 2, 3],
}),
};
</script>
<style scoped>
.highlighted {
background: grey;
}
</style>
Info.vue :
<template>
<div><slot /></div>
</template>
<script>
export default {
name: "Info",
beforeUpdate() {
console.log("beforeUpdate Info.vue");
},
};
</script>
What I don't understand is : when the mouseover event in the HelloWorld SFC is triggered, the beforeUpdate method of Info.vue is called 3 times each time (as many times as there are items in my list). How come this method is called (since no data passed to the Info component is changed) and how can I prevent this potentially costly re-render? Interestingly, the re-render doesn't happen if I remove the class attribute in HelloWorld that toggles the line highlight.
The full code is here : https://codesandbox.io/s/tender-swanson-57oev
This is happening because highlighted class has to be evaluated for each element, every time mouseover event takes place.
This happens because highlighted prop changes every time mouseover takes place, which re-triggers Vue to figure out which element to attach the highlighted css class to.

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>

Is there a way, to "emit" a built in event, by hard coding it in Vue instance?

I would like to link two components with each other in my Vue project.
I use two-way binding for that, so I have a parent, and two child components.
The concept:
We see a carousel in the left side of the screen, and we see an accordion in the right side. I built the carousel and the accordions with v-for from a database file.
When I click in some of the accordion it drops down, and I need a reaction from a carousel component, to slide exactly there, where I clicked in the accordion.
Like:
carousel: banana, apple, house
accordion: banana, apple house
So when im clicking in the apple accordion button, I need the slider to go to the where are the apple is displayed, and reverse.
As I said, I already bind the two components to each other, so when I'm clicking one of the accordion buttons like #click="onShowStart(index)", I get that index in the another child too, and it's changing dynamically vica-versa by sliding or clicking. So the indexes are already linked and its dynamic.
My problem is I don't know how to trigger an event, like #sliding-start from vue instance in the watch field. So I watch the "actualPosition" prop in my component, and when its changed (from 3 to 1 for example), I would like to start a sliding event to the new value of the actualPosition.
So i need something like:
this.$emit('sliding-start', actualPosition);
I've been sitting at this problem for days, but I think my whole thinking is wrong. But before i believe this, im asking you first.
Here is my code for the Parent component:
<div class="row">
<carousel :actualPosition="actualPosition" class="col bg-dark" #sendTheCarouselPosition="updateAccordion($event)"></carousel>
<accordion :actualPosition="actualPosition" class="col bg-dark" #sendTheAccordionlPosition="updateCarousel($event)"></accordion>
</div>
<script>
export default {
data() {
return {
actualPosition: null,
}
},
methods:{
updateAccordion: function (updatedAccordion){
this.actualPosition = updatedAccordion;
},
updateCarousel: function(updatedSlider){
this.actualPosition = updatedSlider
}
},
}
</script>
My Accordion component:
<template>
<div role="tablist">
<b-card no-body class="mb-1" v-for="(item, index) in dataForProject">
<b-card-header header-tag="header" class="p-1" role="tab">
<b-button block href="#" v-b-toggle="'accordion-' + index" variant="info" #click="onShowStart(index)" >{{ item.title }}</b-button>
</b-card-header>
<b-collapse :id="'accordion-' + index" visible accordion="my-accordion" role="tabpanel">
<b-card-body>
<div>
<h1>data from Carousel sibling: {{ actualPosition }}</h1>
</div>
<b-card-text>{{ item.content }}</b-card-text>
</b-card-body>
</b-collapse>
</b-card>
</div>
</template>
<script>
import myDataBase from '../data2'
export default {
props:['actualPosition'],
watch:{
actualPosition: function () {
},
},
data() {
return {
dataForProject: myDataBase,
}
},
methods:{
onShowStart: function (accordionIndex) {
this.$emit('sendTheAccordionlPosition', accordionIndex);
},
},
}
</script>
And my Carousel component:
<template>
<div>
<p class="mt-4 text-white">
data from Accordion sibling: {{ actualPosition }}
</p>
<b-carousel
id="carousel-1"
:interval="0"
controls
indicators
background="#ababab"
img-width="1024"
img-height="480"
style="text-shadow: 1px 1px 2px #333;"
ref="slider"
#sliding-start="onSlideStart"
#sliding-end="onSlideEnd"
>
<b-carousel-slide v-for="(item, index) in dataForProject" :id="index" >
<img
slot="img"
class="d-block img-fluid w-100"
width="1024"
height="480"
:src="item.image_url"
alt="image slot"
>
</b-carousel-slide>
</b-carousel>
</div>
</template>
<script>
import myDataBase from '../data2'
export default {
props:['actualPosition'],
watch: {
actualPosition: function () {
},
},
data() {
return {
//slide: 0,
dataForProject: myDataBase,
}
},
methods: {
onSlideStart(slide) {
this.$emit('sendTheCarouselPosition', slide);
},
onSlideEnd(slide) {
},
}
}
</script>
I can get this done by two ways.
1 - Global EventBus
I will create an eventBus and register events on it from any file and listen it anywhere -
import { EventBus } from '#/eventBus'
// simply import it to component which need listen the event
//Register Event where you have your methods - like In your COMP_B_TWO
EventBus.$on('changeValue', () => { this.doSomething() })
// Emit event from another component
EventBus.$emit('changeValue')// Like directly from your COMP_A_TWO
To know how to create a eventBus follow this - Global Event Bus Vue
2 - Use state management - Vuex Follow this link - Vuex
Basically, it will have centralized store for all the components in an application. Whenever you wish to update state you will update to store. And all the other component using that state will react accordingly
Okay, so my whole concept was wrong, and i was wrong about this.
I should have used the v-model for this whole thing. And nothing more.
I added the v-model to the target tags, and my problem is solved.

Modal component in VueJS

I am building a web app. I have few components that are modals (that show data about customers, lessons, ...).
I search a way to show one of the components easily.
And if possible doing lazy loading.
What's the best way to perform this?
Check out conditional rendering, specifically v-if. This would only load the modal if the button is clicked for example.
https://v2.vuejs.org/v2/guide/conditional.html#v-if
Single page component:
<template>
<div>
<div
v-if="showModal"
class="modal">
Stuff
</div>
<button #click="toggleModal">
Toggle Modal
</button>
</div>
</template>
<script>
export default {
data () {
return {
showModal: false
}
},
methods: {
toggleModal() {
this.showModal = !this.showModal
}
},
}
</script>

Slot doesn't render well on child component VueJS

I'm trying to loop over a component, I fill up slot with some data but they are not rendering well.
Weird behaviors :
Data are displayed but not visible.
In chrome if i toggle the device toolbar in the debug panel, data are now visible.
Changing font-size in the debug panel make my data visible
When i put a Child component outside the loop, the looped ones are rendered well.
Snippet from my parent Component :
<li class="cards__item" v-for="(staffMember, index) in staff">
<card-profil>
<h3 slot="title">{{staffMember.name}}</h3>
</card-profil>
</li>
Snippet from my child Component :
<template>
<section class="card-profil">
<figure class="fig-card-profil">
<figcaption class="figcaption-card-profil">
<slot name="title"></slot>
</figcaption>
</figure>
</section>
</template>
I get my data this way in my parent component:
export default {
data: function () {
return {
staff: []
}
},
mounted () {
this.getStaff()
},
methods: {
getStaff: async function () {
const staff = await axios({ url: 'https://randomuser.me/api/?results=8' })
this.staff = staff.data.results
}
}
}
Is this problem of lifehook ? Do i have to use Scoped slot instead ? V-for issue ?
Thanks for sharing your thoughts.