How to build a bridge of events between different components in Vue? - vue.js

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 |´・ω・)ノ

Related

Vue - Render new component based on page count

I'm working on an onboarding process that will collect a users name, location, job , etc.
It needs to be one question per page but as an SPA so I currently have around 20 components to conditionally render.
Atm, I have a counter and Prev/Next buttons that decrease/increase the counter respectively. I'm then using v-if to check what number the counter is on and render the appropriate page.
Is there a better way around this that is less repetitive and bulky?
Any ideas appreciated!
data() {
return {
onboardingStep: 0,
}
},
methods: {
prevStep() {
this.onboardingStep -= 1;
},
nextStep() {
this.onboardingStep += 1;
}
}
<intro-step v-if="onboardingStep === 0"></intro-step>
<first-name v-if="onboardingStep === 1"></first-name>
<last-name v-if="onboardingStep === 2"></last-name>
...etc.
Suggestion :
You can make your field components to show or hide based on the prev/next state. Dynamic components provide that platform in an efficient and simple way.
Syntax :
<component :is="componentName"></component>
Then, You can create each component instance dynamically by putting a watcher on components array.
watch: {
components: {
handler() {
this.components.forEach(cName => {
Vue.component(cName, {
template: `template code will come here`
})
});
}
}
}
Live Demo :
new Vue({
el: '#app',
data() {
return {
components: [],
onboardingStep: 0
}
},
mounted() {
this.components = ['intro-step', 'first-name', 'last-name'];
},
watch: {
components: {
handler() {
this.components.forEach(cName => {
Vue.component(cName, {
data() {
return {
modelName: cName
}
},
template: '<input type="text" v-model="modelName"/>'
})
});
}
}
},
methods: {
prevStep() {
this.onboardingStep -= 1;
},
nextStep() {
this.onboardingStep += 1;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(cName, index) in components" :key="index">
<component :is="cName" v-if="index === onboardingStep"></component>
</div>
<button #click="prevStep" :disabled="onboardingStep < 1">Prev</button>
<button #click="nextStep" :disabled="onboardingStep === components.length - 1">Next</button>
</div>
You could create an array with all your component names in the right order.
const components = ['intro-step', 'first-name', 'last-name' ]
And then with a v-for loop set all the components in your template:
<template v-for="(component, index) in components" :key="component">
<component :is="component" v-if="index === onboardingStep">
</template>
Hope this helps.

How do I trigger a recalculation in a Vue app?

I'm working on a project with Vue and VueX. In my component, I have a calculated method that looks like this:
...mapState([
'watches',
]),
isWatched() {
console.log('check watch');
if (!this.watches) return false;
console.log('iw', this.watches[this.event.id]);
return this.watches[this.event.id] === true;
},
And in my store, I have the following:
addWatch(state, event) {
console.log('add', state.watches);
state.watches = {
...state.watches,
[event]: true,
};
console.log('add2', state.watches);
},
However, this doesn't trigger a recalculation. What's going on?
Try changing return this.watches[this.event.id] === true;
to
return this.$store.commit("addWatch", this.event.id);
The code you have shown is correct, so the problem must be elsewhere.
I assume by 'calculated method' you mean computed property.
Computed properties do not watch their dependencies deeply, but you are updating the store immutably, so that is not the problem.
Here is a bit of sample code to give you the full picture.
Add event numbers until you hit '2', and the isWatched property becomes true.
Vue.use(Vuex);
const mapState = Vuex.mapState;
const store = new Vuex.Store({
state: {
watches: {}
},
mutations: {
addWatch(state, event) {
state.watches = { ...state.watches, [event]: true };
}
}
});
new Vue({
el: "#app",
store,
data: {
numberInput: 0,
event: { id: 2 }
},
methods: {
addNumber(numberInput) {
this.$store.commit("addWatch", Number(numberInput));
}
},
computed: {
...mapState(["watches"]),
isWatched() {
if (!this.watches) return false;
return this.watches[this.event.id] === true;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.1.0/vuex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div>Watches: {{ watches }}</div>
<div>isWatched: {{ isWatched }}</div>
<br>
<input v-model="numberInput" type="number" />
<button #click="addNumber(numberInput)">
Add new event
</button>
</div>

Unable to trigger render when switching between multiple objects containing content in different languages

I read Reactivity in Depth but can't solve the issue.
I'm creating a small single page app that contains images and text.
When the user clicks a button I want the language to change.
Currently I am storing the content in two files that export an object.
export default {
projects: [
{
title: 'Project Title',
year: 2016,
...
},
]
}
and importing that
import contentEn from './assets/content.en.js'
import contentDe from './assets/content.de.js'
new Vue({
el: '#app',
components: { App },
data: {
mainContent: {
content: contentEn
}
},
methods: {
switchToGerman(){
this.mainContent.content = contentDe
}
},
template: '<App :mainData="mainContent"/>',
})
When I assign another object to mainContent.content the rendering is not triggered.
I understand that adding and deleting properties from object don't lead to change detection but I switch out a whole object. I tried assigning it with this.$set with no success.
I also tried this and googled a lot but can't get it work.
Or is my approach just wrong?
Thank you for helping,
best,
ccarstens
EDIT:
See below the code for the App component and the ProjectElement
// App.vue
<template>
<div id="app">
<button #click="switchGerman">Deutsch</button>
<ProjectElement v-for="(project, key) in fullData.content.projects" :key="key" :content="project"/>
</div>
</template>
<script>
import ProjectElement from './components/ProjectElement'
export default {
name: 'App',
props: [
'mainData'
],
data () {
return{
fullData: {}
}
},
methods: {
switchGerman(){
this.$root.switchToGerman()
}
},
created(){
this.fullData = this.$props.mainData
},
watch: {
mainData: {
handler: function(newData){
this.fullData = newData
},
deep: true
}
},
components: {
ProjectElement,
}
}
</script>
And the ProjectElement
//ProjectElement.vue
<template>
<article :class="classObject" v-observe-visibility="{
callback: visibilityChanged,
throttle,
intersection: {
threshold
}
}">
<header v-html="description"></header>
<div class="content">
<carousel :per-page="1" :pagination-enabled="false">
<slide v-for="(slide, index) in projectContent.media" :key="index">
<VisualElement :content="slide" ></VisualElement>
</slide>
</carousel>
</div>
</article>
</template>
<script>
import {Carousel, Slide} from 'vue-carousel'
import VisualElement from './VisualElement'
export default {
name: "ProjectElement",
components: {
Carousel,
Slide,
VisualElement
},
props: [
'content'
],
data () {
return {
projectContent: {},
isVisible: false,
throttle: 300,
threshold: 0.8
}
},
created(){
this.projectContent = this.content
},
methods: {
visibilityChanged(isVisible){
this.isVisible = isVisible
}
},
computed: {
description(){
return `
<p>${ this.projectContent.title } - ${this.projectContent.year}</p>
<p>${ this.projectContent.description }</p>
`
},
classObject(){
return {
visible: this.isVisible,
'project-element': true
}
}
}
}
</script>
Did you try doing deep copy:
switchToGerman () {
const copyContent = JSON.parse(JSON.stringify(this.mainContent))
copyContent.content = contentDe
this.mainContent = copyContent
}
I found the solution (thank you #EricGuan for pointing out that the mistake must lay somewhere else)
As you can see in the original post I created a watcher for the mainData property and expected that this would trigger the re render.
What was missing is, that I didn't watch the content property on the ProjectElement component, thus not triggering a re render there.
I added this to ProjectElement.vue and now it works like a charm:
watch: {
content(newContent){
this.projectContent = newContent
}
},
Thank you everybody for helping me! <3

Fetch data in component on initiation using parameters from Vuex store

I am new to Vue and am trying to build a simple movie app, fetching data from an API and rendering the results. I want to have an incremental search feature. I have an input field in my navbar and when the user types, I want to redirect from the dashboard view to the search results view. I am unsure of how to pass the query params from the navbar to the search results view.
Here is my App.vue component
<template>
<div id="app">
<Navbar></Navbar>
<router-view/>
</div>
</template>
<script>
import Navbar from './components/Navbar.vue'
export default {
name: 'App',
components: {
Navbar
},
}
</script>
And here is my navbar component where I have the input field
<template>
<nav class="navbar">
<h1 class="logo" v-on:click="goToHome">Movie App</h1>
<input class="search-input" v-on:keyup="showResults" v-model="query" type="text" placeholder="Search..."/>
</nav>
</template>
<script>
import router from '../router/index'
export default {
data: function () {
return {
query: this.query
}
},
methods: {
goToHome () {
router.push({name: 'Dashboard'})
},
showResults () {
//here on each key press I want to narrow my results in the SearchedMovies component
}
}
}
</script>
If I use router.push to the SearchedMovies component then I am only able to pass the query as a parameter once. I thought about using Vuex to store the query and then access it from the SearchedMovies component, but surely there is a better way of doing it?
I also read about using $emit but since my parent contains all the routes, I'm not sure how to go about this.
You don't need to redirect user anywhere. I've made a small demo to show how one might do it. I used this navbar component as you described and emit an event from it:
const movies = {
data: [
{
id: 0,
title: 'Eraserhead',
},
{
id: 1,
title: 'Erazerhead',
},
{
id: 2,
title: 'Videodrome',
},
{
id: 3,
title: 'Videobrome',
},
{
id: 4,
title: 'Cube',
},
]
};
Vue.component('navbar', {
template: '<input v-model="filter" #input="onInput" placeholder="search">',
data() {
return {
filter: '',
};
},
methods: {
onInput() {
this.$emit('filter', this.filter);
}
}
});
// this is just a request imitation.
// just waiting for a second until we get a response
// from the datasample
function request(title) {
return new Promise((fulfill) => {
toReturn = movies.data.filter(movie => movie.title.toLowerCase().indexOf(title.toLowerCase()) !== -1)
setTimeout(() => fulfill(toReturn), 1000);
});
}
new Vue({
el: '#app',
data: {
movies: undefined,
loading: false,
filter: '',
lastValue: '',
},
methods: {
filterList(payload) {
// a timeout to prevent
// instant request on every input interaction
this.lastValue = payload;
setTimeout(() => this.makeRequest(), 1000);
},
makeRequest() {
if (this.loading) {
return;
}
this.loading = true;
request(this.lastValue).then((response) => {
this.movies = response;
this.loading = false;
});
}
},
mounted() {
this.makeRequest('');
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<navbar v-on:filter="filterList"></navbar>
<ul v-if="!loading">
<li v-for="movie in movies" :key="movie.id">{{ movie.title }}</li>
</ul>
<p v-else>Loading...</p>
</div>
Also jsfiddle: https://jsfiddle.net/oniondomes/rsyys3rp/
If you have any problem to understand the code above let me know.
EDIT: Fixed some bugs and added a couple of comments
EDIT2(after the comment below):
Here's what you can do. Every time user inputs something inside a navbar you call a function:
// template
<navbar v-on:input-inside-nav-bar="atInputInsideNavBar"></navbar>
// script
methods: {
atInputInsideNavBar(userInput) {
this.$router.push({
path: '/filtred-items',
params: {
value: userInput
}
})
}
}
Then inside you 'searched movies' page component you can access this value so:
this.$route.params.value // returns userInput from root component

Vuejs DOM doesn't update after fetching data

I have bound an array events to a component tag <scheduler> containing events to fill in a scheduler app (dhtmlx scheduler). However, the DOM doesn't seeem to refresh itself when data is retrieved by the getEvents methods triggered when vue instance is created.
There is 2 vue files I work with: App.vue containing the main app component and the Scheduler.vue file containing the scheduler component.
The thing is that when I modify something in the Scheduler.vue file and save it, it correctly take the updated events array into account.
Scheduler parse the data in the events prop when DOM is mounted in scheduler component.
Therefore is there something I can do to get the updated array ?
Here is the App.vue:
<template>
<div id="app">
<div class="container">
<scheduler v-bind:events="events"></scheduler>
</div>
</div>
</template>
<script>
import Scheduler from './components/Scheduler.vue';
import auth from './components/auth/index'
import data from './components/data/index'
export default {
name: 'app',
components: {
Scheduler
},
data() {
return {
events: []
}
},
created() {
this.getEvents();
},
watch: {
events: function(value) {
console.log('updated');
}
},
methods: {
async getEvents() {
try {
const token = await auth.getToken(this);
const thattoken = await auth.getThatToken(this, token);
await data.getOgustData(this, token, '/calendar/events', 307310564, this.events);
} catch (e) {
console.log(e);
}
},
}
}
</script>
Here is Scheduler.vue:
<template lang="html">
<div ref="scheduler_here" class="dhx_cal_container" style='width:100%; height:700px;'>
<div class="dhx_cal_navline">
<div class="dhx_cal_prev_button"> </div>
<div class="dhx_cal_next_button"> </div>
<div class="dhx_cal_today_button"></div>
<div class="dhx_cal_date"></div>
<div class="dhx_cal_tab" name="day_tab" style="right:204px;"></div>
<div class="dhx_cal_tab" name="week_tab" style="right:140px;"></div>
<div class="dhx_cal_tab" name="month_tab" style="right:76px;"></div>
</div>
<div class="dhx_cal_header"></div>
<div class="dhx_cal_data"></div>
</div>
</template>
<script>
import 'dhtmlx-scheduler'
import 'dhtmlx-scheduler/codebase/locale/locale_fr';
import 'dhtmlx-scheduler/codebase/ext/dhtmlxscheduler_readonly.js';
export default {
name: 'scheduler',
props: {
events: {
type: Array,
default () {
return [{
id: '',
text: '',
start_date: '',
end_date: '',
}]
}
}
},
mounted() {
scheduler.config.xml_date = '%Y-%m-%d %H:%i';
// disable left buttons on lightbox
scheduler.config.buttons_left = [];
// enable cancel button on lightbox's right wing
scheduler.config.buttons_right = ['dhx_cancel_btn'];
// changing cancel button label
scheduler.locale.labels['icon_cancel'] = 'Fermer';
// hide lightbox in month view
scheduler.config.readonly_form = true;
// hide select bar in day and week views
scheduler.config.select = false;
scheduler.config.lightbox.sections = [
{
name: "description",
height: 20,
map_to: "text",
type: "textarea",
focus: true
}
];
scheduler.init(this.$refs.scheduler_here, new Date(), 'month');
scheduler.parse(this.$props.events, 'json');
},
}
</script>
<style lang="css" scoped>
#import "~dhtmlx-scheduler/codebase/dhtmlxscheduler.css";
</style>
getOgustData can't populate events in a way that Vue can observe. Since you're passing it as an argument, the array itself can be updated, but it's not a reactive array. Try
var newEvents;
await data.getOgustData(this, token, '/calendar/events', 307310564, newEvents);
this.events = newEvents;
Assigning to this.events is something Vue can notice.
Problem is solved. The issue didn't come from Vue but rather from the dhtmlx scheduler which wasn't parsing events when events was updated.
I ended up watching for any changes to events and thus, parsing it when it updates.
Thanks again for the help provided.
App.vue :
<template>
<div id="app">
<div class="container">
<scheduler v-bind:events="events"></scheduler>
</div>
</div>
</template>
<script>
import Scheduler from './components/Scheduler.vue';
import auth from './components/auth/index'
import data from './components/data/index'
import 'dhtmlx-scheduler'
export default {
name: 'app',
components: {
Scheduler
},
data() {
return {
events: []
}
},
created() {
this.getEvents();
},
watch: {
events: function(value) {
scheduler.parse(this.events, 'json');
}
},
methods: {
async getEvents() {
const token = await auth.getToken(this);
const apiToken = await auth.getApiToken(this, token);
this.events = await data.getApiData(this, apiToken, '/calendar/events', 307310564, this.events);
}
},
}
</script>