How could I change an image in a child page when pressing a button in its parent page? - vue.js

I have a DefaultLayout component with a dark mode toggle button which is its own component. One if its children (DefaultLayout's) is About.vue where I want a specific image to change its src depending on a localStorage value that can be set to either 'dark' or 'light'.
I've managed to read the localStorage value but the image does not change unless I refresh the page.
I'm new to Vue so I'm lost on how I can create a method to do this in DefaultLayout and change a variable in its child. I've tried to use an emit with no luck.
Could anyone point me in the right direction?

Yes, the local storage is for keeping data not propagate events.
The simplest way for you is to make a prop in child component and pass the value by this prop. But if you want to implement it as global variable the suggested way is by Pinia.

Below is a simple example
Vue.component('About', {
name: 'About',
template: `<div>
<div v-if="mode==='dark'">Dark</div>
<div v-else>Light</div>
</div>
`,
data() {
return {
mode: 'light',
};
},
mounted() {
this.setMode('white'); // In realtime use `this.getMode()` instead of 'white'
},
methods: {
setMode(val) {
this.mode = val;
},
getMode() {
return JSON.parse(localStorage.getItem('mode'));
}
}
});
var app = new Vue({
el: "#app",
template: `<div>
<input type="checkbox" v-model="toggler" #input="setVal" />
<About ref="about" />
</div>`,
data() {
return {
toggler: false,
};
},
methods: {
setVal() {
const mode = this.toggler === false ? 'dark' : 'light';
// localStorage.setItem('mode', mode); // In realtime uncomment this line
this.$refs.about.setMode(mode);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
</div>

Related

Vue stored valued through props not being reactive

So I pass value using [props] and stored it in child component's data. However, when passing [props] value changes from parent, it's not updating in child component's data. Is there a fix for this..?
Here is the link to w3 test (I tried to clarify the problem as much as possible here)
<div id='app'>
<div id='parent'>
<button #click='current_value()'>Click to see parent value</button>
<br><br>
<button #click='change_value($event)'>{{ txt }}</button>
<br><br>
<child-comp :test-prop='passing_data'></child-comp>
</div>
<br><br>
<center><code>As you can see, this methods is <b>NOT</b> reactive!</code></center>
</div>
<script>
new Vue({
el: "#parent",
data: {
passing_data: 'Value',
txt: 'Click to change value'
},
methods: {
current_value(){
alert(this.passing_data);
},
change_value(e){
this.passing_data = 'New Vaule!!';
this.txt = 'Now click above button again to see new value';
e.target.style.backgroundColor = 'red';
e.target.style.color = 'white';
}
},
components: {
"child-comp": {
template: `
<button #click='test()'>Click here to see child (stored) value</button>
`,
props: ['test-prop'],
data(){
return {
stored_data: this.testProp
}
},
methods: {
test(){
alert(this.stored_data);
}
},
watch: {
stored_data(){
this.stored_data = this.testProp;
}
}
}
}
});
Props have one way data flow, that's why it doesn't react when you update it from the parent component. Define a clone of your prop at data to make it reactive, and then you can change the value within the child component.
Short answer: you don't need stored_data. Use alert(this.testProp) directly.
Long answer: when child component is created, stored_data get it's value from this.testProp. But data is local, it won't change automatically. That's why you need to watch testProp and set it again. But is not working because of a simple mistake, your watch should be:
watch: {
testProp(){ // here was the mistake
this.stored_data = this.testProp;
}
}

Emitting custom events on components

I am following Component Basics guide on vuejs.org, but I cannot produce the result as guided. I don't know whether to put the methods property on component or on root Vue instance.
When I put method onIncreaseCount inside component, an error is thrown.
But when I put method onIncreaseCount inside root Vue instance, although there is no error, nothing is applied when I click the button.
// JS
Vue.component('button-counter', {
data: function(){ return {count: 0} }
template: `<button v-on:click="$emit('increase-count')">You clicked me {{ count }} times.</button>`,
methods: {
onIncreaseCount: function(){ this.count++ }
}
})
new Vue({
el: "#main"
})
// HTML
<main class="main" id="main">
<button-counter title="Example component" #increase-count="onIncreaseCount"></button-counter>
</main>
I expect the {{ count }} value will be increased each time I click the button, but it doesn't change.
you don't need any emit jsut you have to call the increase function
handle in component itself
// JS
Vue.component('button-counter', {
data: function(){ return {count: 0} }
template: `<button v-on:click="increaseCount">You clicked me {{ count }} times.</button>`,
methods: {
increaseCount: function(){ this.count++ }
}
})
new Vue({
el: "#main"
})
// HTML
<main class="main" id="main">
<button-counter title="Example component"></button-counter>
</main>

Reusable component to render button or router-link in Vue.js

I'm new using Vue.js and I had a difficulty creating a Button component.
How can I program this component to conditional rendering? In other words, maybe it should be rendering as a router-link maybe as a button? Like that:
<Button type="button" #click="alert('hi!')">It's a button.</Button>
// -> Should return as a <button>.
<Button :to="{ name: 'SomeRoute' }">It's a link.</Button>
// -> Should return as a <router-link>.
You can toggle the tag inside render() or just use <component>.
According to the official specification for Dynamic Components:
You can use the same mount point and dynamically switch between multiple components using the reserved <component> element and dynamically bind to it's is attribute.
Here's an example for your case:
ButtonControl.vue
<template>
<component :is="type" :to="to">
{{ value }}
</component>
</template>
<script>
export default {
computed: {
type () {
if (this.to) {
return 'router-link'
}
return 'button'
}
},
props: {
to: {
required: false
},
value: {
type: String
}
}
}
</script>
Now you can easily use it for a button:
<button-control value="Something"></button-control>
Or a router-link:
<button-control to="/" value="Something"></button-control>
This is an excellent behavior to keep in mind when it's necessary to create elements that may have links or not, such as buttons or cards.
You can create a custom component which can dynamically render as a different tag using the v-if, v-else-if and v-else directives. As long as Vue can tell that the custom component will have a single root element after it has been rendered, it won't complain.
But first off, you shouldn't name a custom component using the name of "built-in or reserved HTML elements", as the Vue warning you'll get will tell you.
It doesn't make sense to me why you want a single component to conditionally render as a <button> or a <router-link> (which itself renders to an <a> element by default). But if you really want to do that, here's an example:
Vue.use(VueRouter);
const router = new VueRouter({
routes: [ { path: '/' } ]
})
Vue.component('linkOrButton', {
template: `
<router-link v-if="type === 'link'" :to="to">I'm a router-link</router-link>
<button v-else-if="type ==='button'">I'm a button</button>
<div v-else>I'm a just a div</div>
`,
props: ['type', 'to']
})
new Vue({ el: '#app', router })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
<link-or-button type="link" to="/"></link-or-button>
<link-or-button type="button"></link-or-button>
<link-or-button></link-or-button>
</div>
If you're just trying to render a <router-link> as a <button> instead of an <a>, then you can specify that via the tag prop on the <router-link> itself:
Vue.use(VueRouter);
const router = new VueRouter({
routes: [ { path: '/' } ]
})
new Vue({ el: '#app', router })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
<router-link to="/">I'm an a</router-link>
<router-link to="/" tag="button">I'm a button</router-link>
</div>
You can achieve that through render functions.
render: function (h) {
if(this.to){ // i am not sure if presence of to props is your condition
return h(routerLink, { props: { to: this.to } },this.$slots.default)
}
return h('a', this.$slots.default)
}
That should help you start into the right direction
I don't think you'd be able to render a <router-link> or <button> conditionally without having a parent element.
What you can do is decide what to do on click as well as style your element based on the props passed.
template: `<a :class="{btn: !isLink, link: isLink}" #click="handleClick"><slot>Default content</slot></a>`,
props: ['to'],
computed: {
isLink () { return !!this.to }
},
methods: {
handleClick () {
if (this.isLink) {
this.$router.push(this.to)
}
this.$emit('click') // edited this to always emit
}
}
I would follow the advice by #Phil and use v-if but if you'd rather use one component, you can programmatically navigate in your click method.
Your code can look something like this:
<template>
<Button type="button" #click="handleLink">It's a button.</Button>
</template>
<script>
export default {
name: 'my-button',
props: {
routerLink: {
type: Boolean,
default: false
}
},
methods: {
handleLink () {
if (this.routerLink) {
this.$router.push({ name: 'SomeRoute' })
} else {
alert("hi!")
}
}
}
}
</script>

Update component when prop changes

How do I force a component to re-render when a prop changes?
Say I have a component which holds:
<test-sub-component :layoutSetting="layoutSetting"></test-sub-component>
And I have my data object like so:
data() {
return {
layoutSetting: 'large'
}
},
Then with a click on a button i would like to change the settings of the prop passed, and the component should re-render, something like
<button #click="changeLayout">Change Layout</button>
And a method like
changeLayout() {
this.layoutSetting = 'small';
},
This changed the data object, but it doesnt re-render the component with the changed prop?
When you pass a property that is defined as camelCased in the component, you need to use the kebab-cased property name in the template.
<test-sub-component :layout-setting="layoutSetting"></test-sub-component>
console.clear()
Vue.component("test-sub-component", {
props: ["layoutSetting"],
template:`<div>{{layoutSetting}}</div>`
})
new Vue({
el: "#app",
data() {
return {
layoutSetting: 'large'
}
},
methods: {
changeLayout() {
this.layoutSetting = 'small';
},
}
})
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<div id="app">
<test-sub-component :layout-setting="layoutSetting"></test-sub-component>
<button #click="changeLayout">Change Layout</button>
</div>

Vue.js 2.3 - Element UI Dialog - Sync and Props

I'm using vue-js 2.3 and element-ui. Since the update 2.3 of vue-js and the reintroduction of sync, things have changed and I have had a hard time figuring out my problem.
Here is the jsfiddle : https://jsfiddle.net/7o5z7dc1/
Problem
The dialog is only opened once. When I close it I have this error:
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:
"showDialog"
Questions
What am I doing wrong?
How can I fix the current situation?
EDIT
If I'm creating a data I manage to remove the error message but the dialog does not close anymore
<div id="app">
<left-panel v-on:show="showDialog = true">></left-panel>
<popup v-if="showDialog":show-dialog.sync="showDialog"></popup>
</div>
<template id="left-panel-template">
<button #click="$emit('show')">Show Component PopUp</button>
</template>
<template id="popup">
<el-dialog :visible.sync="showDialog" #visiblechange="updateShowDialog">I am the popup</el-dialog>
</template>
Vue.component('left-panel', {
template: '#left-panel-template',
methods: {
},
});
Vue.component('popup', {
template: '#popup',
props : ['showDialog'],
methods: {
updateShowDialog(isVisible) {
if (isVisible) return false;
this.$emit('update:showDialog', false )
},
},
});
var vm = new Vue({
el: '#app',
data: {
showDialog: false,
},
methods: {
}
});
You avoid the mutation warning by making a copy of the prop and mutating that instead of mutating the property directly.
Vue.component('popup', {
template: '#popup',
props : ['showDialog'],
data(){
return {
show: this.showDialog
}
},
methods: {
updateShowDialog(isVisible) {
if (isVisible) return false;
this.$emit('update:showDialog', false )
},
},
});
I also changed your template to handle the visible-change event correctly.
<el-dialog :visible.sync="show" #visible-change="updateShowDialog">I am the popup</el-dialog>
Updated fiddle.