Vuejs How to use $emit function properly? - vue.js

I have two components SingleTaskItem and ControlArea. ControlArea has a collapse button and when that button is clicked I want to call a function in SingleTaskItem. Here is my code so far. Can you please tell me what am I doing wrong?
SingleTaskItem:
<template>
<div class="SingleTaskItem">
<ControlArea v-bind:collapsed="collapsed"
v-bind:onClickCollapse="onClickCollapse"/>
</div>
</template>
<script>
export default {
name: "SingleTaskItem",
data() {
return {
collapsed: false
};
},
methods: {
onClickCollapse(value) {
console.log("on Click Collapse called");
this.collapsed = value;
}
}
};
</script>
ControlArea:
<template>
<div class="ControlArea">
<div class="action-btn edit">
<i class="fas fa-ellipsis-h"></i>
</div>
<div class="action-btn collapsible">
<i v-if="collapsed" v-on:click="uncollapse" class="fas fa-chevron-down"></i>
<i v-else v-on:click="collapse" class="fas fa-chevron-up"></i>
</div>
</div>
</template>
<script>
export default {
name: "ControlArea",
props: {
collapsed: Boolean
},
methods: {
collapse(event) {
console.log("collapse function is called");
this.$emit("onClickCollapse", "true");
},
uncollapse(event) {
this.$emit("onClickCollapse", "false");
}
}
};
</script>

Instead of v-bind:onClickCollapse="onClickCollapse" you should use v-on:onClickCollapse. This is kind of easy to miss because you used the word 'on' in your event name - it might be clearer to remove that.
Also, to pass that true/false string you need to pass $event into your function call: v-on:onClickCollapse($event). To clean this up you should probably also pass true/false booleans rather than strings.

Related

Is it possible to call a function in one vue component from another vue component?

LogoutModal.vue
<template>
<div class="modal LogoutModal" v-bind:class="{'is-active':confirmLogout}">
<div class="modal-background"></div>
<div class="modal-content">
</div>
<button class="modal-close is-large" aria-label="close" #click="closeModal"></button>
</div>
</template>
<script>
export default {
name: "LogoutModal",
data() {
return {
confirmLogout: false
};
},
methods: {
closeModal: function() {
this.confirmLogout = false;
},
showModal: function() {
this.confirmLogout = true;
}
}
};
</script>
Navigation.vue
<template>
<aside class="menu">
<ul class="menu-list">
<li>
<a #click="showModal">Logout</a>
</li>
</ul>
<LogoutModal />
</aside>
</template>
<script>
import LogoutModal from "#/components/LogoutModal.vue";
export default {
name: "Navigation",
components: {
LogoutModal
}
};
</script>
I want to call the showModal function when i click on the Logout link. How can i achive that?
Or is that possible to change the variable in LogoutModal.vue from Navigation.vue. I have a variable called confirmLogout in LogoutModal.vue it has to be updated from Navigation.vue. How can I do that?
You should have confirmLogout in the navigation component, then pass that into your modal, you will also need a way to close the modal so in your modal you should this.$emit('close') when your user has signaled they want to close it.
<LogoutModal :open="confirmLogout" #close="confirmLogout=false" />
In your logout modal add a prop
<script>
export default {
name: "LogoutModal",
props: ['open'],
...
then in the template
<div class="modal LogoutModal" v-bind:class="{'is-active':open}">
You could define a dynamic property in LogoutModal, that sets it's state.
Here's more about that: https://v2.vuejs.org/v2/guide/components-props.html

Component with optional value prop

I'm writing a re-usable component. It's basically a section with a header and body, where if you click the header, the body will expand/collapse.
I want to allow the consumer of the component to use v-model to bind a boolean to it so that it can expand/collapse under any condition it wants, but within my component, the user can click to expand/collapse.
I've got it working, but it requires the user of the component to use v-model, if they don't, then the component doesn't work.
I essentially want the consumer to decide if they care about being able to see/change the state of the component or not. If they don't, they shouldn't have to supply a v-model attribute to the component.
Here's a simplified version of my component:
<template>
<div>
<div #click="$emit('input', !value)">
<div>
<slot name="header">Header</slot>
</div>
</div>
<div :class="{ collapse: !value }">
<div class="row">
<div class="col-xs-12">
<div>
<slot></slot>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from "vue-property-decorator";
#Component
export default class CollapsibleSection extends Vue {
#Prop({ default: true }) public value: boolean;
}
</script>
Update:
I've come up with a solution that meets my requirements functionally. It's a little more verbose than I would like, so if anyone has a more terse solution, I would love to read about it, and I will gladly mark it as the accepted answer if it meets my requirements with less code/markup.
<template>
<div>
<div #click="toggle">
<div>
<slot name="header">Header</slot>
</div>
</div>
<div :class="{ collapse: !currentValue }">
<div class="row">
<div class="col-xs-12">
<div>
<slot></slot>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Watch } from "vue-property-decorator";
#Component
export default class CollapsibleSection extends Vue {
#Prop({ default: true }) public value: boolean;
public currentValue = true;
public toggle() {
this.currentValue = !this.currentValue;
this.$emit('input', this.currentValue);
}
public mounted() {
this.currentValue = this.value;
}
#Watch('value')
public valueChanged() {
this.currentValue = this.value;
}
}
</script>
Your update works and has the right gist in general, but instead of a watcher it would be better to use a computed property. See the docs for computed properties and watchers for more info.
I've excluded the class notation in the below snippet to have it runnable on-site.
Vue.component('expandable', {
props: {
value: {
// Just to be explicit, not required
default: undefined,
validator(value) {
return typeof value === 'boolean' || typeof value === 'undefined';
},
},
},
template: `
<div class="expandable">
<p #click="toggle()">toggle</p>
<slot v-if="isOpen" />
</div>
`,
data() {
return {
internalValue: true,
};
},
computed: {
isOpen() {
return (typeof this.value !== 'undefined') ? this.value : this.internalValue;
},
},
methods: {
toggle() {
this.internalValue = !this.internalValue;
this.$emit('input', !this.isOpen);
}
}
});
new Vue({
el: '#app',
data() {
return {
isOpen: false,
}
}
})
.expandable {
border: 2px solid blue;
margin-bottom: 1rem;
}
<script src="https://unpkg.com/vue"></script>
<div id="app">
<expandable>
<p>no model</p>
</expandable>
<expandable v-model="isOpen">
<p>has model</p>
</expandable>
</div>

Send data from one component to another in vue

Hi I'm trying to send data from one component to another but not sure how to approach it.
I've got one component that loops through an array of items and displays them. Then I have another component that contains a form/input and this should submit the data to the array in the other component.
I'm not sure on what I should be doing to send the date to the other component any help would be great.
Component to loop through items
<template>
<div class="container-flex">
<div class="entries">
<div class="entries__header">
<div class="entries__header__title">
<p>Name</p>
</div>
</div>
<div class="entries__content">
<ul class="entries__content__list">
<li v-for="entry in entries">
{{ entry.name }}
</li>
</ul>
</div>
<add-entry />
</div>
</div>
</template>
<script>
import addEntry from '#/components/add-entry.vue'
export default {
name: 'entry-list',
components: {
addEntry
},
data: function() {
return {
entries: [
{
name: 'Paul'
},
{
name: 'Barry'
},
{
name: 'Craig'
},
{
name: 'Zoe'
}
]
}
}
}
</script>
Component for adding / sending data
<template>
<div
class="entry-add"
v-bind:class="{ 'entry-add--open': addEntryIsOpen }">
<input
type="text"
name="addEntry"
#keyup.enter="addEntries"
v-model="newEntries">
</input>
<button #click="addEntries">Add Entries</button>
<div
class="entry-add__btn"
v-on:click="openAddEntry">
<span>+</span>
</div>
</div>
</template>
<script>
export default {
name: 'add-entry',
data: function() {
return {
addEntryIsOpen: false,
newEntries: ''
}
},
methods: {
addEntries: function() {
this.entries.push(this.newEntries);
this.newEntries = '';
},
openAddEntry() {
this.addEntryIsOpen = !this.addEntryIsOpen;
}
}
}
</script>
Sync the property between the 2:
<add-entry :entries.sync="entries"/>
Add it as a prop to the add-entry component:
props: ['entries']
Then do a shallow merge of the 2 and emit it back to the parent:
this.$emit('entries:update', [].concat(this.entries, this.newEntries))
(This was a comment but became to big :D)
Is there a way to pass in the key of name? The entry gets added but doesn't display because im looping and outputting {{ entry.name }}
That's happening probably because when you pass "complex objects" through parameters, the embed objects/collections are being seen as observable objects, even if you sync the properties, when the component is mounted, only loads first level data, in your case, the objects inside the array, this is performance friendly but sometimes a bit annoying, you have two options, the first one is to declare a computed property which returns the property passed from the parent controller, or secondly (dirty and ugly but works) is to JSON.stringify the collection passed and then JSON.parse to convert it back to an object without the observable properties.
Hope this helps you in any way.
Cheers.
So with help from #Ohgodwhy I managed to get it working. I'm not sure if it's the right way but it does seem to work without errors. Please add a better solution if there is one and I'll mark that as the answer.
I follow what Ohmygod said but the this.$emit('entries:update', [].concat(this.entries, this.newEntries)) didn't work. Well I never even need to add it.
This is my add-entry.vue component
<template>
<div
class="add-entry"
v-bind:class="{ 'add-entry--open': addEntryIsOpen }">
<input
class="add-entry__input"
type="text"
name="addEntry"
placeholder="Add Entry"
#keyup.enter="addEntries"
v-model="newEntries"
/>
<button
class="add-entry__btn"
#click="addEntries">Add</button>
</div>
</template>
<script>
export default {
name: 'add-entry',
props: ['entries'],
data: function() {
return {
addEntryIsOpen: false,
newEntries: ''
}
},
methods: {
addEntries: function() {
this.entries.push({name:this.newEntries});
this.newEntries = '';
}
}
}
</script>
And my list-entries.vue component
<template>
<div class="container-flex">
<div class="wrapper">
<div class="entries">
<div class="entries__header">
<div class="entries__header__title">
<p>Competition Entries</p>
</div>
<div class="entries__header__search">
<input
type="text"
name="Search"
class="input input--search"
placeholder="Search..."
v-model="search">
</div>
</div>
<div class="entries__content">
<ul class="entries__content__list">
<li v-for="entry in filteredEntries">
{{ entry.name }}
</li>
</ul>
</div>
<add-entry :entries.sync="entries"/>
</div>
</div>
</div>
</template>
<script>
import addEntry from '#/components/add-entry.vue'
import pickWinner from '#/components/pick-winner.vue'
export default {
name: 'entry-list',
components: {
addEntry,
pickWinner
},
data: function() {
return {
search: '',
entries: [
{
name: 'Geoff'
},
{
name: 'Stu'
},
{
name: 'Craig'
},
{
name: 'Mark'
},
{
name: 'Zoe'
}
]
}
},
computed: {
filteredEntries() {
if(this.search === '') return this.entries
return this.entries.filter(entry => {
return entry.name.toLowerCase().includes(this.search.toLowerCase())
})
}
}
}
</script>

template v-if="main", not working with return this.$route.path.indexOf('/') === 0 in computed "main"

works with my other routes like "/dashboard", etc, but is appearing in all routes. I basically want this template to only appear when the url is "/". Have tried it throughout my project and just plum doesn't work. PLease help and thank you!! for any suggestions.
<template v-if="main">
<div id="accordion-nav">
<div class="accordion-panels">
Dashboard <i class="fa fa-caret-right" aria-hidden="true"></i>
</div>
<div class="accordion-panels">
Shifts <i class="fa fa-caret-right" aria-hidden="true"></i>
</div>
<div class="accordion-panels">
Other <i class="fa fa-caret-right" aria-hidden="true"></i>
</div>
</div>
</template>
<script>
export default {
name: 'accordion-nav',
computed: {
main: function () {
return this.$route.path.indexOf('/') === 0
}
}
}
</script>
<style scoped>
</style>
Setup the v-if in the sidebar component itself
'<template>
<div id="sidebar" class="sidebar"
:class="{ isOpen: isOpen }">
<div class="sidebar-opener"
#click="toggle">{{openerText}}</div>
<accordion-nav v-if="main"></accordion-nav>
<accordion></accordion>
</div>
</template>'
<script>
export default {
data () {
return {
}
},
computed:{
main(){
return this.$route.path === '/'
}
}
}
</script>
With this.$route.path you get a string that equals the path of the current route resolved as absolute path in any component of your app.
So you can use this to check whether you are in the root route using:
this.$route.path === '/'
Here is the example fiddle
Use v-if="main" as a method v-if="main()" instead of a computed property
methods: {
main: function () {
return this.$route.path.indexOf('/') === 0
}
Methods do such complex updates much better than computed properties. Computed = lazy

Vue Multiselect: How to send console.log once selection has been made

Using Vue Multiselect, I am trying to send a console.log once I have made a selection. I thought it would work by putting it in the watch but it does not work. Where should it be placed. Please see my component below.
Component
<template>
<div>
<label v-for="topic in topics" class="radio-inline radio-thumbnail" style="background-image: url('http://s3.hubsrv.com/trendsideas.com/profiles/74046767539/photo/3941785781469144249_690x460.jpg')">
<input type="radio" v-model="internalValue" name="topics_radio" :id="topic.id" :value="topic.name">
<span class="white-color lg-text font-regular text-center text-capitalize">{{ topic.name }}</span>
</label>
</div>
</template>
<script>
export default {
props: ['value'],
data () {
return {
internalValue: this.value,
topics: []
}
},
mounted(){
axios.get('/vuetopics').then(response => this.topics = response.data);
},
watch: {
internalValue(v){
this.$emit('input', v);
console.log('topic has been chosen!!!');
}
}
}
</script>
It fires events, so you may catch them.
<multiselect ... #select="doSomething" ...>
Then add your method
...
methods: {
doSomething(selectedOption, id) {
console.log(selectedOption);
}
}
Make sure you implemented vue-multiselect correctly, I don't see the component in your code.