No binding using ngClass with map zoom event in google maps - javascript

I have this:
<i [ngClass]="{'active': isGeoLocationButtonEnabled }" class="toggle fa fa-crosshairs" >
And the handler is:
private onStartZoomEvent(event) {
if (event.zoom != 12) {
console.log("Current zoom level is: ", event.zoom);
this._eventAggregator.trigger(new DisableGeolocationEvent());
this.disableGeolocationButton();
}
}
The event that is triggered looks like this:
private mapZoomChangedHandler() {
this._mapsWrapper.getMap().then((map: google.maps.Map) => {
let zoom = map.getZoom();
this._eventAggregator.trigger(new MapStartZoomEvent(zoom));
});
}
I am subscribing like this:
this._eventAggregator.subscribe(MapStartZoomEvent, this.onStartZoomEvent.bind(this));
The problem is that for the first time I change the map zoom, the button is not disabled. The second time I click it , it gets disabled. When I debug, everything seems to be ok, my boolean isGeoLocationButtonEnabled is set to false, but the active class stays (it is just the background color highlighting).

You don't show where isGeoLocationButtonEnabled is updated. I'm pretty sure that the Google Maps code runs outside Angulars zone.
Use zone.run(...) to run the code that updates the model inside Angulars zone so change detection is invoked afterwards
constructor(private zone:NgZone) {}
methodWhereEnabledIsUpdated() {
...
this.zone.run(() => {
isGeoLocationButtonEnabled = someValue;
});
...
}

Related

Angular how to get height of dynamically created div and send it to sibling component?

I have one parent and 2 child components like below
Parent-component.html
<child-component-1 [id]="id"></child-component-1>
<child-component-2></child-component-2>
child-component-1 has ngb-carousel component to show warnings and alerts to user.And this component is being created due to id and its result comes from API like below
child-component-1.ts
alertFound;
this.service.checkAlert(id).subscribe((result:any)=>{
if(result.alertFound){
this.alertFound=true;
}
})
child-component-1.html
<ngb-carousel #carousel *ngIf="alertFound" class="carousel-alert" [interval]="false" [wrap]="false"
[showNavigationIndicators]="false">
........................(Things go on here after result comes)
</ngb-carousel>
In child-component-2,I need to get the height of this ngb-alert to make dynamic height calculation on the screen.If it exists,I need to subtract it from window.innerheight.
child-component-2.html
<div [style.height.px]="actualHeight"></div>
child-component-2.ts
this.actualHeight = window.innerHeight - 269;
Interestingly on child-component-1 when I tried to track it like below
#ViewChild('carousel', {read: ElementRef, static:false}) elementView: ElementRef;
ngAfterViewInit(){
console.log(this.elementView.nativeElement.offSetHeight);
//throws height of created carousel first,then if next id doesnt have alert still shows me the height which previously created
}
it shows previously created carousel height even though result is false and carousel couldn't be created at that moment.I guess this happening because result sometimes arrives very late,sometimes fast.To listen detections on viewchild I found something like below which works exactly as I wanted and solved emitting non-created alert height problem
#ViewChildren('carousel', {read: ViewContainerRef}) viewContainerRefs;
ngAfterViewInit() {
this.viewContainerRefs.changes.subscribe(item => {
if (item) {
console.log(item._results[0]._lContainer[0][0].offsetHeight);
}
})
}
Now problem is sending this height to child-component-2 where I should calculate height dynamically.
I considered 2 options.First one is creating subject on a service,emitting height to it from child-component-1 and listening it on child-component-2 like below
service.ts
alertActive = new BehaviorSubject(0);
setAlertHeight(value:number){
this.alertActive.next(value)
}
child-component-1
ngAfterViewInit() {
this.viewContainerRefs.changes.subscribe(item => {
if (item) {
this.service.setAlertHeight(item._results[0]._lContainer[0][0].offsetHeight);
}
})
}
child-component-2
this.alertActive.subscribe(value=>{
if(value){
this.actualHeight-=value;
}
})
On the example above,it causes the problem to bring height,even though alert not created.I logged subscribes on console from child-2 and child-1 and noticed that child-2 prints to console even though child1 didnt emit anything to it.
So I considered another option to send height from child-1 to parent via #Output and then via Input to
child2 like below
child-1
#Output() transferHeight: EventEmitter<any> = new EventEmitter();
this.viewContainerRefs.changes.subscribe(item => {
if (item) {
this.transferHeight.emit(item._results[0]._lContainer[0][0].offsetHeight);
}
})
parent
<child-component-1 (transferHeight)="transferedHeight($event)" [id]="id"></child-component-1>
<child-component-2 [transferredHeight]="transferredHeight"></child-component-2>
alertHeight;
transferedHeight(comingHeight){
this.alertHeight=comingHeight;
}
child-2
#Input() transferredHeight;
ngOnInit(){
this.actualHeight-=this.transferredHeight;
}
this one really handles the problem that I mentioned previously.But if carousel created after child-2 created due to late network resut ,it returns undefined.Therefore I tried to use ngOnChanges to listen changes on the Input variable like below
ngOnChanges(changes: SimpleChanges) {
console.log(changes.transferredHeight.currentValue)
}
this console.log doesnt print anything although I implemented onChanges.So Im waiting for your help.If there's a way to listen viewContainerRefs.changes of child-1 from child-2 would be the best case
The reason this attempt:
alertActive = new BehaviorSubject(0);
setAlertHeight(value:number){
this.alertActive.next(value)
}
triggered the height to be set even when the alert was not active was because you used a BehaviorSubject rather than just a Subject. A BehaviorSubject provides an initial value (0 in your case), so immediately upon subscription the subscriber will receive that initial value (or the latest emitted value).
So you were on the right track, you just needed to use a regular Subject instead because then subscribers won't receive any values until the Subject emits.
So your service could look like this:
private _alertActive = new Subject<number>();
get alertActive(): Observable<number> {
return this._alertActive.asObservable();
}
setAlertHeight(height: number) {
this._alertActive.next(height);
}
(Note that the Subject is a private member but is exposed via a getter as an Observable. In general, subscribers shouldn't have access to the raw Subject unless they'll also be emitting values from it.)
And your child-2 component would subscribe to it like so:
this.service.alertActive.subscribe((height: number) => {
this.actualHeight -= height;
});
You were also on the right track using ViewChildren and subscribing to its changes, but it might be simpler if you reference the ElementRefs rather than the ViewContainerRefs. That would look like this:
import { NgbCarousel } from "#ng-bootstrap/ng-bootstrap";
#ViewChildren(NgbCarousel, { read: ElementRef }) carousels: QueryList<ElementRef>;
ngAfterViewInit() {
this.carousels.changes.subscribe((els: QueryList<ElementRef>) => {
const height = els.first ? els.first.nativeElement.offsetHeight : 0;
this.service.setAlertHeight(height);
});
}
Here's a StackBlitz showing it working with this approach.

Why am I obtaining this strange behavior trying to use different color for different event type dragged into a PrimeNG FullCalendar component?

I am working on an Angular application using PrimeNG Full Calendar component, this one: https://primefaces.org/primeng/showcase/#/fullcalendar
That is based on the Angular FullCalendar component, this one: https://fullcalendar.io/
Here you can find my entire code: https://bitbucket.org/dgs_poste_team/soc_calendar/src/master/
I am finding some difficulties trying to dinamically change the background color of the event rendered on my calendar. I have to have different event background color based on different event information (the start event time, for example: if an event start at 07:00 is green, if it start at 15:00 it is red, if it start at 23:00 it is blue, but this logic is not important at this time).
In my project I am dragging external event into my calendar, something like this: https://fullcalendar.io/docs/external-dragging-demo
So, as you can see in my BitBucket repository I have this FullcalendarComponent handling the component that contains the calendar that recives events from an external component:
import { Component, OnInit, ViewChild, ElementRef } from '#angular/core';
import { EventService } from '../event.service';
import dayGridPlugin from '#fullcalendar/daygrid';
import timeGridPlugin from '#fullcalendar/timegrid';
import listPlugin from '#fullcalendar/list';
import interactionPlugin, { Draggable } from '#fullcalendar/interaction';
import { FullCalendar } from 'primeng';
#Component({
selector: 'app-fullcalendar',
templateUrl: './fullcalendar.component.html',
styleUrls: ['./fullcalendar.component.css']
})
export class FullcalendarComponent implements OnInit {
events: any[];
options: any;
header: any;
//people: any[];
#ViewChild('fullcalendar') fullcalendar: FullCalendar;
constructor(private eventService: EventService) {}
ngOnInit() {
//this.eventService.getEvents().then(events => { this.events = events;});
this.eventService.getEvents().subscribe(events => { this.events = events.map((event) => {
var date = new Date(event.start);
var hour = date.getHours();
//event['backgroundColor'] = hour === 7? 'red': (hour === 7 ? 'green' : 'black');
if(hour === 7) {
event['backgroundColor'] = 'red';
}
else if(hour === 15) {
event['backgroundColor'] = 'green';
}
else if(hour === 23) {
event['backgroundColor'] = 'black';
}
return event;
})});
this.options = {
plugins:[ dayGridPlugin, timeGridPlugin, interactionPlugin, listPlugin ],
defaultDate: '2017-02-01',
header: {
left: 'prev,next',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
editable: true,
nextDayThreshold: '09:00:00',
allDayDefault: false,
dateClick: (dateClickEvent) => { // <-- add the callback here as one of the properties of `options`
console.log("DATE CLICKED !!!");
},
eventClick: (eventClickEvent) => {
console.log("EVENT CLICKED !!!");
},
eventDragStop: (eventDragStopEvent) => {
console.log("EVENT DRAG STOP !!!");
},
eventReceive: (eventReceiveEvent) => {
console.log(eventReceiveEvent);
//eventReceiveEvent.event.setAllDay(false, {maintainDuration: true});
this.eventService.addEvent(eventReceiveEvent);
}
};
}
}
As you can see this object contains the eventReceive() method listening for the drag and drop event. So I can drag an object from a list event component into this calendar. At the moment I have commented out this line:
eventReceiveEvent.event.setAllDay(false, {maintainDuration: true});
to avoid event duplication in my calendar (I will explain this soon).
So the when an event is dragged into my calendar it is inserted into an array of events by this service method:
addEvent(event) {
const newEvent = {id: 5, title: event.event.title, start: event.event.start, end: event.event.end};
this.events.push(newEvent);
this.eventData.next([...this.events]);
}
where eventData is defined as BehaviorSubject into my service class, in this way:
public eventData = new BehaviorSubject(this.events);
To show the event on the calendar I am using this approach (instead use the eventReceiveEvent.event.setAllDay(false, {maintainDuration: true}); because to make this color decision automatic whenever a event is added I do: whenever I will push new event through my addEvent() method of service --> my subscription in ngoninit of component will receive updated data and apply background colors displaying the event with the right color.
Ok it seems to works fine excetp the night use case (hour value equal to 23).
Here a printscreen showing the problem:
1) I insert a MORNING EVENT (starting at 07:00) and I obtain this correct behavior:
2) I insert a AFTERNOON EVENT (starting at 15:00) and I obtain this correct behavior:
3) I insert now a NIGHT EVENT (from 23:00) and I obtain this absolutly strange behavior:
As you can see the problem is that the event is dublicated, in particualar:
The BLACK background event was correctly added into the subscription function defined into the ngOnInit() (this is the correct one).
The BLUE background event (that have not to be added) is added by:
eventReceive: (eventReceiveEvent) => {
console.log(eventReceiveEvent);
//eventReceiveEvent.event.setAllDay(false, {maintainDuration: true});
this.eventService.addEvent(eventReceiveEvent);
}
And I can't understand why !!! At the beginning of the post I say that I removed this line from the **eventReceive() method:
eventReceiveEvent.event.setAllDay(false, {maintainDuration: true});
this because, if enabled, also the morning and afternoon event will have the same duplication behavior.
What is the problem? What is wrong with my code? How can I try to fix it?
Resolved the issue :-
Update your addEvent method to :-
addEvent(event) {
console.log(event.event.end);
const newEvent = {id: 5, title: event.event.title, start: event.event.start, end: event.event.end};
event.event.remove();
this.events.push(newEvent);
this.eventData.next([...this.events]);
}
I have added a call to remove the dropped event. Got the reference of remove method from :- https://fullcalendar.io/docs/Event-remove
Adding screenshot to show, that it works :-
Reason of issue :- in this case there became two duplicate events, because when you started at any time with duration of 2 hours which ended up from current date to next date and pushed it into events array. these two became different events in this case. because dropped one was having different attributes then newly created one. which didn't happen in other cases.
After above solution even if you keep
eventReceiveEvent.event.setAllDay(false, {maintainDuration: true});
as uncommented, it will not lead to issue. because everytime my code will remove dropped event and because we are pushing new event to events array, only pushed one will display. dropped one will be removed.

NgbModal opens very slowly for *some* modal windows

I'm using ng-bootstrap#6.0.0. Some modal dialogs are opening very quickly, while others are opening extremely slowly (sometimes taking 15 seconds to open).
The slow behavior is: The modal window shows with a maximum width regardless of the size selected, but has not modal-body content (yet). Approximately 15 seconds later, the modal window resizes to the requested size, and then populates with the modal-body as expected.
I have compared the modal components that work fine with the modal components that don't, and see very few differences aside from functionality differences like you would expect.
I've tried changing the "backdrop", I've tried setting the "container", I've tried converting the modal between an in-line ng-template and a separate component. Nothing is changing the behavior of the slow modals.
Does anyone have any ideas?
I am using ngx-simplemde, and launching a modal from a custom toolbar item in the markdown editor. For some reason, when launching a modal, popover, or tooltip from within the markdown editor, it behaves very slowly. I had to abstract my logic out of the custom ngx-simplemde toolbar button and trigger the modal being opened from outside the ngx-simplemde custom toolbar item. This was fairly difficult to figure out, as I had to create a hidden button, and then trigger the hidden button using the DOM's .click() method, and the ngClick of the button was setup to initiate the modal popup. Not ideal, fairly hacky, but it works now.
component.html
<simplemde [(ngModel)]="value" [options]="{ toolbar: toolbar }" (ngModelChange)="valueChange.emit($event)" #smde></simplemde>
<button type="button" class="btn btn-primary" (click)="insertImage()" style="display: none" #buttonElement>Test</button>
component.ts
createImageListToolbar() {
return {
name: 'image-list',
className: 'fas fa-images',
title: this.imageListButtonTitle,
// This action is called when the user clicks the button
// It will open the imageListModal that is embedded in the HTML of this component
// When the modal closes, the user will have selected the image they want inserted
action: async () => {
this.buttonEle.nativeElement.click();
}
};
}
async insertImage() {
const image = await this.modalService.getMediaSelection(this.implementationGuideId, this.mediaReferences);
const doc = this.simplemde.Instance.codemirror.getDoc();
const cursor = doc.getCursor();
const altTag = image.title ? `alt="${image.title}" ` : '';
const replaceText = `<table><tr><td><img src="${image.name}" ${altTag}/></td></tr></table>`;
doc.replaceRange(replaceText, cursor);
}
modal.service.ts:
async getMediaSelection(implementationGuideId?: string, mediaReferences?: MediaReference[]): Promise<ImageItem> {
const modalRef = this.modalService.open(MediaSelectionModalComponent, { container: 'body', backdrop: 'static' });
modalRef.componentInstance.implementationGuideId = implementationGuideId;
modalRef.componentInstance.mediaReferences = mediaReferences;
return await modalRef.result;
}
This could be caused by change detection issues.
Try to open modal in change detection zone.
Inject Zone:
constructor(
private modalService: NgbModal,
private zone: NgZone,
) { }
and wrap modalService.open with zone.run:
confirmDelete(user: DSUser): void {
this.zone.run(() => {
const modalRef = this.modalService.open(ConfirmModalComponent, {
backdrop: true,
centered: true,
}).componentInstance;
modalRef.message = `<strong>Are you sure you want to delete <span class="text-primary">"${user.name.firstName} ${user.name.lastName}"</span> profile?</strong>`;
modalRef.confirmCallback = () => this.deleteUser.emit(user);
});
}

Angular not detecting changes in ngFor in a new window from a postMessage

Background: my main page opens up an external window (same origin) of another module within my project upon button click.
I also set up a BroadcastChannel so that these two windows can now communicate. Now, if this window is already open & the user clicks the triggering button once again, I want to communicate this to the window:
onAddNewFieldClick() {
if (this.window === null) {
this.window = window.open(window.location.origin + '/wizard', 'Field Wizard', 'resizable,scrollbar');
this.channel = new BroadcastChannel('edit-spec-wizard-channel');
} else {
this.channel.postMessage(1);
}
}
The new window listens on this channel and appends the message data to an array that is used in an ngFor. To be extra safe. I go ahead and create a brand new array each time a new value is pushed to cause a rebind. Here is the logic that powers the component in the new window.
export class EntryComponent implements OnInit, OnDestroy {
newFieldChannel: BroadcastChannel;
newFields: number[] = [];
constructor() { }
ngOnInit() {
this.newFieldChannel = new BroadcastChannel('edit-spec-wizard-channel');
this.newFieldChannel.onmessage = this.newFieldChannelOnMessage.bind(this);
this.newFields.push(1);
}
func() {
this.newFields.push(1);
this.newFields = this.newFields.slice();
}
private newFieldChannelOnMessage(event: MessageEvent) {
this.newFields.push(event.data as number);
this.newFields = this.newFields.slice();
}
ngOnDestroy() {
this.newFieldChannel.close();
}
}
And here is the template HTML:
<div class="row">
<div class="col" *ngFor="let newField of newFields">
<div style="width: 300px; height: 600px; background-color: white;">
NEW FIELD BOX
</div>
</div>
<button class="btn btn-primary" (click)="func()">Click me</button>
</div>
I have also included a button that triggers a function ("func()") that has the exact same logic as the postMessage handler.
Now, when I click the button in this window, I'll get the expected behavior: The correct number of "NEW FIELD BOX" divs will appear in this new window. However, when I press the original button from the main screen that posts a message over the BroadcastChannel, it WILL NOT update the UI to display the right number of "NEW FIELD BOX" divs. Using break points I can see that the array newFields does contain the right number of values, but ngFor does not re-render.
Example: I click the button on the main page that fires the onAddNewFieldClick(). It opens a new window which has one "NEW FIELD BOX" div. I click this button again which posts a message to add another. Still, only one remains on the window. I now click the button within the window that fires the function "func()." This will now render 3 "NEW FIELD BOX" divs (the original one from initialization, the one from the post message that didn't render, and the one from clicking this button).
Any ideas why change detection doesn't seem to happen from a postMessage?
The newFieldChannelOnMessage event handler is probably running outside of the Angular zone and does not trigger change detection. Try wrapping the code in NgZone.run():
import { NgZone } from "#angular/core";
constructor(private ngZone: NgZone) { ... }
private newFieldChannelOnMessage(event: MessageEvent) {
this.ngZone.run(() => {
this.newFields.push(event.data as number);
});
}

JavaScript Listener - Angular 2 zone

I have a JavaScript Listener within a Google Maps object:
map.data.addListener('click', (event) => {
var mapElement = event.feature.getProperty('type');
switch(mapElement){
case 'cameras':
var cameraID = event.feature.getProperty('trafficID');
this.cameraService.createCamera(cameraID);
break; ...
This 'map' Listener calls a function within a service called 'cameraService' which initialises the camera. I have a DOM object which uses Angular 2's ngIf directive to determine whether it exists:
<div *ngIf="cameraService.camera1 != null">
<app-camera></app-camera>
</div>
My problem: ngIf doesn't run after the click event happens. Console logs show that the camera object is created, and if I click on another element (outside of the map), the view updates (ngIf change detection starts up again).
I assume this issue to do with the Angular 2 zone's - so when I click on the Google Map, it is outside of the Angular 2 zone and when I click on another element (within Angular 2), I re-enter the Angular 2 zone [Correct me if I am wrong!]... How do I manually trigger ngIf or re-enter the Angular 2 zone at the end of the click event to restart the Angular 2 change detection?
You can either use zone.run() to force the execution back into Angulars zone or you can invoke change detection manually:
constructor(private zone:NgZone, private cdRef:ChangeDetectorRef) {}
map.data.addListener('click', (event) => {
this.zone.run(() => {
var mapElement = event.feature.getProperty('type');
switch(mapElement){
case 'cameras':
var cameraID = event.feature.getProperty('trafficID');
this.cameraService.createCamera(cameraID);
break; ...
});
}
or
map.data.addListener('click', (event) => {
var mapElement = event.feature.getProperty('type');
switch(mapElement){
case 'cameras':
var cameraID = event.feature.getProperty('trafficID');
this.cameraService.createCamera(cameraID);
this.cdRef.detectChanges(); // <<<=== added
break; ...
}

Categories

Resources