Ionic2 - Alternate way to route without using injected component? - ionic-framework

I am building a mobile app and I'd like the user to have the ability to set their starting page via a settings-page. The idea is that the user can select a page from a list of options, the setting gets stored to local-storage and later, when the user logs back in, the user is automatically taken to that page first.
I have a page-service which contains a mapping of Id's to page-components. This is what I use to find the page I want to use when I read in my user's saved start-page data.
My issue is that I have developed a cyclic-dependency that I don't think I can break without finding a way to route in Ionic2 that doesn't involve using the injected component. As far as I can tell, the only way routing is achieved in Ionic2 is with the NavController.push(component) or Nav.setRoot(component).
PageService.ts
import {Injectable} from "#angular/core";
import {HomePage} from "../pages/home/home";
import {SettingsPage} from "../pages/settings/settings";
import {CartPage} from "../pages/cart/cart";
#Injectable()
export class PageService {
public pages = [
{
id: "HOME",
component: HomePage
}, {
id: "SETTINGS",
component: SettingsPage
}, {
id: "CART",
component: CartPage
}
];
constructor() {
}
getPageById(id: string) {
return this.pages.find(page => (page.id === id));
}
}
settings.ts:
My SettingsPage component has the PageService injected so that it can get access to get the list of pages. This is where my cyclical dependency occurs. The SettingsPage is injecting PageService which has a reference to SettingsPage in it.
import {Component} from "#angular/core";
import {PageService} from "../../providers/page-service";
import {UserService} from "../../providers/user-service";
#Component({
selector: "page-settings",
templateUrl: "settings.html",
})
export class SettingsPage {
startPages = [];
constructor(private pageService: PageService, private userService: UserService) {
this.startPages = this.pageService.getStartPages();
}
}
settings.html:
Just a simple list with a card to output the selection.
<ion-content padding>
<ion-list>
<ion-card padding>
<ion-card-title>Starting Page</ion-card-title>
<ion-item>
<ion-select [(ngModel)]="userService.activeUser.startPage">
<ion-option *ngFor="let page of startPages" value="{{page.id}}">
{{page.id}}
</ion-option>
</ion-select>
</ion-item>
</ion-card>
</ion-list>
</ion-content>
...and finally, when the app starts up and I want to automatically go to my start page I execute the following:
const startPage = this.pageService.getPageById(this.userService.activeUser.startPage);
this.nav.setRoot(startPage.component);

Updated answer
Use forward ref in the SettingsPage constructor..
constructor(#Inject(forwardRef(() => PageService)) private pageService: PageService) {
this.startPages = this.pageService.getStartPages();
}
Old Answer - not appropriate because angular should be handling the instantiation of services through injection. "new"ing is a bad idea (but it did work).
I changed how the PageService was loaded in the SettingsPage and the cyclical dependency was resolved. I moved the PageService code out of the constructor and put it into the ngAfterViewInit() function. Now the PageService is only instantiated when the view is loaded.
ngAfterViewInit(){
this.pageService = new PageService();
this.startPages = this.pageService.getStartPages();
}

Related

Ionic 4 Scroll Position in Service/Guard

I am trying to implement a feature similar to whats available in Facebook i.e. if you have scrolled the news feed, pressing hardware back button takes you to the top of the list.
For this I think believe canDeactivate of Router Guards would be the proper ways.
But I am unable to find a way to check if the page has been scrolled or not.
I have tried window.pageYOffset but this always returns 0, accessing ViewChild within a Guard always returns null.
Can anyone please guide how to achieve this?
There are two approaches for this that should help you.
First, starting with Ionic 4, you can register you back button handler using the Platform features:
https://www.freakyjolly.com/ionic-4-overridden-back-press-event-and-show-exit-confirm-on-application-close/
this.platform.backButton.subscribeWithPriority(999990, () => {
//alert("back pressed");
});
Secondly, you can use more features of Ionic 4 called scrollEvents.
I have explained how to use this feature in other answers:
How to detect if ion-content has a scrollbar?
How to detect scroll reached end in ion-content component of Ionic 4?
ionic 4 - scroll to an x,y coordinate on my webView using typeScript
Hopefully that will get you moving in the right direction.
I think that last answer should solve most of your issue, so something like this:
Freaky Jolly has a tutorial explaining how to scroll to an X/Y coord.
First, you need scrollEvents on the ion-content:
<ion-header>
<ion-toolbar>
<ion-title>
Ion Content Scroll
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [scrollEvents]="true">
<!-- your content in here -->
</ion-content>
In the code you need to use a #ViewChild to get a code reference to the ion-content then you can use its ScrollToPoint() api:
import { Component, ViewChild } from '#angular/core';
import { Platform, IonContent } from '#ionic/angular';
#Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
// This property will save the callback which we can unsubscribe when we leave this view
public unsubscribeBackEvent: any;
#ViewChild(IonContent) content: IonContent;
constructor(
private platform: Platform
) { }
//Called when view is loaded as ionViewDidLoad() removed from Ionic v4
ngOnInit(){
this.initializeBackButtonCustomHandler();
}
//Called when view is left
ionViewWillLeave() {
// Unregister the custom back button action for this page
this.unsubscribeBackEvent && this.unsubscribeBackEvent();
}
initializeBackButtonCustomHandler(): void {
this.unsubscribeBackEvent = this.platform.backButton.subscribeWithPriority(999999, () => {
this.content.scrollToPoint(0,0,1500);
});
/* here priority 101 will be greater then 100
if we have registerBackButtonAction in app.component.ts */
}
}

How to hide header on scroll in ionic 4?

I wanted to know how I can hide a header in Ionic 4 by scrolling down the page, and re-show it when scrolling up.
I found many solutions on how to do that, but they all turned out to not working or being out-of-date.
So I collected all piece of information I could find to provide this answer.
Thanks to this video I got it to work.
First of all call ionic g directive directives/hide-header. You can of course replace directive/hide-header with your own path and name.
hide-header.directive.ts
import { Directive, HostListener, Input, OnInit, Renderer2 } from '#angular/core';
import { DomController } from '#ionic/angular';
#Directive({
selector: '[appHideHeader]'
})
export class HideHeaderDirective implements OnInit {
#Input('header') header: any;
private lastY = 0;
constructor(
private renderer: Renderer2,
private domCtrl: DomController
) { }
ngOnInit(): void {
this.header = this.header.el;
this.domCtrl.write(() => {
this.renderer.setStyle(this.header, 'transition', 'margin-top 700ms');
});
}
#HostListener('ionScroll', ['$event']) onContentScroll($event: any) {
if ($event.detail.scrollTop > this.lastY) {
this.domCtrl.write(() => {
this.renderer.setStyle(this.header, 'margin-top', `-${ this.header.clientHeight }px`);
});
} else {
this.domCtrl.write(() => {
this.renderer.setStyle(this.header, 'margin-top', '0');
});
}
this.lastY = $event.detail.scrollTop;
}
}
After that, in your template:
<ion-header #header>
<ion-toolbar><ion-title>Test</ion-title></ion-toolbar>
</ion-header>
<ion-content scrollEvents="true" appHideHeader [header]="header">
</ion-content>
Take care of the scrollEvents, appHideHeader and the [header] attributes! The last one takes the header element as argument, in this case #header.
Most of the code is the same as shown in the video. I changed the host-property from the #Directive and used the more up-to-date HostListener.
If you want to use the directive in more than one directive, you need to create a SharedModule.
To do so, create the module with ng g module shared. After that, add the HideHeaderDirective to the declarations and the exports array.
shared.module.ts
import { NgModule } from '#angular/core';
import { CommonModule } from '#angular/common';
import { HideHeaderDirective } from './directives/hide-header.directive';
#NgModule({
declarations: [HideHeaderDirective],
exports: [HideHeaderDirective],
imports: [
CommonModule
]
})
export class SharedModule {}
Now add the shared module to all the modules you want to use the directive in.
Note: You cannot import the directive in app.module.ts and use it in a submodule! You have to import the shared module in every direct module you want to use the directive in.
My current versions of node, npm and ionic:
For this you can just place the ion-header before the ion-content. this is the simple answer for that.

How to open a modal component from inside of another modal component without having a circular dependency?

I have two ngx-bootstrap modals created as a standalone components (not with template variables) - Login modal and Register modal. Each of the modals are have separate components which are located in my shared module and can be called from other modules. But the thing is that there is an option these modals to call each other - you can click a button from the login modal which has to bring you the Register modal and vice versa. When I try doing this using the BsModalService I get circular dependency errors since I have imported the login component in the register component and the register component in the login component.
I've tried to put this modal switching logic in a service with the hope that I won't get a circular dependency but it didn't help.
import { Component, OnInit } from '#angular/core';
import { FormGroup, FormBuilder, Validators } from '#angular/forms';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { UserService } from 'src/app/core/services';
import { User } from 'src/app/core';
import { RegisterModalComponent } from '../register-modal/register-modal.component';
#Component({
selector: 'app-login-modal',
templateUrl: './login-modal.component.html',
styleUrls: ['./login-modal.component.css']
})
export class LoginModalComponent implements OnInit {
loginForm: FormGroup = this.fb.group({
// form definition
});
constructor(
public loginModalRef: BsModalRef,
private fb: FormBuilder,
private router: Router,
private user: UserService,
private modalService: BsModalService
) { }
ngOnInit() {
}
onSubmit() {
// form submit code ...
// hide the current modal
this.loginModalRef.hide();
}
openRegisterModal() {
// hide the current modal
this.loginModalRef.hide();
// open the new modal
this.modalService.show(RegisterModalComponent, {
animated: true,
class: 'modal-lg'
});
}
}
I have included only the code from the login modal since the situation on the other side is similar.
Just to mention that as a temporary solution I just made one modal component to serve the purpose as modal and I refactored the login and the register components to be like a regular components so I can include them inside the modal and switch them with ngIf depending on the parameters that I'm calling the modal with.

Detect a click outside of an element

There is some components in Ionic that do not provide an event that is emitted when focus is lost.
For example ion-input provides ionBlur. On the other hand there is other elements like ion-content where I need to detect an outside click, but without knowing which event to use.
Is there a way to achieve that without being limited to the proposed events in the documentation?
I found this article that shows a way to use a custom directive to detect an outside click:
import {Directive, ElementRef, Output, EventEmitter, HostListener} from '#angular/core';
#Directive({
selector: '[clickOutside]'
})
export class ClickOutsideDirective {
constructor(private _elementRef : ElementRef) {
}
#Output()
public clickOutside = new EventEmitter();
#HostListener('document:click', ['$event.target'])
public onClick(targetElement) {
const clickedInside = this._elementRef.nativeElement.contains(targetElement);
if (!clickedInside) {
this.clickOutside.emit(null);
}
}
}
The directive can then be used this way, after declaring it in the concerned module:
<!-- HTML Template -->
<ion-content (clickOutside)="handleOutsideClick()"><!-- ... --></ion-content>
<!-- Typescript code -->
handleOutsideClick() {
//Handle My outside Click
}
Yeah, It's been 7 months since asked.
Stucked with the same issue; this solved the issue
TS
#ViewChild('content') content: ElementRef
#HostListener('document:click', ['$event'])
andClickEvent(event) {
if (!this.content.nativeElement.contains(event.target)) {
if (!this.navCtrl.isTransitioning() && this.navCtrl.getActive()) {
this.close()
}
}
}
HTML
<ion-content #content>

Ionic Iframe continuesly reload while typing

I created a page with an iframe. The url that the iframe will render has some input fields. When I type something, it reload all the page and I can do nothing.
View
<ion-content no-padding>
<iframe [src]="urlpaste()"></iframe>
</ion-content>
Controller
import { Component } from '#angular/core';
import { DomSanitizer } from '#angular/platform-browser';
#Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
my_url: any;
constructor(private sanitize: DomSanitizer) {}
urlpaste(){
this.my_url = "http://example.com/";
return this.sanitize.bypassSecurityTrustResourceUrl(this.my_url);
}
}
It looks like this may have to do with the resize event(s) generated from clicking into the input field, in which case a zoom in happens or the on-screen keyboard pushes the form up. See this:
Ionic 2 Form goes up when keyboard shows