Handling select event from Google Charts in Angular v2 - events

I am using Angular v2 (2.0.0-beta-1) and displaying a simple chart using Google Charts.
import {Component, View} from "angular2/core";
import {Http, HTTP_PROVIDERS} from "angular2/http";
import {OnInit, OnDestroy} from 'angular2/core';
declare let io: any;
declare let google: any;
#Component({
selector:'default',
viewProviders: [HTTP_PROVIDERS]
})
#View({
templateUrl: 'app/default/default.html'
})
export class DefaultPage implements OnInit, OnDestroy {
charttitle: string;
data: any;
options: any;
timerToken: any;
chart: any;
socket: any;
constructor(http: Http) {
}
ngOnInit() {
console.log("onInit");
this.charttitle = "Sample Graph using live data";
this.options = {
title: "My Daily Activities",
is3D: true
};
this.socket = io();
this.socket.on("data_updated", (msg) => {
this.data = new google.visualization.DataTable();
this.data.addColumn('string', 'Task');
this.data.addColumn('number', 'Hours per Day');
this.data.addRows(5);
let data = JSON.parse(msg).activityData;
for (let i = 0; i < data.length; i++) {
let act = data[i];
this.data.setCell(i, 0, act.act);
this.data.setCell(i, 1, act.value);
}
this.chart.draw(this.data, this.options);
});
this.chart = new google.visualization.PieChart(document.getElementById('chart_div'));
google.visualization.events.addListener(this.chart, 'select', this.mySelectHandler);
}
mySelectHandler() {
console.trace();
console.log("Chart: " + this);
//let selectedItem = this.chart.getSelection()[0];
/*if (selectedItem) {
let value = this.data.getValue(selectedItem.row, 0);
console.log("The user selected: " + value);
}*/
}
ngOnDestroy() {
console.log("onDestroy");
this.socket.disconnect();
}
}
The problem I have is the following line.
google.visualization.events.addListener(this.chart, 'select', this.mySelectHandler);
The event is registered is when an element on the pie chart is selected the actual event handler is fired. But all the Angular JS 2 scope variables referenced by this aren't in scope. It's as if the Google Chart visualization library is running in its own scope.
I know that Angular JS has the Angular-Charts directive but we cannot use that as the company wants to use Angular v2 only.
Is there a way I can get the Google Charts API to 'bubble' an event to the event handler running on the scope of the Angular component.

If you want that your mySelectHandler takes part within the Angular2 context / change detection, you could leverage NgZone, as described below. This way, the changes you make in this function will update the view accordingly.
import {NgZone} from 'angular2/core';
export class DefaultPage implements OnInit, OnDestroy {
constructor(private ngZone:NgZone) {
}
ngOnInit()
this.chart = new google.visualization.PieChart(
document.getElementById('chart_div'));
google.visualization.events.addListener(
this.chart, 'select', () => {
this.ngZone.run(() => {
this.mySelectHandler();
});
}
);
}
}
Hope that I correctly understood your question.
Thierry

Related

Openlayers 6.3.1 - rendering tilelayers

In Openlayers 6 each layer has an independent renderer (previously, all layer rendering was managed by a single map renderer and depended on a single rendering strategy - https://openlayers.org/workshop/en/webgl/meteorites.html). In my project I have more then 20 TileLayers (TileWMS), and the loading, panning, scrolling performance worse then in openlayers 5. Can I set the rendering strategy? How can I increase performance?
The tiles are loading fast, but then (after loading tiles) panning on map is slow. The GPU usage not critical (below 30%)
Angular 9 project, logic in service classes:
#Injectable({
providedIn: 'root'
})
export class EMap {
private eMap: OlMap;
public createMapObject(): void {
this.eMap = new OlMap({
layers: [],
view: new View({
projection,
resolutions: resolutionsArray,
constrainResolution: true,
enableRotation: false
}),
controls: defaultControls({
rotate: false,
attribution: false,
zoom: false
}).extend([
mousePositionControl,
scalelineControl
])
});
}
public initMap(center: Coordinate, zoom: number, target: string): void {
this.eMap.getView().setCenter(center);
this.eMap.getView().setZoom(zoom);
this.eMap.setTarget(target);
}
public addLayer(layer: TileLayer | ImageLayer | VectorLayer): void {
this.eMap.addLayer(layer);
}
}
#Injectable({
providedIn: 'root'
})
export class EMapSupportlayers extends EMapNetworklayers {
constructor(private readonly eMap: EMap) {}
public addTilelayer(networklayerInfo: NetworklayerInfo): void {
const layer: TileLayer = this.createTileLayer(tileLayerInitValues);
this.eMap.addLayer(layer);
}
private createTileLayer(tileLayerInitValues: TileLayerInitValues): TileLayer {
const tileGrid: TileGrid = new TileGrid({
extent: tileLayerInitValues.tileGridExtent,
resolutions: tileLayerInitValues.resolutions,
tileSize: tileLayerInitValues.tileSize
});
const source = new TileWMS({
url: tileLayerInitValues.url,
params: {
LAYERS: tileLayerInitValues.layerName,
FORMAT: tileLayerInitValues.layerFormat
},
tileLoadFunction: (image: any, src: string) => this.customLoader(image, src),
tileGrid
});
return new TileLayer({
visible: tileLayerInitValues.visible,
maxZoom: tileLayerInitValues.maxZoom,
minZoom: ttileLayerInitValues.minZoom,
source,
zIndex: tileLayerInitValues.zindex
});
}
private async customLoader(tile: any, sourceUrl: string): Promise<void> {
const response = await fetch(sourceUrl, {
method: 'POST',
credentials: 'include',
headers: new Headers({
Authorization: `Bearer ${...}`
}),
body: requestBody ? requestBody : null
});
const blob = await response.blob();
tile.getImage().src = URL.createObjectURL(blob);
}
}
--- 07.19.
I have created a dummy axample (Angular9, Openlayers 6.3.1):
Layers tiles are loading fast. On small screen panning is fast, but on large screen panning is slow (after loading and cacheing tiles). The performance was better in openlayers 5.
import { AfterViewInit, Component } from '#angular/core';
import TileLayer from 'ol/layer/Tile';
import Map from 'ol/Map';
import { OSM } from 'ol/source';
import View from 'ol/View';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements AfterViewInit {
ngAfterViewInit(): void {
const mapElement = document.createElement('div');
mapElement.style.cssText = 'position:absolute;width:100%;height:100%';
const layers = [];
for (let i = 0; i < 30; ++i) {
const layer = new TileLayer({
source: new OSM(),
// className: 'layer' + i => create own canvas by layers, same performance
});
layer.setOpacity(0.03);
layers.push(layer);
}
const map = new Map({
layers,
view: new View({
center: [0, 0],
zoom: 1
})
});
document.body.appendChild(mapElement);
map.setTarget(mapElement);
}
}
I have found a solution, not perfect, but the performance is better.
map.on('movestart', () => {
layers.forEach(layer => {
layer.setExtent(map.getView().calculateExtent());
});
});
map.on('moveend', () => {
layers.forEach(layer => {
layer.setExtent(undefined);
});
});
URL.createObjectURL can cause memory leaks, try
const blob = await response.blob();
const objectURL = URL.createObjectURL(blob)
tile.getImage().onload = function(){
URL.revokeObjectURL(objectURL);
};
tile.getImage().src = objectURL;
Also do any of your layers use the same WMS URL with a diiferent WMS layerName? It would be more efficient to combine them into a single OpenLayers layer with a list of WMS layer names in the LAYERS parameter.

Angular 4 How wait for loaded all images

I using ui-routing for NG4 (each section is different ui-view).
In some section I use (waypoint.js - imakewebthings.com/waypoints/) with ngZone and I need wait for load all images and videos (in all ui-view in page) to get true page height. Is it posible and if is how can I do this?
Something like addEventListener('load', ...) not working because I have got some pages (each have multiple sections (ui-view)) and it's work only for first open page.
My code:
My page container (similar for evry page)
<ui-view name="header"></ui-view>
<ui-view name="moving-car"></ui-view>
<ui-view name="aaa"></ui-view>
<ui-view name="bbb"></ui-view>
for example moving-car component:
<section class="moving-car" id="moving-car" [ngClass]="{'is-active': isActive}">
<!-- content -->
</section>
TS:
import { Component, OnInit, AfterViewInit, OnDestroy, NgZone,
ChangeDetectorRef } from '#angular/core';
declare var Waypoint: any;
import 'waypoints/lib/noframework.waypoints.js';
#Component({
selector: 'app-avensis-moving-car',
templateUrl: './avensis-moving-car.component.html',
styleUrls: ['./avensis-moving-car.component.scss']
})
export class AvensisMovingCarComponent implements OnInit, OnDestroy, AterViewInit {
constructor(
private $zone: NgZone,
private ref: ChangeDetectorRef
) {}
private waypoint: any;
public isActive: boolean = false;
ngOnInit() {}
ngAfterViewInit(): void {
const activate = () => {
this.isActive = true;
this.ref.detectChanges();
};
this.$zone.runOutsideAngular(
() => {
/* this code below I want run after loaded all image in my
subpage (not only in this component) */
this.waypoint = new Waypoint({
element: document.getElementById('moving-car'),
handler: function (direction) {
activate();
this.destroy();
},
offset: '70%'
});
}
);
}
ngOnDestroy() {
this.waypoint.destroy();
}
}
I modify my ngAfterViewInit function and now it's look like working but I not sure if this is good way to resolve my problem
ngAfterViewInit(): void {
const activate = () => {
this.isActive = true;
this.ref.detectChanges();
};
const images = document.querySelectorAll('img');
let counter = 0;
for (let i = 0; i < images.length; i++) {
images[i].addEventListener('load', () => {
console.log('image loaded');
if (++counter === images.length) {
this.$zone.runOutsideAngular(
() => {
this.waypoint = new Waypoint({
element: document.getElementById('moving-car'),
handler: function (direction) {
activate();
this.destroy();
},
offset: '70%'
});
}
);
}
}, false);
}
}

NativeScript one-way databinding doesn't update view

This seems like a pretty simple case to me, but I'm obviously missing something. I have a Model to be bound to the View. I then load the Model with an Http call. Why doesn't the View update? I thought that was the whole point of one-way binding.
I have verified that I'm getting back the data I'm expecting from the http call.
Update
I added a button to the screen and databinding will actually update the screen with the http loaded data for both fields on button push, even though the button method only sets one of the values. So either there's a bug in NativeScript or I'm not doing something incorrectly.
Update 2 Just the act of clicking the button will trigger the binding to happen. I've modified the code to have an empty tap handler, and just clicking the button makes it bind.
typescript
import { Component, ChangeDetectionStrategy, OnInit } from "#angular/core";
import { Job } from "../../shared/customer/job";
import { Http, Headers, Response } from "#angular/http";
import { Observable } from "rxjs/Rx";
#Component({
selector: "my-app",
templateUrl: "pages/job-details/job-details.html",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class JobDetailsComponent implements OnInit {
job: Job;
salesAssociateName: string = "x";
constructor(private http: Http) {
this.job = new Job();
}
ngOnInit() {
this.getJob(1234);
}
getJob(leadId: number) {
var url = "https://url-not-for-you/job?franchiseeid=48&leadid=" + leadId;
var headers = this.createRequestHeader();
this.http.get(url, { headers: headers }).map(response => response.json())
.do(data => this.setData(data[0]))
.subscribe(
() => this.success(),
(error) => this.error()
);
}
private createRequestHeader() {
let headers = new Headers();
headers.append("AuthKey","blah");
headers.append("AuthToken", "blee");
return headers;
}
setData(job) {
this.job.FullName = job["FullName"];
this.job.SalesAssociateName = job["SalesAssociateName"];
this.salesAssociateName = this.job.SalesAssociateName;
console.log("Found job for customer: " + job["FullName"]);
}
success() {
// nothing useful
}
error() {
alert("There was a problem retrieving your customer job.");
}
changeSA() {
}
}
html
<StackLayout>
<Label [text]="job.FullName"></Label>
<Label [text]="salesAssociateName"></Label>
<Button text="Push" (tap)="changeSA()"></Button>
</StackLayout>
Your code will work as expected with the default ChangeDetectionStrategy. however, you have changed the strategy to onPush
In order to make your binding work as expected in the default changeStrategy delete the following line
changeDetection: ChangeDetectionStrategy.OnPush
or change it to
changeDetection: ChangeDetectionStrategy.Default
More about the Angular-2 ChangeDetectionStrategy here and here
If you still want to use onPush instead of the default strategy then your properties should be declared as #input() and once the change is made (in your case in setData) marked with markForCheck()
The reason your binding is working when triggered from Button tap is because
application state change can be triggered by:
Events - tap, swipe,
XHR - Fetching data from a remote server
Timers - e.g. setTimeout()
For testing purposes and if someone is interested of how to implement the scenario with onPush here is a sample code:
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit, NgZone, Input } from "#angular/core";
import { Http, Headers, Response } from "#angular/http";
import { Observable as RxObservable } from "rxjs/Rx";
import "rxjs/add/operator/map";
import "rxjs/add/operator/do";
#Component({
selector: "my-app",
templateUrl: "app.component.html",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent implements OnInit {
#Input() public job: any = { salesAssociateName: "default job" };
#Input() public salesAssociateName: string = "default name";
constructor(private http: Http, private change:ChangeDetectorRef) { }
ngOnInit() {
this.getJob();
}
getJob() {
var url = "http://httpbin.org/get";
var headers = this.createRequestHeader();
this.http.get(url, { headers: headers })
.map(response => response.json())
.do(data => {
this.setData();
}).subscribe(
() => this.success(),
(error) => this.error()
);
}
private createRequestHeader() {
let headers = new Headers();
return headers;
}
setData() {
this.job.salesAssociateName = "NEW job SalesAssociateName";
this.salesAssociateName = "NEW job FullName";
this.change.markForCheck();
}
success() {
alert("success");
}
error() {
alert("There was a problem retrieving your customer job.");
}
}

NativeScript handling back button event

I am trying to handle the hardware back button in a NativeScript app. I am using NativeScript version 2.3.0 with Angular.
Here is what I have in main.ts file
// this import should be first in order to load some required settings (like globals and reflect-metadata)
import { platformNativeScriptDynamic, NativeScriptModule } from "nativescript-angular/platform";
import { NgModule,Component,enableProdMode } from "#angular/core";
import { AppComponent } from "./app.component";
import { NativeScriptRouterModule } from "nativescript-angular/router";
import { routes, navigatableComponents } from "./app.routing";
import { secondComponent } from "./second.component";
import {AndroidApplication} from "application";
#Component({
selector: 'page-navigation-test',
template: `<page-router-outlet></page-router-outlet>`
})
export class PageNavigationApp {
}
#NgModule({
declarations: [AppComponent,PageNavigationApp,secondComponent
// ...navigatableComponents
],
bootstrap: [PageNavigationApp],
providers:[AndroidApplication],
imports: [NativeScriptModule,
NativeScriptRouterModule,
NativeScriptRouterModule.forRoot(routes)
],
})
class AppComponentModule {
constructor(private androidapplication:AndroidApplication){
this.androidapplication.on("activityBackPressed",()=>{
console.log("back pressed");
})
}
}
enableProdMode();
platformNativeScriptDynamic().bootstrapModule(AppComponentModule);
I am importing application with
import {AndroidApplication} from "application";
Then in the constrouctor of appComponentModule I am registering the event for activityBackPressed and just doing a console.log.
This does not work.
What am I missing here?
I'm using NativeScript with Angular as well and this seems to work quite nicely for me:
import { RouterExtensions } from "nativescript-angular";
import * as application from "tns-core-modules/application";
import { AndroidApplication, AndroidActivityBackPressedEventData } from "tns-core-modules/application";
export class HomeComponent implements OnInit {
constructor(private router: Router) {}
ngOnInit() {
if (application.android) {
application.android.on(AndroidApplication.activityBackPressedEvent, (data: AndroidActivityBackPressedEventData) => {
if (this.router.isActive("/articles", false)) {
data.cancel = true; // prevents default back button behavior
this.logout();
}
});
}
}
}
Note that hooking into the backPressedEvent is a global thingy so you'll need to check the page you're on and act accordingly, per the example above.
import { Component, OnInit } from "#angular/core";
import * as Toast from 'nativescript-toast';
import { Router } from "#angular/router";
import * as application from 'application';
#Component({
moduleId: module.id,
selector: 'app-main',
templateUrl: './main.component.html',
styleUrls: ['./main.component.css']
})
export class MainComponent {
tries: number = 0;
constructor(
private router: Router
) {
if (application.android) {
application.android.on(application.AndroidApplication.activityBackPressedEvent, (args: any) => {
if (this.router.url == '/main') {
args.cancel = (this.tries++ > 0) ? false : true;
if (args.cancel) Toast.makeText("Press again to exit", "long").show();
setTimeout(() => {
this.tries = 0;
}, 2000);
}
});
}
}
}
Normally you should have an android activity and declare the backpress function on that activity. Using AndroidApplication only is not enough. Try this code:
import {topmost} from "ui/frame";
import {AndroidApplication} from "application";
let activity = AndroidApplication.startActivity ||
AndroidApplication.foregroundActivity ||
topmost().android.currentActivity ||
topmost().android.activity;
activity.onBackPressed = function() {
// Your implementation
}
You can also take a look at this snippet for example
As far as I know, NativeScript has a built-in support for this but it's not documented at all.
Using onBackPressed callback, you can handle back button behaviour for View components (e.g. Frame, Page, BottomNavigation).
Example:
function pageLoaded(args) {
var page = args.object;
page.onBackPressed = function () {
console.log("Returning true will block back button default behaviour.");
return true;
};
page.bindingContext = homeViewModel;
}
exports.pageLoaded = pageLoaded;
What's tricky here is to find out which view handles back button press in your app. In my case, I used a TabView that contained pages but the TabView itself handled the event instead of current page.

Angular 2: Testing [(ngModel)] two way data binding

I am trying to test the two way data binding of ngModel with the following code, but when I am running my test I always get: Expected '' to be 'test#wikitude.com', 'searchQuery property changes after text input'. Maybe it has something to do with the searchField.dispatchEvent part, but so far I couldn't figure out why the test is not changing the textContent of my displayField. The project was built with angular-cli": "1.0.0-beta.15. I tried to follow this guide but so far had no luck. Would be nice if you could help me make my test pass. I am not sure if I have to use fixture.whenStable() - as I've seen it used in the answer to another question - but I don't think that typing text into an input field is an asynchronous activity - I also implemented the sendInput() method mentioned in this question, but so far without any success.
This is my component:
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.css']
})
export class SearchComponent implements OnInit {
searchQuery: string;
active: boolean = false;
onSubmit(): void {
this.active = true;
}
}
This is my component template:
<input id="name" [(ngModel)]="searchQuery" placeholder="customer">
<h2><span>{{searchQuery}}</span></h2>
And here are is my spec:
/* tslint:disable:no-unused-variable */
import {TestBed, async, ComponentFixture, tick} from '#angular/core/testing';
import { SearchComponent } from './search.component';
import {CommonModule} from "#angular/common";
import {FormsModule} from "#angular/forms";
import {By} from "#angular/platform-browser";
describe('Component: SearchComponent', () => {
let component: SearchComponent;
let fixture: ComponentFixture<SearchComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [SearchComponent],
imports: [
CommonModule,
FormsModule
]
});
fixture = TestBed.createComponent(SearchComponent);
component = fixture.componentInstance;
});
it('should bind the search input to the searchQuery variable', () => {
const searchInputText: string = 'test#wikitude.com';
const searchField: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
const displayField: HTMLElement = fixture.debugElement.query(By.css('span')).nativeElement;
searchField.value = searchInputText;
searchField.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(displayField.textContent).toBe(searchInputText, 'searchQuery property changes after text input');
});
});
Update:
I changed my test to the following, which made it pass - as long as the input field is not inside a form tag:
/* tslint:disable:no-unused-variable */
import {TestBed, async, ComponentFixture, tick} from '#angular/core/testing';
import { SearchComponent } from './search.component';
import {CommonModule} from "#angular/common";
import {FormsModule} from "#angular/forms";
import {By} from "#angular/platform-browser";
describe('Component: SearchComponent', () => {
let component: SearchComponent;
let fixture: ComponentFixture<SearchComponent>;
function sendInput(text: string, inputElement: HTMLInputElement) {
inputElement.value = text;
inputElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
return fixture.whenStable();
}
beforeEach(done => {
declarations: [SearchComponent],
imports: [
CommonModule,
FormsModule
]
});
TestBed.compileComponents().then(() => {
fixture = TestBed.createComponent(SearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
done();
})
});
it('should bind the search input to the searchQuery variable', done => {
const searchInputText: string = 'test#wikitude.com';
const searchField: HTMLInputElement = fixture.debugElement.query(By.css('#name')).nativeElement;
sendInput(searchInputText, searchField).then(() => {
expect(component.searchQuery).toBe(searchInputText);
done();
});
});
});
Here is the updated template:
<form>
<input id="name" [(ngModel)]="searchQuery" placeholder="customer" name="name" #name="ngModel">
</form>
<h2><span>{{searchQuery}}</span></h2>
The test result I get is: Expected undefined to be 'test#wikitude.com'.

Resources