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>
Related
I'm learning Angular 9 and have gone through the Tour of Heroes app and tutorial. I've used this tutorial as a base to add new features such as CRUD operations on a remote resource and I have added #ng-bootstrap/ng-bootstrap to the projects but I cannot get angular-bootstrap-toggle to work.
The instructions on Bootstrap Toggle don't match what I have learned so far and I can't find a solution anywhere.
For example, I don't know how this command angular.module('myApp', ['ui.toggle']); fits in with Angular 9 and the tutorial I have used.
How can I get the system to call onChange() when I click the toggle?
angular.json
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/bootstrap4-toggle/css/bootstrap4-toggle.min.css",
"src/styles.css"
],
"scripts": [
"node_modules/jquery/dist/jquery.min.js",
"node_modules/bootstrap/dist/js/bootstrap.bundle.js",
"node_modules/bootstrap4-toggle/js/bootstrap4-toggle.min.js"
]
navigation-bar.component.html This displays correctly and toggles as expected
<input id="local-browse" (change)="onChange()" type="checkbox" checked="" data-toggle="toggle" data-on="Local" data-off="Remote" data-onstyle="success" data-offstyle="danger" data-size="sm">
navigation-bar.component.ts If I put in a standard checkbox the onChange() does work as expected but not with angular-bootstrap-toggle
import { Component, OnInit } from '#angular/core';
declare var $: any;
#Component({
selector: 'app-navigation-bar',
templateUrl: './navigation-bar.component.html',
styleUrls: ['./navigation-bar.component.css'],
})
export class NavigationBarComponent implements OnInit {
localUrl = 'http://192.168.253.53';
remoteUrl = 'https://remoteaddress.com';
local = true;
constructor() {}
ngOnInit(): void {
$(document).ready(() => {
console.log('The document ready for jQuery!');
});
}
onChange() {
if (this.local === true) {
this.local = false;
} else {
this.local = true;
}
}
}
I dont think it is compatible with Angular 2+ (or at least angular 9).
You may like to use ng-toggle from https://www.npmjs.com/package/#nth-cloud/ng-toggle
Which is tested on Angular 9.
More information about installation in https://nth-cloud.github.io/ng-toggle/#/home
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 */
}
}
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.
I'm trying to create a custom form control in Angular (v5). The custom control is essentially a wrapper around an Angular Material component, but with some extra stuff going on.
I've read various tutorials on implementing ControlValueAccessor, but I can't find anything that accounts for writing a component to wrap an existing component.
Ideally, I want a custom component that displays the Angular Material component (with some extra bindings and stuff going on), but to be able to pass in validation from the parent form (e.g. required) and have the Angular Material components handle that.
Example:
Outer component, containing a form and using custom component
<form [formGroup]="myForm">
<div formArrayName="things">
<div *ngFor="let thing of things; let i = index;">
<app-my-custom-control [formControlName]="i"></app-my-custom-control>
</div>
</div>
</form>
Custom component template
Essentially my custom form component just wraps an Angular Material drop-down with autocomplete. I could do this without creating a custom component, but it seems to make sense to do it this way as all the code for handling filtering etc. can live within that component class, rather than being in the container class (which doesn't need to care about the implementation of this).
<mat-form-field>
<input matInput placeholder="Thing" aria-label="Thing" [matAutocomplete]="thingInput">
<mat-autocomplete #thingInput="matAutocomplete">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{ option }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
So, on the input changing, that value should be used as the form value.
Things I've tried
I've tried a few ways of doing this, all with their own pitfalls:
Simple event binding
Bind to keyup and blur events on the input, and then notify the parent of the change (i.e. call the function that Angular passes into registerOnChange as part of implementing ControlValueAccessor).
That sort of works, but on selecting a value from the dropdown it seems the change events don't fire and you end up in an inconsistent state.
It also doesn't account for validation (e.g. if it's "required", when a value isn;t set the form control will correctly be invalid, but the Angular Material component won't show as such).
Nested form
This is a bit closer. I've created a new form within the custom component class, which has a single control. In the component template, I pass in that form control to the Angular Material component. In the class, I subscribe to valueChanges of that and then propagate the changes back to the parent (via the function passed into registerOnChange).
This sort of works, but feels messy and like there should be a better way.
It also means that any validation applied to my custom form control (by the container component) is ignored, as I've created a new "inner form" that lacks the original validation.
Don't use ControlValueAccessor at all, and instead just pass in the form
As the title says... I tried not doing this the "proper" way, and instead added a binding to the parent form. I then create a form control within the custom component as part of that parent form.
This works for handling value updates, and to an extent validation (but it has to be created as part of the component, not the parent form), but this just feels wrong.
Summary
What's the proper way of handling this? It feels like I'm just stumbling through different anti-patterns, but I can't find anything in the docs to suggest that this is even supported.
Edit:
I've added a helper for doing just this an angular utilities library I've started: s-ng-utils. Using that you can extend WrappedFormControlSuperclass and write:
#Component({
selector: 'my-wrapper',
template: '<input [formControl]="formControl">',
providers: [provideValueAccessor(MyWrapper)],
})
export class MyWrapper extends WrappedFormControlSuperclass<string> {
// ...
}
See some more documentation here.
One solution is to get the #ViewChild() corresponding to the inner form components ControlValueAccessor, and delegating to it in your own component. For example:
#Component({
selector: 'my-wrapper',
template: '<input ngDefaultControl>',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NumberInputComponent),
multi: true,
},
],
})
export class MyWrapper implements ControlValueAccessor {
#ViewChild(DefaultValueAccessor) private valueAccessor: DefaultValueAccessor;
writeValue(obj: any) {
this.valueAccessor.writeValue(obj);
}
registerOnChange(fn: any) {
this.valueAccessor.registerOnChange(fn);
}
registerOnTouched(fn: any) {
this.valueAccessor.registerOnTouched(fn);
}
setDisabledState(isDisabled: boolean) {
this.valueAccessor.setDisabledState(isDisabled);
}
}
The ngDefaultControl in the template above is to manually trigger angular to attach its normal DefaultValueAccessor to the input. This happens automatically if you use <input ngModel>, but we don't want the ngModel here, just the value accessor. You'll need to change DefaultValueAccessor above to whatever the value accessor is for the material dropdown - I'm not familiar with Material myself.
I'm a bit late to the party but here is what I did with wrapping a component which might accept formControlName, formControl, or ngModel
#Component({
selector: 'app-input',
template: '<input [formControl]="control">',
styleUrls: ['./app-input.component.scss']
})
export class AppInputComponent implements OnInit, ControlValueAccessor {
constructor(#Optional() #Self() public ngControl: NgControl) {
if (this.ngControl != null) {
// Setting the value accessor directly (instead of using the providers) to avoid running into a circular import.
this.ngControl.valueAccessor = this;
}
}
control: FormControl;
// These are just to make Angular happy. Not needed since the control is passed to the child input
writeValue(obj: any): void { }
registerOnChange(fn: (_: any) => void): void { }
registerOnTouched(fn: any): void { }
ngOnInit() {
if (this.ngControl instanceof FormControlName) {
const formGroupDirective = this.ngControl.formDirective as FormGroupDirective;
if (formGroupDirective) {
this.control = formGroupDirective.form.controls[this.ngControl.name] as FormControl;
}
} else if (this.ngControl instanceof FormControlDirective) {
this.control = this.ngControl.control;
} else if (this.ngControl instanceof NgModel) {
this.control = this.ngControl.control;
this.control.valueChanges.subscribe(x => this.ngControl.viewToModelUpdate(this.control.value));
} else if (!this.ngControl) {
this.control = new FormControl();
}
}
}
Obviously, don't forget to unsubscribe from this.control.valueChanges
I have actually been wrapping my head around this for a while and I figured out a good solution that is very similar (or the same) as Eric's.
The thing he forgot to account for, is that you can't use the #ViewChild valueAccessor until the view has actually loaded (See #ViewChild docs)
Here is the solution: (I am giving you my example which is wrapping a core angular select directive with NgModel, since you are using a custom formControl, you will need to target that formControl's valueAccessor class)
#Component({
selector: 'my-country-select',
templateUrl: './country-select.component.html',
styleUrls: ['./country-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: CountrySelectComponent,
multi: true
}]
})
export class CountrySelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges {
#ViewChild(SelectControlValueAccessor) private valueAccessor: SelectControlValueAccessor;
private country: number;
private formControlChanged: any;
private formControlTouched: any;
public ngAfterViewInit(): void {
this.valueAccessor.registerOnChange(this.formControlChanged);
this.valueAccessor.registerOnTouched(this.formControlTouched);
}
public registerOnChange(fn: any): void {
this.formControlChanged = fn;
}
public registerOnTouched(fn: any): void {
this.formControlTouched = fn;
}
public writeValue(newCountryId: number): void {
this.country = newCountryId;
}
public setDisabledState(isDisabled: boolean): void {
this.valueAccessor.setDisabledState(isDisabled);
}
}
NgForm is providing an easy way to manage your forms without injecting any data in a HTML form. Input data must be injected at the component level not in a classic html tag.
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)>...</form>
Other way is to create a form component where all the data model is binded using ngModel ;)
What is the method for redirecting the user to a completely external URL in Angular 2. For example, if I need to redirect the user to an OAuth2 server in order to authenticate, how would I do that?
Location.go(), Router.navigate(), and Router.navigateByUrl() are fine for sending the user to another section (route) within the Angular 2 app, but I can't see how they could be used to redirect to an external site?
You can use this-> window.location.href = '...';
This would change the page to whatever you want..
An Angular approach to the methods previously described is to import DOCUMENT from #angular/common (or #angular/platform-browser in Angular
< 4) and use
document.location.href = 'https://stackoverflow.com';
inside a function.
some-page.component.ts
import { DOCUMENT } from '#angular/common';
...
constructor(#Inject(DOCUMENT) private document: Document) { }
goToUrl(): void {
this.document.location.href = 'https://stackoverflow.com';
}
some-page.component.html
<button type="button" (click)="goToUrl()">Click me!</button>
Check out the platformBrowser repo for more info.
The solution, as Dennis Smolek said, is dead simple. Set window.location.href to the URL you want to switch to and it just works.
For example, if you had this method in your component's class file (controller):
goCNN() {
window.location.href='http://www.cnn.com/';
}
Then you could call it quite simply with the appropriate (click) call on a button (or whatever) in your template:
<button (click)="goCNN()">Go to CNN</button>
I think you need à target="_blank", so then you can use window.open :
gotoGoogle() : void {
window.open("https://www.google.com", "_blank");
}
If you've been using the OnDestry lifecycle hook, you might be interested in using something like this before calling window.location.href=...
this.router.ngOnDestroy();
window.location.href = 'http://www.cnn.com/';
that will trigger the OnDestry callback in your component that you might like.
Ohh, and also:
import { Router } from '#angular/router';
is where you find the router.
---EDIT---
Sadly, I might have been wrong in the example above. At least it's not working as exepected in my production code right now - so, until I have time to investigate further, I solve it like this (since my app really need the hook when possible)
this.router.navigate(["/"]).then(result=>{window.location.href = 'http://www.cnn.com/';});
Basically routing to any (dummy) route to force the hook, and then navigate as requested.
in newer versions of Angular with window as an any
(window as any).open(someUrl, "_blank");
There are 2 options:
if you want to redirect in same window/tab
gotoExternalDomain(){
window.location.href='http://google.com/'
}
if you want to redirect in new tab
gotoExternalDomain(){
(window as any).open("http://google.com/", "_blank");
}
After ripping my head off, the solution is just to add http:// to href.
Go somewhere
I used window.location.href='http://external-url';
For me the the redirects worked in Chrome, but didn't work in Firefox.
The following code resolved my problem:
window.location.assign('http://external-url');
I did it using Angular 2 Location since I didn't want to manipulate the global window object myself.
https://angular.io/docs/ts/latest/api/common/index/Location-class.html#!#prepareExternalUrl-anchor
It can be done like this:
import {Component} from '#angular/core';
import {Location} from '#angular/common';
#Component({selector: 'app-component'})
class AppCmp {
constructor(location: Location) {
location.go('/foo');
}
}
You can redirect with multiple ways:
like
window.location.href = 'redirect_url';
another way Angular document:
import document from angular and the document must be inject as well as bellow otherwise you will get error
import { DOCUMENT } from '#angular/common';
export class AppComponent {
constructor(
#Inject(DOCUMENT) private document: Document
) {}
this.document.location.href = 'redirect_url';
}
None of the above solutions worked for me, I just added
window.location.href = "www.google.com"
event.preventDefault();
This worked for me.
Or try using
window.location.replace("www.google.com");
To use #Inject, you must import it. I didn't see this in any of the answers.
TS file:
import { Component, Inject } from '#angular/core';
import { DOCUMENT } from '#angular/common';
#Component({
selector: 'app-my-comp.page',
templateUrl: './my-comp.page.component.html',
styleUrls: ['./my-comp.page.component.scss']
})
export class MyCompPageComponent {
constructor(
#Inject(DOCUMENT) private document: Document
) { }
goToUrl(): void {
this.document.location.href = 'https://google.com/';
}
}
HTML file:
<button type="button" (click)="goToUrl()">Google</button>
In your component.ts
import { Component } from '#angular/core';
#Component({
...
})
export class AppComponent {
...
goToSpecificUrl(url): void {
window.location.href=url;
}
gotoGoogle() : void {
window.location.href='https://www.google.com';
}
}
In your component.html
<button type="button" (click)="goToSpecificUrl('http://stackoverflow.com/')">Open URL</button>
<button type="button" (click)="gotoGoogle()">Open Google</button>
<li *ngFor="item of itemList" (click)="goToSpecificUrl(item.link)"> // (click) don't enable pointer when we hover so we should enable it by using css like: **cursor: pointer;**
Just simple as this
window.location.href='http://www.google.com/';