Add component v-on events (#mousenter) within a mixin - vue.js

I want to make a Vue Mixin that applies similar hover events and classes.
Right now I add this to each component, but would prefer to make this into a mixin.
Is this possible, or is there an easier way to accomplish this without having to include #mouseenter and #mouseleave?
<div
#mousenter="hovering=true"
#mouseleave="hovering=false"
:class="[hovering ? 'elevation-4' : 'elevation-2']">`
I'd prefer to import something like:
export default {
data: () => ({ hovering: false }),
mounted(){
// something here to use mouseenter/mouseleave
}
}

You could do it like this:
Vue.config.devtools = false
Vue.config.productionTip = false
const myMixin = {
data: () => ({ hovering: false }),
}
new Vue({
el: "#app",
mixins: [myMixin]
})
section {
height: 200px;
width: 200px;
}
.elevation-4 {
background-color: red
}
.elevation-2 {
background-color: green
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<section
#mouseenter="hovering=true"
#mouseleave="hovering=false"
:class="[hovering ? 'elevation-4' : 'elevation-2']">
</section>
</div>

You can define a mixin like so:
lib/mixins/hover.js
export default {
data() {
return { isHovering: false };
},
computed: {
klass() {
return this.isHovering ? 'o-hoverable--on' : 'o-hoverable--off';
},
},
methods: {
onEnter() {
this.isHovering = true;
},
onLeave() {
this.isHovering = false;
},
},
};
And then use it like this:
index.vue
<template>
<div :class="klass" #mouseenter="onEnter" #mouseleave="onLeave">hover me</div>
</template>
<script>
import hover from '~/lib/mixins/hover';
export default {
mixins: [hover],
};
</script>
Note that you'll still need to manually bind the events and the class, but you'll get to reuse the definitions of both.

Related

VUE.JS 3 Changing boolean value of one sibling component from another

I have two components - component A and component B that are siblings.
I need to change the boolean value inside of Component-A from the Watcher in Component-B.
Component A code:
<template>
<div></div>
</template>
<script>
export default {
data() {
return {
editIsClicked: false,
}
}
}
</script>
Component B code:
<template>
<v-pagination
v-model="currentPage"
:length="lastPage"
:total-visible="8"
></v-pagination>
</template>
<script>
export default {
props: ["store", "collection"],
watch: {
currentPage(newVal) {
this.paginatePage(newVal);
// NEED TO TOGGLE VALUE HERE - when i switch between pages
},
},
},
};
</script>
The Vue Documentation proposes communicating between Vue Components using props and events in the following way
*--------- Vue Component -------*
some data => | -> props -> logic -> event -> | => other components
*-------------------------------*
It's also important to understand how v-model works with components in Vue v3 (Component v-model).
const { createApp } = Vue;
const myComponent = {
props: ['modelValue'],
emits: ['update:modelValue'],
data() {
return {
childValue: this.modelValue
}
},
watch: {
childValue(newVal) {
this.$emit('update:modelValue', newVal)
}
},
template: '<label>Child Value:</label> {{childValue}} <input type="checkbox" v-model="childValue" />'
}
const App = {
components: {
myComponent
},
data() {
return {
parentValue: false
}
}
}
const app = createApp(App)
app.mount('#app')
<div id="app">
Parent Value: {{parentValue}}<br />
<my-component v-model="parentValue"/>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
I have made a new playground. Hope it helps you now to understand the logic.
You can store data in the main Vue App instance or use a Pinia store for it.
But I would suggest you to start without Pinia to make your app simpler. Using Pinia will make your App much more complicated and your knowledge of Vue seems to be not solid enough for that.
const { createApp } = Vue;
const myComponentA = {
props: ['editIsClicked', 'currentPage'],
template: '#my-component-a'
}
const myComponentB = {
emits: ['editIsClicked'],
data() {
return {
currentPage: 1,
}
},
watch: {
currentPage(newVal) {
this.$emit('editIsClicked', newVal)
}
},
template: '#my-component-b'
}
const App = {
components: {
myComponentA, myComponentB
},
data() {
return {
editIsClicked: false,
currentPage: 1
}
},
methods: {
setEditIsClicked(val) {
this.editIsClicked = true;
this.currentPage = val;
}
}
}
const app = createApp(App)
app.mount('#app')
#app { line-height: 2; }
.comp-a { background-color: #f8f9e0; }
.comp-b { background-color: #d9eba7; }
<div id="app">
<my-component-a :edit-is-clicked="editIsClicked" :current-page="currentPage"></my-component-a>
<my-component-b #edit-is-clicked="setEditIsClicked"></my-component-b>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<script type="text/x-template" id="my-component-a">
<div class="comp-a">
My Component A: <br />editIsClicked: <b>{{editIsClicked}}</b><br/>
currentPage: <b>{{currentPage}}</b><br/>
</div>
</script>
<script type="text/x-template" id="my-component-b">
<div class="comp-b">
My Component B: <br />
<label>CurrentPage:</label> <input type="number" v-model="currentPage" />
</div>
</script>

Vue.js 3 - How can I pass data between Vue components and let both views also update?

I tried the following.
Please note the commented line in parent.vue that doesn't even commit the new state for me.
However maybe someone can guide me to a better solution for a global state shared by multiple components?
main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createStore } from 'vuex'
const app = createApp(App);
export const store = createStore({
state: {
textProp: 'test',
count: 1
},
mutations: {
setState(state, newState) {
console.log('setState');
state = newState;
}
},
getters: {
getAll: (state) => () => {
return state;
}
}
});
app.use(store);
app.mount('#app')
parent.vue
<template>
<div class="parent">
<div class="seperator" v-bind:key="item" v-for="item in items">
<child></child>
</div>
<button #click="toonAlert()">{{ btnText }}</button>
<button #click="veranderChild()">Verander child</button>
</div>
</template>
<script>
import child from "./child.vue";
import {store} from '../main';
export default {
name: "parent",
components: {
child,
},
store,
data: function () {
return {
items: [
{
id: 1,
valueText: "",
valueNumber: 0,
},
{
id: 2,
valueText: "",
valueNumber: 0,
},
{
id: 3,
valueText: "",
valueNumber: 0,
},
],
};
},
props: {
btnText: String,
},
methods: {
toonAlert() {
alert(JSON.stringify(this.$store.getters.getAll()));
},
veranderChild() {
console.log('child aan het veranderen (parentEvent)');
this.$store.commit('setState', { // This is especially not working.
textProp: 'gezet via de parent',
count: 99
})
this.$store.commit({type: 'setState'}, {
'textProp': 'gezet via de parent',
'count': 99
});
},
},
};
</script>
<style>
.seperator {
margin-bottom: 20px;
}
.parent {
/* background: lightblue; */
}
</style>
child.vue
<template>
<div class="child">
<div class="inputDiv">
text
<input #change="update" v-model="deText" type="text" name="deText" />
</div>
<div class="inputDiv">
nummer
<input v-model="hetNummer" type="number" name="hetNummer" />
</div>
<button #click="toonState">Toon huidige state</button>
</div>
</template>
<script>
import {store} from '../main';
export default {
name: "child",
store,
data: function() {
return {
'hetNummer': 0
}
},
methods: {
update(e) {
let newState = this.$store.state;
newState.textProp = e.target.value;
// this.$store.commit('setState', newState);
},
toonState()
{
console.log( this.$store.getters.getAll());
}
},
computed: {
deText: function() {
return '';
// return this.$store.getters.getAll().textProp;
}
}
};
</script>
<style>
.inputDiv {
float: right;
margin-bottom: 10px;
}
.child {
max-width: 300px;
height: 30px;
margin-bottom: 20px;
/* background: yellow; */
margin: 10px;
}
</style>
You have a misconception about JavaScript unrelated to Vue/Vuex. This doesn't do what you expect:
state = newState;
Solution (TL;DR)
setState(state, newState) {
Object.assign(state, newState);
}
Instead of setting the state variable, merge the new properties in.
Explanation
Object variables in JavaScript are references. That's why if you have multiple variables referring to the same object, and you change a property on one, they all mutate. They're all just referring to the same object in memory, they're not clones.
The state variable above starts as a reference to Vuex's state object, which you know. Therefore, when you change properties of it, you mutate Vuex's state properties too. That's all good.
But when you change the whole variable-- not just a property-- to something else, it does not mutate the original referred object (i.e. Vuex's state). It just breaks the reference link and creates a new one to the newState object. So Vuex state doesn't change at all. Here's a simpler demo.
Opinion
Avoid this pattern and create an object property on state instead. Then you can just do state.obj = newState.
You should use a spread operator ... to mutate your state as follows :
state = { ...state, ...newState };
LIVE EXAMPLE
but I recommend to make your store more organized in semantic way, each property in your state should have its own setter and action, the getters are the equivalent of computed properties in options API they could be based on multiple state properties.
export const store = createStore({
state: {
count: 1
},
mutations: {
SET_COUNT(state, _count) {
console.log("setState");
state.count=_count
},
INC_COUNT(state) {
state.count++
}
},
getters: {
doubleCount: (state) => () => {
return state.count*2;
}
}
});
**Note : ** no need to import the store from main.js in each child because it's available using this.$store in options api, but if you're working with composition api you could use useStore as follows :
import {useStore} from 'vuex'
setup(){
const store=useStore()// store instead of `$store`
}

How to build a bridge of events between different components in Vue?

I need to solve it
1) click mainMidLeft component
2) after clicked, to move slideLeftTop component
http://joxi.ru/ZrJBvERH1JVa8r
The problem I dont quite understand how to do this in right way..
Is it okay to create in mainMidLeft a method where I will do somethik like this:
move: () => {
document.querySelector(`.slideLeftTop`).style.position .....
}
The best practice is to use Vuex State manager with computed methods (getters) and watchers
I have made a working example for you on jsfiddle.
https://jsfiddle.net/n4e_m16/wujafg5e/4/
For more info on how vuex works please go to Here
Please let me know if you need more help :)
const store = new Vuex.Store({
state: {
mainMidLeftState: false,
},
getters: {
mainMidLeftState: state => state.mainMidLeftState,
},
mutations: {
toggleMainMidLeft: (state, payload) => {
state.mainMidLeftState = !state.mainMidLeftState
},
},
})
Vue.component('main-mid-left', {
data() {
return {
}
},
computed: {
mainMidLeftState() {
return this.$store.state.mainMidLeftState
},
},
methods: {
toggleMainMidLeft() {
this.$store.commit('toggleMainMidLeft')
// alert(this.mainMidLeftState)
},
}
})
Vue.component('slide-left-top', {
data() {
return {
}
},
computed: {
mainMidLeftState() {
return this.$store.state.mainMidLeftState
},
},
watch: {
mainMidLeftState: function(val) {
alert("YES, computed property changed and alert have been triggered by watcher in slide top left component")
}
},
methods: {
}
})
const app = new Vue({
el: '#app',
store,
})
div {
color: black;
}
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex#3.0.1/dist/vuex.js"></script>
<div id="app">
<!-- inlining the template to make things easier to read - all of below is held on the component not the root -->
<main-mid-left inline-template>
<div>
<h4>
main mid left
</h4>
<button v-on:click="toggleMainMidLeft()">toggle Main Mid Left State</button>
<div v-show="mainMidLeftState == true">State is true</div>
<div v-show="mainMidLeftState == false">State is false</div>
</div>
</main-mid-left>
<slide-left-top inline-template>
<div>
<h4>
slide left top
</h4>
<div v-show="mainMidLeftState == true">State is true</div>
<div v-show="mainMidLeftState == false">State is false</div>
</div>
</slide-left-top>
</div>
If you don't want to use vuex, you can create a new Vue instance as an event bus (I believe this is mentioned somewhere in the Vue tutorial):
const EventBus = new Vue()
Import EventBus to where you need it and you can send an event by:
EventBus.$emit('event-name', data)
And add the following script in created() of your receiver component:
EventBus.$on('event-name', ($event) => {
// Do something
})
I hope this helps |´・ω・)ノ

vuejs data doesn't change when property change

I am very new to Vuejs so although I can probably devise a solution myself by using a watcher or perhaps a lifecycle hook I would like to understand why the following does not work and what should be done instead.
The problem is that the mutated local data doesn't update whenever the component consumer changes the property cellContent. The parent owns cellContent so using the property directly is a no-no (Vue seems to agree).
<template>
<textarea
v-model="mutableCellContent"
#keyup.ctrl.enter="$emit('value-submit', mutableCellContent)"
#keyup.esc="$emit('cancel')">
</textarea>
</template>
<script>
export default {
name: 'CellEditor',
props: ['cellContent', 'cellId'],
data () {
return {
mutableCellContent: this.cellContent
}
}
}
</script>
<style>
...
</style>
In data (mutableCellContent: this.cellContent) you are creating a copy of the prop, that's why when the parent changes, the local copy (mutableCellContent) is not updated. (If you must have a local copy, you'd have to watch the parent to update it.)
Instead, you should not keep a copy in the child component, just let the state be in the parent (and change it through events emitted in the child). This is a well known the best practice (and not only in Vue, but in other frameworks too, if I may say it).
Example:
Vue.component('cell-editor', {
template: '#celleditor',
name: 'CellEditor',
props: ['cellContent', 'cellId'],
data () {
return {}
}
});
new Vue({
el: '#app',
data: {
message: "Hello, Vue.js!"
}
});
textarea { height: 50px; width: 300px; }
<script src="https://unpkg.com/vue"></script>
<template id="celleditor">
<textarea
:value="cellContent"
#keyup.ctrl.enter="$emit('value-submit', $event.currentTarget.value)"
#keyup.esc="$event.currentTarget.value = cellContent">
</textarea>
</template>
<div id="app">
{{ message }}
<br>
<cell-editor :cell-content="message" #value-submit="message = $event"></cell-editor>
<br>
<button #click="message += 'parent!'">Change message in parent</button>
</div>
You have to create a watcher to the prop cellContent.
Vue.config.productionTip = false
Vue.config.devtools = false
Vue.config.debug = false
Vue.config.silent = true
Vue.component('component-1', {
name: 'CellEditor',
props: ['cellContent', 'cellId'],
data() {
return {
mutableCellContent: this.cellContent
}
},
template: `
<textarea
v-model="mutableCellContent"
#keyup.ctrl.enter="$emit('value-submit', mutableCellContent)"
#keyup.esc="$emit('cancel')">
</textarea>
`,
watch: {
cellContent(value) {
this.mutableCellContent = value;
}
}
});
var vm = new Vue({
el: '#app',
data() {
return {
out: "",
cellContent: ""
}
},
methods: {
toOut(...args) {
this.out = JSON.stringify(args);
},
changeCellContent() {
this.cellContent = "changed at " + Date.now();
}
}
});
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<component-1 :cell-content="cellContent" #value-submit="toOut" #cancel="toOut"></component-1>
<p>{{out}}</p>
<button #click="changeCellContent">change prop</button>
</div>

Custom vue directive to render only if present

Frequently, I want to render a div (or other element) only if it has content. This means repeating the reference to the content in the tag, and in v-if, like this...
<div v-if="store.sometimesIWillBeEmpty">{{store.sometimesIWillBeEmpty}}</div>
With custom directives, I want to create a directive, v-fill, that behaves just like the code above, but with simplified syntax...
<div v-fill="store.sometimesIWillBeEmpty"></div>
updated The following works when message is not empty. What do I set or clear to render nothing when message is empty?
var store = {message: "hello cobber"}
Vue.directive('fill',
function (el, binding, vnode) {
if(binding.value)
el.innerHTML = binding.value
else
el = null
}
);
new Vue({
el: '#fill-example',
data: {
store: store
}
})
I'm one line away. Here's my fiddle. Anyone have any ideas?
It is possible to make a straightforward component to do what you want. A directive requires a bit more manipulation to be able to remove the element and put it back in the right place.
const vm = new Vue({
el: '#fill-example',
data: {
empty: '',
notEmpty: 'I have content'
},
components: {
renderMaybe: {
props: ['value'],
template: `<div v-if="value" class="boxy">{{value}}</div>`
}
},
directives: {
fill: {
bind(el, binding) {
Vue.nextTick(() => {
el.vFillMarkerNode = document.createComment('');
el.parentNode.insertBefore(el.vFillMarkerNode, el.nextSibling);
if (binding.value) {
el.textContent = binding.value;
} else {
el.parentNode.removeChild(el);
}
});
},
update(el, binding) {
if (binding.value) {
el.vFillMarkerNode.parentNode.insertBefore(el, el.vFillMarkerNode);
el.textContent = binding.value;
} else {
if (el.parentNode) {
el.parentNode.removeChild(el);
}
}
}
}
}
});
setTimeout(() => {
vm.empty = "Now I have content, too.";
}, 1500);
.boxy {
border: thin solid black;
padding: 1em;
}
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="fill-example">
Components:
<render-maybe :value="empty"></render-maybe>
<render-maybe :value="notEmpty"></render-maybe>
Directives:
<div class="boxy" v-fill="empty"></div>
<div class="boxy" v-fill="notEmpty"></div>
</div>