In my Vue3 app, I'm using the mitt eventbus library to emit and receive events between components.
I put this in onMounted of a list component that needs to refresh:
mitt.on("list_refresh", (evt) => {
refresh();
});
In another component that contains the list-component as a child (or grandchild), I do this in a method:
mitt.emit("list_refresh", {});
This works ok, but while developing with hot-reload on, the events seem to be emitted multiple times, as if they're created extra each time the app reloads, instead of overwriting the old ones.
When I reload the entire page in the browser, it works fine again.
Any idea to prevent this?
It looks like your component is missing a corresponding off() call to remove the event listener. During hot reload, the current component instances unmount, and new ones mount; so if you're not removing current event listeners, you'll just pile on new event listeners. To resolve the issue, use the onUnmounted hook to remove the event listener when the component is removed from the DOM.
Also, make sure to pass cached function references (instead of inline functions) to mitt.on() and mitt.off() to ensure the given event listener lookup succeeds in mitt.off():
// mitt.on('list_refresh', () => refresh()) ❌
mitt.on('list_refresh', refresh) ✅
mitt.off('list_refresh', refresh)
Your setup() should look similar to this:
import { onMounted, onUnmounted } from 'vue'
export default {
setup() {
const refresh = () => { /*...*/ }
onMounted(() => mitt.on('list_refresh', refresh))
onUnmounted(() => mitt.off('list_refresh', refresh)) 👈
}
}
I have a react-native app (without expo) called myapp.
I have a private custom package called myapp-core, where I handle AwsAmplify services (Auth, Storage) - to do login/signOut/etc.
I want to use myapp-core in myapp project, so I added it as a dependency in package.json ("myapp-core": "file:../myapp-core",) and then yarn install.
The problem I’m facing is that when I call myapp-core.authService.login(username, password) from the mobile project, I catch the error:
“ { “line”:177826, “column”: 17, “sourceURL”:
“http://10.0.2.2:8081/index.delta?platform=android&dev=true&minify=false”
} ”
From my research, that means my custom library cannot make api calls - but I don’t know exactly.
When I use aws-amplify's Auth object directly in my mobile project, it works.
Hopefully relevant code:
/**=============================**/
/** myapp/CoreServices.js **/
import { AmplifyService } from “myapp-core";
export default class CoreServices {
constructor() {
AmplifyService.configure();
const auth = AmplifyService.authService();
auth
.login(“myusername”, “mypassword”)
.then(user => console.warn("success", user))
.catch(error => console.warn("error", error));
}
}
/**=============================**/
/** myapp-core/AmplifySevice.js **/
import Amplify from 'aws-amplify';
import AuthService from '../AuthService/AuthService';
import awsConfigs from '../aws-exports';
class AmplifyService {
static authServiceInstance = null;
static storageServiceInstance = null;
static configure(config = awsConfigs) {
if (config === null || config === undefined) {
throw new Error('AmplifyService must be initialized with Auth and Storage configurations.');
}
Amplify.configure({
Auth: { /*...*/ },
Storage: { /*...*/ }
});
}
static authService() {
if (!this.authServiceInstance) {
this.authServiceInstance = new AuthService();
}
return this.authServiceInstance;
}
static storageService() {
console.warn('storage service');
// initialize storage service
// return storage service
}
}
I managed to solve my project's issue.
Maybe someone will benefit from my solution.
The problem didn't have anything to do with AwsAmplify, but with the way I linked the projects: myapp-core with myapp.
The issue was that in the myapp-core I am using the aws-amplify package that I would normally link to the mobile projects (react-native link) but in my case I assumed (wrongly) that it wouldn't be the case.
The solution was to link whatever packages were needed in the iOS/Android projects to install the proper pods/gradle libraries, like react-native link amazon-cognito-identity-js for authentication.
... and now I am finally happy :))
Links that shed some light:
https://github.com/facebook/create-react-app/issues/1492
https://eshlox.net/2018/11/12/aws-amplify-react-native-typeerror-cannot-read-property-computemodpow-of-undefined/
In case somebody thinks this isn't the solution and I got lucky or something, please comment or post another response.
I have a bit of a pickle.
I am using Route guard (implementing CanActivate interface) to check if user is granted access to particular route:
const routes: Routes = [
{
path: '',
component: DashboardViewComponent
},
{
path: 'login',
component: LoginViewComponent
},
{
path: 'protected/foo',
component: FooViewComponent,
data: {allowAccessTo: ['Administrator']},
canActivate: [RouteGuard]
},
{
path: '**',
component: ErrorNotFoundViewComponent
}
];
Now it works great in protecting the '/protected/foo' route from activating, but I would like to tell the user that route he is trying to access is forbidden (similar to 403 Forbidden you may get from server).
The problem:
How do I show the user this special error view without redirecting him to error route which seams to be the preferred option by so many sources I have found?
And how do I still use my RouteGuard without actually loading the forbidden route, because if I check access inside my FooViewComponent and display different view it kind of defeats point of having RouteGuard in the first place.
Ideally I would like to have my RouteGuard not only returning false in canActivate() method, but also replace component completely with say ErrorForbiddenViewComponent. But I have no idea how to do it, or is it event possible. Any alternatives?
This is how my route guard looks now:
import {Injectable} from '#angular/core';
import {Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '#angular/router';
import {AuthService} from '../services/auth.service';
#Injectable()
export class RouteGuard implements CanActivate {
constructor(
private router: Router,
private auth: AuthService
) {}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const { auth, router } = this;
const { allowAccessTo } = next.data;
const identity = auth.getIdentity();
if (
identity &&
allowAccessTo.indexOf(identity.role)
) {
// all good, proceed with activating route
return true;
}
if (identity) {
// TODO show ErrorForbiddenViewComponent instead of redirecting
console.log('403 Forbidden >>', next);
}
else {
// not logged in: redirect to login page with the return url
const [returnUrl, returnQueryParams] = state.url.split('?');
console.log('401 Unauthorised >>', returnUrl, returnQueryParams, next);
router.navigate(['/login'], {queryParams: {returnUrl, returnQueryParams}});
}
return false;
}
}
So I am just preventing route from loading, but I am not redirecting. I only redirect non logged visitors to login route.
Reasoning:
Routes should reflect certain state of application - visiting a route
url should recreate that state
To have error routes (except for 404 Not Found) would mean your application can actually recreate error states. This makes no sense
as why would you keep error state as state of your application? For
debugging purpose one should use logs (console or server), revisiting
error page (i.e. page refresh) might interfere with that.
Also by redirecting to error route app should provide some insights of error to user. For that matter either some parameter would need to
be passed via url or (far worse) keeping the error sate in some error
service and retrieve it upon accessing error route.
Also, ignoring the RouteGuard and just loading the component and checking access inside it may result in some extra dependencies
loaded which would not be used anyway (as user is not allowed), makes
the whole lazy loading much harder.
Does anyone have some kind of solution for this? I also wonder how come that after Angular 2+ being around for so long nobody had this kind of situation before? Everybody is just ok with redirecting?
Also keep in mind that although I am currently using the FooViewComponent synchronously, that may change in future!
I had once worked on the similar problem.
Sharing my stackblitz poc where I have created -
Authenticated Component (with guard)
Login Component
Permission Guard
Route (/auth route is provided with PermissionGuardService guard)
The guard is evaluating the user type and handling the redirection / error accordingly.
The use cases are -
User is not logged in (shows a toast with log in message)
User is not admin (shows a toast with unauthorised message)
User is admin (show a toast with success messaage)
I have stored the user in local storage.
EDIT - DEMO
Let me know if you need a special handling in it and I will update the code base.
Cheers!
After looking at angular2 example provided by Tarun Lalwani in comments of question and after taking deeper look into Dynamic component loader article on Angular docs I have managed to apply it to my code:
I no longer use my RouteGuard when specifying routes:
{
path: 'protected/foo',
component: FooViewComponent,
data: {allowAccessTo: ['Administrator']}, // admin only
canActivate: [RouteGuard]
},
Instead I have created special RouteGuardComponent and here is how I use it:
{
path: 'protected/foo',
component: RouteGuardComponent,
data: {component: FooViewComponent, allowAccessTo: ['Administrator']}
},
This is the code of RouteGuardComponent:
#Component({
selector: 'app-route-guard',
template: '<ng-template route-guard-bind-component></ng-template>
// note the use of special directive ^^
})
export class RouteGuardComponent implements OnInit {
#ViewChild(RouteGuardBindComponentDirective)
bindComponent: RouteGuardBindComponentDirective;
// ^^ and here we bind to that directive instance in template
constructor(
private auth: AuthService,
private route: ActivatedRoute,
private componentFactoryResolver: ComponentFactoryResolver
) {
}
ngOnInit() {
const {auth, route, componentFactoryResolver, bindComponent} = this;
const {component, allowAccessTo} = route.snapshot.data;
const identity = auth.getIdentity();
const hasAccess = identity && allowAccessTo.indexOf(identity.role);
const componentFactory = componentFactoryResolver.resolveComponentFactory(
hasAccess ?
component : // render component
ErrorForbiddenViewComponent // render Forbidden view
);
// finally use factory to create proper component
routeGuardBindComponentDirective
.viewContainerRef
.createComponent(componentFactory);
}
}
Also, this requires special directive to be defined (I am sure this can be done some other way, but I have just applied that Dynamic component example from Angular docs):
#Directive({
selector: '[route-guard-bind-component]'
})
export class RouteGuardBindComponentDirective {
constructor(public viewContainerRef: ViewContainerRef) {}
}
It isn't full answer to my own question (but its a start), so if somebody provides something better (i.e. a way to still use canActivate and ability to lazy load) I'll make sure to take that into account.
Your RouteGuard can inject whatever service you're using for modal windows, and the .canActivate() can pop the modal without redirection to inform the user without disturbing the current state of the app.
We use toastr and its angular wrapper for this, since it creates a modeless pop-up that self-dismisses after so-many seconds, no OK/Cancel buttons needed.
I've recently come across the same problem. In the end, I couldn't manage to do this using CanActivate guard, so I've implemented the authorisation logic in the component that holds the <router-outlet>.
Here is its template:
<div class="content">
<router-outlet *ngIf="(accessAllowed$ | async) else accessDenied"></router-outlet>
</div>
<ng-template #accessDenied>
<div class="message">
<mat-icon>lock</mat-icon>
<span>Access denied.</span>
</div>
</ng-template>
And its source code:
import { ActivatedRoute, ActivationStart, Router } from '#angular/router';
import { filter, switchMap, take } from 'rxjs/operators';
import { merge, Observable, of } from 'rxjs';
import { Component } from '#angular/core';
#Component({
selector: 'app-panel-content',
templateUrl: './content.component.html',
styleUrls: ['./content.component.scss'],
})
export class PanelContentComponent {
/**
* A stream of flags whether access to current route is permitted.
*/
accessAllowed$: Observable<boolean>;
constructor(
permissions: UserPermissionsProviderContract, // A service for accessing user permissions; implementation omitted
route: ActivatedRoute,
router: Router,
) {
const streams: Observable<boolean>[] = [];
/*
The main purpose of this component is to replace `<router-outlet>` with "Access denied"
message, if necessary. Such logic will be universal for all possible route components, and
doesn't require any additional components - you will always have at least one component with
`<router-outlet>`.
This component contains `<router-outlet>`, which by definition means that all possible authorisable
routes are beneath it in the hierarchy.
This implicates that we cannot listen to `route.data` observable of `ActivatedRoute`, because the route
itself in this component will always be the parent route of the one we need to process.
So the only real (the least hacky, IMO) solution to access data of child routes is to listen to
router events.
However, by the time an instance of this component is constructed, all routing events will have been
triggered. This is especially important in case user loads the page on this route.
To solve that, we can merge two streams, the first one of which will be a single access flag
for **activated route**, and the second will be a stream of flags, emitted from router
events (e.g. caused by user navigating through app).
This approach requires that the authorised route is bottom-most in the hierarchy, because otherwise the
last value emitted from the stream created from router events will be `true`.
*/
const deepestChild = this.findDeepestTreeNode(route);
const currentData = deepestChild.routeConfig.data;
// `data.authActions` is just an array of strings in my case
if (currentData &&
currentData.authActions &&
Array.isArray(currentData.authActions) &&
currentData.authActions.length > 0) {
streams.push(
// `hasPermissions(actions: strings[]): Observable<boolean>`
permissions.hasPermissions(currentData.authActions).pipe(take(1))
);
} else {
// If the route in question doesn't have any authorisation logic, simply allow access
streams.push(of(true));
}
streams.push(router.events
.pipe(
filter(e => e instanceof ActivationStart),
switchMap((event: ActivationStart) => {
const data = event.snapshot.data;
if (data.authActions &&
Array.isArray(currentData.authActions) &&
data.authActions.length > 0) {
return permissions.hasPermissions(data.authActions);
}
return of(true);
}),
));
this.accessAllowed$ = merge(...streams);
}
/**
* Returns the deepest node in a tree with specified root node, or the first
* encountered node if there are several on the lowest level.
*
* #param root The root node.
*/
findDeepestTreeNode<T extends TreeNodeLike>(root: T): T {
const findDeepest = (node: T, level = 1): [number, T] => {
if (node.children && node.children.length > 0) {
const found = node.children.map(child => findDeepest(child as T, level + 1));
found.sort((a, b) => a[0] - b[0]);
return found[0];
} else {
return [level, node];
}
};
return findDeepest(root)[1];
}
}
interface TreeNodeLike {
children?: TreeNodeLike[];
}
I've explained the approach in comments in the source code, but in short: access authorisation data in route.data using router events, and replace <router-outlet> with an error message if access is denied.
I initialised a weex and vue app using weex create awesome-project. In the entry.js file I am trying to register a global mixin but it does not seem to be registering. The code is is:
import weex from 'weex-vue-render'
import Vue from 'vue'
Vue.mixin({
computed: {
device () {
return 'mobile'
},
browser () {
return this.device === 'browser'
},
mobile () {
return this.device === 'mobile'
}
}
})
weex.init(Vue)
None of the component have this.mobile as true. Works fine in the web simulator but not when I scan the QR code on my phone.
FYI figured this out. Seems I just had a broken version of Weex! If you find yourself suffering from this problem, simply update Weex, init a new project and move all your code over. Worked like a charm for me.
I have an Aurelia project with navigation in app.html and app.js. The project includes a home page that has a different style to it, including navigation that is different than the non-home page views.
I would like to turn off navigation for the home view so I tried setting a variable (showMenu) to toggle the visibility. In fact, I am able to use jQuery to do this, but I wonder if there is an Aurelia way of doing it. If I set this.showMenu to true it shows the menu container, and false hides it. Like this for example:
app.html
<div class="container" if.bind="showMenu">
app.js
constructor(router){
this.router = router;
this.showMenu = true;
...other things
}
What I would like to do is set showMenu to false from home.js. I tried this (among 20 or so other attempts), but it does not work.
home.js
activate() {
this.showMenu = false;
}
Is there a way through $parent or some other means to hide the menu in app.html using a view model?
EDIT
This works but it feels a little like a hack.
home.js
import {inject} from 'aurelia-framework';
import {Router} from 'aurelia-router';
#inject(Router)
export class Home {
constructor(router) {
this.router = router;
}
attached(){
$("#navbarMenu").hide();
this.router.refreshNavigation();
}
}
You should be able to use router to achieve that. Since this is required for one page only, you can have something like this assuming your route name is home (or you could use other properties of RouteConfig that you have set in configureRouter):
<div class="container" if.bind="router.currentInstruction.config.name !== 'home'">
I approach this problem by using separate shells. By default Aurelia will start your app with app.js (or ts). But you can change that default and also use the same command to redirect to a new shell after authentication.
In your main.ts (or .js) you will have a line to start your aurelia app:
aurelia.start().then(() => aurelia.setRoot());
This line is telling aurelia to start and to set the root view model for your app, when aurelia.setRoot() has no value given it defaults to app.ts (or .js).
So I create a landing for my app where I can display with the page and styles I wish completely separately from the main app, including a limited router and navigation.
export function configure(aurelia: Aurelia) {
aurelia.use
.standardConfiguration()
if (environment.debug) {
aurelia.use.developmentLogging();
}
if (environment.testing) {
aurelia.use.plugin('aurelia-testing');
}
aurelia.start().then(() => aurelia.setRoot('authPage'));
}
authPage.ts is my usual app.ts with a router configuration but it will only have the authPage configured in it and perhaps one or two other welcome pages.
The authPage takes care of authentication and obtaining appropriate tokens. I use a 3rd party for authentication services so all I have on this page is a link. Either way after successful authentication is confirmed you now just want to redirect to an alternative aurelia shell.
#autoinject
export class AuthPage {
private app : Aurelia;
private router : Router;
constructor(router : Router, app: Aurelia) {
this.app = app;
this.router = router;
}
authenticate {
//some kind of authentication procedure...
if(authenticationSuccess) {
this.router.navigate('/', { replace: true, trigger: false});
this.router.reset();
this.router.("authenticatedApp");
}
}
The lines this.router.navigate('/', { replace: true, trigger: false}); and this.router.reset(); are provided to deal with issues mentioned here and also on SO here. The shell switch line this.router.("authenticatedApp"); doesn't work for me without the other two.
My authenticatedApp configures a full router and navigation menu for the user in just the same way as you would normally do with app.ts but now separated into its own shell.
Of course there is nothing to prevent someone linking straight to authenticatedApp but at this point there is no data displayed without an api call which all require an access token to be presented.
This is a useful link on building an Aurelia app with multiple shells for authentication.
The end result is a separated landing pages and application pages which can have different styles and different navigation.On logout you can do the same thing in reverse to reload the auth page.