Angular2 routing: canDeactivate limitations - angular2-routing

I've got a form in angular2. If it's dirty (with unsaved changes), I want to restrict the user from navigating away.
Research shows that the canDeactivate route guard is the way to do it.
Google led me to this github file which seems to implement what I want.
import { CanDeactivate } from '#angular/router';
import { FormGroup } from '#angular/forms';
export interface FormComponent {
form: FormGroup;
}
export class PreventUnsavedChangesGuard implements CanDeactivate < FormComponent > {
canDeactivate(component: FormComponent) {
if (component.form.dirty)
return confirm('You have unsaved changes. Are you sure you want to navigate away?');
return true;
}
}
Now, I've put this service in my project, injected it in my form component, added it to my routing module as so...
const routes: Routes = [{
path: '',
loadChildren: 'app/main/main.module#MainModule',
canActivate: [AuthenticationGuard],
}, {
path: 'login',
component: LoginComponent,
canDeactivate: [PreventUnsavedChangesGuard]
}, ]
and included it in the app module's providers array.
Now, it seems to work. In case I have unsaved changes when I click the browser's back button. I get the confirmation dialog. However, when I input a new URL in the address bar, I don't get the confirmation. Also, I'm able to close the tab when I have unsaved changes.
Are these known limitations of canDeactivate, or am I doing something wrong. How do I get the behavior I want? (Confirmation dialog if the user attempts to close tab or navigate away using the address bar?)

Related

VurRouter transitions on more than one router

I prepared a boiled-down example on stackblitz:
https://stackblitz.com/edit/quasarframework-vy4eiw?file=README.md
The problem I try to resolve is this:
A quasar 2 app build with vite and vue 3 (and GSAP) uses layouts
Currently there are 2 layouts: StartpageLayout for the startpage at route ´/´and MainpageLayout for all the other pages at route ´/main´ and any children of it (/main/:child´`)
The MainpageLayout also contains the navigation menu
The navigation menu should be created (later on with an animation) when any route starting with ´/main´ is hit and destroyed, when there is a change to any other route
While navigating through any ´/main[/:child]´ route, the nav menu shall remain "stable" (not rebuild or anything like that)
The app uses 2 router-views for this, one in App.vue, one in MainLayout.vue. Changes between those states should mainly be handled in onBeforeRouteLeave and onBeforeRouteUpdate
To check, whether the app is in a "layout context", the routes have a meta.layoutKey, which is used in router guards to check, whether sth changed or not:
// Example: src/layouts/MainLayout.vue
onBeforeRouteUpdate((to, from, next) => {
console.log('%cMAIN_LAYOUT: onBeforeRouteUpdate invoked', consColRouter);
// compare meta.layoutKeys of routes
if (from.meta.layoutKey !== to.meta.layoutKey) {
console.warn(' YES, invoke router guard onBeforeRouteUpdate - animate!');
next() // this would be actually be called form an onComplete animation callback
} else {
console.log(' NOPE, do not invoke router guard onBeforeRouteUpdate');
next() // invoked as written
}
})
A pinia store manages state that (should) remember(s) activateMenu:
// Pinia store "pageTransitions.js" (composition API)
import { ref, reactive, computed } from 'vue'
import { defineStore } from 'pinia'
export const usePageTransitionsStore = defineStore('pageTransitions', () => {
// Pinia state in composition API
const pageTransitions = ref({
parent: false,
activateMenu: false
})
const setPageTransitions = (level, bool) => {
switch(level) {
case 'parent': {
pageTransitions.value.parent = bool
break
}
default: { console.log('default... must never happen!'); }
}
}
const setActivateMenu = (bool) => {
pageTransitions.value.activateMenu = bool
}
return {
pageTransitions,
setPageTransitions,
setActivateMenu
}
})
If store.pageTransitions.activateMenu is true, show the menu, if false, remove it. It is imported in MainLayout in order to use the activateMenu constant to manage the state of the nav menu. The onMount method sets this store variable to true. And it should be set to false in a ònBeforeRouteLeave`... (not yet implemented)
While the change from the startpage at ´/´to the MainPage at ´/main´ and vice versa works fine (even with animation, due to the store variable store.pageTransitions.parent), I keep having troubles with changes from ´/main´ to any child route ´/main/:child´ and vice versa. E.g. when the app is at /main and the user clicks on ´items 101´, the whole MainLayout is reloaded - also App.vue runs through its onAppear hooks again (see console) – and the nav is set to false again.
The goal is to not influence the MainLayout not its nested nav menu at all.
I wonder, why those reloads happen? MainLayout's onBeforeRoute checks against meta.layoutKey which does not change. But then I also observe that the pinia store gets loaded again, and the actiavteMenu var is set up false again...
Does anybody see my error(s)?

How to route to different components in Vue router based on query param

I'm using firebase for user authentication. Except for login/signup flows there are 3 flows called email actions - these include reset password, recover email and verify email.
You can customize the URL that Firebase is calling you, but it has the be the same URL for all actions, while the actual action is passed as query param. Here's an example:
https://example.com/usermgmt?mode=resetPassword&oobCode=ABC123&apiKey=AIzaSy...&lang=fr
// ^^^ from here on starts the dynamic part
My question - how can I use Vue router to load different component based on mode query param?
I know I can create a single component that will dynamically load one of the other 3 based on the query param, but I'm looking for a cleaner way.
I tried looking for "Vue router based on query params" and only found examples how to pass query param into the component as a prop.
Also looked for examples of implementation of Firebase authentication in Vue, but all of the examples focus just on login and signup.
EDIT:
Right now I have a router with this config:
{
path: '/auth/actions',
component: () => import('pages/EmailActions.vue'),
props: (route) => {
return {
mode: route.query.mode
}
}
}
Then, inside EmailActions I have to dynamically show one of 3 components based on mode props.
This file is really redundant as it only contains routing code.
What I'd really like to do is something like this in the router config:
{
path: '/auth/actions',
children:[
{
query: {mode: 'resetPassword'},
component: () => import ('pages/ResetPassword.vue')
},
{
query: {mode: 'recoverEmail'},
component: () => import ('pages/RecoverEmail.vue')
},
{
query: {mode: 'verifyEmail'},
component: () => import ('pages/VerifyEmail.vue')
}
]
}

Displaying sidebar items according to user role

I want to display the sidebar items of vx-sidebar according to user role. I'm using acl plugin. There are two users admin and editor. Both have different sidebar items. I have set the roles and everything.But everytime user login, the sidebar items doesn't render initially, I have to refresh the page then the sidebar items render according to role. I am using firebase firestore. And i am setting the initial role as the user.uid found in two different collections of admin and editor.
I have also set the user role in the local storage and vuex.store, but still the elements don't render on initial login.Please help me with this. I have spend days with solving this. But doesn't find solution
acl.js
import Vue from 'vue'
import { AclInstaller, AclCreate, AclRule } from 'vue-acl'
import router from '#/router'
Vue.use(AclInstaller)
let initialRole = localStorage.getItem('userRole')
console.log(initialRole)
export default new AclCreate({
initial: initialRole,
notfound: '/pages/not-authorized',
router,
acceptLocalRules: true,
globalRules: {
admin: new AclRule('admin').generate(),
editor: new AclRule('editor').generate(),
}
})
VxSidebarItem.vue
<div :class="[{'vs-sidebar-item-active':activeLink}, {'disabled-item pointer-events-none': isDisabled}]" class="vs-sidebar--item" v-if="canSee">
computed: {
canSee() {
this.$acl.check(localStorage.getItem('userRole'));
console.log(localStorage.getItem('userRole'))
if(this.to) {
return this.$acl.check(this.$router.match(this.to).meta.rule)
}
return true
}
},
router.js
{
path: '/',
redirect: '/dashboard/jobs'
},
{
path: '/dashboard/jobs',
name: 'Jobs',
component: () => import('./views/tpo/Jobs.vue'),
meta: {
rule:'admin',
}
},

Can't access Meteor.user() property

I've installed a Meteor phone authentication package mys:accounts-phone, which should add a phone.number subfield into users collection. I try to access this field as follows:
Meteor.user().phone.number
but typescript shows error
Property 'phone' does not exist on type 'User'.
On the other hand, I have custom props in users.profile, and can easily access them in this way.
Insecure is not yet removed. Autopublish is ON.
this happens sometime when our angular component is initialized but our meteor data is not reached from server.
try to use user injection in place of Meteor.user()
import {Component} from "#angular/core";
import { InjectUser } from 'angular2-meteor-accounts-ui';//<--**** import this****
#Component({
selector: "login-buttons",
template
})
#InjectUser('user') //<--*** add this***
export class LoginButtonsComponent {
user: Meteor.User; //<--*** add this ***
constructor(private router: Router) {}
}
now in user variable you will have all values of Meteor.User
if you want to print in html part use this
<div *ngIf="user">
{{user.phone.number}}
</div>
don't forget to install
meteor add accounts-password
meteor npm install --save angular2-meteor-accounts-ui
and in app.module.ts file
import { AccountsModule } from 'angular2-meteor-accounts-ui';
#NgModule({
imports: [
... other modules here
AccountsModule
],
hope this will work. if this not work let me know i will tell you one other solution
Got probable answer from Meteor docs.
It explains why username property appears. By default, Meteor publish only a number of fields considered to be public. To exposure any additional fields they must be published explicitly.
Not yet have time to test, but think it should work.
The other reason, with the same sympthoms, when publication code do not executed at server side. Try to put
Meteor.startup(() => {
// code to run on server at startup
Meteor.publish('userData', function() {
if(!this.userId) return null;
return Meteor.users.find(this.userId
//, {fields: {lastname: 1,}}
);
});
});
in a file within /server folder of your application.

Structuring a reducer for a simple CRUD application in redux

So I'm creating what is at it's core a very simple CRUD-style application, using React + Redux. There is a collection of (lets call them) posts, with an API, and I want to be able to list those and then when the user clicks on one, go into a detail page about that post.
So I have a posts reducer. Originally I started using the approach taken from the redux real-world example. This maintains a cache of objects via an index reducer, and when you do a "get post" it checks the cache and if it's there, it returns that, else it makes the appropriate API call. When components mount they try to get things from this cache, and if they're not there they wait (return false) until they are.
Whilst this worked OK, for various reasons I now need to make this non-caching i.e. everytime I load the /posts/:postId page I need to get the post via the API.
I realise in the non-redux world you would just do a fetch() in the componentDidMount, and then setState() on that. But I want the posts stored in a reducer as other parts of the app may call actions that modify those posts (say for example a websocket or just a complex redux-connected component).
One approach I've seen people use is an "active" item in their reducer, like this example: https://github.com/rajaraodv/react-redux-blog/blob/master/public/src/reducers/reducer_posts.js
Whilst this is OK, it necessitates that each component that loads the active post must have a componentWillUnmount action to reset the active post (see resetMe: https://github.com/rajaraodv/react-redux-blog/blob/master/public/src/containers/PostDetailsContainer.js). If it did not reset the active post, it will be left hanging around for when the next post is displayed (it will probably flash for a short time whilst the API call is made, but it's still not nice). Generally forcing every page that wants to look at a post to do a resetMe() in a componentWillUnmount fells like a bad-smell.
So does anyone have any ideas or seen a good example of this? It seems such a simple case, I'm a bit surprised I can't find any material on it.
How to do it depends on your already existing reducers, but i'll just make a new one
reducers/post.js
import { GET_ALL_POSTS } from './../actions/posts';
export default (state = {
posts: []
}, action) => {
switch (action.type) {
case GET_ALL_POSTS:
return Object.assign({}, state, { posts: action.posts });
default:
return state;
}
};
It is very easy to understand, just fire an action to get all your posts and replace your previous posts with the new ones in the reducer.
Use componentDidMount to fire the GET_ALL_POSTS action, and use a boolean flag in the state to know if the posts where loaded or not, so you don't reload them every single time, only when the component mounts.
components/posts.jsx
import React from 'react';
export default class Posts extends React.Component {
constructor(props) {
super(props);
this.state = {
firstLoad: false
};
}
componendDidMount() {
if (!this.state.firstLoad) {
this.props.onGetAll();
this.setState({
firstLoad: true
});
}
}
// See how easy it is to refresh the lists of posts
refresh() {
this.props.onGetAll();
}
render () {
...
// Render your posts here
{ this.props.posts.map( ... ) }
...
}
}
We're just missing the container to pass the posts and the events to the component
containers/posts.js
import { connect } from 'react-redux';
import { getPosts } from './../actions/posts';
import Posts from './../components/posts.jsx';
export default connect(
state => ({ posts: state.posts }),
dispatch => ({ onGetAll: () => dispatch(getPosts()) })
);
This is a very simple pattern and I've used it on many applications
If you use react-router you can take advantage of onEnter hook.

Resources