In my angular app want to make tabs swipable .I am using directive and module in directive i am defined onTabInitialized method and calling in tabs.page.ts but it not working it gives error-> ERROR TypeError: Cannot read property 'onTabInitialized' of undefined. How to solve this problem plese help me..
swipe-tab.directive.ts
import { Directive, ElementRef, Output, EventEmitter, OnInit, Renderer2, OnDestroy } from '#angular/core';
import 'hammerjs';
#Directive({
selector: '[appSwipeTab]'
})
export class SwipeTabDirective implements OnInit, OnDestroy {
#Output() tabChange = new EventEmitter();
private currentTabIndex = 0;
private tabCount = 0;
private swipeCoords: [number, number];
private swipeDuration: number;
private browserSwipeGesture: HammerManager;
private touchListenersFns = [];
tabNames: String[] = [];
constructor(
public _el: ElementRef,
private _renderer: Renderer2
) {
console.log('[SwipeTabDirective] constructor');
}
ngOnInit() {
const tabsList = this._el.nativeElement.querySelectorAll('ion-tab-button');
for (let i = 0, len = tabsList.length; i < len; i += 1) {
this.tabNames.push(tabsList[i].tab);
}
this.tabCount = this.tabNames.length - 1;
console.log('[SwipeTabDirective] ngOnInit, tabNames: ', this.tabNames);
}
onTabInitialized(tabName: string): void {
console.log('[SwipeTabDirective] onTabInitialized, tabName: ', tabName);
this.currentTabIndex = this.tabNames.indexOf(tabName);
const currentTabName = `app-${tabName}`;
const elem = this._el.nativeElement.querySelectorAll(currentTabName)[0];
if (!elem) {
throw new Error('Make sure tab selector has app prefix');
} else {
const content = elem.getElementsByTagName('ion-content')[0];
if (content.querySelector('.swipe-area') === null) {
console.log('[SwipeTabDirective] adding swipe area');
this.createWrapperDiv(content);
}
}
}
createWrapperDiv(content: HTMLElement): void {
const divElement = this._renderer.createElement('div');
this._renderer.addClass(divElement, 'swipe-area');
this._renderer.insertBefore(content, divElement, null);
while (content.children.length > 1) {
const child = content.children[0];
this._renderer.removeChild(content, child);
this._renderer.appendChild(divElement, child);
}
this.addEventListeners(divElement);
}
addEventListeners(divElement: HTMLElement) {
if ('ontouchstart' in document.documentElement) {
this.touchListenersFns.push(
this._renderer.listen(divElement, 'touchstart', ($event) => {
this.deviceSwipeHandler($event, 'start');
}),
this._renderer.listen(divElement, 'touchend', ($event) => {
this.deviceSwipeHandler($event, 'end');
})
);
} else {
this.browserSwipeGesture = new Hammer(divElement);
this.browserSwipeGesture.on('swipe', (event) => {
this.browserSwipeHandler(event);
});
}
}
deviceSwipeHandler(event: TouchEvent, status: string): void {
console.log('[SwipeTabDirective] deviceSwipeHandler, status: ', status);
const coords: [number, number] = [event.changedTouches[0].pageX, event.changedTouches[0].pageY];
const time = new Date().getTime();
if (status === 'start') {
this.swipeCoords = coords;
this.swipeDuration = time;
} else if (status === 'end') {
const direction = [coords[0] - this.swipeCoords[0], coords[1] - this.swipeCoords[1]];
const duration = time - this.swipeDuration;
if (duration < 1000 && Math.abs(direction[0]) > 50
&& Math.abs(direction[0]) > Math.abs(direction[1] * 3)) {
if (direction[0] > 0) {
this.moveBackward();
} else {
this.moveForward();
}
}
}
}
browserSwipeHandler(event) {
console.log('[SwipeTabDirective] browserSwipeHandler, direction: ', event.direction);
switch (event.direction) {
case 2:
this.moveForward();
break;
case 4:
this.moveBackward();
break;
default:
break;
}
}
moveForward(): void {
console.log('[SwipeTabDirective] moveForward');
if (this.currentTabIndex < this.tabCount) {
this.currentTabIndex++;
this.tabChange.emit(this.tabNames[this.currentTabIndex]);
}
}
moveBackward(): void {
console.log('[SwipeTabDirective] moveBackward');
if (this.currentTabIndex > 0) {
this.currentTabIndex--;
this.tabChange.emit(this.tabNames[this.currentTabIndex]);
}
}
ngOnDestroy() {
if (this.touchListenersFns.length > 0) {
this.touchListenersFns.forEach(fn => fn());
} else if (this.browserSwipeGesture) {
this.browserSwipeGesture.off('swipe');
}
}
}
also make a module for export the directives which code show in below..
directives.module.ts
import { NgModule } from '#angular/core';
import { CommonModule } from '#angular/common';
import { SwipeTabDirective } from './swipe-tab.directive';
#NgModule({
declarations: [SwipeTabDirective],
imports: [
CommonModule
],
exports: [
SwipeTabDirective
]
})
export class DirectivesModule { }
The directive function use in tabs.page.ts and import directives module in tabs.page.ts
tabs.page.ts
import { Component, ViewChild } from '#angular/core';
import { IonTabs } from '#ionic/angular';
import { SwipeTabDirective } from '../directives/swipe-tab.directive';
#Component({
selector: 'app-tabs',
templateUrl: 'tabs.page.html',
styleUrls: ['tabs.page.scss']
})
export class TabsPage {
#ViewChild(SwipeTabDirective) swipeTabDirective: SwipeTabDirective;
#ViewChild('myTabs') tabRef: IonTabs;
constructor() { }
ionTabsDidChange($event) {
console.log('[TabsPage] ionTabsDidChange, $event: ', $event);
this.swipeTabDirective.onTabInitialized($event.tab);
}
onTabChange($event) {
console.log('[TabsPage] onTabChange, $event: ', $event);
this.tabRef.select($event);
}
}
In tabs.page.html i am define two function ionTabsDidChange and onTabChange ...
tabs.page.html
<ion-tabs appSwipetab (ionTabsDidChange)="ionTabsDidChange($event)" (tabChange)="onTabChange($event)" #myTabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="tab1">
<ion-icon name="flash"></ion-icon>
<ion-label>Tab One</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab2">
<ion-icon name="apps"></ion-icon>
<ion-label>Tab Two</ion-label>
</ion-tab-button>
<ion-tab-button tab="tab3">
<ion-icon name="send"></ion-icon>
<ion-label>Tab Three</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
I did some change in swipe-tab.directive.ts
from
const currentTabName = `app-${tabName}`;
to
const currentTabName = `${tabName}`;
and the error has gone.
Related
In ionic 4 or 5, tabbar is not hided on subpages.
Of course, it works well in ionic 2 or 3.
Please let me know how to solve this issue.
This is my solution.
But hope the best solution.
create TabsService
import this in app.module.ts
Here is full code of TabsService
import { Injectable } from '#angular/core';
import { filter } from 'rxjs/operators';
import { NavigationEnd, Router } from '#angular/router';
import { Platform } from '#ionic/angular';
#Injectable({
providedIn: 'root'
})
export class TabsService {
constructor(private router: Router, private platform: Platform) {
this.platform.ready().then(() => {
this.navEvents();
});
}
public hideTabs() {
const tabBar = document.getElementById('kidesiaTabBar');
if (tabBar && tabBar.style.display !== 'none') {
tabBar.style.display = 'none';
}
}
public showTabs() {
const tabBar = document.getElementById('kidesiaTabBar');
if (tabBar && tabBar.style.display !== 'flex') {
tabBar.style.display = 'flex';
}
}
private navEvents() {
this.router.events
.pipe(filter(e => e instanceof NavigationEnd))
.subscribe((e: any) => {
this.showHideTabs(e);
});
}
private showHideTabs(e: any) {
const urlArray = e.url.split('/');
if (urlArray.length >= 3) {
let shouldHide = true;
if (urlArray.length === 3 && urlArray[1] === 'tabs') {
shouldHide = false;
}
try {
setTimeout(() => (shouldHide ? this.hideTabs() : this.showTabs()), 300);
} catch (err) {}
}
}
}
i am consuming Rest JSONP Web Service in an ionic App which works fine on PC but returns null value on mobile devices
My page.ts file
import {
Component,
OnInit
} from '#angular/core';
import {
AlertController,
LoadingController
} from '#ionic/angular';
import {
ActionSheetController
} from '#ionic/angular';
import {
Router
} from '#angular/router'
import {
ProApiService
} from './../../../../services/pro-api.service';
#Component({
selector: 'app-ranked-diagnosis',
templateUrl: './ranked-diagnosis.page.html',
styleUrls: ['./ranked-diagnosis.page.scss'],
})
export class RankedDiagnosisPage implements OnInit {
tabSelect: string = 'show10';
show10Data: Array < any >= [];
showAllData: Array < any >= [];
redFlagsData: Array < any >= [];
loading: any;
constructor(
private api: ProApiService,
public alertController: AlertController,
public loadingController: LoadingController,
public actionSheetController: ActionSheetController,
private router: Router
) {}
segmentChanged(event: any) {
this.tabSelect = event.detail.value;
}
async presentActionSheet(buttons) {
const actionSheet = await this.actionSheetController.create({
header: 'Sub Diagnosis',
buttons: buttons
});
await actionSheet.present();
}
async presentAlert(msg: string, header: string) {
const alert = await this.alertController.create({
header: '',
subHeader: header,
message: msg,
buttons: ['OK']
});
await alert.present();
}
async presentLoading() {
this.loading = await this.loadingController.create({
message: 'loading...',
});
return await this.loading.present();
}
ngOnInit() {
}
ionViewWillEnter() {
if (this.api.ProApiData.diagnoses_checklist.diagnoses) {
this.showAllData =
this.api.ProApiData.diagnoses_checklist.diagnoses;
for (let i = 0; i < 10; i++) {
this.show10Data.push(this.showAllData[i]);
}
this.showAllData.forEach(item => {
if (item.red_flag == 'true') {
this.redFlagsData.push(item);
}
});
console.log(this.showAllData);
} else {
console.log('error');
this.router.navigateByUrl('isabel-pro');
}
}
why_diagnosis(url: any, weightage: any) {
this.presentLoading();
this.api.why_diagnosisApi(url).subscribe(res => {
let matched_terms = res._body.why_diagnosis.matched_terms;
console.log(matched_terms);
let alertMsg = `We matched the terms: ${matched_terms}<br><hr>Degree of match between query entered and database: ${weightage}`;
this.presentAlert(alertMsg, 'Why did this diagnosis come up ?');
this.loadingController.dismiss();
}, err => {
this.loadingController.dismiss();
console.log('error');
});
}
}
in the above code i am calling why_diagnosis function which calls the function from a service file.
My service.ts file
import {
Injectable
} from '#angular/core';
import {
HttpClient,
HttpHeaders
} from '#angular/common/http';
import {
Jsonp
} from '#angular/http';
import {
Observable
} from 'rxjs';
import {
map
} from 'rxjs/operators';
import {
ConstantsService
} from './../../../services/constants.service';
#Injectable({
providedIn: 'root'
})
export class ProApiService {
apiRoot = this.root.APIroot;
diagnosisPROData: any;
drugData: any;
ProApiData: any;
drugApiData: any;
constructor(
private jsonp: Jsonp,
private http: HttpClient,
private root: ConstantsService) {}
why_diagnosisApi(url: any): Observable < any > {
let whyUrl = `${this.apiRoot}Mob_isabelPRO.php?
why_url=${url}&callback=JSONP_CALLBACK`;
return this.jsonp.request(whyUrl, 'callback')
.pipe(
map(
res => {
let why_diagnosis = res;
return why_diagnosis;
}
)
);
}
}
above code is from my service file.
this is the value i am getting in PC
this is the return on mobile
i dont know whats wrong with it. please suggest me the solution
Thanks
I am making like and dislike functionality in ionic app when user like then want to show heart icon and when user dislike then want to show empty heart icon.When like the post then icon show but when refreshing page then its show icon of empty likes.Please help me how to resolve this problem in ionic....
tab1.page.html
<div>
<img src="{{getImage.image}}" (click)="tapPhotoLike(getImage.id)" (click)="toggleIcon(getImage.id)">
</div>
<p no-margin no-padding>
<button clear ion-button icon-only class="like-btn">
<!-- <ion-icon no-padding name="like_btn.icon_name" color="{{ like_btn.color }}" class="icon-space"></ion-icon> -->
<ion-icon name="heart-empty" *ngIf="!iconToggle[getImage.id]" color="{{ like_btn.color }}"></ion-icon>
<ion-icon name="heart" *ngIf="iconToggle[getImage.id]" color="{{ like_btn.color }}"></ion-icon>
</button>
</p>
tab1.page.ts
import { Component, OnInit } from '#angular/core';
import { UserService } from '../user.service';
import { User } from '../user';
import { first } from 'rxjs/operators';
import { Storage } from '#ionic/storage';
import { ToastController } from '#ionic/angular';
import { LoadingController, NavController } from '#ionic/angular';
#Component({
selector: 'app-tab1',
templateUrl: 'tab1.page.html',
styleUrls: ['tab1.page.scss']
})
export class Tab1Page implements OnInit {
num
getImages: User[] = [];
getImages2: User[] = [];
getImages3 = [];
getcounts;
countLikes
counts
unlikes
likesID
iconToggle = [];
like_btn = {
color: 'danger',
icon_name: 'heart-empty'
};
public tap: number = 0;
public status: false;
constructor(private userService: UserService,
public toastController: ToastController,
private storage: Storage,
public navCtrl: NavController,
public loadingController: LoadingController) {
}
doRefresh(event) {
this.userPost();
setTimeout(() => {
event.target.complete();
}, 2000);
}
ngOnInit() {
this.userPost();
//this.getCountOfLikes();
}
async userPost() {
const loading = await this.loadingController.create({
message: 'Please wait...',
spinner: 'crescent',
duration: 2000
});
await loading.present();
this.userService.getUserPost().pipe(first()).subscribe(getImages => {
this.getImages3 = getImages;
loading.dismiss();
});
}
likeButton() {
const detail_id = this.userService.getCurrentIdpostId();
this.storage.get('userId').then((val) => {
if (val) {
let user_id = val
this.userService.userPostLikes(user_id, detail_id).pipe(first()).subscribe(
async data => {
this.iconToggle[this.num] = true;
if (data['status'] === 'you have already liked this post') {
const toast = await this.toastController.create({
message: 'you have already liked this post before.',
duration: 2000
});
toast.present();
}
this.userPost();
}
);
}
});
}
tapPhotoLike($detail_id, num) {
this.userService.setCurrentIdpostId($detail_id);
this.tap++;
setTimeout(() => {
if (this.tap == 1) {
this.tap = 0;
//this.unlikePost();
} if (this.tap > 1) {
this.tap = 0;
}
}, 250);
}
setIconToggles() {
let x = 0;
this.getImages3.forEach(() => {
this.iconToggle[x] = false;
x++;
});
}
toggleIcon(num) {
if (this.iconToggle[num]) {
this.iconToggle[num] = false;
this.unlikePost();
} else {
this.iconToggle[num] = true;
this.likeButton();
}
}
ionViewWillEnter() {
this.userPost();
}
unlikePost() {
let detail_id = this.userService.getCurrentIdpostId();
this.storage.get('userId').then((val) => {
if (val) {
let user_id = val;
this.userService.unLikes(user_id, detail_id).subscribe(async dislikes => {
this.unlikes = dislikes;
this.iconToggle[this.num] = false;
this.userPost();
})
}
});
}
}
How manage icon , i am inserting status on like and dislike in database..In this code how to manage status please help me.
I'm trying to implement a directive to hide the Toolbar while scrolling.
I tried using this tutorial:
https://medium.com/#gregor.srdic/ionic3-hidding-header-on-footer-on-content-scroll-15ab95b05dc5
This worked with Ionic 3, but doesn't work with Ionic 4.
On the following code I get the error:
private adjustElementOnScroll(ev) {
if (ev) {
console.log(ev);
ev.domWrite(() => {
let scrollTop: number = ev.scrollTop > 0 ? ev.scrollTop : 0;
let scrolldiff: number = scrollTop - this.lastScrollPosition;
this.lastScrollPosition = scrollTop;
let newValue = this.lastValue + scrolldiff;
newValue = Math.max(0, Math.min(newValue, this.config.maxValue));
this.renderer.setStyle(this.element.nativeElement, this.config.cssProperty, `-${newValue}px`);
this.lastValue = newValue;
});
}
}
Error:
ev.domWrite is not a function
I checked, and ev in Ionic 4 is a CustomEvent, not a ScrollEvent.
Any suggestions?
Above solutions no longer works. Ionic 4 beta APIs has changed a lot in recent months.
you have to import IonContent instead of Content.
//scroll-hide.directive.ts
import { IonContent, DomController } from '#ionic/angular';
import { Directive, ElementRef, Input, Renderer2, SimpleChanges } from '#angular/core';
import the directive where you want to use it, instead of app.module.ts
for example in relevant module,
//ex: home.module.ts
import { ScrollHideDirective } from '../../directives/scroll-hide.directive';
#NgModule({
...
declarations: [...,ScrollHideDirective],
...
})
then in the ts file,
//ex: home.page.ts
import { ScrollHideConfig } from '../../directives/scroll-hide.directive';
export class HomePage implements OnInit {
...
footerScrollConfig: ScrollHideConfig = { cssProperty: 'margin-bottom', maxValue: undefined };
headerScrollConfig: ScrollHideConfig = { cssProperty: 'margin-top', maxValue: 54 };
...
}
reversed version of directive
//ex: scroll-hide.directive.ts
import { IonContent, DomController } from '#ionic/angular';
import { Directive, ElementRef, Input, Renderer2, SimpleChanges } from '#angular/core';
#Directive({
selector: '[scrollHide]'
})
export class ScrollHideDirective {
#Input('scrollHide') config: ScrollHideConfig;
#Input('scrollContent') scrollContent: IonContent;
contentHeight: number;
scrollHeight: number;
lastScrollPosition: number;
lastValue: number = 0;
constructor(private element: ElementRef, private renderer: Renderer2, private domCtrl: DomController) {
}
ngOnChanges(changes: SimpleChanges) {
if(this.scrollContent && this.config) {
this.scrollContent.scrollEvents = true;
let scrollStartFunc = async (ev) => {
const el = await this.scrollContent.getScrollElement();
this.contentHeight = el.offsetHeight;
this.scrollHeight = el.scrollHeight;
if (this.config.maxValue === undefined) {
this.config.maxValue = this.element.nativeElement.offsetHeight;
}
this.lastScrollPosition = el.scrollTop;
};
if(this.scrollContent && this.scrollContent instanceof IonContent) {
this.scrollContent.ionScrollStart.subscribe(scrollStartFunc);
this.scrollContent.ionScroll.subscribe(async (ev) => this.adjustElementOnScroll(ev));
this.scrollContent.ionScrollEnd.subscribe(async (ev) => this.adjustElementOnScroll(ev));
} else if(this.scrollContent instanceof HTMLElement) {
(this.scrollContent as HTMLElement).addEventListener('ionScrollStart', scrollStartFunc);
(this.scrollContent as HTMLElement).addEventListener('ionScroll',async (ev) => this.adjustElementOnScroll(ev));
(this.scrollContent as HTMLElement).addEventListener('ionScrollEnd',async (ev) => this.adjustElementOnScroll(ev));
}
}
}
private adjustElementOnScroll(ev) {
if (ev) {
this.domCtrl.write(async () => {
const el = await this.scrollContent.getScrollElement();
let scrollTop: number = el.scrollTop > 0 ? el.scrollTop : 0;
let scrolldiff: number = scrollTop - this.lastScrollPosition;
this.lastScrollPosition = scrollTop;
let newValue = this.lastValue + scrolldiff;
newValue = Math.max(0, Math.min(newValue, this.config.maxValue));
this.renderer.setStyle(this.element.nativeElement, this.config.cssProperty, `-${newValue}px`);
this.lastValue = newValue;
});
}
}
}
export interface ScrollHideConfig {
cssProperty: string;
maxValue: number;
}
the html ex: page.html (No Change)
<ion-header [scrollHide]="headerScrollConfig" [scrollContent]="pageContent">
...
</ion-header>
<ion-content #pageContent fullscreen>
...
</ion-content>
<ion-footer [scrollHide]="footerScrollConfig" [scrollContent]="pageContent">
...
</ion-footer>
hope it helps.
Modified directive for Ionic 4.
import { Content, DomController } from '#ionic/angular';
import { Directive, ElementRef, Input, Renderer2, SimpleChanges } from '#angular/core';
#Directive({
selector: '[scrollHide]'
})
export class ScrollHideDirective {
#Input('scrollHide') config: ScrollHideConfig;
#Input('scrollContent') scrollContent: Content;
contentHeight: number;
scrollHeight: number;
lastScrollPosition: number;
lastValue: number = 0;
constructor(private element: ElementRef, private renderer: Renderer2, private domCtrl: DomController) {
}
ngOnChanges(changes: SimpleChanges) {
if (this.scrollContent && this.config) {
this.scrollContent.ionScrollStart.subscribe(async (ev) => {
const el = await this.scrollContent.getScrollElement();
this.contentHeight = el.offsetHeight;
this.scrollHeight = el.scrollHeight;
if (this.config.maxValue === undefined) {
this.config.maxValue = this.element.nativeElement.offsetHeight;
}
this.lastScrollPosition = el.scrollTop;
});
this.scrollContent.ionScroll.subscribe((ev) => this.adjustElementOnScroll(ev));
this.scrollContent.ionScrollEnd.subscribe((ev) => this.adjustElementOnScroll(ev));
}
}
private adjustElementOnScroll(ev) {
if (ev) {
this.domCtrl.write(async () => {
const el = await this.scrollContent.getScrollElement();
let scrollTop: number = el.scrollTop > 0 ? el.scrollTop : 0;
let scrolldiff: number = scrollTop - this.lastScrollPosition;
this.lastScrollPosition = scrollTop;
let newValue = this.lastValue + scrolldiff;
newValue = Math.max(0, Math.min(newValue, this.config.maxValue));
this.renderer.setStyle(this.element.nativeElement, this.config.cssProperty, `-${newValue}px`);
this.lastValue = newValue;
});
}
}
}
export interface ScrollHideConfig {
cssProperty: string;
maxValue: number;
}
The event may have changed in Ionic 4, but you can still import the DomController from #ionic/angular
import {..., DomController } from "#ionic/angular";
and inject it in the constructor
constructor(
// ...
private domCtrl: DomController
) { }
And then use the write() method like this:
private adjustElementOnScroll(ev) {
if (ev) {
this.domCtrl.write(() => {
// ...
});
}
}
The DomController is just a way for Ionic to create a queue for callbacks that would write or read the DOM in order to use the window.requestAnimationFrame() method behind the scenes.
For more information, please visit:
DomController source code
requestAnimationFrame MDN
The below code worked for me, with minor corrections from above code
import { Content, DomController } from '#ionic/angular';
import { Directive, ElementRef, Input, Renderer2, SimpleChanges } from '#angular/core';
#Directive({
selector: '[scrollHide]'
})
export class ScrollHideDirective {
#Input('scrollHide') config: ScrollHideConfig;
#Input('scrollContent') scrollContent: Content;
contentHeight: number;
scrollHeight: number;
lastScrollPosition: number;
lastValue: number = 0;
constructor(private element: ElementRef, private renderer: Renderer2, private domCtrl: DomController) {
}
ngOnChanges(changes: SimpleChanges) {
if(this.scrollContent && this.config) {
this.scrollContent.scrollEvents = true;
let scrollStartFunc = async (ev) => {
const el = await this.scrollContent.getScrollElement();
this.contentHeight = el.offsetHeight;
this.scrollHeight = el.scrollHeight;
if (this.config.maxValue === undefined) {
this.config.maxValue = this.element.nativeElement.offsetHeight;
}
this.lastScrollPosition = el.scrollTop;
};
if(this.scrollContent && this.scrollContent instanceof Content) {
this.scrollContent.ionScrollStart.subscribe(scrollStartFunc);
this.scrollContent.ionScroll.subscribe(async (ev) => this.adjustElementOnScroll(ev));
this.scrollContent.ionScrollEnd.subscribe(async (ev) => this.adjustElementOnScroll(ev));
} else if(this.scrollContent instanceof HTMLElement) {
(this.scrollContent as HTMLElement).addEventListener('ionScrollStart', scrollStartFunc);
(this.scrollContent as HTMLElement).addEventListener('ionScroll',async (ev) => this.adjustElementOnScroll(ev));
(this.scrollContent as HTMLElement).addEventListener('ionScrollEnd',async (ev) => this.adjustElementOnScroll(ev));
}
}
}
private adjustElementOnScroll(ev) {
if (ev) {
this.domCtrl.write(async () => {
const el = await this.scrollContent.getScrollElement();
let scrollTop: number = el.scrollTop > 0 ? el.scrollTop : 0;
let scrolldiff: number = scrollTop - this.lastScrollPosition;
this.lastScrollPosition = scrollTop;
let newValue = this.lastValue + scrolldiff;
newValue = Math.max(0, Math.min(newValue, this.config.maxValue));
this.renderer.setStyle(this.element.nativeElement, this.config.cssProperty, `-${newValue}px`);
this.lastValue = newValue;
});
}
}
}
export interface ScrollHideConfig {
cssProperty: string;
maxValue: number;
}
I'm trying to wrap my head around the following problem:
I have a 'google-place-autocomplete' directive that adds the autocomplete functionality to an input field.
Now I also wanted it to be able to force a google place selection and only be 'valid' if the user has selected a place.
E.g:
#Directive({
selector: '[googlePlace][formControlName], [googlePlace][ngModel]',
providers: [{provide: NG_VALIDATORS, useExisting: GooglePlaceDirective, multi: true}]
})
export class GooglePlaceDirective implements Validator, OnChanges {
valid = false;
#Output() googlePlaceAddressChange: any = new EventEmitter();
#Input() googlePlaceAddress: any;
#Output() ngModelChange: any = new EventEmitter();
private autocomplete: any;
constructor(private googleMapService: GoogleMapsService,
private element: ElementRef,
private zone: NgZone) {
}
ngOnInit() {
let self = this;
this.googleMapService
.load()
.subscribe(
() => {
this.autocomplete = new google.maps.places.Autocomplete(this.element.nativeElement);
this.autocomplete.addListener('place_changed', function () {
self.placeChanged(this.getPlace());
});
}
);
}
private placeChanged(place) {
this.zone.run(() => {
this.googlePlaceAddress = {
address: this.element.nativeElement.value,
formattedAddress: place.formatted_address,
latitude: place.geometry.location.lat(),
longitude: place.geometry.location.lng()
};
this.valid = true;
this.googlePlaceAddressChange.emit(this.googlePlaceAddress);
this.ngModelChange.emit(this.element.nativeElement.value);
});
}
ngOnChanges(changes): void {
let googlePlaceDefined = typeof (changes.googlePlaceAddress) !== 'undefined';
let modelDefined = typeof (changes.ngModel) !== 'undefined';
if(modelDefined && !googlePlaceDefined) {
this.valid = false;
} else if(googlePlaceDefined && !modelDefined) {
this.valid = true;
}
}
validate(control: AbstractControl) {
return this.valid === false ? {'googlePlaceAddress': true} : null;
}
}
If I use this directive in an template driven form:
...
<input name="addr" type="text" [(ngModel)]="textValue" [(googlePlaceAddress)]="googleAddress" required>
<p *ngIf="addr.errors.googlePlaceAddress">Please select a proposed address</p>
...
it works fine.
Now I need to use this in an Reactive Form using FormGroup
let groups = [
new FormControl('', [Validators.required])
];
/** HTML **/
...
<input [id]="addr"
[formControlName]="address"
class="form-control"
type="text"
googlePlace
[placeholder]="question.label"
[(googlePlaceAddress)]="googleAddress">
...
However in this case the validation from the directive is never triggered.
I suppose angular2 expects it to be given through, when using Reactive Forms:
new FormControl('', [Validators.required, ???])
I must have taken a wrong path somewhere.
For future reference:
I solved my problem creating a component out of it together with a Value accessor:
#Component({
selector: 'app-google-place',
templateUrl: './google-place.component.html',
styleUrls: ['./google-place.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GooglePlaceComponent),
multi: true
}
]
})
export class GooglePlaceComponent implements OnInit, ControlValueAccessor {
#ViewChild('inputElement') inputElement: ElementRef;
#Input() public placeholder: string = "Address";
#Input() public textValue: string = "";
private autocomplete: any;
private _place = null;
constructor(
private googleMapService: GoogleMapsService,
private zone: NgZone
) {
}
ngOnInit() {
this.googleMapService
.load()
.subscribe(
() => {
this.autocomplete = new google.maps.places.Autocomplete(this.inputElement.nativeElement);
this.autocomplete.addListener('place_changed', () => this.placeChanged());
}
);
}
placeChanged() {
this.zone.run(() => {
let place = this.autocomplete.getPlace();
this._place = {
address: this.inputElement.nativeElement.value,
formattedAddress: place.formatted_address,
latitude: place.geometry.location.lat(),
longitude: place.geometry.location.lng()
};
this.propagateChange(this._place);
});
}
onNgModelChange($event) {
if(this._place !== null) {
if(this._place.address !== $event) {
this._place = null;
this.propagateChange(this._place);
}
}
}
onBlur() {
this.propagateTouched();
}
writeValue(obj: any): void {
if(obj !== undefined) {
this._place = obj;
}
}
propagateChange = (_: any) => {};
registerOnChange(fn) {
this.propagateChange = fn;
}
propagateTouched = () => {};
registerOnTouched(fn: any): void {
this.propagateTouched = fn;
}
}
Using this I can use it in a FormGroup with the Validators.required and it will only be valid if a user has selected a google place.
EDIT
The html:
<input type="text"
(blur)="onBlur()"
#inputElement
class="form-control"
[(ngModel)]="textValue"
(ngModelChange)="onNgModelChange($event)">
The service:
import {Injectable} from '#angular/core';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';
#Injectable()
export class GoogleMapsService {
private key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
private loaded = false;
private currentRequest = null;
constructor() {
}
load() {
if (this.loaded) {
return Observable.create((observer) => {
observer.next();
observer.complete();
});
}
if (this.currentRequest === null) {
//http://reactivex.io/rxjs/manual/overview.html#multicasted-observables
const source = Observable.create((observer) => {
this.loadMaps(observer);
});
const subject = new Subject();
this.currentRequest = source.multicast(subject);
this.currentRequest.connect();
}
return this.currentRequest;
}
private loadMaps(observer: any) {
const script: any = document.createElement('script');
script.src = 'https://maps.googleapis.com/maps/api/js?key=' + this.key + '&libraries=places';
if (script.readyState) { // IE, incl. IE9
script.onreadystatechange = () => {
if (script.readyState == 'loaded' || script.readyState == 'complete') {
script.onreadystatechange = null;
this.loaded = true;
observer.next();
observer.complete();
this.currentRequest = null;
}
};
} else {
script.onload = () => { // Other browsers
this.loaded = true;
observer.next();
observer.complete();
this.currentRequest = null;
};
}
script.onerror = () => {
observer.error('Unable to load');
this.currentRequest = null;
};
document.getElementsByTagName('head')[0].appendChild(script);
}
}
The 'usage':
With template ngModel
<app-google-place ([ngModel)]="place"></app-google-place>