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.
I have an object which contains an array - this array can contain one or more objects of the same type as the parent one. There is no boundary on the amount of levels possible. To display all data in the current data set I have this code:
<div *ngIf="selectedUserMappings">
<ul *ngFor="let group of selectedUserMappings.childGroups">
<li style="font-weight:600;">{{group.name}}</li>
<div *ngFor="let child of group.childGroups">
<li *ngIf="child.active">{{child.name}}</li>
<div *ngFor="let n2child of child.childGroups">
<li *ngIf="n2child.active">{{n2child.name}}</li>
<div *ngFor="let n3child of n2child.childGroups">
<li *ngIf="n3child.active">{{n3child.name}}</li>
</div>
</div>
</div>
</ul>
</div>
Which is not a very good way to achieve the wanted result. Any pointers on a better approach?
Edit: From the suggestions so far, I think the way to go will be a dummy component. I need to show the outer parents with different styling in the list, which I will be able to now. But, I also need to hide top-level parents where no childs are active (all the objects have an active-boolean), as well as childs that are not active. The children of non-active childs should still be visible though. Any ideas? This is what I've got so far:
import { Component, OnInit, Input } from '#angular/core';
import { UserGroupMapping } from 'src/app/models/models';
#Component({
selector: 'list-item',
templateUrl: './list-item.component.html',
styleUrls: ['./list-item.component.scss']
})
export class ListItemComponent implements OnInit {
#Input() data;
list: Array<any> = [];
constructor() { }
ngOnInit() {
this.data.forEach(item => {
this.createList(item, true);
});
}
createList(data, parent?) {
if (parent) {
this.list.push({'name': data.name, 'class': 'parent'});
if (data.childGroups && data.childGroups.length > 0) {
this.createList(data, false);
}
} else {
data.childGroups.forEach(i => {
this.list.push({'name': i.name, 'class': 'child'});
if (i.childGroups && i.childGroups.length > 0) {
this.createList(i, false);
}
});
}
console.log(this.list);
}
}
Called from the parent component like this:
<div *ngIf="mappings">
<list-item [data]="mappings.childGroups"></list-item>
</div>
You describing a tree. Try using some sort of tree component. For example prime ng has a tree component.
I ended up scrapping the dummy component, building the lists directly in my component before showing it - so it will be easier to display changes immediately.
createSummary() {
this.listFinished = false;
this.mappings.childGroups.forEach(item => {
this.list = [];
this.createList(item, true);
this.list.forEach(i => {
if (i.active) {
this.list[0].active = true;
}
});
this.lists.push(this.list);
});
this.listFinished = true;
}
createList(data, parent?) {
if (parent) {
this.list.push({'name': data.name, 'class': 'parent', 'active': false});
if (data.childGroups && data.childGroups.length > 0) {
this.createList(data, false);
}
} else {
data.childGroups.forEach(i => {
this.list.push({'name': i.name, 'class': 'child', 'active': i.active});
if (i.childGroups && i.childGroups.length > 0) {
this.createList(i, false);
}
});
}
}
Then I show it like this:
<div *ngIf="listFinished">
<ul *ngFor="let list of lists">
<div *ngFor="let item of list">
<li [class]="item.class" *ngIf="item.active">
{{item.name}}
</li>
</div>
</ul>
</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];
hi i have array in servcie and I have a strange problem in the saveData array function is visible but in removeItems no
my service.ts
export class FormService {
myArray = []
constructor(){
}
saveData(key, value) {
this.myArray.push({
key: key,
value: value
});
//HERE IS VISIBLE and i can add may elements
console.log('myArray')
console.log(this.myArray)
}
removeItems(myIndex) {
//here is not visible is empty
console.log(this.myArray)
}
}
UPDATE
my component
export class Step3AdditionalServicesComponent implements OnInit {
constructor(private formService: FormService) {
}
ngOnInit() {
this.stepsGuard.getAccess(false);
}
addToMyArray(key,value) {
this.formService.saveData(key,value)
}
removeItemFromArray(event) {
var myIndex = event.target.id;
console.log('myIndex ' + myIndex);
this.formService.removeItems(myIndex);
}
}
my html
<button (click)="addToMyArray('key1','simple1')">Add Simle1</button>
<button (click)="addToMyArray('key2','simple2')">Add Simle2</button>
<button (click)="addToMyArray('key3','simple3')">Add Simle3</button>
<button id="1" (click)="removeItemFromArray($event)">Remove 1</button>
<button id="2" (click)="removeItemFromArray($event)">Remove 2</button>
<button id="3" (click)="removeItemFromArray($event)">Remove 3</button>
And when i click removeItemFromArray($event) i see index in console
----SOLUTION----
I declare my service in component providers and in app.module
my.component
#Component({
selector: 'app-navbar',
templateUrl: './navbar.component.html',
styleUrls: ['./navbar.component.css'],
providers: [FormService] <---delete this and will be working
})
#brijmcq Thanks for help
Most of the time, declare your service in the module level and not in the component level unless you know what you are doing. You are probably getting the wrong data because if you declare it in component level, you will always get a new instance of FormService and the myArray = [] will always be empty.
You probably know what happens next if you access an empty array by index :)
Here is how I remove and add elements in my array:
elementsArray = [];
addElementList(event) {
let element = event.data;
if (!this.elementsArray.includes(element)) {
this.elementsArray.push(element);
console.log("Elements in array = " + this.elementsArray);
}
}
removeElementList(event) {
let element = event.data;
let index = this.elementsArray.indexOf(element, 0);
if (index > -1) {
this.elementsArray.splice(index, 1);
console.log("Elements in array = " + this.elementsArray);
}
}
...for example...
<div class="month" *ngFor="#item of myCollection; #i = index">
...
</div>
Is possible to do something like...
<div class="month" *ngFor="#item of 10; #i = index">
...
</div>
...without appeal to a non elegant solution like:
<div class="month" *ngFor="#item of ['dummy','dummy','dummy','dummy','dummy',
'dummy','dummy','dummy']; #i = index">
...
</div>
?
Within your component, you can define an array of number (ES6) as described below:
export class SampleComponent {
constructor() {
this.numbers = Array(5).fill().map((x,i)=>i); // [0,1,2,3,4]
this.numbers = Array(5).fill(4); // [4,4,4,4,4]
}
}
See this link for the array creation: Tersest way to create an array of integers from 1..20 in JavaScript.
You can then iterate over this array with ngFor:
#Component({
template: `
<ul>
<li *ngFor="let number of numbers">{{number}}</li>
</ul>
`
})
export class SampleComponent {
(...)
}
Or shortly:
#Component({
template: `
<ul>
<li *ngFor="let number of [0,1,2,3,4]">{{number}}</li>
</ul>
`
})
export class SampleComponent {
(...)
}
#OP, you were awfully close with your "non-elegant" solution.
How about:
<div class="month" *ngFor="let item of [].constructor(10); let i = index">
...
</div>
Here I'm getting the Array constructor from an empty array: [].constructor, because Array isn't a recognized symbol in the template syntax, and I'm too lazy to do Array=Array or counter = Array in the component typescript like #pardeep-jain did in his 4th example. And I'm calling it without new because new isn't necessary for getting an array out the Array constructor.
Array(30) and new Array(30) are equivalent.
The array will be empty, but that doesn't matter because you really just want to use i from ;let i = index in your loop.
Edit to respond to comments:
Q. How can I use a variable to set the length of the NgFor loop?
Here is an example on how to render a table with variable columns/rows
<table class="symbolTable">
<tr *ngFor="let f of [].constructor(numRows); let r = index">
<td class="gridCell" *ngFor="let col of [].constructor(numCols); let c = index">
{{gridCards[r][c].name}}
</td>
</tr>
</table>
export class AppComponent implements OnInit {
title = 'simbologia';
numSymbols = 4;
numCols = 5;
numRows = 5;
guessCards: SymbolCard[] = [];
gridCards: SymbolCard[][] = [];
ngOnInit(): void {
for (let c = 0; c < this.numCols; c++) {
this.guessCards.push(new SymbolCard());
}
for (let r = 0; r < this.numRows; r++) {
let row: SymbolCard[] = [];
for (let c = 0; c < this.numCols; c++) {
row.push(
new SymbolCard({
name: '' + r + '_' + c
}))
}
this.gridCards.push(row);
}
}
}
No there is no method yet for NgFor using numbers instead collections,
At the moment, *ngFor only accepts a collection as a parameter, but you could do this by following methods:
Using pipe
demo-number.pipe.ts:
import {Pipe, PipeTransform} from 'angular2/core';
#Pipe({name: 'demoNumber'})
export class DemoNumber implements PipeTransform {
transform(value, args:string[]) : any {
let res = [];
for (let i = 0; i < value; i++) {
res.push(i);
}
return res;
}
}
For newer versions you'll have to change your imports and remove args[] parameter:
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({name: 'demoNumber'})
export class DemoNumber implements PipeTransform {
transform(value) : any {
let res = [];
for (let i = 0; i < value; i++) {
res.push(i);
}
return res;
}
}
html:
<ul>
<li>Method First Using PIPE</li>
<li *ngFor='let key of 5 | demoNumber'>
{{key}}
</li>
</ul>
Using number array directly in HTML(View)
<ul>
<li>Method Second</li>
<li *ngFor='let key of [1,2]'>
{{key}}
</li>
</ul>
Using Split method
<ul>
<li>Method Third</li>
<li *ngFor='let loop2 of "0123".split("")'>{{loop2}}</li>
</ul>
Using creating New array in component
<ul>
<li>Method Fourth</li>
<li *ngFor='let loop3 of counter(5) ;let i= index'>{{i}}</li>
</ul>
export class AppComponent {
demoNumber = 5 ;
counter = Array;
numberReturn(length){
return new Array(length);
}
}
#Working demo
I couldn't bear the idea of allocating an array for plain repeat of components, so I've written a structural directive. In simplest form, that doesn't make the index available to the template, it looks like this:
import { Directive, Input, TemplateRef, ViewContainerRef } from '#angular/core';
#Directive({ selector: '[biRepeat]' })
export class RepeatDirective {
constructor( private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef) { }
#Input('biRepeat') set count(c:number) {
this.viewContainer.clear();
for(var i=0;i<c;i++) {
this.viewContainer.createEmbeddedView(this.templateRef);
}
}
}
http://plnkr.co/edit/bzoNuL7w5Ub0H5MdYyFR?p=preview
you can also use like that
export class SampleComponent {
numbers:Array<any> = [];
constructor() {
this.numbers = Array.from({length:10},(v,k)=>k+1);
}
}
HTML
<p *ngFor="let i of numbers">
{{i}}
</p>
This can also be achieved like this:
HTML:
<div *ngFor="let item of fakeArray(10)">
...
</div>
Typescript:
fakeArray(length: number): Array<any> {
if (length >= 0) {
return new Array(length);
}
}
Working Demo
Use a pipe to transform the number to an array.
#Pipe({
name: 'enumerate',
})
export class EnumeratePipe implements PipeTransform {
transform(n: number): number[] {
return [...Array(n)].map((_,i) => i);
}
}
Then use the pipe in your template.
<p *ngFor="let i of 5 | enumerate">
Index: {{ i }}
</p>
https://stackblitz.com/edit/angular-ivy-pkwvyw?file=src/app/app.component.html
I solved it like this using Angular 5.2.6 and TypeScript 2.6.2:
class Range implements Iterable<number> {
constructor(
public readonly low: number,
public readonly high: number,
public readonly step: number = 1
) {
}
*[Symbol.iterator]() {
for (let x = this.low; x <= this.high; x += this.step) {
yield x;
}
}
}
function range(low: number, high: number) {
return new Range(low, high);
}
It can be used in a Component like this:
#Component({
template: `<div *ngFor="let i of r">{{ i }}</div>`
})
class RangeTestComponent {
public r = range(10, 20);
}
Error checking and assertions omitted on purpose for brevity (e.g. what happens if step is negative).
You can use lodash:
#Component({
selector: 'board',
template: `
<div *ngFor="let i of range">
{{i}}
</div>
`,
styleUrls: ['./board.component.css']
})
export class AppComponent implements OnInit {
range = _.range(8);
}
I didn't test code but it should work.
<div *ngFor="let number of [].constructor(myCollection)">
<div>
Hello World
</div>
</div>
This is a nice and quick way to repeat for the amount of times in myCollection.
So if myCollection was 5, Hello World would be repeated 5 times.
My-component.ts
numbers: number[] = [];
constructor() {
this.numbers = new Array<number>(10)
}
My-component.html
<div *ngFor="let num of numbers; let i = index">{{ i }}</div>
Since the fill() method (mentioned in the accepted answer) without arguments throw an error, I would suggest something like this (works for me, Angular 7.0.4, Typescript 3.1.6)
<div class="month" *ngFor="let item of items">
...
</div>
In component code:
this.items = Array.from({length: 10}, (v, k) => k + 1);
Using custom Structural Directive with index:
According Angular documentation:
createEmbeddedView Instantiates an embedded view and inserts it into this container.
abstract createEmbeddedView(templateRef: TemplateRef, context?: C, index?: number): EmbeddedViewRef.
Param Type Description
templateRef TemplateRef the HTML template that defines the view.
context C optional. Default is undefined.
index number the 0-based index at which to insert the new view into this container. If not specified, appends the new view as the last entry.
When angular creates template by calling createEmbeddedView it can also pass context that will be used inside ng-template.
Using context optional parameter, you may use it in the component,
extracting it within the template just as you would with the *ngFor.
app.component.html:
<p *for="number; let i=index; let c=length; let f=first; let l=last; let e=even; let o=odd">
item : {{i}} / {{c}}
<b>
{{f ? "First,": ""}}
{{l? "Last,": ""}}
{{e? "Even." : ""}}
{{o? "Odd." : ""}}
</b>
</p>
for.directive.ts:
import { Directive, Input, TemplateRef, ViewContainerRef } from '#angular/core';
class Context {
constructor(public index: number, public length: number) { }
get even(): boolean { return this.index % 2 === 0; }
get odd(): boolean { return this.index % 2 === 1; }
get first(): boolean { return this.index === 0; }
get last(): boolean { return this.index === this.length - 1; }
}
#Directive({
selector: '[for]'
})
export class ForDirective {
constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) { }
#Input('for') set loop(num: number) {
for (var i = 0; i < num; i++)
this.viewContainer.createEmbeddedView(this.templateRef, new Context(i, num));
}
}
Please find attached my dynamic solution if you want to increase the size of an array dynamically after clicking on a button (This is how I got to this question).
Allocation of necessary variables:
array = [1];
arraySize: number;
Declare the function that adds an element to the array:
increaseArrayElement() {
this.arraySize = this.array[this.array.length - 1 ];
this.arraySize += 1;
this.array.push(this.arraySize);
console.log(this.arraySize);
}
Invoke the function in html
<button md-button (click)="increaseArrayElement()" >
Add element to array
</button>
Iterate through array with ngFor:
<div *ngFor="let i of array" >
iterateThroughArray: {{ i }}
</div>
A simplest way that i have tried
You can also create an array in your component file and you can call it with *ngFor directive by returning as an array .
Something like this ....
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-morning',
templateUrl: './morning.component.html',
styleUrls: ['./morning.component.css']
})
export class MorningComponent implements OnInit {
arr = [];
i: number = 0;
arra() {
for (this.i = 0; this.i < 20; this.i++) {
this.arr[this.i]=this.i;
}
return this.arr;
}
constructor() { }
ngOnInit() {
}
}
And this function can be used in your html template file
<p *ngFor="let a of arra(); let i= index">
value:{{a}} position:{{i}}
</p>
Here is something quite clean and simple for Angular:
In .ts:
max = 10;
In .html:
<div *ngFor="let dummy of ','.repeat(max).split(','); index as ix">
- {{ix + 1}}:
</div>
I think the simple and short solution is here..
Your my.component.ts file
setArrayFromNumber(i: number) {
return new Array(i);
}
Your my.component.html file
<li *ngFor='let in of setArrayFromNumber(5); let i = index'>{{ i }}</li>
That's it!!