Bootstrap-vue: how to pass data to modal? - vue.js

I'm trying to use bootstrap-vue modal to show details from a collection of items.
What I want is to pass data to modal to show a simple message.
I first loop over recordset to show button.
<ul>
<li v-for="item in items">{{ item.first_name }}
<b-button size="sm" v-b-modal="'myModal'" user="'item'">
Saluta {{item.first_name}}
</b-button>
</li>
</ul>
And then display name in modal:
<b-modal id="myModal" :user="'user'">
Hello {{user}}!
</b-modal>
Here's my fiddle https://jsfiddle.net/bptLavov/259/

This works just fine:
HTML:
<div id="app">
<ul>
<li v-for="item in items">
{{ item.first_name }}
<b-button size="sm" v-b-modal="'myModal'" user="'item'" click="sendInfo(item)">
Saluta {{item.first_name}}
</b-button>
</li>
</ul>
<b-modal id="myModal">
Hello {{selectedUser.first_name}} {{selectedUser.last_name}} !
</b-modal>
</div>
JAVASCRIPT:
new Vue({
el: '#app',
data: {
items :
[
{ first_name: 'Dickerson', last_name: 'Macdonald' },
{ first_name: 'Larsen', last_name: 'Shaw' },
{ first_name: 'Geneva', last_name: 'Wilson' },
{ first_name: 'Jami', last_name: 'Carney' }
],
selectedUser: '',
},
methods: {
sendInfo(item) {
this.selectedUser = item;
}
}
})
What it does is:
1) Execute a method named sendInfo
2) That methods will set the selectedUser variable inside data with the selected user which information is sent thanks to the v-on:click (#click) directive depending on the v-for iteration. Because of that, each button will send the right information.
3) Display the information inside the modal

You can use vuex and your components won't have to be in the same file or related.
Component which will open the modal:
<ul>
<li v-for="item in items">{{ item.first_name }}
<b-button #click="$store.dispatch('modals/openModal', { data: item, modalId: 'myModal' })">
Saluta {{item.first_name}}
</b-button>
</li>
</ul>
Modal's template:
<b-modal id="myModal">
Hello {{selectedUser.first_name}} {{selectedUser.last_name}} !
</b-modal>
Modal's computed property:
selectedUser() { return this.$store.state.modals.modalData },
Vuex module (modals.js):
const state = {
modalData: {},
}
const mutations = {
setModalData(state, data) { state.modalData = data },
}
const actions = {
openModal(context, data) {
context.commit('setModalData', data.data)
$('#' + data.modalId).modal('show')
},
}

Related

Removing specific object from array keeps removing last item

Here is what I have and I will explain it as much as I can:
I have a modal inside my HTML code as shown below:
<div id="favorites-modal-edit" class="modal">
<div class="modal-background"></div>
<div class="modal-card px-4">
<header class="modal-card-head">
<p class="modal-card-title">Favorites</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="container">
<div id="favorites-modal-edit-wrapper" class="columns is-multiline buttons">
<favorites-edit-component v-for="(favorite, index) in favorites_list" :key="favorite.id" :favorite="favorite" />
</div>
</div>
</section>
<footer class="modal-card-foot">
<button class="button" #click="addItem">
Add Item
</button>
</footer>
</div>
</div>
The id="favorites-modal-edit" is the Vue.js app, then I have the <favorites-edit-component /> vue.js component.
Here is the JS code that I have:
I have my favorites_list generated which is an array of objects as shown below:
const favorites_list = [
{
id: 1,
name: 'Horse',
url: 'www.example.com',
},
{
id: 2,
name: 'Sheep',
url: 'www.example2.com',
},
{
id: 3,
name: 'Octopus',
url: 'www.example2.com',
},
{
id: 4,
name: 'Deer',
url: 'www.example2.com',
},
{
id: 5,
name: 'Hamster',
url: 'www.example2.com',
},
];
Then, I have my vue.js component, which is the favorites-edit-component that takes in the #click="removeItem(this.index) which is coming back as undefined on the index.
Vue.component('favorites-edit-component', {
template: `
<div class="column is-half">
<button class="button is-fullwidth is-danger is-outlined mb-0">
<span>{{ favorite.name }}</span>
<span class="icon is-small favorite-delete" #click="removeItem(this.index)">
<i class="fas fa-times"></i>
</span>
</button>
</div>
`,
props: {
favorite: Object
},
methods: {
removeItem: function(index) {
this.$parent.removeItem(index);
},
}
});
Then I have the vue.js app that is the parent as shown below:
new Vue({
el: '#favorites-modal-edit',
// Return the data in a function instead of a single object
data: function() {
return {
favorites_list
};
},
methods: {
addItem: function() {
console.log('Added item');
},
removeItem: function(index) {
console.log(index);
console.log(this.favorites_list);
this.favorites_list.splice(this.favorites_list.indexOf(index), 1);
},
},
});
The problem:
For some reason, each time I go to delete a item from the list, it's deleting the last item in the list and I don't know why it's doing it, check out what is happening:
This is the guide that I am following:
How to remove an item from an array in Vue.js
The item keeps coming back as undefined each time the remoteItem() function is triggered as shown below:
All help is appreciated!
There is an error in your favorites-edit-component template, actually in vue template, when you want to use prop, data, computed, mehods,..., dont't use this
=> there is an error here: #click="removeItem(this.index)"
=> in addition, where is index declared ? data ? prop ?
you're calling this.$parent.removeItem(index); then in removeItem you're doing this.favorites_list.splice(this.favorites_list.indexOf(index), 1); this means that you want to remove the value equal to index in you array no the value positioned at the index
=> this.favorites_list[index] != this.favorites_list[this.favorites_list.indexOf(index)]
In addition, I would suggest you to modify the favorites-edit-component component to use event so it can be more reusable:
favorites-edit-component:
<template>
<div class="column is-half">
<button class="button is-fullwidth is-danger is-outlined mb-0">
<span>{{ favorite.name }}</span>
<span class="icon is-small favorite-delete" #click="$emit('removeItem', favorite.id)">
<i class="fas fa-times"></i>
</span>
</button>
</div>
</template>
and in the parent component:
<template>
...
<div id="favorites-modal-edit-wrapper" class="columns is-multiline buttons">
<favorites-edit-component
v-for="favorite in favorites_list"
:key="favorite.id"
:favorite="favorite"
#removeItem="removeItem($event)"
/>
</div>
...
</template>
<script>
export default {
data: function () {
return {
favorites_list: [],
};
},
methods: {
...
removeItem(id) {
this.favorites_list = this.favorites_list.filter((favorite) => favorite.id !== id);
}
...
},
};
I would restructure your code a bit.
In your favorites-edit-component
change your removeItem method to be
removeItem() {
this.$emit('delete');
},
Then, where you are using your component (in the template of the parent)
Add an event catcher to catch the emitted "delete" event from the child.
<favorites-edit-component v-for="(favorite, index) in favorites_list" :key="favorite.id" :favorite="favorite" #delete="removeItem(index)"/>
The problem you have right now, is that you are trying to refer to "this.index" inside your child component, but the child component does not know what index it is being rendered as, unless you specifically pass it down to the child as a prop.
Also, if you pass the index down as a prop, you must refer to it as "index" and not "this.index" while in the template.

Change component state form another component rendered in a v-for

I have a list of components rendered in a a v-for. I want to set the "show" Boolean property as false in the other components when one of them is set to true:
To simplify I am only adding two components
Main component code:
<template>
<aside class="main-sidebar">
<section class="sidebar">
<ul class="sidebar-menu" data-widget="tree">
<nav-bar-user-profile-item></nav-bar-user-profile-item>
<nav-bar-item></nav-bar-item>
<nav-bar-item></nav-bar-item>
</ul>
</section>
</aside>
</template>
<script>
import NavBarUserProfileItem from '#/components/NavBar/NavBarUserProfileItem';
import NavBarItem from '#/components/NavBar/NavBarItem';
export default {
name: 'NavBar',
components: {
NavBarUserProfileItem,
NavBarItem
},
methods: {
MenuHasBeenToggled(event) {
console.log(event);
}
}
}
NavBarItemComponent
<template>
<li class="treeview2 item" :class="{'menu-open': isOpen, 'active': menu.active}" #click="ToggleState">
<a href="#">
<i class="fa fa-th"></i>
<span>{{ menu.title }}</span>
<span class="pull-right-container">
<i class="fa fa-angle-right pull-right"></i>
</span>
</a>
<collapse-transition>
<ul class="treeview-menu" v-show="isOpen">
<li v-for="submenu in menu.submenus" :key="submenu.title" :class="{'active': (('active' in submenu) ? submenu.active : false)}">
<b-link :href="submenu.link">
<i class="fa fa-circle-thin"></i>
{{ submenu.title }}
</b-link>
</li>
</ul>
</collapse-transition>
</li>
</template>
<script>
export default {
name: 'NavBarItem',
data: function () {
return {
isOpen: false
}
},
computed: {
},
methods: {
ToggleState() {
this.isOpen = !this.isOpen;
this.$emit("toggle-state");
}
},
props: {
menu: {
type: Object,
default: function() {
return {
link: "#",
title: "Main menu",
active: true,
submenus: [
{
link: "#",
title: "Submenu 1",
},
{
link: "#",
title: "Submenu 2",
active: true
},
{
link: "#",
title: "Submenu 3",
},
]
}
}
}
}
}
</script>
<style scoped>
</style>
The goal is to click on one of the and show the menu contents while at the same time collapse the other components.
I thought about using an array of variables and bind it to the "show" prop and with an event listen to it and set every variable to false except the one form the component that sent the event.
How can I know which component sent the event?
Any better idea on how to accomplish this task?
I think, the best way to do it is to add a uniuque identifier property to each NavBarItem and a property for a selected NavBarItem. Then in the main component you can on click on NavBarItem set selected NavBarItem and in NavBarItem make the isOpen computed on the basis if current NavBarItem identifier equals the clicked NavBarItem. Something like this:
<template>
<aside class="main-sidebar">
<section class="sidebar">
<ul class="sidebar-menu" data-widget="tree">
<nav-bar-user-profile-item></nav-bar-user-profile-item>
<nav-bar-item item-id="1" :selected-item-id="selectedNavbarItemId" #click="selectedNavBarItemId = 1"></nav-bar-item>
<nav-bar-item item-id="2" :selected-item-id="selectedNavbarItemId" #click="selectedNavBarItemId = 2"></nav-bar-item>
</ul>
</section>
</aside>
</template>
<script>
import NavBarUserProfileItem from '#/components/NavBar/NavBarUserProfileItem';
import NavBarItem from '#/components/NavBar/NavBarItem';
export default {
name: 'NavBar',
components: {
NavBarUserProfileItem,
NavBarItem
},
data: function(){
return {
selectedNavBarItemId: 0
}
},
methods: {
MenuHasBeenToggled(event) {
console.log(event);
}
}
}
And in NavBarItem
<template>
<li class="treeview2 item" :class="{'menu-open': isOpen, 'active': menu.active}" #click="ToggleState">
<a href="#">
<i class="fa fa-th"></i>
<span>{{ menu.title }}</span>
<span class="pull-right-container">
<i class="fa fa-angle-right pull-right"></i>
</span>
</a>
<collapse-transition>
<ul class="treeview-menu" v-show="isOpen">
<li v-for="submenu in menu.submenus" :key="submenu.title" :class="{'active': (('active' in submenu) ? submenu.active : false)}">
<b-link :href="submenu.link">
<i class="fa fa-circle-thin"></i>
{{ submenu.title }}
</b-link>
</li>
</ul>
</collapse-transition>
</li>
</template>
<script>
export default {
name: 'NavBarItem',
data: function () {
return {
}
},
computed: {
isOpen:function(){
return itemId == selectedItemId;
}
},
methods: {
},
props: {
itemId:Number,
selectedItemId:Number,
menu: {
type: Object,
default: function() {
return {
link: "#",
title: "Main menu",
active: true,
submenus: [
{
link: "#",
title: "Submenu 1",
},
{
link: "#",
title: "Submenu 2",
active: true
},
{
link: "#",
title: "Submenu 3",
},
]
}
}
}
}
}
</script>
<style scoped>
</style>

Using a vue-apollo component with different queries on the same page

I would like to make a wrapper component using Apollo, which receives different GraphQL queries and variables within props, queries GraphQL source and and passes response to its child component. I would like to use this component twice on a page with different queries.
But I think I'm stuck at some point. I end up having both instances of the component working with same inputs: query prop of the first dropdown component is being used in all dropdowns on the page. Although they have a different scope, different keywords and items, all dropdowns on the page are using the query of first dropdown.
Here is my DropdownSearch component. It passes keyword from searchbar component to query-list component:
<template>
<div class="dropdown-search">
<div class="dropdown-search-display" #click="toggleDropdown">
<span>{{value[nameProp]}}</span>
<span class="dropdown-search-dropdown-toggle">
<i v-if="!isOpen" class="fa fa-caret-down" aria-hidden="true"></i>
<i v-if="isOpen" class="fa fa-caret-up" aria-hidden="true"></i>
</span>
</div>
<div :class="{ 'dropdown-search-dropdown': true, 'dropdown-search-dropdown--open': isOpen}" >
<search-bar v-model="searchKeyword" class="dropdown-search-searchbar"></search-bar>
<div class="dropdown-search-list">
<query-list
:query="query"
:keyword="searchKeyword"
:listItemComponent="ListItem"
></query-list>
</div>
</div>
</div>
</template>
<script>
const ListItem = {
template: '<div class="dropdown-search-list-item">{{record.name}}</div>',
props: {
record: { required: true },
},
};
export default {
name: 'dropdownSearch',
props: {
name: String,
nameProp: String,
value: Object,
query: Object,
},
components: {
'dropdown-list-item': ListItem,
},
data() {
return {
isOpen: false,
searchKeyword: '',
ListItem,
};
},
methods: {
toggleDropdown() {
this.isOpen = !this.isOpen;
},
},
};
</script>
QueryList component, which is using Apollo to make queries and displays a list of results:
<template>
<div class="">
<loading-indicator :isLoading="loading > 0"></loading-indicator>
<ul class="app__list">
<li class="app__list-item" v-for="item in items" :key="item.id">
<component
:is="listItemComponent"
:record="item"
></component>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'queryList',
props: {
query: Object,
keyword: String,
listItemComponent: { required: true },
},
data() {
return {
items: [],
loading: 0,
};
},
apollo: {
items: {
query() {
return this.query;
},
variables() {
return {
keyword: this.keyword || '',
};
},
loadingKey: 'loading',
},
},
};
</script>
This is how I use Dropdown Search components on a page:
<label class="edit-record-field">
<span class="edit-record-field-label">Category</span>
<dropdown-search
class="edit-record-field-input"
v-model="record.category"
:nameProp="'name'"
:query="getCategories"
></dropdown-search>
</label>
<label class="edit-record-field">
<span class="edit-record-field-label">Location</span>
<dropdown-search
class="edit-record-field-input"
v-model="record.location"
:nameProp="'name'"
:query="getLocation"
></dropdown-search>
</label>
I'm looking for a solution. I would be grateful if you could help me to make my components working.

why doesn't my interpolated html display properly in vue.js?

The data in my app.js file is not being interpolated and displayed on the screen. I tried the older v-repeat. Could it be some missing items in the installation? Any help would be appreciated.
Here is my html
<!-- show the events -->
<div class="col-sm-6">
<div class="list-group">
<a href="#" class="list-group-item" v-for="event in events">
<!-- <h1>{{text}}</h1> -->
<h4 class="list-group-item-heading">
<i class="glyphicon glyphicon-bullhorn"></i>
{{ event.name }}
</h4>
<h5>
<i class="glyphicon glyphicon-calendar" v-if="event.date"></i>
{{ event.date }}
</h5>
<p class="list-group-item-text" v-if="event.description">{{ event.description }}</p>
<button class="btn btn-xs btn-danger" v-on="click: deleteEvent($index)">Delete</button>
</a>
</div>
</div>
And here is my js file
new Vue({
data: {
text: 'hello world',
event: { name: '', description: '', date: '' },
events: []
},
// Anything within the ready function will run when the application loads
ready: function() {
// When the application loads, we want to call the method that initializes
// some data
this.fetchEvents();
},
// Methods we want to use in our application are registered here
methods: {
// We dedicate a method to retrieving and setting some data
fetchEvents: function() {
var events = [
{
id: 1,
name: 'TIFF',
description: 'Toronto International Film Festival',
date: '2015-09-10'
},
{
id: 2,
name: 'The Martian Premiere',
description: 'The Martian comes to theatres.',
date: '2015-10-02'
},
{
id: 3,
name: 'SXSW',
description: 'Music, film and interactive festival in Austin, TX.',
date: '2016-03-11'
}
];
// $set is a convenience method provided by Vue that is similar to pushing
// data onto an array
this.$set('events', events);
},
// Adds an event to the existing events array
addEvent: function() {
if(this.event.name) {
this.events.push(this.event);
this.event = { name: '', description: '', date: '' };
}
}
}
});
Thanks for any help.
So, there are a few issues here. I've cleaned them up for you to look over.
Added the el: "#app" property to tell Vue where to mount your Vue.
Changed your ready to mounted (you could also use created).
Changed this.$set('events',events) to this.events = events.
Added the unreferenced deleteEvent method.
Fixed the click handler syntax for v-on:click.
So the code ends up looking like this.
new Vue({
el: "#app",
data: {
text: 'hello world',
event: { name: '', description: '', date: '' },
events: []
},
mounted: function() {
this.fetchEvents();
},
// Methods we want to use in our application are registered here
methods: {
// We dedicate a method to retrieving and setting some data
fetchEvents: function() {
var events = [...];
this.events = events;
},
// Adds an event to the existing events array
addEvent: function() {
if(this.event.name) {
this.events.push(this.event);
this.event = { name: '', description: '', date: '' };
}
},
deleteEvent(){
alert('delete this one')
}
}
});
Template
<div id="app">
<!-- show the events -->
<div class="col-sm-6">
<div class="list-group">
<a href="#" class="list-group-item" v-for="event in events">
<!-- <h1>{{text}}</h1> -->
<h4 class="list-group-item-heading">
<i class="glyphicon glyphicon-bullhorn"></i>
{{ event.name }}
</h4>
<h5>
<i class="glyphicon glyphicon-calendar" v-if="event.date"></i>
{{ event.date }}
</h5>
<p class="list-group-item-text" v-if="event.description">{{ event.description }}</p>
<button class="btn btn-xs btn-danger" v-on:click=" deleteEvent(event)">Delete</button>
</a>
</div>
</div>
</div>
Working example.

Having trouble passing data between Components

How do I pass an Object Array created by a method within my component to another component? I've tried using props but perhaps my understanding of how props work isn't correct.
<template>
<div class="media-body">
<ul>
<li v-for="item in items" v-bind:class="{ active: item.active }">{{ item.text }}
</li>
</ul>
<button type="button" class="btn btn-danger" v-on:click="addItem">Test</button>
</template>
<script>
export default {
data() {
return {
item: "a",
item2: "b",
item3: "c",
item4: "d",
items: [],
}
},
methods: {
addItem: function () {
var testArray = this.item.concat(this.item2, this.item3, this.item4);
for (var i = 0; i < testArray.length; i++) {
this.items.push({
text: testArray[i],
active: false });
}
// if I check console.log(this.items) at this point I can see the data I want to pass
},
}
}
</script>
Secondary Component I'm trying to pass Data to.
<template>
<div class="media-body">
<div class="media-label">Title:
<textarea class="form-control" placeholder="Title"></textarea>
</div>
</div>
</template>
<script>
export default {
props: ['items'],
data() {
return {
}
},
</script>
To pass the props to other component you have to write following code in the first component:
added <secondComponent :items="items" /> in HTML code.
import and use secondComponent in vue component like this: components: [secondComponent]
Here is complete code with these changes:
<template>
<div class="media-body">
<ul>
<li v-for="item in items" v-bind:class="{ active: item.active }">{{ item.text }}
</li>
</ul>
<button type="button" class="btn btn-danger" v-on:click="addItem">Test</button>
<secondComponent :items="items" />
</template>
<script>
import secondComponent from '/path/of/secondComponent'
export default {
components: [secondComponent]
data() {
return {
item: "a",
item2: "b",
item3: "c",
item4: "d",
items: [],
}
},
methods: {
addItem: function () {
var testArray = this.item.concat(this.item2, this.item3, this.item4);
for (var i = 0; i < testArray.length; i++) {
this.items.push({
text: testArray[i],
active: false });
}
// if I check console.log(this.items) at this point I can see the data I want to pass
},
}
}
</script>
In the second component you have already defined items as props, which you can as well use in template/HTML of second component.