Angular 5: Route animations for navigating to the same route, but different parameters - angular5

In my Angular 5 application, the user may navigate to a route which uses the same route, but with different parameters. For example, they may navigate from /page/1 to /page/2.
I want this navigation to trigger the routing animation, but it doesn't. How can I cause a router animation to happen between these two routes?
(I already understand that unlike most route changes, this navigation does not destroy and create a new PageComponent. It doesn't matter to me whether or not the solution changes this behavior.)
Here's a minimal app that reproduces my issue.

This is an old question but that's it if you're still searching.
Add this code to your app.Component.ts file.
import { Router, NavigationEnd } from '#angular/router';
constructor(private _Router: Router) { }
ngOnInit() {
this._Router.routeReuseStrategy.shouldReuseRoute = function(){
return false;
};
this._Router.events.subscribe((evt) => {
if (evt instanceof NavigationEnd) {
this._Router.navigated = false;
window.scrollTo(0, 0);
}
});
}
By using this code the page is going to refresh if you clicked on the same route no matter what is the parameter you added to the route.
I hope that helps.
Update
As angular 6 is released with core updates you don't need this punch of code anymore just add the following parameter to your routs import.
onSameUrlNavigation: 'reload'
This option value set to 'ignore' by default.
Example
#NgModule({
imports: [RouterModule.forRoot(routes, { onSameUrlNavigation: 'reload'})],
exports: [RouterModule]
})
Stay up to date and happy coding.

I ended up creating a custom RouteReuseStrategy which got the job done. It's heavily based on this answer.
export class CustomReuseStrategy implements RouteReuseStrategy {
storedRouteHandles = new Map<string, DetachedRouteHandle>();
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return false;
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
this.storedRouteHandles.set(route.routeConfig.path, handle);
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return false;
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
return this.storedRouteHandles.get(route.routeConfig.path);
}
// This is the important part! We reuse the route if
// the route *and its params* are the same.
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig &&
future.params.page === curr.params.page;
}
}
Check it out on StackBlitz!

Related

How to detect the user comes back to a page rather than starts browsing it?

I've got a grid component that I use in many routes in my app. I'd like to persist its state (ie. paging, search param) and restore it when the user comes back to the grid (ie. from editing a row). On the other hand, when the user starts a new flow (ie. by clicking a link) then the page is set to zero and web service is called with the default param.
How can I recognise the user does come back rather then starts a new flow?
When I was researching the problem I've come across the following solutions.
Unfortunatelly they didn't serve me
1/ using router scroll behaviour
scrollBehavior(to, from, savedPosition) {
to.meta.comeBack = savedPosition !== null;
}
It does tell me if the user comes back. Unfortunately the scroll behaviour runs after grid's created and mounted hooks are called. This way I have no place to put my code to restore the state.
2/ using url param
The grid's route would have an optional param. When the param is null then the code would know it's a new flow and set a new one using $router.replace routine. Then the user would go to editing, come back and the code would know they come back because the route param != null. The problem is that calling $router.replace re-creates the component (ie. calling hooks etc.). Additionally the optional param mixes up and confuses vue-router with other optional params in the route.
HISTORY COMPONENT
// component ...
// can error and only serves the purpose of an idea
data() {
return {
history: []
}
},
watch: {
fullRoute: function(){
this.history.push(this.fullRoute);
this.$emit('visited', this.visited);
}
},
computed: {
fullRoute: function(){
return this.$route.fullPath
},
visited: function() {
return this.history.slice(-1).includes(this.fullRoute)
}
}
the data way
save the information in the browser
// component ...
// can error and only serves the purpose of an idea
computed: {
gridData: {
get: function() {
return JSON.parse(local.storage.gridData)
},
set: function(dataObj){
local.storage.gridData = JSON.stringify(dataObj)
}
}
}
//...
use statemanagement
// component ...
// can error and only serves the purpose of an idea
computed: {
gridData: {
get: function() {
return this.$store.state.gridData || {}
},
set: function(dataObj){
this.$store.dispatch("saveGrid", gridData)
}
}
}
//...
use globals
// component ...
// can error and only serves the purpose of an idea
computed: {
gridData: {
get: function() {
return window.gridData || {}
},
set: function(dataObj){
window.gridData = dataObj
}
}
}

Vuex-module-decorator, modifying state inside an action

Using the vuex-module-decorator I have a authenticate action that should mutate the state.
#Action
public authenticate(email: string, password: string): Promise<Principal> {
this.principal = null;
return authenticator
.authenticate(email, password)
.then(auth => {
const principal = new Principal(auth.username);
this.context.commit('setPrincipal', principal);
return principal;
})
.catch(error => {
this.context.commit('setError', error);
return error;
});
}
// mutations for error and principal
But this fail with the following message:
Unhandled promise rejection Error: "ERR_ACTION_ACCESS_UNDEFINED: Are you trying to access this.someMutation() or this.someGetter inside an #Action?
That works only in dynamic modules.
If not dynamic use this.context.commit("mutationName", payload) and this.context.getters["getterName"]
What I don't understand is that it works well with #MutationAction and async. However I miss the return type Promise<Principal>.
#MutationAction
public async authenticate(email: string, password: string) {
this.principal = null;
try {
const auth = await authenticator.authenticate(email, password);
return { principal: new Principal(auth.username), error: null };
} catch (ex) {
const error = ex as Error;
return { principal: null, error };
}
}
--
At this time I feel blocked and would like to have some help to implement an #Action that can mutate the state and return a specific type in a Promise.
Just add rawError option to the annotation so it becomes
#Action({rawError: true})
And it display error normally. this is because the the library "vuex-module-decorators" wrap error so by doing this you will able to get a RawError that you can work with
You can vote down this answer if you would like because it isn't answering the specific question being posed. Instead, I am going to suggest that if you are using typescript, then don't use vuex. I have spent the past month trying to learn vue /vuex and typescript. The one thing I am committed to is using typescript because I am a firm believer in the benefits of using typescript. I will never use raw javascript again.
If somebody would have told me to not use vuex from the beginning, I would have saved myself 3 of the past 4 weeks. So I am here to try and share that insight with others.
The key is Vue 3's new ref implementation. It is what really changes the game for vuex and typescript. It allows us to not have to rely on vuex to automatically wrap state in a reactive. Instead, we can do that ourselves with the ref construct in vue 3. Here is a small example from my app that uses ref and a typescript class where I was expecting to use vuex in the past.
NOTE1: the one thing you lose when using this approach is vuex dev tools.
NOTE2: I might be biased as I am ported 25,000 lines of typescript (with 7000 unit tests) from Knockout.js to Vue. Knockout.js was all about providing Observables (Vue's ref) and binding. Looking back, it was kind of ahead of its time, but it didn't get the following and support.
Ok, lets create a vuex module class that doesn't use vuex. Put this in appStore.ts. To simplify it will just include the user info and the id of the club the user is logged into. A user can switch clubs so there is an action to do that.
export class AppClass {
public loaded: Ref<boolean>;
public userId: Ref<number>;
public userFirstName: Ref<string>;
public userLastName: Ref<string>;
// Getters are computed if you want to use them in components
public userName: Ref<string>;
constructor() {
this.loaded = ref(false);
initializeFromServer()
.then(info: SomeTypeWithSettingsFromServer) => {
this.userId = ref(info.userId);
this.userFirstName = ref(info.userFirstName);
this.userLastName = ref(info.userLastName);
this.userName = computed<string>(() =>
return this.userFirstName.value + ' ' + this.userLastName.value;
}
}
.catch(/* do some error handling here */);
}
private initializeFromServer(): Promise<SomeTypeWithSettingsFromServer> {
return axios.get('url').then((response) => response.data);
}
// This is a getter that you don't need to be reactive
public fullName(): string {
return this.userFirstName.value + ' ' + this.userLastName.value;
}
public switchToClub(clubId: number): Promise<any> {
return axios.post('switch url')
.then((data: clubInfo) => {
// do some processing here
}
.catch(// do some error handling here);
}
}
export appModule = new AppClass();
Then when you want to access appModule anywhere, you end up doing this:
import { appModule } from 'AppStore';
...
if (appModule.loaded.value) {
const userName = appModule.fullName();
}
or in a compositionApi based component. This is what would replace mapActions etc.
<script lang="ts">
import { defineComponent } from '#vue/composition-api';
import { appModule } from '#/store/appStore';
import footer from './footer/footer.vue';
export default defineComponent({
name: 'App',
components: { sfooter: footer },
props: {},
setup() {
return { ...appModule }
}
});
</script>
and now you can use userId, userFirstName, userName etc in your template.
Hope that helps.
I just added the computed getter. I need to test if that is really needed. It might not be needed because you might be able to just reference fullName() in your template and since fullName() references the .value variables of the other refs, fullName might become a reference itself. But I have to check that out first.
I sugest this simple solution, work fine for me 👌:
// In SomeClassComponent.vue
import { getModule } from "vuex-module-decorators";
import YourModule from "#/store/YourModule";
someMethod() {
const moduleStore = getModule(YourModule, this.$store);
moduleStore.someAction();
}
If the action has parameters, put them.
Taken from: https://github.com/championswimmer/vuex-module-decorators/issues/86#issuecomment-464027359

Screen Freezing when Setting Multiple Roots

When my aurelia app starts I send them first to the login page and check and see if they are logged in and if so, set set the root to app, otherwise, have them log in.
aurelia.start().then(() => aurelia.setRoot(PLATFORM.moduleName("modules/login")));
This should work, according to everything I could find. It does actually set the root to as far as the code is concerned as I see activity in the console, but the html on the screen never moves from the login screen. Even typing something manually in the address bar does not change the html. So it seems the router has stopped functioning. No errors are logged in the console.
import { AuthenticateStep, AuthService } from 'aurelia-authentication';
import { Router} from 'aurelia-router';
import { autoinject, PLATFORM, Aurelia } from "aurelia-framework";
#autoinject()
export class Login {
constructor(private router: Router, private authService: AuthService, private aurelia:Aurelia) {
console.log("Starting Login...")
}
activate() {
if (this.authService.authenticated) {
console.log("is authenticate")
this.router.navigate('/', { replace: true, trigger: false });
console.log("setting root to 'app'");
this.aurelia.setRoot(PLATFORM.moduleName("app"));
}
else {
console.log("not auth");
}
}
}
In app.ts
activate() {
console.log("app.activate");
...
}
Is there something else I should be doing?
I have tried: https://github.com/aurelia/framework/issues/400
And this: https://ilikekillnerds.com/2017/07/aurelia-routing-switching-root-using-setroot/
Here are a few things you can try:
Chain the promises (make sure the navigation is done before you tell aurelia to switch the root)
this.router.navigate('/', { replace: true, trigger: false })
then(() => this.aurelia.setRoot(PLATFORM.moduleName("app")));
Resolve the promises (would be necessary if the router still has work to do after the current activate because that work would need to be aborted)
return this.router.navigate('/', { replace: true, trigger: false })
then(() => this.aurelia.setRoot(PLATFORM.moduleName("app")));
Verify that the AppRouter is reconfigured after you switch root (breakpoint in configureRouter, you may need to manually .reset() the router is the isConfigured flag is somehow still true)
You could try a different approach altogether.
Personally when I need to switch root between a public and an authenticated shell, I just have a dedicated path prefix for either (or both) and in my main method I set the root to the correct App based on the current window.location.
Example (in main):
if (/\/public/.test(window.location.pathname)) {
au.setRoot(PLATFORM.moduleName("shell/public"));
} else if ((/\/admin/.test(window.location.pathname)) {
au.setRoot(PLATFORM.moduleName("shell/admin"));
} else {
au.setRoot(PLATFORM.moduleName("shell/app"));
}
Redirecting between these roots goes outside of the router, simply with window.location.href = "...";
Although it's arguably a little hacky, the nice thing about the approach is that you'll always have a completely clean Aurelia state after switching, and thus less you potentially need to clean up after.
In the non-public roots, you try to grab the auth token from localStorage and simply kick the user back to public if there is none (or they don't have sufficient privileges).
Setting the root is quite easy, but there is a caveat:
Set it either in response to a user generated event, or in the attached event.
Attempting to set it in the activated event or constructor will result in the screen becoming frozen on the root screen.
This took me pretty much a day to figure out, so I thought I would pass it on.
Here is what worked for me: I created a "app-shell" which is set to the root by main.
In app-shell, I check whether or not the person is already logged in, and then I set the root depending on the results.
import { AuthenticateStep, AuthService } from 'aurelia-authentication';
import { AppRouter } from 'aurelia-router';
import { autoinject, PLATFORM, Aurelia } from "aurelia-framework";
#autoinject()
export class AppShell {
constructor(private router: AppRouter, private authService: AuthService, private aurelia: Aurelia) {
}
attached() {
this.setRoot();
}
setRoot() {
this.router.navigate('/', { replace: true, trigger: false }); //Not actually needed here, but is if the router has already been configured.
if (this.authService.authenticated) {
this.aurelia.setRoot(PLATFORM.moduleName("app"));
}
else {
this.aurelia.setRoot(PLATFORM.moduleName("modules/login"));
}
}
}

Angular 2 - Use ComponentResolver to load component downloaded at runtime

I'm trying to load a component dynamically in my Angular2 app.
By dynamically I don't just mean to insert into the DOM a component that is missing from the parent component's template; I mean to download from the server another component, because the User might not ever need it, and then insert it into the DOM.
So I followed this to understand how to load the component with Systemjs and now I see that, in RC4, we need to use this and ComponentResolver to inject things into the DOM.
So here is my code for the module-wrapper component that should put things together:
export class ModuleWrapperComponent implements OnInit {
#ViewChild("module", { read: ViewContainerRef }) target: ViewContainerRef;
#Input() isOpen: boolean;
#Input() moduleId: number;
type: string = null;
moduleRef: ComponentRef<any> = null;
private isViewInitialized: boolean = false;
constructor(private resolver: ComponentResolver) { }
private _loadModule(moduleId: number): Promise<string> {
if (!!this.type)
return Promise.resolve(this.type);
var mod = {
component: "DashboardComponent",
path: "/app/modules/dashboard.component"
};
return System
.import(mod.path)
.then(() => this.type = mod.component);
}
updateComponent() {
if (!this.isViewInitialized)
return;
if (!!this.moduleRef)
this.moduleRef.destroy();
if (this.isOpen) {
this._loadModule(this.moduleId)
.then(type => {
this.resolver.resolveComponent(type)
.then(factory => {
this.moduleRef = this.target.createComponent(factory)
// to access the created instance use
// this.compRef.instance.someProperty = 'someValue';
// this.compRef.instance.someOutput.subscribe(val => doSomething());
});
})
}
}
ngOnChanges() {
this.updateComponent();
}
ngAfterViewInit() {
this.isViewInitialized = true;
this.updateComponent();
}
ngOnDestroy() {
if (!!this.moduleRef)
this.moduleRef.destroy();
}
}
Sadly, I get this error from ComponentResolver:
Error: Cannot resolve component using 'DashboardComponent'. at new BaseException$1 (http://localhost:63815/lib/#angular/compiler//bundles/compiler.umd.js:971:27)
I reckon that loading the module in Systemjs is not enough...so how do I tell ComponentResolver that I've downloaded a new component? The official documentation is still young and lacking...
On a side note, say that my Dashboard component loads additional stuff with imports...Systemjs will handle all that automatically, won't it?
Thanks in advance!
This does not exactly answer your question.
BUT,
We had a similar requirement and were loading components dynamically using SystemJs.
It was all working fine until we started thinking about build and deployment strategies and came across Webpack
Since the value of moduleId is only known at runtime, so webpack will not be able to resolve the actual module components at compile time and will not bundle those files.
You will need to have static imports for your dynamic module components somewhere in your code if you want them to be bundled as part of your single app.js

IE not calling lifecycle hooks or filling #Input until change detected?

I'm using beta.0 because this outstanding bug prevents angular 2 from working in IE in beta.1 and beta.2.
Relevant code from SearchBar.ts
#Component({
selector : 'search-bar',
templateUrl: 'views/searchbar.html'
})
export class SearchBar {
private history: SearchHistoryEntry[] = [];
#Output() onHistory = new EventEmitter();
constructor() {
this.history = JSON.parse(localStorage.getItem('SearchHistory')) || [];
}
ngOnInit() {
// The constructor doesn't have #Outputs initialized yet. Emit from this
// life cycle hook instead to be sure they're received on the other side
debugger;
this.onHistory.emit(this.history);
}
}
Relevant code from home.html
<search-bar (onHistory)="SearchBarHistory($event)"></search-bar>
Relevant code from home.ts
SearchBarHistory(history: SearchHistoryEntry[]) {
debugger;
this.history = history;
}
In Chrome this works just fine. The SearchBar's constructor correctly reads from localStorage, in ngOnInit it emits to my Home component who receives it, it's stored locally and the UI bindings tied to history update to show the information as it all should.
In IE 11 this does not work. ngOnInit won't run until I click inside my search bar. It seems that any #Input or lifecycle hook (specifically I've tested ngOnInit, ngAfterContentInit, and ngAfterViewInit and they all behave the same) doesn't run until the component's change detection is triggered. If I refresh the page then it runs exactly like Chrome where no interaction is required for #Inputs or lifecycle hooks to be called and my history goes through and gets bound like it should.
I think this is a bug of the beta but in the mean time is there anything I can do to make it work the first time without an interaction or page refresh?
I am having the same issue I tried it to resolve by forcing detectChanges like:
import {Injectable,ApplicationRef, NgZone} from '#angular/core';
#Injectable()
export class IeHackService {
constructor(
private _appRef: ApplicationRef,
private _zone: NgZone) {}
private isIe() {
let ua = window.navigator.userAgent;
let msie = ua.indexOf('MSIE ');
if (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./)) // If Internet Explorer, return version number
return true;
return false;
}
onComponentLoadInIe() {
if (this.isIe()) {
this._zone.run(() => setTimeout(() => this._appRef.tick(), 5));
}
}
}
Then in Every Route component that uses Lifecycle Hooks I called
constructor(private dialogService: ModalDialogService,ieHackService: IeHackService) {
ieHackService.onComponentLoadInIe();
}
I had this issue as well, I used a workaround to automatically refresh the page if the bug occurs, hoping that the bug will eventually be solved.
It's very ugly, but for now it works at least.
declare var Modernizr: any;
#Component({
selector : 'search-bar',
templateUrl: 'views/searchbar.html'
})
export class SearchBar {
private history: SearchHistoryEntry[] = [];
#Output() onHistory = new EventEmitter();
ie11hack: boolean = true;
constructor() {
this.history = JSON.parse(localStorage.getItem('SearchHistory')) || [];
if (!Modernizr.es6collections || navigator.userAgent.toLowerCase().indexOf("safari") !== -1) {
setTimeout(() => {
if (this.ie11hack) {
window.location.reload();
}
}, 500);
}
}
ngOnInit() {
ie11hack = false;
// The constructor doesn't have #Outputs initialized yet. Emit from this
// life cycle hook instead to be sure they're received on the other side
debugger;
this.onHistory.emit(this.history);
}
}
Edit, a less ugly partial fix:
The issues is (I think) caused by using a direct url rather then using the angular router (javascript).
If you want your website to work if people enter their url manually then you still need the above hack, but otherwise you may do what I did below (you may want to do that anyway).
I changed this:
<!-- html -->
<a href="#/objects/objectthingy/{{myObject.id}}" class="my-object-class">
To this:
<!-- html -->
<a href="javascript:void(0)" (click)="openMyObject(myObject.id)" class="my-object-class">
// typescript
openMyObject(objectId: number) {
this.router.navigate(['/Objects', 'ObjectThingy', { id: objectId}]);
}
and ngAfterViewInit method was called again.