I have a parent component in Vue.js which looks like this:
<template>
<ul class="list-group">
<li class="list-group-item" v-for="item in items">
<div class="row">
<div class="col-md-6">
{{ item.title }}
</div>
<div class="col-md-6 text-right">
<a href="#" class="btn btn-primary btn-sm">
<span class="glyphicon glyphicon-pencil"></span>
</a>
<a href="#" class="btn btn-success btn-sm">
<span class="glyphicon glyphicon-link"></span>
</a>
<a href="#" class="btn btn-danger btn-sm">
<span class="glyphicon glyphicon-remove"></span>
</a>
</div>
<div class="col-md-12">
<preview></preview>
</div>
</div>
</li>
</ul>
</template>
The script:
<script>
import Preview from './Preview.vue';
export default {
data() {
return {
items: '',
template: []
}
},
created() {
this.fetchItems();
this.$on('preview-build', function (child) {
console.log('new preview: ')
console.log(child)
})
},
components: {
Preview
},
methods: {
fetchItems: function () {
var resource = this.$resource('api/preview');
resource.get({}).then((response) => {
this.items = response.body.item;
}, (response) => {
console.log('Error fetching tasks');
}).bind(this);
},
}
}
</script>
The child component "preview" has a template-like structure, for example {{ item.title }} again. The preview is loaded correct but it's not rendered.
I really do not know if it is possible in Vue 2.0 but hopefully someone had the same problem and can help me here.
EDIT (thanks Patrick):
<template>
<textarea rows="20" class="form-control">
{{ template.content }}
</textarea>
</template>
<script>
export default {
data() {
return {
template: [],
}
},
created() {
this.fetchTemplate();
},
methods: {
fetchTemplate: function () {
var resource = this.$resource('api/preview');
resource.get({}).then((response) => {
this.template = response.body.template;
}, (response) => {
console.log('Error fetching template');
}).bind(this);
},
}
}
</script>
This is the Preview.vue content which is similar to the Item.vue content.
As a little explanation:
The template data comes from an database as predefined html content including the {{ item.title }} and some other placeholder. I want this to be rendered with the specific stuff coming from the item.
In Vue.js, components can't directly access data from their parent. If you want preview to be able to render {{ item.title }}, you'll have to pass item down to it as a prop. So in preview.vue, declare it like this:
export default {
...
props: ['item']
}
Then, in your parent component's template, you can v-bind that item prop to something from the parent's items array:
<li class="list-group-item" v-for="item in items">
...
<preview v-bind:item="item"></preview>
...
</li>
Now your preview component has an item that it can render in its template as if it were part of the data object.
Related
I am trying to make my vue component as flexible as possibile, So i have some button and a search i need to use on all my table component . For now it is hard coded, but i wnat to make it more flexible, everything work until now except the search . I see an error like this on console
ERROR: [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "query"
I know the problem is when i try to change the query input but does now know how to fix this problem. Does anyone know ?
//custom action buttons
import Button from './button.js';
export default {
components: {
'v-button': Button
},
props: {
query: {
type: String
},
selected: {
type: Array
},
url: {
type: String
}
},
// props: [
// 'selected',
// 'query',
// 'url'
// ],
methods: {
showModal() {
this.$emit('showModal')
},
massDeleteRecord() {
this.$emit('massDeleteRecord')
}
},
template: `
<div class="add-user py-4">
<div class="d-flex">
<div class="col-10">
<v-button type="success" #click.prevent="showModal" >
Add Record
</v-button>
<v-button type="primary" data-mdb-toggle="collapse" data-mdb-target="#collapseFilter" aria-expanded="false" aria-controls="collapseFilter" >
Filter
<i class="fa-solid fa-filter"></i>
</v-button>
<v-button v-show="selected.length>0" type="danger" class="dropdown dropdown-toggle" id="dropdownMenuButton" data-mdb-toggle="dropdown" aria-expanded="false" >
Action ({{selected.length}})
</v-button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<li>
<a :href="url" class="dropdown-item btn-warning text-center">
Export
<i class="fa-solid fa-file-excel"></i>
</a>
</li>
<li>
<v-button #click="massDeleteRecord()" type="danger" class="dropdown-item text-center">
Delete
<i class="fa-solid fa-trash"></i>
</v-button>
</li>
</ul>
</div>
<div class="col-2">
<input #input="$emit('updatedQuery', $event.target.value)"
:value="query"
type="search"
class="form-control border-0"
placeholder="Search"
/>
</div>
</div>
</div>
`,
}
User Table Component
<template>
<table-custom-action
#massDeleteRecord="massDeleteUser"
#showModal="showModal"
#updated-query="(newQuery) => query = newQuery"
:selected="selected"
:query="query"
:url="url"
></table-custom-action>
</template>
<script>
import TableCustomAction from './partials/modules/TableCustomAction.js';
export default {
components: {
TableCustomAction,
},
data(){
return {
....
query: '',
....
url: ''
}
},
watch: {
query: function(newQ, old) {
if (newQ === "") {
this.getRecord();
} else {
this.searchRecord();
}
},
},
</script>
Rather than using v-model on query, send back the update to the parent. It will update in the parent, be transferred through the props to your child and produce the same result. If you mutate it in the child, then it will at some point mutate in the parent, hence do twice the work.
Here is a nice article on the subject.
Also, you don't update props actually, they are read-only.
In the child
<input #input="$emit('updatedQuery', $event.target.value)"
:value="query"
type="search"
class="form-control border-0"
placeholder="Search"
/>
In the parent
<table-custom-action #updated-query="(newQuery) => query = newQuery">
</table-custom-action>
UPDATE: here is a working repro.
More details here in the doc.
In the following Vue Component I want to loop through dwarfs array. And as long as I am in the current component, everything is fine (TEST) and also all the following properties are correct.
Currenct_Component.vue :
<template>
<div>
<h2>Stamm: {{ tribeName }}</h2>
<div class="card-container">
<div class="card" style="width: 18rem;" v-for="dwarf in dwarfs" :key="dwarf.name">
<!-- TEST -->
<p>{{dwarf}}</p>
<!-- CHILD COMPONENT -->
<app-modal
:showModal="showModal"
:targetDwarf="dwarf"
#close="showModal = false"
#weaponAdded="notifyApp"
/>
<!-- <img class="card-img-top" src="" alt="Card image cap">-->
<div class="card-body">
<h3 class="card-title" ref="dwarfName">{{ dwarf.name }}</h3>
<hr>
<ul class="dwarf-details">
<li><strong>Alter:</strong> {{ dwarf.age }}</li>
<li><strong>Waffen:</strong>
<ul v-for="weapon in dwarf.weapons">
<li><span>Name: {{ weapon.name }} | Magischer Wert: {{ weapon.magicValue }}</span></li>
</ul>
</li>
<li><strong>Powerfactor:</strong> {{ dwarf.weapons.map(weapon => weapon.magicValue).reduce((accumulator, currentValue) => accumulator + currentValue) }}</li>
</ul>
<button class="card-button" #click="showModal = true"><span class="plus-sign">+</span> Waffe</button>
</div>
</div>
</div>
<button id="backBtn" #click="onClick">Zurück</button>
</div>
</template>
<script>
import Modal from './NewWeaponModal.vue';
export default {
data() {
return {
showModal: false,
}
},
components: { appModal : Modal },
props: ['tribeName', 'dwarfs'],
methods: {
onClick() {
this.$emit('backBtn')
},
notifyApp() {
this.showModal = false;
this.$emit('weaponAdded');
}
},
}
</script>
But when I bind the element dwarf to the Child Component <app-modal/> it changes to the next dwarf in the array dwarfs (TEST) - (So as the result when i add a new weapon in the modal-form it gets added to the second dwarf...):
Child_Component.vue :
<template>
<div>
<div class="myModal" v-show="showModal">
<div class="modal-content">
<span #click="$emit('close')" class="close">×</span>
<h3>Neue Waffe</h3>
<!-- TEST -->
<p>{{ targetDwarf }}</p>
<form>
<input
type="text"
placeholder="Name..."
v-model="weaponName"
required
/>
<input
type="number"
placeholder="Magischer Wert..."
v-model="magicValue"
required
/>
<button #click.prevent="onClick">bestätigen</button>
</form>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
weaponName: '',
magicValue: '',
}
},
props: ['showModal', 'targetDwarf'],
methods: {
onClick() {
if(this.weaponName !== '' &&
Number.isInteger(+this.magicValue)) {
let newData = {...this.dwarf};
newData['weapons'] = [
...this.dwarf['weapons'],
{
"name": this.weaponName,
"magicValue": Number.parseInt(this.magicValue)
},
];
this.$http.post("https://localhost:5019/api", newData)
.then(data => data.text())
.then(text => console.log(text))
.catch(err => console.log(err));
this.$emit('weaponAdded');
} else {
alert('You should fill all fields validly')
}
},
}
}
</script>
It looks like you have the <app-modal/> component inside of the v-for="dwarf in dwarfs" loop, but then the control for showing all of the modal components created by that loop is just in one variable: showModal. So when showModal is true, the modal will show each of the dwarfs, and I'm guessing the second dwarf's modal is just covering up the first one's.
To fix this, you could move the <app-modal /> outside of that v-for loop, so there's only one instance on the page, then as part of the logic that shows the modal, populate the props of the modal with the correct dwarf's info.
Something like this:
<div class="card-container">
<div class="card" v-for="dwarf in dwarfs" :key="dwarf.name">
<p>{{dwarf}}</p>
<div class="card-body">
<button
class="card-button"
#click="() => setModalDwarf(dwarf)"
>
Waffe
</button>
</div>
</div>
<!-- Move outside of v-for loop -->
<app-modal
:showModal="!!modalDwarfId"
:targetDwarf="modalDwarfId"
#close="modalDwarfId = null"
#weaponAdded="onDwarfWeaponAdd"
/>
</div>
export default {
//....
data: () => ({
modalDwarfId: null,
)},
methods: {
setModalDwarf(dwarf) {
this.modalDwarfId = drawf.id;
},
onDwarfWeaponAdd() {
//...
}
},
}
You could then grab the correct dwarf data within the modal, from the ID passed as a prop, or pass in more granular data to the modal so it's more "dumb", which is the better practice so that the component isn't dependent on a specific data structure. Hope that helps
Courtesy of #Joe Dalton's answer, a bit alternated for my case:
<div class="card" style="width: 18rem;" v-for="dwarf in dwarfs" :key="dwarf.name">
...
<button class="card-button" #click="setModalDwarf(dwarf)"><span class="plus-sign">+</span> Waffe</button>
<div>
<app-modal
:showModal="showModal"
:targetDwarf="currentDwarf"
#close="showModal = false"
#weaponAdded="notifyApp"
/>
<script>
import Modal from './NewWeaponModal.vue';
export default {
data() {
return {
showModal: false,
currentDwarf: null,
}
},
components: { appModal : Modal },
props: ['tribeName', 'dwarfs'],
methods: {
setModalDwarf(dwarf) {
this.currentDwarf = dwarf;
this.showModal = true;
},
...
}
</script>
What I have
I'm importing the same component twice. This component is used to display a dropdown with collections.
When an item in the dropdown is selected, an event is triggered.
The component is imported in list.js and in BulkActions.vue.
The problem
An event is fired when a collection in the dropdown is selected. It then triggers an event using $emit. Somehow this event is only catched in list.blade.php and not in BulkActions.vue.
For both dropdowns (loading from the same component) there should be a different behaviour.
I have no idea why this happens or why the event is only catched at my root.
What I've tried
I've tried to pass an additional prop in the HTML to have a "variable event name", but that didn't work. I've tried various ways of importing the component as well.
Does anyone know how to solve this issue?
The files
list.blade.php:
<div class="media-list-navbar mt-3 mb-3">
<shown-results :text="resultText"></shown-results>
<search-bar #update-filters="updateFilters"></search-bar>
<document-types #update-filters="updateFilters"></document-types>
<collection-dropdown eventname="update-filters"
#update-filters="updateFilters"></collection-dropdown>
<div class="clearfix"></div>
</div>
<bulk-actions #select-all="selectAll"
#deselect-all="deselectAll"
:items="items"
:multiselect="multiSelect"></bulk-actions>
BulkActions.vue
<template>
<div class="multiselect-list-navbar mt-3 mb-3" v-if="multiselect">
<div class="float-left">
<button type="button"
class="btn btn-outline-secondary"
#click="$emit('deselect-all')">
<i class="fas fa-fw fa-times"></i> {{ Lang.get('media/item.index.list.multi-select.deselect-all') }}
</button>
<button type="button"
class="btn btn-outline-secondary"
#click="$emit('select-all')">
<i class="fas fa-fw fa-check"></i> {{ Lang.get('media/item.index.list.multi-select.select-all') }}
</button>
</div>
<bulk-collection
#update-filters="doSomething"></bulk-collection>
<div class="clearfix"></div>
</div>
</template>
<script>
export default {
name: "Bulk",
props: {
multiselect: Boolean,
items: Array
},
components: {
'bulk-collection': () => import('./Collections')
},
methods: {
doSomething() {
console.log(this.items)
}
}
}
</script>
<style scoped>
</style>
list.js
import MediaItem from '../components/MediaItem';
import UploadModal from '../components/media/UploadModal';
import ItemDetail from '../components/media/ItemDetail';
import ShownResults from '../components/media/list/ShownResults';
import SearchBar from '../components/media/list/SearchBar';
import DocumentTypes from '../components/media/list/DocumentTypes';
import {default as CollectionDropdown} from '../components/media/list/Collections';
import Order from '../components/media/list/Order';
import BulkActions from '../components/media/list/BulkActions';
if (document.getElementById('media-list')) {
const mediaList = new Vue({
el: '#media-list',
components: {
MediaItem,
ShownResults,
SearchBar,
UploadModal,
ItemDetail,
DocumentTypes,
CollectionDropdown,
Order,
BulkActions
},
[...]
Collections.vue
<template>
<div class="dropdown float-left">
<button class="btn btn-secondary dropdown-toggle"
type="button"
data-toggle="dropdown">
{{ Lang.get('media/item.index.list.filters.collections.title') }}
</button>
<div class="dropdown-menu" ref="collectionDropdown">
<div class="dropdown-item no-pseudo">
<input type="search"
class="form-control"
name="search"
:placeholder="Lang.get('media/item.index.list.filters.collections.filter')"
v-model="query"
#keyup="search">
</div>
<div class="dropdown-item last-item no-pseudo">
<alert type="warning">
<template v-slot:body>
{{ Lang.get('media/item.index.list.filters.collections.none-filter') }}
</template>
</alert>
</div>
<div v-for="item in list"
class="dropdown-item"
v-if="!item.hidden">
<span class="custom-control custom-checkbox">
<input type="checkbox"
class="custom-control-input"
name="collection[]"
:checked="item.checked"
:id="item.slug"
:value="item.id"
#change="selectItem">
<label class="custom-control-label" :for="item.slug">
{{ item.name }}
</label>
</span>
</div>
</div>
</div>
</template>
<script>
import Alert from '../../partials/Alert';
export default {
name: "Collections",
components: {
Alert
},
data() {
return {
displayAmount: 10, // amount of items displayed without search
list: [],
query: ''
}
},
computed: {
/**
* Return an array of selected items only
*/
checked() {
return this.list.filter(item => {
return item.checked === true;
})
}
},
methods: {
/**
* Mark an item as selected
*/
selectItem(e) {
let selectedId = e.target.value;
this.markItem(selectedId);
},
/**
* Mark an item from the list as selected
*
* #param {Number} itemId
*/
markItem(itemId) {
this.list.forEach(item => {
if (item.id === parseInt(itemId)) {
item.checked = !item.checked;
}
});
this.$emit('update-filters', {
props: this.checked.map(item => item.id).join(',')
});
},
/**
* Search in the current URL for collection ids
*/
markItemsFromUrl() {
let urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('collection')) {
let urlFilters = urlParams.get('collection').split(','); // split url parameters
urlFilters.forEach(itemId => {
this.markItem(itemId);
});
}
},
},
mounted() {
this.fetchList();
this.markItemsFromUrl();
/**
* Prevent Bootstrap dropdown from closing after clicking inside it
*/
$(this.$refs.collectionDropdown).on('click.bs.dropdown', function (e) {
e.stopPropagation();
});
}
}
</script>
Below is a GIF animation to demonstrate the problem. The first dropdown (the one on the top) has normal behaviour. The second one (that appears later on) does not. When clicking the second one, an event of the first one is happening.
I am facing a problem in deleting item from an array. Array splice supposed to work but its not working like I want. Its always delete the item from last. I am using Vue.js . I am pushing item dynamically to an array. But after click remove its delete from the last. why I am facing this. I am attaching the codes.
<template>
<div>
<span class="badge badge-pill mb-10 px-10 py-5 btn-add" :class="btnClass" #click="addBtn"><i class="fa fa-plus mr-5"></i>Button</span>
<div class="block-content block-content-full block-content-sm bg-body-light font-size-sm" v-if="buttons.length > 0">
<div v-for="(item, index) in buttons">
<div class="field-button">
<div class="delete_btn"><i #click="remove(index)" class="fa fa-trash-o"></i></div>
<flow-button v-model="item.title" :showLabel="false" className="btn btn-block min-width-125 mb-10 btn-border" mainWrapperClass="mb-0" wrapperClass="pt-0" placeholder="Button Title"></flow-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import flowButton from '../assets/flow-button'
export default {
name: "textArea",
props:{
index : Number
},
data() {
return {
buttons : [],
btnClass : 'badge-primary',
}
}
components : {
flowButton
},
methods : {
addBtn () {
if(this.buttons.length >= 2) {
this.btnClass = 'btn-secondary'
}
if(this.buttons.length < 3) {
this.buttons.push({
title : ''
});
}
},
remove(index) {
this.buttons.splice(index, 1)
}
}
}
</script>
This must be because of your flow-button I have tried to replicate your error but endup to this code. I just replaced the flow-button with input and it works. Try the code below.
Use v-bind:key="index", When Vue is updating a list of elements rendered with v-for, by default it uses an “in-place patch” strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, Vue will patch each element in-place and make sure it reflects what should be rendered at that particular index. This is similar to the behavior of track-by="$index"
You have missing comma between data and components, I remove the component here it won't cause any error now, and more tips don't mixed double quotes with single qoutes.
<template>
<div>
<span class="badge badge-pill mb-10 px-10 py-5 btn-add" :class="btnClass" #click="addBtn"><i class="fa fa-plus mr-5"></i>Button</span>
<div class="block-content block-content-full block-content-sm bg-body-light font-size-sm" v-if="buttons.length > 0">
<div v-for="(item, index) in buttons" v-bind:key="index">
<div class="field-button">
<div class="delete_btn"><i #click="remove(index)" class="fa fa-trash-o">sdfsdff</i></div>
<input type="text" v-model="item.title" :showLabel="false" className="btn btn-block min-width-125 mb-10 btn-border" mainWrapperClass="mb-0" wrapperClass="pt-0" placeholder="Button Title"/>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'textArea',
props: {
index: Number
},
data () {
return {
buttons: [],
btnClass: 'badge-primary'
}
},
methods: {
addBtn () {
if (this.buttons.length >= 2) {
this.btnClass = 'btn-secondary'
}
if (this.buttons.length < 3) {
this.buttons.push({
title: ''
})
}
},
remove (index) {
this.buttons.splice(index, 1)
}
}
}
</script>
I think that you may be facing a conflict with the index prop of your component. Try to use a different name for the index of your v-for loop:
<div v-for="(item, ind) in buttons">
<div class="field-button">
<div class="delete_btn"><i #click="remove(ind)" class="fa fa-trash-o"></i></div>
<flow-button v-model="item.title" :showLabel="false" className="btn btn-block min-width-125 mb-10 btn-border" mainWrapperClass="mb-0" wrapperClass="pt-0" placeholder="Button Title"></flow-button>
</div>
</div>
Try this. Removing an item correctly using this.
<div v-for="(item, ind) in buttons" :key="JSON.stringify(item)">
First time with vue. I am learning it playing around with some examples from Laracasts. I cannot get external template to render and the console shows cannot find element: #toolbar-chat.
My template is:
<template>
<div id="toolbar-chat">
<div class="toolbar-chat">
<ul v-for="chat in messages">
<li><b>#{{ chat.nickname }} says:</b> #{{ chat.message }}</li>
</ul>
</div>
<div class="input-group input-group-sm">
<input class="form-control" value="" placeholder="Type message..." required="required" maxlength="140" v-model="newMsg">
<div class="input-group-btn">
<button class="btn btn-primary" type="button" #click="press">
<i class="fa fa-paper-plane"></i>
</button>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('Component mounted.')
},
data() {
return {
nickname: [],
message: ''
}
},
ready() {
Echo.channel(chat_channel)
.listen('ChatMessageWasReceived', (data) => {
// Push data to messages list.
this.messages.push({
message: data.chat.message,
nickname: data.player.nickname
});
});
},
methods: {
press() {
// Send message to backend.
this.$http.post(chat_send_route, {message: this.newMsg})
.then((response) => {
// Clear input field.
this.newMsg = '';
});
}
}
};
</script>
My HTML contains the following tag:
<div class="col-xs-12 col-md-4" id="toolbarChat">
<my-chat></my-chat>
</div>
My vue component call is inside a document ready function like this:
require('./../app/bootstrap');
$(document).ready(function()
{
....
// Set up chat
Vue.component('my-chat', require('./../generic/chat.vue'));
const app = new Vue({
el: '#toolbar-chat'
});
});
And I include vue in my bootstrap file like this, then compile with webpack and no errors.
window.Vue = require('vue');
Why is my HTML template not rendering?
In your HTML you have the following div:
<div class="col-xs-12 col-md-4" id="toolbarChat">
<my-chat></my-chat>
</div>
Change it to
<div class="col-xs-12 col-md-4" id="toolbar-chat">
<my-chat></my-chat>
</div>
Because that is the id that new Vue({el: "#toolbar-chat",...}) is looking for.