I have Component which has a member array variable. This array is bind to DOM with *ngFor. When I add new variable to array my view changes accordingly. Array holds tab names and initially it is set to have only 1 tab. When I refresh page array reinitialized which is what I was expecting. But when I logout and then log back in(router navigation) I see all previous tabs. It is weird to me, because if I console.log(myTabs) array has only 1 element(homeTab).
UPDATE:
.html
<div style="display: table-caption" id="notify-tabs">
<ul class="nav nav-tabs" role="tablist" id="nav-bar">
<li role="presentation" data-toggle="tab" id="homeTab" [class.active]="activeTab==='homeTab'"><a (click)="setValues('home')">Home</a>
<li role="presentation" *ngFor="let tab of myTabs" data-toggle="tab" id={{tab}} [class.active]="activeTab===tab.toString()"><a (click)="setValues(tab)">{{tab}}</a>
</ul>
</div>
.component.ts
#Component({
selector: 'notify-homepage',
templateUrl: 'app/home/home.component.html',
styleUrls: ['styles/css/bootstrap.min.css', 'styles/home.css'],
directives: [DynamicComponent, TileComponent, MapComponent, HeaderComponent, ConversationComponent, ROUTER_DIRECTIVES]
})
export class HomeComponent{
public myTabs: number[] = [21442];
public activeTab: string = 'homeTab';
ngOnInit() {
//Assume fully operating MapService here
this.subscription = this.mapService.conversationId.subscribe(
(id: number) => {
this.myTabs.push(id);
this.setValues(id);
this.activeTab = id.toString();
})
}
ngOnDestroy() {
this.subscription.unsubscribe();
...
}
}
map.service.ts
#Injectable()
export class MapService {
private conversationIdSource = new ReplaySubject<number>();
public conversationId = this.conversationIdSource.asObservable();
...
showConversation(id: number) {
this.conversationIdSource.next(id);
}
}
The answer of #Andrei works, but in my opinion there's a better and more elegant solution.
Just use a combination of #ViewChild() and setters.
For example:
// component.html
<ng-el ... #myElement>
// component.ts
#ViewChild('myElement') set(el) {
if (el) {
console.log('element loaded!');
}
}
Check Lifecycle hooks:
OnChanges https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#onchanges
DoCheck https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#docheck
They help tracking changing in Input and local variables.
OnChanges for Input variables:
ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
for (let propName in changes) {
let chng = changes[propName];
let cur = JSON.stringify(chng.currentValue);
let prev = JSON.stringify(chng.previousValue);
this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
}
}
DoCheck for everything:
ngDoCheck() {
if (this.hero.name !== this.oldHeroName) {
this.changeDetected = true;
this.changeLog.push(`DoCheck: Hero name changed to "${this.hero.name}" from "${this.oldHeroName}"`);
this.oldHeroName = this.hero.name;
}
}
Related
Component A has 2 components inside:
<div class="classStyleLeft">
<app-picture [IdSectionPicture]="2" [PData]="modelPage"></app-picture>
</div>
<div class="classStyleRight">
<app-list [IdSectionList]="2" [PData]="modelPage"></app-list>
</div>
In the component app-list, select an item from the list and click the method showInfo:
<ul *ngFor="let value of PData.content[IdSectionList].content; let valueIndex = index;">
<li (click)="showInfo(IdSectionList, valueIndex)">
<b>{{value.value}}</b>
</li>
</ul>
I want the method parameters showInfo they were transferred to the component app-picture. Component content app-picture:
<div>{{PData.content[IdSectionList].value[valueIndex].value}}</div>
I do not know how to do it.
I would ask for help.
Thank you
Sharing the data using service won't be enough as they events has to be listened to.
Since it is a sibling component, create a service with BehaviorSubject as:
export class InfoService{
private infoSubject = new BehaviorSubject<any>({id: '',value: ''});
showInfoEventEmitter(data){
this.infoSubject.next(data);
}
showInfoEventListener(){
return this.infoSubject.asObservable();
}
}
and then use it in component as
app-list.component
constructor(public infoSvc: InfoService){}
showInfo(IdSectionList, valueIndex){
this.infoSvc.showInfoEventEmitter({id:IdSectionList, value: valueIndex})
}
and in app-picture.component
eventSubscription: Subscription;
constructor(public infoSvc: InfoService){}
ngOnInit(){
this.eventSubscription = this.infoSvc.showInfoEventListener().subscribe(res => {
console.log(res); // you have the event here
})
}
ngOnDestroy(){
this.eventSubscription.unsubscribe(); // to stop listening to this event after component is destroyed
}
I have a page with three components:
1. Products list component which gets some products as input and display them.
2. Filters component which displays some filters list i.e. (size, colour,...) and also display the added filters.
3. Main component which is the root component
Let say a user adds 1 filter which fires a http request to get new filtered products and while the request is pending he removes the added filter which fires another http request to fetch all the products
How to cancel the first request so we don't display the filtered products?
Here is my code:
class FiltersService {
private _filters: any[];
get filters() {
return this._filters;
}
addFilter(filter) {
this._filters.push(filter);
}
removeFilter(filter) {
// Remove filter logic ...
}
}
class DataService_ {
constructor(private http: HttpClient) {
}
getProducts(filters) {
return this.http.post<any[]>('api/get-products', filters)
}
}
#Component({
selector: 'app-main',
template: `
<div>
<app-filters [filtersChanged]="onFiltersChange()"></app-filters>
<app-products-list [products]="products"> </app-products-list>
</div>
`
})
class MainComponent {
products: any[];
constructor(private dataService: DataService_, private filtersService: FiltersService) {
}
ngOnInit() {
this.setProducts()
}
setProducts() {
let filters = this.filtersService.filters;
this.dataService.getProducts(filters)
.subscribe(products => this.products = products)
}
onFiltersChange() {
this.setProducts();
}
}
#Component({
selector: 'app-filters',
template: `
<div>
Filters :
<ul>
<li *ngFor="let filter of filters" (click)="addFilter(filter)"> {{ filter.name }}</li>
</ul>
<hr>
Added Filters:
<ul>
<li *ngFor="let filter of filtersService.filters"> {{ filter.name }} <button (click)="removeFilter(filter)"> Remove</button></li>
</ul>
</div>
`
})
class FiltersComponent {
filters = [{ name: 'L', tag: 'size' }, { name: 'M', tag: 'size' }, { name: 'White', tag: 'colour' }, { name: 'Black', tag: 'colour' }]
#Output() filtersChanged = new EventEmitter()
constructor(public filtersService: FiltersService) {
}
addFilter(filter) {
const isAdded = this.filtersService.filters.find(x => x.name === filter.name);
if (isAdded) return;
this.filtersService.addFilter(filter);
this.filtersChanged.emit()
}
removeFilter(filter) {
this.filtersService.remove(filter);
this.filtersChanged.emit()
}
}
#Component({
selector: 'app-products-list',
template: `
<div>
<h1>Products</h1>
<ul *ngIf="products.length">
<li *ngFor="let product of products">
{{product.name }}
</li>
</ul>
</div>
`
})
class ProductsListComponent {
#Input() products
constructor() {
}
}
Long story short:
Easiest way to handle such situations is by using the switchMap operator. What this does is cancel the internal subscription as soon as a new event comes along.
One implementation would be:
class MainComponent {
products: any[];
private _filters$ = new Subject();
constructor(private dataService: DataService_, private filtersService: FiltersService) {
}
ngOnInit() {
this.setProducts()
}
setProducts() {
this._filters$
.switchMap((filters)=> this.dataService.getProducts(filters)) // or .let(switchMap...) if you are using rxjs >5.5
.subscribe(products => this.products = products);
}
onFiltersChange() {
this._filters$.next(this.filtersService.filters);
}
}
Long story:
What happens here is:
When you change filter the onFilterChange is triggered. You then emit the latest filters (inside this.filtersService.filters) through the _filters$ Subject (a subject is almost identical to an EventEmitter).
Back in time during component initialization the ngOnInit method has called setProducts, which has subscribed to the _filters$ subject for future events (none has happened at this point). When an event arrives on _filters$ then we trigger the getProducts method of dataservice, passing it the filters that where contained in the event. We will be waiting on this line until the http call has completed. As soon as it completes the result of the http call will be assigned to the products of the component.
If while we are waiting for the http response to get back, onFiltersChange is fired again, then a new event will arive at the switchMap and it will cancel the previous http request so that it can handle the new event.
This is a very powerful approach as changing a single operator, you can easily change the behavior of your app. For instance, changing switchMap to concatMap will make the request wait for the previous one to complete (will happen serially). Changing it to flatMap will have the same behaviour as the original code you posted (http requests will happen as soon as filters change, without affecting previous ones, order of responses will not predictable) and so on.
Note : to cancel the request just use unsubscribe.
For exmple
const course$ = this.service$.getCourses(`/api/courses`).subscribe(courses => { console.log(courses) }
setTimeout(() => course$.unsubscribe(),1000) // cancel the request
I need to create new list item(value from api)on button press but don't know how to do it. Any help please?
here is the code:
<ul>
<li *ngFor="let joke of jokes">{{joke.value}}</li>
</ul>
<button (click)="loadMore">more jokes</button>
`,
providers: [RandomService]
})
export class PocetnaComponent {
jokes: Joke[];
constructor(private jokesService: RandomService){
this.jokesService.getRandomJokes().subscribe(jokes => {this.jokes =
[jokes]});
}
loadMore(){
this.jokes.push();
}
}
interface Joke{
id: number;
value: string;
}
here is the service:
#Injectable()
export class RandomService {
constructor(private http: Http){
console.log('working');
}
getRandomJokes(){
return this.http.get('https://api.chucknorris.io/jokes/random')
.map(res => res.json());
}
}
Just push an empty object
this.jokes.push({});
or if its going to be hooked up to a modal
Create a class and push that
Class IJoke {
id: number;
value: string;
constructor(){
}
}
this.jokes.push(new IJoke());
Or if you want to push from an API
#Injectable()
export class RandomService {
constructor(private http: Http){
console.log('working');
}
getRandomJokes(){
return this.http.get('https://api.chucknorris.io/jokes/random')
.map(res => res.json());
}
getNextJoke(){
return this.http.get('https://api.chucknorris.io/jokes/next')
.map(res => res.json());
}
}
Directive
loadMore(){
this.jokesService.getNextJoke().subscribe(joke => {
this.jokes.push(joke);
});
}
I'm not sure if you load some random jokes and you want to load one more, or if you want to keep loading random jokes. If the later, you will want to take out the next function, and instead init your jokes array and keep pushing/applying to it. like so
jokes: Joke[] = new Array();
constructor(private jokesService: RandomService){
this.jokesService.getRandomJokes().subscribe(jokes => {
this.jokes.push(jokes)
});
You have a few problems...
You have this interface:
interface Joke{
id: number;
value: string;
}
what you are receiving is much more properties, so you'd need to pick the properties you want:
getRandomJokes(){
return this.http.get('https://api.chucknorris.io/jokes/random')
.map(res => res.json());
// pick the properties you want/need
.map(joke => <Joke>{id: joke.id, value: joke.value})
}
Then you have problems in the subscribe, you should push the data to your jokes array and not do:
.subscribe(jokes => {this.jokes = [jokes]})
but:
.subscribe(joke => this.jokes.push(joke)}
notice above that I named this (joke => this.jokes.push(joke)) to make it clearer that you are actually just receiving one joke.
Also I would remove the request from the constructor, we have the OnInit hook for this. Also I would apply the request in a separate function, so that it's easy to call when you want to retrieve new jokes and also therefore reuse the function, so something like this:
ngOnInit() {
this.getJoke()
}
getJoke() {
this.jokesService.getRandomJokes()
.subscribe(joke => {
this.jokes.push(joke)
})
}
So then in your template just call getJoke when you want to retrieve a new joke:
<ul>
<li *ngFor="let joke of jokes">{{joke.value}}</li>
</ul>
<button (click)="getJoke()">more jokes</button>
Here's a DEMO
I'm writing Angular 2 application and inside it I have dropdown menu written on Bootstrap
<li class="dropdown" dropdown>
<a class="dropdown-toggle" data-toggle="dropdown">
User <span class="caret"></span>
</a>
<ul class="dropdown-menu" aria-labelledby="download">
<li><a routerLink="/user/profile">My Profile</a></li>
<li><a (click)="logout()">Log Out</a></li>
</ul>
</li>
All what I want is to write down a small directive for toggling menu. End here is it:
#Directive({
selector: "[dropdown]"
})
export class DropdownDirective implements OnInit {
private isOpen = false;
private defaultClassName: string;
#HostListener('click') toggle() {
let that = this;
if (!this.isOpen) {
this.elRef.nativeElement.className = this.defaultClassName + " open";
document.addEventListener("click", () => {
that.elRef.nativeElement.className = that.defaultClassName;
that.isOpen = false;
document.removeEventListener("click");
});
this.isOpen = !this.isOpen;
}
}
constructor(private elRef: ElementRef) {
}
ngOnInit(): void {
this.defaultClassName = this.elRef.nativeElement.className;
}
}
Looks good. But doesn't work. After short debug I found that event listener, which was added to the document, fires just after it has been assigned.
document.addEventListener("click", () => {
that.elRef.nativeElement.className = that.defaultClassName;
that.isOpen = false;
document.removeEventListener("click");
});
As a fact menu closing just after it has been opened. How to fix it and why this happening?
I've solved this same situation with a #HostListener(). On the component holding the dropdown:
#HostListener('document:click', ['$event'])
private clickAnywhere(event: MouseEvent): void {
if (this.IsSelected && !this.elementRef.nativeElement.contains(event.target)) {
this.IsSelected = false;
}
}
this.IsSelected is the binding property I use to show the dropdown.
The condition in the if() is checking whether the user has clicked on the menu or the document body in general.
Make sure to inject elementRef into the constructor so you can access the rendered HTML to check if that is what was clicked:
public constructor(private elementRef: ElementRef) { }
You can find out more about HostListener here.
I'm working on an Application with a lot of dropdowns, I would like to be able to close the dropdown whenever a click happens outside of this one.
I found some good solutions, but none of them handle the case of having a ngFor in it, when I log the click event target in the ngFor, I get the element but this one doesn't have any parent. I can not detect it with 'find' or 'contains' neither.
Does someone have a solution to detect if this target is part of the dropdown ?
the directive
import {
Directive,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
SimpleChange
} from '#angular/core';
#Directive({selector: '[clickOutside]'})
export class ClickOutside implements OnInit {
#Output() clickOutside:EventEmitter<Event> = new EventEmitter<Event>();
constructor(private _el:ElementRef) {
this.onClickBody = this.onClickBody.bind(this);
}
ngOnInit() {
document.body.addEventListener('click', this.onClickBody);
}
private onClickBody(e:Event) {
if (!this.isClickInElement(e)) {
this.clickOutside.emit(e);
}
}
private isClickInElement(e:any):boolean {
var current = e.target;
do {
console.log(current);
if ( current === this._el.nativeElement ) {
return( true );
}
current = current.parentNode;
} while ( current );
return false;
}
}
Example of where I call the directive
<div (clickOutside)="onClickedOutside($event)">
<ul>
<li *ngFor="let item of itemsList" (click)="selectItem(item)">
<span class="item">
{{item.name}}
</span>
</li>
</ul>
</div>
When I click on item.name, console.log(current); returns me two lines
<span>Item</span>
<li>
<span>Item</span>
</li>
#Directive({selector: '[clickOutside]'})
export class ClickOutside implements OnInit {
#Output() clickOutside:EventEmitter<Event> = new EventEmitter<Event>();
constructor(private _eref: ElementRef) { }
#HostListener('window:click')
private onClickBody(e:Event) {
if (!this.isClickInElement(e)) {
this.clickOutside.emit(e);
}
}
private isClickInElement(e:any):boolean {
return this._eref.nativeElement.contains(event.target);
}
}
See also https://stackoverflow.com/a/35713421/217408
This solution works with Chrome but unfortunately not with IE. I'm still looking for another way to do it
private isClickInElement(e:any):boolean {
var current = e.target;
if(current == this._el.nativeElement) {
return true;
}
for(let parentKey in e.path) {
if(e.path[parentKey] == this._el.nativeElement) {
return true;
}
}
return false;
}