I am using Azure Maps and am trying to create a reusable library around their maps web sdk. I want to provide a way for a user to pass in their own popup, along with an array strongly typed objects, to add markers to the page. I have the map and the markers working, but am stuck on how to pass data to the popup. Currently I have:
az-map.component.html
<div class="map-wrapper">
<div id="az-map"></div>
</div>
<ng-template #defaultMarker>
<div>
<img alt="" src="https://maps.gstatic.com/mapfiles/api-3/images/spotlight-poi2.png" />
</div>
</ng-template>
<ng-template #defaultPopup>
<div>
<h2>~name~</h2>
</div>
</ng-template>
<div style="display:none">
<div id="marker" #marker>
<ng-container *ngTemplateOutlet="markerTemplate ? markerTemplate : defaultMarkerTemplate"></ng-container>
</div>
<div id="popup" #popup>
<ng-content select="[popupTemplate]"></ng-content>
</div>
</div>
az-map.component.ts
#Component({
selector: 'az-map',
templateUrl: './azure-map.component.html',
styleUrls: ['./azure-map.component.scss']
})
export class AzureMapComponent implements OnInit {
#Input() locations: Location[];
#Input() mapConfig: AzureMapConfig;
#Input() markerTemplate: TemplateRef<any>;
#ViewChild('defaultMarker') defaultMarkerTemplate: TemplateRef<any>;
#ViewChild("popup") popupElement: ElementRef;
#ViewChild("marker") marker: ElementRef;
/* These are the map components we can interact with */
public map: any;
public popup: atlas.Popup;
public symbolLayer: atlas.layer.SymbolLayer;
private dataSource: atlas.source.DataSource;
constructor(private configService: ConfigurationService) { }
ngOnInit() {
const centerPosition: any = this.mapConfig.center.length
? this.mapConfig.center
: ( this.locations && this.locations.length > 0 ? [this.locations[0].longitude, this.locations[0].latitude] : [-95, 38]);
this.map = new atlas.Map('az-map', {
center: centerPosition,
zoom: this.mapConfig.zoom,
authOptions: {
authType: atlas.AuthenticationType.subscriptionKey,
subscriptionKey: this.configService.get().azureMapKey
}
});
this.map.events.add('ready', () => {
this.dataSource = new atlas.source.DataSource();
this.map.sources.add(this.dataSource);
this.addMarkers();
this.symbolLayer = new atlas.layer.SymbolLayer(this.dataSource, null)
this.map.layers.add(this.symbolLayer);
if (this.mapConfig.showControls) {
this.map.controls.add(new atlas.control.ZoomControl(), {
position: "top-right"
});
}
if(this.mapConfig.showPopups){
this.popup = new atlas.Popup({
pixelOffset: [0, -18],
closeButton: false
});
const _this = this;
this.map.events.add('mouseover', this.symbolLayer, (e) => {
if (e.shapes && e.shapes.length > 0) {
var coordinate;
const popupHtml = _this.popupElement.nativeElement.innerHTML;
var props = e.shapes[0].getProperties();
coordinate = e.shapes[0].getCoordinates();
_this.popup.setOptions({
//Update the content of the popup.
content: popupHtml,
//Update the popup's position with the symbol's coordinate.
position: coordinate
});
//Open the popup.
_this.popup.open(_this.map);
}
});
this.map.events.add('mouseleave', this.symbolLayer, (e) => {
this.popup.close()
});
}
});
}
private addMarkers(): void {
const html = this.marker.nativeElement.innerHTML;
this.locations.forEach(l => {
const point = new atlas.Shape( new atlas.data.Point([l.longitude, l.latitude]));
this.dataSource.add(point);
});
}
}
and an example of it being used here:
<h1>Account Coverage</h1>
<div class="map-wrapper col-12">
<az-map [mapConfig]="mapConfig" [locations]="locations" class="col-12 h-50">
<coverage-popup popupTemplate></coverage-popup>
</az-map>
</div>
This works and shows the popup, but I need to find a way to pass data to the CoveragePopupComponent in the mouseover event in the az-maps.component.ts
One method we tried to use, which works well when you only have a single popup type that needs to be handled was to use the ComponentFactoryResolver like this:
const factory = this.componentFactoryResolver.resolveComponentFactory(PopupComponent);
const component = factory.create(this.injector);
component.instance.machine = shape.getProperties();
component.changeDetectorRef.detectChanges();
const popupContent = component.location.nativeElement;
this.map.setCamera({
center: shape.getCoordinates(),
type: 'ease',
});
this.popup.setOptions({
position: shape.getCoordinates(),
content: popupContent,
pixelOffset: [0, -20]
});
this.popup.open(this.map);
but then you always have to use the PopupComponent type and couldn't use the ng-content stuff we have in the map template.
Related
I have been having this problem for the past week, I have searched everywhere but wasn't able to find a problem.
My service
private texst!: Posts[];
public getPosts(): Observable<Posts[]>
{
return this.http.get<Posts[]>("http://localhost/projects/php_rest_api/api/post/read.php").pipe(map((data) => {
return this.texst = data;
}));
}
My Component, here i add the service and run the function to get the data from my database
public test: Posts[] = [];]
constructor(public postService: PostsService,
private http: HttpClient) {}
ngOnInit(): void {
this.getPosts();
}
public getPosts()
{
this.postService.getPosts().subscribe((response) => {
this.test = response;
console.log(this.test);
})
}
My html
<div>
<button (click)="getPosts()"></button>
<div *ngFor="let test of test">
{{test.title}}
</div>
</div>
Change this, rename your var test to testItem because it's already used:
<div>
<button (click)="getPosts()"></button>
<div *ngFor="let testItem of test">
{{testItem.title}}
</div>
</div>
Managed to fix it
changed the response to object.values in my getPosts() function
public getPosts()
{
this.postService.getPosts().subscribe((response) => {
this.test = Object.values(response);
console.log(this.test);
})
}
It means you're trying to iterate over object, but you think it's an array.
If that image is a log of the response, you better do:
public getPosts()
{
this.postService.getPosts().subscribe((response) => {
this.test = response.data; // <.. this changes!
})
}
and in template:
<div *ngFor="let testItem of test">
{{testItem.title}}
</div>
I want to bind the data that i get from my JSON request to the *NgFor to be able to read it out, This is how the data looks like that i get in:
{Id: null, Code: "9348892084", HasInfo: true, Info: Array(26)}
HasInfo:true
Info:
Array(26)
0:{ProductId: 32631, Name: "JNOOS", Image: "http://sd-m-mg", …}
1:{ProductId: 32969, Name: "SWIFT", Image: "http://sd-33087.jpg",…}
2:{ProductId: 32570, Name: "GABIX", Image: "http://sd-c7273.jpg", …}
3:{ProductId: 32473, Name: "MISMA", Image: "http://sd-mt8-8343e4d95.jpg", …}
I was working with AngularJS before and there i made the request as such:
$scope.getAll{
$http({
method: 'Get',
url: "http://URL/" + Code
})
.success(function (data) {
$scope.All = data.Info;
})
No i am moving to Angular5 and i would like to get the same array of information bind to the :
<div *ngFor="let x of info">
{{ x.Name }}
</div>
How would i adjust the below to get the same as above?
export class AppComponent {
readonly ROOT_URL = 'http://url/content/'
constructor(private http: HttpClient) {}
ngOnInit() {
this.getData()
}
getData() {
this.http.get(this.ROOT_URL + 'Getscope')
.subscribe(
data => {
var test = data;
// var info = data.Info = not valid!;
// var test can't be approached by *ngFor
console.log(test);
console.log(test.info);
//$scope.info = data.Info;
//this.info = data;
}, error => {
console.error("HTTP FAILURE ERROR!!!");
}
)
};
}
Also the json output has an array inside an object, do i say this correct?
From your code you are using info in html but not assigning to any class variable,
Take a public variable public info; and assign data using this.info = data.Info
Component:
export class AppComponent {
readonly ROOT_URL = 'http://url/content/'
public info;
constructor(private http: HttpClient) {}
ngOnInit() {
this.getData()
}
getData() {
this.http.get(this.ROOT_URL + 'Getscope')
.subscribe(
data => {
this.info = data['Info']; // here is the change
}, error => {
console.error("HTTP FAILURE ERROR!!!");
}
)
};
}
Your HTML can be same:
<div *ngFor="let x of info">
{{ x.Name }}
</div>
The simplest solution would be to use an async pipe. This way you do not need to subscribe to the stream returned by http.get.
app.component.ts
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';
export class AppComponent {
readonly url = 'http://url/content/'
constructor(private http: HttpClient) {}
info$: Observable<any[]>;
ngOnInit() {
this.info$ = this.http.get(this.url + 'Getscope')
.pipe(map(resp => resp.Info));
}
}
app.component.html
<div *ngFor="let x of info$ | async">
{{ x.Name }}
</div>
I want to have a filter based on active-states from available checkboxes.
First everything should gets displayed, after a filter is selected, in this case a selection of hero names should only display heroes which contains atleast the name.
The interesting part is that: If I try to change the value back to the "full"-object it is not taking the complete object but an altered version of that.
I do not know what is happening there. Because I only initialized the full-object in the constructor of the app. With full-object I mean fullHeroes.
App.Component.ts :
import { Component, OnInit } from '#angular/core';
interface Hero {
name: string;
}
#Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
readonly fullHeroes: Hero[] = [];
heroes: Hero[] = [];
constructor() {
this.heroes.push(
{ name: 'Bob' }, { name: 'Alice' }, { name: 'Mallory' }
);
this.fullHeroes = this.heroes.slice(0);
}
filter(name, checked) {
if (checked) {
for (let i = 0; i <= this.heroes.length; i++) {
let found = false;
const currentHero = this.heroes[i];
if (currentHero && currentHero.name === name) {
found = true;
}
if (found) {
this.heroes.splice(i, 1);
}
}
} else {
this.heroes = [];
this.heroes = this.fullHeroes;
}
return;
}
}
App.component.html :
<div class="container">
<h1>World of Heroes</h1>
<p>Filter your Heroes based on names!</p>
<div class="filter">
<form #heroForm="ngForm">
<fieldset>
<legend>Choose the names</legend>
<div class="form-check" *ngFor="let hero of heroes">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" [name]="hero.name" (change)="filter($event.target.name, $event.target.checked)"> {{hero.name}}
</label>
</div>
</fieldset>
</form>
</div>
<hr>
<h2>Results:</h2>
<div class="row result-list">
<div class="col-md-4 hero" *ngFor="let hero of heroes">
<h3>Name: {{hero.name}}</h3>
</div>
</div>
</div>
If you want to make a clone for the array this.fullHeroes into the array this.heroes, use:
this.heroes = JSON.parse(JSON.stringify(this.fullHeroes));
This way you will do a full copy of the array this.fullHeroes into this.heroes one, and then changes in this.heroes will not affect the other array.
this.heroes = this.fullHeroes
After you execute that line, every time you mutate heroes, you also mutate fullHeroes, since they both refer to the same array. You need to make a copy of fullHeroes.
Note that you're abusing map() where you should be using forEach(). And you're using a loop where you could use some().
You can use the TypeScript spread operator to clone the array:
const a1 = [1, 2, 3, 4, 5, 6];
const a2 = [...a1];
So, I'm trying to write a unit test for my component. My component is a dynamically created list, and when a user clicks certain items, I'd like my test to validate that the component generates outputs correctly.
To test this, I wrote a demo component that creates my component within its template, and then attempted to get TestComponentBuilder to create this demo component and click items on the lists. Unfortunately, my unit test cases can't find my component's html elements to click them. I can find the demo component's elements, but it seems like the test is having trouble accessing my nested component.
My question is:
Can I get this approach to work?
Or is there a better way to unit test my component's inputs and outputs?
Any help would be appreciated. Here's what I'm doing:
<== UPDATED per Gunter's Answer ==>
CustomListComponent.ts (My component):
import {Component, EventEmitter} from "angular2/core";
import {CustomListItem} from "./CustomListItem";
#Component({
selector: 'custom-list-component',
inputs: ['itemClass', 'items'],
outputs: ['onChange'],
template: `
<ul class="list-unstyled">
<li *ngFor="#item of items" [hidden]="item.hidden">
<div [class]="itemClass" (click)="onItemClicked(item)">
{{item.text}}
</div>
</li>
</ul>
`
})
export class CustomListComponent {
itemClass: String;
items: CustomListItem[];
onChange: EventEmitter<CustomListItem>;
constructor() {
this.items = new Array<CustomListItem>();
this.onChange = new EventEmitter();
}
onItemClicked(item: CustomListItem): void {
let clone = new CustomListItem(item.text, item.hidden);
this.onChange.emit(clone);
}
}
CustomListItem.ts:
export class CustomListItem {
text: String;
hidden: boolean;
constructor(text: String, hidden: boolean) {
this.text = text;
this.hidden = hidden;
}
}
CustomListSpec.ts (Here's where I'm struggling):
import {CustomListComponent} from './CustomListComponent';
import {ComponentFixture, describe, expect, fakeAsync, inject, injectAsync, it, TestComponentBuilder} from 'angular2/testing'
import {Component} from "angular2/core";
import {CustomListItem} from "./CustomListItem";
import {By} from "angular2/src/platform/dom/debug/by";
describe('CustomListComponent', () => {
var el;
var dropdownToggleBtn, dropdownListEl;
var regularListEl;
it('Event output properly when list item clicked', injectAsync([TestComponentBuilder], (tcb) => {
return createComponent(tcb).then((fixture) => {
console.log("el (" + el + ")"); // => [object HTMLDivElement]
console.log("dropdownToggleBtn (" + dropdownToggleBtn + ")"); // => [object HTMLButtonElement]
console.log("dropdownListContainer(" + dropdownListContainer + ")"); // => [object HTMLDivElement]
console.log("dropdownListEl(" + dropdownListEl + ")"); // => [object HTMLUListElement]
//console.log("regularListEl (" + regularListEl+ ")");
//...further testing...
});
}));
function createComponent(tcb: TestComponentBuilder): Promise<ComponentFixture> {
return tcb.createAsync(CustomListComponentDemo).then((fixture) => {
fixture.detectChanges();
el = fixture.debugElement.nativeElement;
dropdownToggleBtn = fixture.debugElement.query(By.css(".btn")).nativeElement;
dropdownListContainer = fixture.debugElement.query(By.css(".dropdown-menu")).nativeElement;
dropdownListEl = dropdownListContainer.children[0].children[0];
//regularListEl = fixture.debugElement.query(By.css(".list-unstyled")); //TIMES OUT unfortunately. Maybe because there are 2.
return fixture;
})
}
});
#Component({
directives: [CustomListComponent],
template: `
<div class="btn-group" dropdown>
<button id="dropdown-toggle-id" type="button" class="btn btn-light-gray" dropdownToggle>
<i class="glyphicon icon-recent_activity dark-green"></i> Dropdown <span class="caret"></span>
</button>
<div class="dropdown-menu" role="menu" aria-labelledby="dropdown-toggle-id">
<custom-list-component id="dropdown-list-id"
[items]="dropdownListItems" [itemClass]="'dropdown-item'"
(onChange)="onDropdownListChange($event)">
</custom-list-component>
</div>
<span class="divider"> </span>
</div>
<custom-list-component id="regular-list-id"
[items]="regularListItems"
(onChange)="onRegularListChange($event)">
</custom-list-component>
`
})
class CustomListComponentDemo {
dropdownListItems: CustomListItem[];
regularListItems: CustomListItem[];
constructor() {
//intialize the item lists
}
onDropdownListChange(item: CustomListItem): void {
//print something to the console logs
}
onRegularListChange(item: CustomListItem): void {
//print something to the console logs
}
}
Call detectChanges() before you query dynamically created elements. They are not created before change detection has been run.
function createComponent(tcb: TestComponentBuilder): Promise<ComponentFixture> {
return tcb.createAsync(CustomListComponentDemo).then((fixture) => {
fixture.detectChanges();
el = fixture.debugElement.nativeElement;
regularListEl = fixture.debugElement.query(By.css(".list-unstyled"));
dropdownListEl = fixture.debugElement.query(By.css(".dropdown-menu")).nativeElement;
dropdownToggleBtn = fixture.debugElement.query(By.css(".btn")).nativeElement;
return fixture;
})
}
Im retrieving a list of objects from a service in Angular2.
On click I want to put the object from my ngFor onto a new array (with the same interface).
I'm able to display the list with items just fine. I'm also able to display the item I selected in the console, but I'm not able to push this object into a new array with the same interface.
My interface:
export interface Item {
id: string,
name: string,
icon_url: string,
exterior: string,
type: string,
tag: string,
addedClass: string,
selected: boolean
}
My component:
import {Component, OnInit, DoCheck} from 'angular2/core';
import {Item} from './../interfaces/interfaces.ts';
import {ItemService} from './../services/item.service.ts';
#Component({
template: `
<h2>Add item</h2>
<div class="itemlist">
<ul class="items">
<li
*ngFor="#item of items"
[class.selected]="item === selectedItem"
(click)="onSelect(item)"
>
<span class="item">
{{item.name}}
</span>
</li>
</ul>
</div>
`
})
export class AddTradeComponent implements OnInit {
public items: Item[];
public selectedItems: Item[];
constructor(private _itemService: ItemService) { }
getUserItems() {
this._itemService.getUserItems().then(userItems => this.items = userItems);
}
ngOnInit() {
this.getUserItems();
}
onSelect(item) {
console.log('items ', item);
this.selectedItems.push(item);
}
}
Errors that I'm getting:
Error during evaluation of "click"
TypeError: Cannot read property 'push' of undefined
Logging the object works fine, I just cannot push it onto this.selectedItems.
You didn't initialize your arrays. Either initialize the arrays when you declare your member variables:
public items: Item[] = [];
public selectedItems: Item[] = [];
Or initialize them in your constructor:
constructor() {
this.items = [];
this.selectedItems = [];
}