How to communicate with modal in bootstrap vue - vue.js

I have a complex modal that I've put in it's own component. It works on a copy of the passed model, to allow the user to cancel the action.
<template>
<b-modal
v-if="itemCopy"
v-model="shown"
#cancel="$emit('input', null)"
#ok="$emit('input', itemCopy)"
>
<!-- content -->
</b-modal>
</template>
<script>
export default {
props: {
value: Object
},
data() {
return {
shown: false,
itemCopy: null
};
},
watch: {
value(itemToDisplay) {
this.shown = !!itemToDisplay;
this.initValue();
},
item(it) {
this.initValue();
}
},
methods: {
initValue() {
this.itemCopy = _.cloneDeep(this.value);
}
}
};
</script>
The Idea to communicate with it is to pass an object in the v-model and, if set, the modal will be shown using that data, and when done, the new state will be communicated back over the v-model as well.
That is, if the user cancel/closed the modal, the v-modal variable will be null, otherwise it will be a new Model that will replace the one in v-modal.
<template>
<!-- omitted for brevity -->
<ItemModal v-model="modalItem" />
<template>
<script>
//...
export default {
data() {
return {
itemNumber: null
};
},
computed: {
modalItem:{
get() {
if (this.itemNumber != null) return this.entries[this.itemNumber];
},
set(newItem) {
if (newItem && this.itemNumber) {
//splice, etc.
}
// in any clase reset the selection to close the modal
this.itemNumber = null;
}
},
//...
<script>
The Problem I have is with the events from b-modal. I can use the #ok but there's no #notOk.
For example #cancel won't be thrown if the user click outside of the modal.
How can this be achieved? Is there another more easier way of doing this?

b-modal emits a generic hide event, which receives as its first argument the trigger that closed the modal (i.e. ok, cancel, esc, backdrop, etc):
<template>
<b-modal
v-if="itemCopy"
v-model="shown"
#hide="handleHide"
>
<!-- content -->
</b-modal>
</template>
<script>
export default {
// ...
methods: {
// ...
handleHide(bvEvt) {
if (bvEvt.trigger === 'ok') {
// User clicked OK button
this.$emit('input', this.itemCopy)
} else {
// The modal was closed not via the `ok` button
this.$emit('input', null)
}
}
}
};
</script>

<template>
<b-modal
:id="id"
#ok="$emit('ok', item)"
>
<!-- content -->
</b-modal>
</template>
<script>
export default {
props: {
item: Object,
id: String
}
};
</script>
<template>
<!-- omitted for brevity -->
<ItemModal :item="modalItem" :id="modalId" #ok="onModalOk" />
<template>
<script>
//...
export default {
data() {
return {
modalId: "myItemModal"
itemNumber: null
modalItem: null
};
},
methods: {
showItemModal(itemNumber) {
this.itemNumber = itemNumber
this.modalItem = _.cloneDeep(this.entries[itemNumber])
this.$bvModal.show(this.modalId)
},
onModalOk(newItem) {
if (newItem && this.itemNumber) {
//splice, etc.
}
}
}
//...
<script>

Related

Passing data from child component to parent and then to another child not working on page load but works after minor potentially unrelated change

I am new to Vuejs and come across this bug which I have no idea what I have done wrong. I am not receiving any console errors. It doesn't work on initial page load but it seems to work after I comment something out (or make a minor change). It will still then continue to work if I reverse the changes I just made and put it back to the original code. But once again on a fresh page load it won't work.
The issue: I am making a to do list and on page load when I add new tasks through the input field, the list does not appear on the page like it should be. I also console log the data array for this and it shows it is getting added to the array but is not getting rendered to the page. No console errors. In my code I will comment out some other data property (there are 2 additional ones below todosList in the TodoList.vue file that are currently not being used yet) and save and then the tasks will automatically appear on the page. So I think oh ok that might be the issue so with this new minor change I decide to refresh the page to see if it works as expected. Nope it doesn't so I then uncomment out what I previously commented out and save and the list appears again. But once again if I refresh the page it doesn't work. It only seems to be if I make a change inside the data function in the TodoList.vue file.
Additional info: The data is stored in the parent todos[] (App.vue), updated/pushed to array in a child (TodoCreate.vue) and sent back to the parent using $emit. This data is then sent through to another child (TodoList.vue) using props so that it can be rendered on the page.
Wondering if there is something that is not quite right in my code which is causing this to bug out like that. I will include everything in case it is something that looks unrelated to me but could be causing it.
Here is also a link to a code sandbox where the issue can be replicated by following the instructions on the page https://codesandbox.io/s/adding-new-todo-not-working-properly-jwwex?file=/src/components/TodoList.vue
main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
App.vue
<template>
<div :class="currentMode">
<the-header #modeToggled="updateMode($event)"></the-header>
<main>
<todo-create #addedTodos="updateTodos"></todo-create>
<todo-list :todos="todos"></todo-list>
</main>
</div>
</template>
<script>
import TheHeader from './components/TheHeader.vue';
import TodoCreate from './components/TodoCreate.vue';
import TodoList from './components/TodoList.vue';
export default {
name: 'App',
components: {
TheHeader,
TodoCreate,
TodoList,
},
data() {
return {
currentMode: {
dark_mode: true,
light_mode: false
},
todos: [],
}
},
methods: {
updateMode(mode) {
this.currentMode = mode;
},
updateTodos(data) {
this.todos = data;
console.log(this.todos);
},
toggleCompleted() {
}
},
// provide() {
// return {
// todos: this.todos,
// };
// }
}
</script>
TheHeader.vue
<template>
<h1>To-do App</h1>
<div>
<label for="toggle-mode" aria-label="Toggle light and dark mode"></label>
<input type="checkbox" id="toggle-mode" #change="toggleMode">
</div>
</template>
<script>
export default {
emits: ['modeToggled'],
data() {
return {
toggleState: false,
}
},
methods: {
toggleMode() {
this.toggleState = !this.toggleState;
this.$emit('modeToggled', this.modeClasses);
}
},
computed: {
modeClasses() {
return {
dark_mode: !this.toggleState,
light_mode: this.toggleState
}
}
}
}
</script>
TodoCreate.vue
<template>
<div>
<label for="newtodo" class="sr-only">Create new to do</label>
<input type="text" id="newtodo" placeholder="Create a new todo..." v-model="todoval" v-on:keyup.enter="addTodo" >
</div>
</template>
<script>
export default {
emits: ['addedTodos'],
data() {
return {
todoval: '',
taskNumber: 0,
todos: [],
};
},
methods: {
addTodo() {
const val = this.todoval;
const taskNumber = this.taskNumber;
this.todos.push({ taskID: taskNumber, value: val, complete : 'not-completed'});
this.todoval = '';
this.taskNumber++;
console.log(this.todos);
this.$emit('addedTodos', this.todos);
},
}
}
</script>
TodoList.vue
<template>
<ul class="todo-items" :class="filterClass">
<li class="drop-zone" v-for="(listItem, index) in todosList" :class="listItem.complete" :key="listItem.taskID"
#drop='onDrop($event, index)'
#dragover.prevent
#dragenter.prevent>
<div class="drag-el" draggable="true"
#dragstart='startDrag($event, index)'>
<label :for="'checkbox-'+index" :aria-label="'Mark task ' + listItem.value + ' as completed'"></label>
<input type="checkbox" :id="'checkbox-'+index" #change="toggleCompleted(index, listItem.value, listItem.complete, listItem.taskID)">
<input type="text" disabled :value="listItem.value">
<img src="../assets/icon-cross.svg" #click="removeTask(index)">
</div>
</li>
</ul>
</template>
<script>
export default {
props: {
todos: Object,
filterClass: String
},
// inject: ['todos'],
data() {
return {
todosList: this.todos,
// completedTodos: [],
// activeTodos: [],
};
},
// watch: {
// todosList(data) {
// data.filter(function(todo) {
// if(todo.completed == 'completed') {
// completedTodos.push(todos);
// }
// });
// }
// },
methods: {
startDrag: (evt, item) => {
evt.dataTransfer.dropEffect = 'move'
evt.dataTransfer.effectAllowed = 'move'
evt.dataTransfer.setData('itemID', item)
},
onDrop (evt, list) {
const itemID = evt.dataTransfer.getData('itemID');
const movedData = this.todosList[itemID];
this.todosList.splice(itemID,1);
this.todosList.splice(list,0, movedData);
},
toggleCompleted() {
// still need to write this method
},
removeTask() {
// still need to write this method
}
}
}
</script>

Vue JS: Show loader initially and hide it after getting response

I want to show loader initially but after the response I want to hide it. I have following code:
<template>
<div id="app">
<Document :loading="loading">
</div>
</template>
<script>
import Document from "./components/Document";
export default {
name: "App",
data() {
return {
loading: true
};
},
components: {
Document
},
methods:{
function(){
let response = await Axios.get(`this-is-url`, {});
if (response.data == null) {
return;
} else {
this.loading = false
}
}
}
};
</script>
The loader will be placed in the document component like below:
<template>
<div>
<div>
<b-spinner type="grow" label="Spinning"></b-spinner>
</div>
</div>
</template>
<script>
export default {
name: "Document",
props: {
loading: null
},
};
</script>
I am writing function in App component because this will come from emit.
i think by changing the value of loading to false at the response it will hide the Document component.
if (response.data == null) {
this.loading= false;
}

Emitting data via EventBus

I would like to emit some data from one component to the other (children components).
In my main.js I created: export const Bus = new Vue({}).
Then in my first child component ,I've got an input with v-model and I would like to pass that v-model.
<template>
<div>
<input type="text" v-model="message" />
<button type="button" #click="submit">Submit</button>
</div>
</template>
<script>
import { Bus } from './../main.js';
export default {
data () {
return {
message: ''
}
},
methods: {
submit() {
if(this.message !== ''){
this.$router.push('location');
Bus.$emit('name', this.message);
}
}
}
}
</script>
My second component:
import { Bus } from './../main.js';
export default {
data() {
return {
recievedMessage: ''
}
},
created() {
Bus.$on('name', (message) => {
this.recievedMessage = message;
})
}
}
Then I try to display passed data: {{ recievedMessage }}, but unfortunately it doesn't work.
Assuming you set up a global EventHub in your main.js, the second component isn't listening because it has not been initialized in the whole Vuejs life-cycle.
However, if you intended on your child component being rendered within the parent then you will need to import the component into the parent.
Parent Component
<template>
<div>
<input type="text" v-model="message" />
<button type="button" #click="submit">Submit</button>
<child-component />
</div>
</template>
<script>
import ChildComponent from '#/components/ChildComponent'
import { Bus } from './../main.js';
export default {
components:{
ChildComponent
}
data () {
return {
message: ''
}
},
methods: {
submit() {
if(this.message !== ''){
this.$router.push('location');
Bus.$emit('name', this.message);
}
}
}
}
</script>
UPDATED
Child Component
<template>
<div>{{recievedMessage}}</div>
</template>
<script>
import { Bus } from './../main.js';
export default {
data() {
return {
recievedMessage: ''
}
},
created() {
Bus.$on('name', this.eventHandlerMethod)
},
methods: {
eventHandlerMethod (message) {
this.recievedMessage = message;
}
}
}
</script>
The listener is calling "eventHandlerMethod" which update the data instance.

Passing in a prop and setting it as data

I'm trying to pass a prop from my drop down button component:
<template>
<div>
<p #click="toggleActive">Open Drop Down</p>
<drop-down :active="this.active"></drop-down>
</div>
</template>
<script>
export default {
data() {
return {
active: false,
}
},
methods: {
toggleActive() {
return this.active = ! this.active;
}
}
}
</script>
To my drop down component:
<template>
<div class="drop-down" v-if="this.passedActive">
<p #click="toggleActive">Close drop down</p>
....
<script>
export default {
props: ['active'],
data() {
return {
passedActive: this.active,
}
},
methods: {
toggleActive() {
return this.passedActive = ! this.passedActive;
}
}
}
</script>
The idea is that I can activate the drop down component from it's parent, and then inside the drop down component I can modify this prop and deactivate the drop down - as if someone is pressing an 'x' inside the component.
I've checked the docs and this does appear to be the correct way to do it, but for some reason it's not working.
The code below works. As noted in the comments under your question, passedActive is initialized once. The parent controls the initial state (only), and the child itself controls any subsequent state. If you start with it false, it never gets to become true, because the controller is never displayed.
That is a design flaw: there should be one data item that controls it, not two. The child component should rely on its prop, and its toggle function should emit an event that the parent handles.
new Vue({
el: '#app',
data: {
active: true
},
methods: {
toggleActive() {
console.log("Toggling");
this.active = !this.active;
}
},
components: {
dropDown: {
props: ['active'],
data() {
return {
passedActive: this.active,
}
},
methods: {
toggleActive() {
return this.passedActive = !this.passedActive;
}
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<p #click="toggleActive">Open Drop Down {{active}}</p>
<drop-down :active="active" inline-template>
<div class="drop-down" v-if="this.passedActive">
<p #click="toggleActive">Close drop down</p>
</div>
</drop-down>
</div>

Vue component data not updating from props

I'm building a SPA with a scroll navigation being populated with menu items based on section components.
In my Home.vue I'm importing the scrollNav and the sections like this:
<template>
<div class="front-page">
<scroll-nav v-if="scrollNavShown" #select="changeSection" :active-section="activeItem" :items="sections"></scroll-nav>
<fp-sections #loaded="buildNav" :active="activeItem"></fp-sections>
</div>
</template>
<script>
import scrollNav from '.././components/scrollNav.vue'
import fpSections from './fpSections.vue'
export default {
data() {
return {
scrollNavShown: true,
activeItem: 'sectionOne',
scrollPosition: 0,
sections: []
}
},
methods: {
buildNav(sections) {
this.sections = sections;
console.log(this.sections)
},
changeSection(e) {
this.activeItem = e
},
},
components: {
scrollNav,
fpSections
}
}
</script>
this.sections is initially empty, since I'm populating this array with data from the individual sections in fpSections.vue:
<template>
<div class="fp-sections">
<keep-alive>
<transition
#enter="enter"
#leave="leave"
:css="false"
>
<component :is="activeSection"></component>
</transition>
</keep-alive>
</div>
</template>
<script>
import sectionOne from './sections/sectionOne.vue'
import sectionTwo from './sections/sectionTwo.vue'
import sectionThree from './sections/sectionThree.vue'
export default {
components: {
sectionOne,
sectionTwo,
sectionThree
},
props: {
active: String
},
data() {
return {
activeSection: this.active,
sections: []
}
},
mounted() {
this.buildNav();
},
methods: {
buildNav() {
let _components = this.$options.components;
for(let prop in _components) {
if(!_components[prop].hasOwnProperty('data')) continue;
this.sections.push({
title: _components[prop].data().title,
name: _components[prop].data().name
})
}
this.$emit('loaded', this.sections)
},
enter(el) {
twm.to(el, .2, {
autoAlpha : 1
})
},
leave(el, done) {
twm.to(el, .2, {
autoAlpha : 0
})
}
}
}
</script>
The buildNav method loops through the individual components' data and pushes it to a scoped this.sections array which are then emitted back to Home.vue
Back in Home.vue this.sections is populated with the data emitted from fpSections.vue and passed back to it as a prop.
When I inspect with Vue devtools the props are passed down correctly but the data does not update.
What am I missing here? The data should react to props when it is updated in the parent right?
:active="activeItem"
this is calld "dynamic prop" not dynamic data. You set in once "onInit".
For reactivity you can do
computed:{
activeSection(){ return this.active;}
}
or
watch: {
active(){
//do something
}
}
You could use the .sync modifier and then you need to emit the update, see my example on how it would work:
Vue.component('button-counter', {
template: '<button v-on:click="counter += 1">{{ counter }}</button>',
props: ['counter'],
watch: {
counter: function(){
this.$emit('update:counter',this.counter)
}
},
})
new Vue({
el: '#counter-sync-example',
data: {
foo: 0,
bar: 0
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js"></script>
<div id="counter-sync-example">
<p>foo {{ foo }} <button-counter :counter="foo"></button-counter> (no sync)</p>
<p>bar {{ bar }} <button-counter :counter.sync="bar"></button-counter> (.sync)</p>
</div>