Sending one time event to child components - vue.js

Im struggling to implement something which I think is pretty easy.
In my Vue app I loop through a list. Each list item is a child component. Each list item has an expand/collapse button. That works fine, but I want to be able to close all open items from the parent and I don't seem to be able to get that working as I would like.
The expand/collapse is controlled via a variable called isOpen so
<div v-if="isOpen">Something here</div>
I have tried using a computed property instead of the isOpen and passing props but that issue is I think it needs to act more like an event.
Consider three open list items. If the list items are controlled by a prop, and setting it to false closes the items, when an item is reopened, the prop is still false so therefore will not work a second time. I know I could change it back on the parent, but it seems wrong.
Whats the best way to accomplish this?

If you passed an "allClosed" prop, you would need to have your child components emit events to reset it every time they opened. That seems hacky to me. I think you are right that it should be more of a parent-to-children event, which calls for an event bus.
new Vue({
el: '#app',
data: {
closeBus: new Vue(),
kids: [1, 2, 3]
},
components: {
expandableThing: {
data() {
return {
isOpen: true
}
},
props: ['bus'],
methods: {
toggle() {
this.isOpen = !this.isOpen;
}
},
created() {
this.bus.$on('close', () => this.isOpen = false);
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
<expandable-thing v-for="kid in kids" inline-template :bus="closeBus">
<div>
<div v-show="isOpen">My stuff!</div>
<button #click="toggle">Toggle</button>
</div>
</expandable-thing>
<button #click="closeBus.$emit('close')">Close all</button>
</div>

Related

How can i refresh only nuxt component in Nuxt?

i want refresh my component after add something. İ try
refresh() {
this.$nuxt.refresh()
}
this but not working this is truelly
Here is my idea to refresh the component. Base on your issue, you can use some ways to achieve this goal. But in my opinion, there is a way that you can bind a key to your component, and by changing the key, your component will refresh.
I implement a simple example to show you how it works:
Vue.component('button-counter', {
data() {
return {
count: 0
}
},
template: '<button #click="count++">You clicked me {{ count }} times.</button>'
})
new Vue({
el: '#app',
data: {
component_key: 0
},
methods: {
refreshComponent() {
this.component_key += 1
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<button #click="refreshComponent()">
Refresh Compoenent
</button>
<button-counter :key="component_key" />
</div>
You can click on the right button in the above example and see the count is going up. After that, when you click on the Refresh Component button, you see that the component refreshes.
Also, you can use this.$forceUpdate() to refresh all of your variables. Maybe it'll help with your situation, but I recommend the key binding solution for your issue.

Vue data vue object variables from component

I am very new to Vue and I am having difficulty accessing data in main Vue instance from component. In the Vue instance I have:
var vm = new Vue({
computed: {},
data: {
show: true
},
and in the component, I want to do something like:
<button v-show="vm.show" #click="setDefaults(styleguide)">Set Default</button>
My goal is when 'show' value changes, I want to display/hide the button. It is little difficult/weird because I create template in the component, not in the html. When I try this code, it doesn't understand 'vm.show'. I feel like I need to create data in the component and tie the data to the 'show' variable, or create computed in the component or something (I believe computed is like watcher?). If there is easy way to handle this, please help.
I'm also new to VueJs, but I believe the issue is you haven't provided the el argument to the Vue instance, and in this case assigning the Vue instance to a variable doesn't do anything.
I think you want something like
<div id="app">
<button v-show="show" #click="setDefaults(styleguide)">Set Default</button>
</div>
<script>
new Vue({
el: '#app',
computed: {},
data: {
show: true
},
...
);
</script>
So I guess my question wasn't very clear, but I got it to figure it out. In the component code, I needed to add:
Vue.component('styleguide', {
computed: {
show: function () {
return vm.show
}
},
props: ['styleguide'],
template: `
<div>
<div>
<p>
<button v-show="show" #click="setDefaults(styleguide)">Set Default</button>
This allows me to access 'show' in the main Vue instance from the component template. Whenever other component modifies 'show' variable in the main Vue instance, the button disappears/reappears. I am not sure if this makes sense, but this is how I got it to work.
Two things, in the template all variables are already scoped from the component so you don't need the vm. in there. The second thing is that the data property of a component expects a function which returns an object.
var vm = new Vue({
computed: {},
data: () => ({
show: true
}),
<button v-show="show" #click="setDefaults(styleguide)">Set Default</button>
If you need to access data from a parent component you will need to pass it on using props. you can also try to do it using provide/inject if that suits your usecase better

2-way binding in Vue 2.3 component

I understand the .sync modifier returned in Vue 2.3, and am using it for a simple child component which implements a 'multiple-choice' question and answer. The parent component calls the child like this:
<question
:stem="What is your favourite colour?"
:options="['Blue', 'No, wait, aaaaargh!']
:answer.sync="userChoice"
>
The parent has a string data element userChoice to store the result from the child component. The child presents the question and radio buttons for the options. The essential bits of the child look like this (I'm using Quasar, hence q-radio):
<template>
<div>
<h5>{{stem}}</h5>
<div class="option" v-for="opt in options">
<label >
<q-radio v-model="option" :val="opt.val" #input="handleInput"></q-radio>
{{opt.text}}
</label>
</div>
</div>
</template>
export default {
props: {
stem: String,
options: Array,
answer: String
},
data: () => ({
option: null
}),
methods: {
handleInput () {
this.$emit('update:answer', this.option)
}
}
}
This is all working fine, apart from the fact that if the parent then changes the value of userChoice due to something else happening in the app, the child doesn't update the radio buttons. I had to include this watch in the child:
watch: {
answer () {
this.option = this.answer
}
}
But it feels a little redundant, and I was worried that emitting the event to update the parent's data would in fact cause the child 'watch' event to also fire. In this case it would have no effect other than wasting a few cycles, but if it was logging or counting anything, that would be a false positive...
Maybe that is the correct solution for true 2-way binding (i.e. dynamic Parent → Child, as well as Child → Parent). Did I miss something about how to connect the 'in' and 'out' data on both sides?
In case you're wondering, the most common case of the parent wanting to change 'userChoice' would be in response to a 'Clear Answers' button which would set userChoice back to an empty string. That should have the effect of 'unsetting' all the radio buttons.
Your construction had some oddities that didn't work, but basically answer.sync works if you propagate it down to the q-radio component where the changing happens. Changing the answer in the parent is handled properly, but to clear values, it seems you need to set it to an object rather than null (I think this is because it needs to be assignable).
Update
Your setup of options is a notable thing that didn't work.
I use answer in the q-radio to control its checked state (v-model has special behavior in a radio, which is why I use value in conjunction with v-model). From your comment, it looks like q-radio wants to have a value it can set. You ought to be able to do that with a computed based on answer, which you would use instead of your option data item: the get returns answer, and the set does the emit. I have updated my snippet to use the val prop for q-radio plus the computed I describe. The proxyAnswer emits an update event, which is what the .sync modifier wants. I also implemented q-radio using a proxy computed, but that's just to get the behavior that should already be baked-into your q-radio.
(What I describe is effectively what you're doing with a data item and a watcher, but a computed is a nicer way to encapsulate that).
new Vue({
el: '#app',
data: {
userChoice: null,
options: ['Blue', 'No, wait, aaaaargh!'].map(v => ({
value: v,
text: v
}))
},
components: {
question: {
props: {
stem: String,
options: Array,
answer: String
},
computed: {
proxyAnswer: {
get() {
return this.answer;
},
set(newValue) {
this.$emit('update:answer', newValue);
}
}
},
components: {
qRadio: {
props: ['value', 'val'],
computed: {
proxyValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', newValue);
}
}
}
}
}
}
},
methods: {
clearSelection() {
this.userChoice = {};
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<div id="app">
<question stem="What is your favourite colour?" :options="options" :answer.sync="userChoice" inline-template>
<div>
<h5>{{stem}}</h5>
<div class="option" v-for="opt in options">
<div>Answer={{answer && answer.text}}, option={{opt.text}}</div>
<label>
<q-radio :val="opt" v-model="proxyAnswer" inline-template>
<input type="radio" :value="val" v-model="proxyValue">
</q-radio>
{{opt.text}}
</label>
</div>
</div>
</question>
<button #click="clearSelection">Clear</button>
</div>

Vue.js passing events up to parents in components

I have a Vue app like this:
<div id="example">
<event-emitting-component #clicked="methodOnRootInstance"></event-emitting-component>
<event-emitting-component-parent></event-emitting-component-parent>
<div v-for="click in clicks">
{{ click }}
</div>
</div>
And here is the JS for it:
// Child
Vue.component('event-emitting-component', {
template: '<div class="event-emitting-component" #click="$emit(\'clicked\')">Event emitter</div>'
});
// Parent
Vue.component('event-emitting-component-parent', {
template: '<div class="event-emitting-component-parent">' +
'A custom parent!'+
'<event-emitting-component></event-emitting-component>' + // <-- Please note child component
'</div>'
});
// create a root instance
new Vue({
el: '#example',
data: {
clicks : []
},
methods : {
methodOnRootInstance : function(){
this.clicks.push('Element clicked');
}
}
})
If you want to play with it it is also here:
https://codepen.io/EightArmsHQ/pen/QgbwPG?editors=1010
When you click the top child component a click is registered on the root element. Perfect.
When the child component is nested inside a parent (the second component in the example), obviously I can't add a #clicked="methodOnRootInstance" as that method doesn't exist inside the component.
What is the best way to pass an event up through a number of nested components?
I've made a stripped back example here, but in reality some components are two or three levels deep. Is the answer (what I think it is) that inside the parent component I would have the following:
Vue.component('event-emitting-component-parent', {
template: '<div class="event-emitting-component-parent">' +
'A custom parent!'+
'<event-emitting-component #clicked="passClicked"></event-emitting-component>' + // <-- Please note child component
'</div>',
'methods': {
passClicked : function(){
this.$emit('clicked')
}
}
});
And then in the html template add the same:
<event-emitting-component-parent #clicked="methodOnRootInstance"></event-emitting-component-parent>
I know I can get it to work like this, however it doesn't seem very elegant. I've looked in the docs and there are functions such as sync although I don't think it's what I need, I'm struggling to find the correct approach.
Bonus question: can vuex help with stuff like this?
This is the type of problem vuex is designed to solve, however, before you consider adding an extra layer of complexity to your app, you may be able to get away with a simple global event bus, which is simply an empty Vue object to emit events onto, which can then be listened for by any component in your app, bypassing the parent-child chain:
const bus = new Vue({});
Vue.component('comp-1', {
template: `<div>Comp 1 <button #click="emitClick">Click</button></div>`,
methods: {
emitClick(){
bus.$emit('comp-1-click');
}
}
})
Vue.component('comp-2', {
template: `<div><comp-1></comp-1></div>`,
})
new Vue({
el: '#app',
created(){
bus.$on('comp-1-click',() => {
console.log('Comp 1 clicked');
});
}
})
Here's the JSFiddle: https://jsfiddle.net/oatLhzLp/

Show string in component

I am trying to learn Vue.js, and am playing around with the modal component example. I'm trying to change it up a bit so that the button clicked can provide data to the component (I'm very new to this so my terminology may be off).
I've updated the app to be:
// start app
var app = new Vue({
el: '#app',
data: {
showModal: false,
title: 'Default Title'
},
methods: {
modalInit: function(title) {
//this.title = title;
this.showModal = true;
}
}
})
The updates were mainly so that I can change the title within the modal based on the button clicked, here is the update to the button:
<button id="show-modal"#click="modalInit('A title')">Show Modal</button>
The relevant portion of the x-template:
<div class="modal-header">
<h2>{{ title }}</h2>
</div>
Not sure if it matters, but the component is:
Vue.component('modal', {
template: '#modal-template',
})
In this state, the modal will open fine but the title won't be there and I get the console error: [Vue warn]: Property or method "title" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
I can't figure out to to properly "declare reactive data properties in the data option".
Thanks!
You've specified a title property for the root component. But, the modal component does not have a title property. Add it like this:
Vue.component('modal', {
template: '#modal-template',
data() {
return {
title: "Default Title"
}
}
})
If you want to pass in a dynamic value for title, make it a property instead:
Vue.component('modal', {
template: '#modal-template',
props: ['title']
})
Then, you can pass the value for the title in the component tag:
<modal :title="dynamicTitle"></modal>