I was using the event emitter from vue to make the main app talk to the components and it was working as long as I had the app and all the components in one .js file.
But now I wish to have them separated, one component per file, and obviously I cannot use the event emitter anymore, because the app appears undefined in the modules.
What do I need to change to be able have the app and components communicate again?
My code:
my-app.js
import My from '/my-module.js';
const app = new Vue({
router: new VueRouter({
mode: 'history',
routes: [{
path: '/',
component: My,
}]),
methods: {
created(){
this.ws = new WebSocket(...);
this.ws.onmessage = event => {
this.$emit('message', e);
}
}
}
}).$mount('#app');
my-module.js
export default {
template: `....`
created(){
// I want to be able to access 'app' here
// or another way to receive the messages
console.log(app);
app.$on('message', ...)
}
}
As you can see I only need this because I'm using websockets and the app holds the websocket client. The components need to do something when certain messages are received.
In your case, you may use those events in multiple components, specialy when app is still growing. i think you better use eventbus to emit and catch all of them.
here how to use eventbus in vuejs : https://alligator.io/vuejs/global-event-bus/.
in you're case :
import { eventBus } from 'path/to/file';
...
methods: {
created(){
this.ws = new WebSocket(...);
this.ws.onmessage = event => {
eventBus.$emit('message', e);
}
}
}
Other component :
import { eventBus } from 'path/to/file';
...
created() {
eventBus.$on('message',() => ) {
// code
}
}
Related
I'm trying to implement a solution for the login page. When user passes the wrong login/password I want the page to reload after 3 seconds. I was trying to do it with 2 solutions but can't make it work. The code would launch after click on a button when there is no match for credentials (else condition):
FIRST IDEA:
...else{
this.comUser = 'WRONG LOGIN OR PASSWORD'
this.alert = ""
setTimeout(function() {
window.location = "I would like it to use the routing to a component instead of URL here"; }
,3000);
SECOND IDEA WITH A PROMISE:
const reloadDelay = () => {
reload = this.router.navigate(['/login']);
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("działa");
resolve(reload)
}, 4000);
});
};
Thanks for any hints!
If you're using angular 9 you can try this
setTimeout(() => {this.domDocument.location.reload()}, 3000);
You need to import:
import { Inject } from '#angular/core';
import { DOCUMENT } from '#angular/common';
In order to use the above-mentioned method and have the constructor of component.ts configured as below.
constructor(#Inject(DOCUMENT) private domDocument: Document){}
This can now be done using the onSameUrlNavigation property of the Router config. In your router config enable onSameUrlNavigation option, setting it to reload . This causes the Router to fire an events cycle when you try to navigate to a route that is active already.
#ngModule({
imports: [RouterModule.forRoot(routes, {onSameUrlNavigation: 'reload'})],
exports: [RouterModule],
})
In your route definitions, set runGuardsAndResolvers to always. This will tell the router to always kick off the guards and resolvers cycles, firing associated events.
export const routes: Routes = [
{
path: 'invites',
component: InviteComponent,
children: [
{
path: '',
loadChildren: './pages/invites/invites.module#InvitesModule',
},
],
canActivate: [AuthenticationGuard],
runGuardsAndResolvers: 'always',
}
]
Finally, in each component that you would like to enable reloading, you need to handle the events. This can be done by importing the router, binding onto the events, and invoking an initialization method that resets the state of your component and re-fetches data if required.
export class InviteComponent implements OnInit, OnDestroy {
navigationSubscription;
constructor(
// … your declarations here
private router: Router,
) {
// subscribe to the router events. Store the subscription so we can
// unsubscribe later.
this.navigationSubscription = this.router.events.subscribe((e: any) => {
// If it is a NavigationEnd event re-initalise the component
if (e instanceof NavigationEnd) {
this.initialiseInvites();
}
});
}
initialiseInvites() {
// Set default values and re-fetch any data you need.
}
ngOnDestroy() {
if (this.navigationSubscription) {
this.navigationSubscription.unsubscribe();
}
}
}
With all of these steps in place, you should have route reloading enabled. Now for your timeperiod, you can simply use settimeout on route.navigate and this should reload it after desired time.
I have two different views that I'd like to show for the same path depending on whether a token is in LocalStorage or not. I could move the logic directly into the view itself, but I was curious to know whether there's a way to it in the Router.
Something like:
export default new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: function() {
if (...) {
return ViewA
} else {
return ViewB
}
}
},
]
});
I tried with the above code but didn't work. The app builds fine but none of the two views is shown.
A getter could be used for this, but, you will need to be sure to import both components:
import ViewA from '#/views/ViewA'
import ViewB from '#/views/ViewB'
export default new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
get component() {
if (...) {
return ViewA;
} else {
return ViewB;
}
}
},
]
});
In my notes, I have written "cannot find documentation on this" pertaining to the above. While not specifically related, however, it might be helpful to review using some information from https://stackoverflow.com/a/50137354/3261560 regarding the render function. I've altered what is discussed there using your example above.
component: {
render(c) {
if (...) {
return c(ViewA);
} else {
return c(ViewB);
}
}
}
I was answering the same question before and you can see it here.
Here is an example:
routes: [
{
path: '*',
beforeEnter(to, from, next) {
let components = {
default: [A, B, C][Math.floor(Math.random() * 100) % 3],
};
to.matched[0].components = components;
next();
}
},
... where A, B, C are components and they are randomly chosen every time the route changes. So in your case you can just alter beforeEnter logic to your needs and set any component you wish before routing to it.
Alright so I found a simple way to do this using the webpack lazy loading functionality in vue router with vuex store state property;
{
path: '/',
component: () => {
if (store.state.domain) {
return import(/* webpackChunkName: "app-home" */ '../views/AppHome.vue');
} else {
return import(/* webpackChunkName: "home" */ '../views/Home.vue');
}
}
},
With the above code, my home component is dynamically imported and used the route component depending on the value of domain property in my vuex store. Please note that you have to set up vuex store and import it in your router for this to work.
The solution in the question would have worked if you returned the component as an import.
I have this watched property called cases:
cases: {
handler: function () {
let vc = this
if (vc.areCasesValid()) {
console.log('schema-cases#handler - casesValid', vc.cases)
vc.setApiModelFromView()
vc.$emit('valid', vc.apiModel)
return true
}
else {
console.log('template-heuristic-cases#handler - casesInvalid', vc.cases)
vc.$emit('invalid')
return false
}
},
deep: true
},
All validation steps are currently inside the areCasesValidfunction, but I want to improve the usability of the application and that process involves putting the different validation steps into different components.
Taking that into account, what's the easiest (not necessarily the most elegant) way to observe the different changes that cases goes through in different components?
If you want to keep it simple and don't want to use state management like vuex, you can use an event bus:
Your event bus:
import Vue from 'vue';
export const EventBus = new Vue();
Your component that emits the event:
<template>
<div class="pleeease-click-me" #click="emitGlobalClickEvent()"></div>
</template>
<script>
// Import the EventBus we just created.
import { EventBus } from './event-bus.js';
export default {
data() {
return {
clickCount: 0
}
},
methods: {
emitGlobalClickEvent() {
this.clickCount++;
// Send the event on a channel (i-got-clicked) with a payload (the click count.)
EventBus.$emit('i-got-clicked', this.clickCount);
}
}
}
</script>
Your component that receives the event:
// Import the EventBus.
import { EventBus } from './event-bus.js';
// Listen for the i-got-clicked event and its payload.
EventBus.$on('i-got-clicked', clickCount => {
console.log(`Oh, that's nice. It's gotten ${clickCount} clicks! :)`)
});
Full example and more explanation can be found here: https://alligator.io/vuejs/global-event-bus/
I'm currently making use of the WordPress REST API, and vue-router to transition between pages on a small single page site. However, when I make an AJAX call to the server using the REST API, the data loads, but only after the page has already rendered.
The vue-router documentation provides insight in regards to how to load data before and after navigating to each route, but I'd like to know how to load all route and page data on the initial page load, circumventing the need to load data each time a route is activated.
Note, I'm loading my data into the acf property, and then accessing it within a .vue file component using this.$parent.acfs.
main.js Router Code:
const router = new VueRouter({
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/tickets', component: Tickets },
{ path: '/sponsors', component: Sponsors },
],
hashbang: false
});
exports.router = router;
const app = new Vue({
router,
data: {
acfs: ''
},
created() {
$.ajax({
url: 'http://localhost/placeholder/wp-json/acf/v2/page/2',
type: 'GET',
success: function(response) {
console.log(response);
this.acfs = response.acf;
// this.backgroundImage = response.acf.background_image.url
}.bind(this)
})
}
}).$mount('#app')
Home.vue Component Code:
export default {
name: 'about',
data () {
return {
acf: this.$parent.acfs,
}
},
}
Any ideas?
My approach is to delay construction of the store and main Vue until my AJAX call has returned.
store.js
import Vue from 'vue';
import Vuex from 'vuex';
import actions from './actions';
import getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
function builder(data) {
return new Vuex.Store({
state: {
exams: data,
},
actions,
getters,
mutations,
});
}
export default builder;
main.js
import Vue from 'vue';
import VueResource from 'vue-resource';
import App from './App';
import router from './router';
import store from './store';
Vue.config.productionTip = false;
Vue.use(VueResource);
Vue.http.options.root = 'https://miguelmartinez.com/api/';
Vue.http.get('data')
.then(response => response.json())
.then((data) => {
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store: store(data),
template: '<App/>',
components: { App },
});
});
I have used this approach with other frameworks such as Angular and ExtJS.
You can use navigation guards.
On a specific component, it would look like this:
export default {
beforeRouteEnter (to, from, next) {
// my ajax call
}
};
You can also add a navigation guard to all components:
router.beforeEach((to, from, next) => {
// my ajax call
});
One thing to remember is that navigation guards are async, so you need to call the next() callback when the data loading is finished. A real example from my app (where the guard function resides in a separate file):
export default function(to, from, next) {
Promise.all([
IngredientTypes.init(),
Units.init(),
MashTypes.init()
]).then(() => {
next();
});
};
In your case, you'd need to call next() in the success callback, of course.
I've comprised my own version based on all the great responses to this post.. and several years having passed by as well giving me more tools.
In main.js, I use async/await to call a prefetch service to load any data that must be there on startup. I find this increases readability. After I get the data comms, I then dispatch it to the appropriate vuex store module in the beforeCreate() hook.
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import { prefetchAppData } from '#/services/prefetch.service';
(async () => {
let comms = await prefetchAppData();
new Vue({
router,
store,
beforeCreate() {
store.dispatch('communityModule/initialize', comms);
},
mounted() {},
render: h => h(App)
}).$mount('#app');
})();
I feel compelled to warn those be careful what you prefetch. Try to do this sparingly as it does delay initial app loading which is not ideal for a good user experience.
Here's my sample prefetch.service.js which does the data load. This of course could be more sophisticated.
import api from '#api/community.api';
export async function prefetchAppData() {
return await api.getCommunities();
}
A simple vue store. This store maintains a list of 'communities' that the app requires to be loaded before application start.
community.store.js (note im using vuex modules)
export const communityModule = {
namespaced: true,
state: {
communities: []
},
getters: {
communities(state) {
return state.communities;
},
},
mutations: {
SET_COMMUNITIES(state, communities) {
state.communities = communities;
}
},
actions: {
// instead of loading data here, it is passed in
initialize({ commit }, comms) {
commit('SET_COMMUNITIES', comms);
}
}
};
Alright, I finally figured this thing out. All I'm doing is calling a synchronous ajax request within my main.js file where my root vue instance is instantiated, and assigning a data property the requested data as so:
main.js
let acfData;
$.ajax({
async: false,
url: 'http://localhost/placeholder/wp-json/acf/v2/page/2',
type: 'GET',
success: function(response) {
console.log(response.acf);
acfData = response.acf;
}.bind(this)
})
const router = new VueRouter({
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/tickets', component: Tickets },
{ path: '/sponsors', component: Sponsors },
],
hashbang: false
});
exports.router = router;
const app = new Vue({
router,
data: {
acfs: acfData
},
created() {
}
}).$mount('#app')
From here, I can use the pulled data within each individual .vue file / component like so:
export default {
name: 'app',
data () {
return {
acf: this.$parent.acfs,
}
},
Finally, I render the data within the same .vue template with the following:
<template>
<transition
name="home"
v-on:enter="enter"
v-on:leave="leave"
v-bind:css="false"
mode="out-in"
>
<div class="full-height-container background-image home" v-bind:style="{backgroundImage: 'url(' + this.acf.home_background_image.url + ')'}">
<div class="content-container">
<h1 class="white bold home-title">{{ acf.home_title }}</h1>
<h2 class="white home-subtitle">{{ acf.home_subtitle }}</h2>
<div class="button-block">
<button class="white home-button-1">{{ acf.link_title_1 }}</button>
<button class="white home-button-2">{{ acf.link_title_2 }}</button>
</div>
</div>
</div>
</transition>
</template>
The most important piece of information to take away, is that all of the ACF data is only being called ONCE at the very beginning, compared to every time a route is visited using something like beforeRouteEnter (to, from, next). As a result, I'm able to get silky smooth page transitions as desired.
Hope this helps whoever comes across the same problem.
Check this section in docs of Vue Router
https://router.vuejs.org/guide/advanced/data-fetching.html
So first of you have to write method that would fetch data from your endpoint, and then use watcher to watch route.
export default {
watch: {
'$route': 'fetchItems'
},
methods: {
fetchItems() {
// fetch logic
}
}
}
Since you are working with WP Rest API, feel free to check my repo on Github https://github.com/bedakb/vuewp/blob/master/public/app/themes/vuewp/app/views/PostView.vue#L39
Lets say we have a component in Aurelia named UserRouter, which is a child router and handles routing to UserProfile, UserImages and UserFriends.
I want the UserRouter to load in the user from the API (on canActivate) and then pass this this user data to sub components.
Loading in the data is fine, how do I pass it down to sub components so they can all read it? e.g. placing an attribute on <router-view>.
I've tried the bindingContext argument on the bind() method of sub components but this hasn't worked.
Thanks
The way I did this, was by adding additional information to the child router definition eg:
configureRouter(config, router){
config.map([
{ route: [''], name: 'empty', moduleId: './empty', nav: false, msg:"choose application from the left" },
{ route: 'ApplicationDetail/:id', name: 'applicationDetail', moduleId: './applicationDetail', nav: false, getPickLists : () => { return this.getPickLists()}, notifyHandler : ()=>{return this.updateHandler()} }
]);
in the first route from this example, I pass in a text message by adding my own porperty 'msg' to the route object.
in the second route I pass in some event handlers, but could have been some custom objects or antything else.
In the childmodel I receive these additional elements in the activate() method:
export class empty{
message;
activate(params, routeconfig){
this.message=routeconfig.msg || "";
}
}
I guess in your case, you would add the user object to the child router definition.
You could bind the router/user object to the <router-view>
<router-view router.bind="router"></router-view>
I have the same issue, so I'd love to see what you come up with!!
You could use the event aggregator to publish out an event with the data and the sub components could subscribe and listen for that event. This would avoid coupling between them. However, there may be a downside I'm not thinking of.
I worked out a solution for this, you simply tell the dependency injector to inject an instance of your parent router component using Parent.of(YourComponent).
import {inject, Parent} from 'aurelia-framework';
import {UsersRouter} from './router';
#inject(Parent.of(UsersRouter))
export class UserImages {
constructor(usersRouter) {
this.usersRouter = usersRouter;
}
activate() {
this.user = this.usersRouter.user;
}
}
You can also actually miss out the Parent.of part because the Aurelia DI system works its way up.
import {inject} from 'aurelia-framework';
import {UsersRouter} from './router';
#inject(UsersRouter)
export class UserImages {
constructor(usersRouter) {
this.usersRouter = usersRouter;
}
activate() {
this.user = this.usersRouter.user;
}
}
You can implement a generic method to pass objects:
configureRouter(config, router){
this.router = router;
...
this.router.navigateWithParams = (routeName, params) => {
let routerParams = this.router.routes.find(x => x.name === routeName);
routerParams.data = params;
this.router.navigate(routeName);
}
}
then you can use it programmatically like:
goToSomePage(){
let obj = { someValue: "some data", anotherValue: 150.44}
this.router.navigateWithParams("someRoute", obj );
}
finally, you can get the param on the destination page:
activate(urlParams, routerParams)
{
this.paramsFromRouter = routerParams.data;
}