I am trying to get started with angular 2.0, now I was wondering how I can initiate an update to the view after some external event changed data.
In details I have a google map and a handler for a click-event on the map. After the user clicks on the map I store latitude and longitude of the click in to variables on the controller
this.lat = event.latLng.lat();
this.lon = event.latLng.lon();
In the view I want to display these values
<div> this is my spot: {{lat}} and {{lon}} </div>
In angular 1 I would simply wrap the assignment in the controller in a call to $scope.$apply().
What is the preferred way to go about updating views in angluar 2.0 ?
Try to import ChangeDetectorRef from angular core as follow
import {Component, ChangeDetectorRef} from '#angular/core';
in constructor
constructor(private chRef: ChangeDetectorRef){
chRef.detectChanges(); //Whenever you need to force update view
}
Mostly, you don't need to do anything to update the view. zone.js will do everything for you.
But if for some reason you want to fire change detection manually (for example if your code is running outside of an angular zone) you can use LifeCycle::tick() method to do it. See this plunker
import {Component, LifeCycle, NgZone} from 'angular2/angular2'
#Component({
selector: 'my-app',
template: `
<div>
Hello world!!! Time: {{ time }}.
</div>
`
})
export class App {
time: number = 0;
constructor(lc: LifeCycle, zone: NgZone) {
zone.runOutsideAngular(() => {
setInterval(() => {
this.time = Date.now();
lc.tick(); // comment this line and "time" will stop updating
}, 1000);
})
}
doCheck() {
console.log('check', Date.now());
}
}
setTimeout(function(){
//whatever u want here
},0)
ref : http://blog.mgechev.com/2015/04/06/angular2-first-impressions/
Related
Here is a stackBlitz demo.
I have a d3.js service file that builds my svg layout. Its a force directed d3 graph that has nodes. Each node carries its own data.
I have extracted that data into an array, capturing the ids of the nodes when selected. In my example, to select a node and capture the ID a user needs to hold/press Ctrl then click on a node.
This is done in a d3 .on click function within my angular service file.
Service.ts
export class DirectedGraphExperimentService {
public idArray = []
_update(_d3, svg, data): any {
...
svg.selectAll('.node-wrapper').on('click', function () {
if (_d3.event.ctrlKey) {
d3.select(this).classed(
'selected',
!d3.select(this).classed('selected')
);
const selectedSize = svg.selectAll('.selected').size();
if (selectedSize <= 2) {
svg
.selectAll('.selected')
.selectAll('.nodeText')
.style('fill', 'blue');
this.idArray = _d3.selectAll('.selected').data();
return this.idArray.filter((x) => x).map((d) => d.id);
}
}
});
...
}
}
My global variable this.idArray = [] does not update with the id strings therefore cant pass the array to the component like this this.directedGraphExperimentService.idArray its always [] empty.
component.ts
import { Component, OnInit, ViewChild, ElementRef, Input } from '#angular/core';
import { DirectedGraphExperimentService } from './directed-graph-experiment.service';
#Component({
selector: 'dge-directed-graph-experiment',
template: `
<style>
.selected .nodeText{
fill:red;
}
</style>
<body>
<button (click)="passValue()">Pass Value</button>
<svg #svgId width="500" height="700"><g [zoomableOf]="svgId"></g></svg>
</body>
`,
})
export class DirectedGraphExperimentComponent implements OnInit {
#ViewChild('svgId') graphElement: ElementRef;
constructor(
private directedGraphExperimentService: DirectedGraphExperimentService
) {}
ngOnInit() {}
#Input()
set data(data: any) {
this.directedGraphExperimentService.update(
data,
this.graphElement.nativeElement
);
}
passValue() {
console.log(this.directedGraphExperimentService.idArray); // returns []
}
}
I've also tried emitting an event with the value via the <svg></svg> container in the component template.
Is there another way I can get the updated this.array values into the parent component? A way to subscribe to the value in the function? Perhaps with a behaviourSubject from rxjs?
Here is a stackBlitz demo. In this demo you will see I have added a button that I press to trigger the update to my component file, obviously its only passing the empty global variable on my service. To add to the array you will need to press Ctrl and click on a node, you will see in console the array filling up(max of 2 string).
The ngAfterViewInit lifecycle hook is not being called for a Component that is transcluded into another component using <ng-content> like this:
<app-container [showContent]="showContentContainer">
<app-input></app-input>
</app-container>
However, it works fine without <ng-content>:
<app-input *ngIf="showContent"></app-input>
The container component is defined as:
#Component({
selector: 'app-container',
template: `
<ng-container *ngIf="showContent">
<ng-content></ng-content>
</ng-container>
`
})
export class AppContainerComponent {
#Input()
showContentContainer = false;
#Input()
showContent = false;
}
The input component is defined as:
#Component({
selector: 'app-input',
template: `<input type=text #inputElem />`
})
export class AppInputComponent implements AfterViewInit {
#ViewChild("inputElem")
inputElem: ElementRef<HTMLInputElement>;
ngAfterViewInit() {
console.info("ngAfterViewInit fired!");
this.inputElem.nativeElement.focus();
}
}
See a live example here: https://stackblitz.com/edit/angular-playground-vqhjuh
There are two issues at hand here:
Child components are instantiated along with the parent component, not when <ng-content> is instantiated to include them. (see https://github.com/angular/angular/issues/13921)
ngAfterViewInit does not indicate that the component has been attached to the DOM, just that the view has been instantiated. (see https://github.com/angular/angular/issues/13925)
In this case, the problem can be solved be addressing either one of them:
The container directive can be re-written as a structural directive that instantiates the content only when appropriate. See an example here: https://stackblitz.com/edit/angular-playground-mrcokp
The input directive can be re-written to react to actually being attached to the DOM. One way to do this is by writing a directive to handle this. See an example here: https://stackblitz.com/edit/angular-playground-sthnbr
In many cases, it's probably appropriate to do both.
However, option #2 is quite easy to handle with a custom directive, which I will include here for completeness:
#Directive({
selector: "[attachedToDom],[detachedFromDom]"
})
export class AppDomAttachedDirective implements AfterViewChecked, OnDestroy {
#Output()
attachedToDom = new EventEmitter();
#Output()
detachedFromDom = new EventEmitter();
constructor(
private elemRef: ElementRef<HTMLElement>
) { }
private wasAttached = false;
private update() {
const isAttached = document.contains(this.elemRef.nativeElement);
if (this.wasAttached !== isAttached) {
this.wasAttached = isAttached;
if (isAttached) {
this.attachedToDom.emit();
} else {
this.detachedFromDom.emit();
}
}
}
ngAfterViewChecked() { this.update(); }
ngOnDestroy() { this.update(); }
}
It can be used like this:
<input type=text
(attachedToDom)="inputElem.focus()"
#inputElem />
If you check the console of your stackblitz, you see that the event is fired before pressing any button.
I can only think of that everything projected as will be initialized/constructed where you declare it.
So in your example right between these lines
<app-container [showContent]="showContentContainer">
{{test()}}
<app-input></app-input>
</app-container>
If you add a test function inside the app-container, it will get called immediatly. So <app-input> will also be constructed immediatly. Since ngAfterVieWInit will only get called once (https://angular.io/guide/lifecycle-hooks), this is where it will be called already.
adding the following inside AppInputComponent is a bit weird however
ngOnDestroy() {
console.log('destroy')
}
the component will actually be destroyed right away and never initialized again (add constructor or onInit log to check).
I have two components : TileComponent.ts and FullScreenComponent.ts.
On clicking the TileComponent, the FullScreenComponent opens up. In the TileComponent,I have the following code. ngOnInit() method gets triggered whenever the TileComponent loads.
TileComponent.ts:
ngOnInit() {
console.log("TileCompnent :ngOnInit");
this.crossDomainService.globalSelectors.subscribe(selectors => {
globalCountries = selectors.jurisdiction || [];
this.getArticles(globalCountries);
});
// Multiple Language
this.crossDomainService.globalLanguage.subscribe(() => {
console.log("TileCompnent :ngOnInit : crossDomainService");
this.getArticles(globalCountries || countries);
});
}
Now on closing the FullScreenComponent leads to the loading of the TileComponent but this time I see that ngOnInit() method is not getting triggered.
Can anyone help me to know any reason this is not working?
tile.component.html:
<div class="carousel-inner">
<a
(click)="openFullScreen(article)"
*ngFor="let article of articles"
[ngClass]="getItemClassNames(article)"
class="item"
>
</div>
tile.component.ts
ngOnInit() {
console.log("TileCompnent :ngOnInit");
const countries =
this.crossDomainService.initialGlobalSelectors &&
this.crossDomainService.initialGlobalSelectors.jurisdiction.length
? this.crossDomainService.initialGlobalSelectors.jurisdiction
: [];
this.getArticles(countries);
let globalCountries;
this.crossDomainService.globalSelectors.subscribe(selectors => {
globalCountries = selectors.jurisdiction || [];
this.getArticles(globalCountries);
});
// Multiple Language
this.crossDomainService.globalLanguage.subscribe(() => {
console.log("TileCompnent :ngOnInit : crossDomainService");
this.getArticles(globalCountries || countries);
});
}
openFullScreen(article: ArticlePreview) {
this.crossDomainService.openFullScreen(article);
}
full-screen.component.html:
<div class="layout-center-wrapper" [hidden]="isPolicyShown">
<app-header></app-header>
<div class="row wrapper">
<app-sidebar></app-sidebar>
<router-outlet></router-outlet>
</div>
</div>
<app-policy [hidden]="!isPolicyShown"></app-policy>
header.component.html:
<header class="row header">
<p class="header__title">
Application Name
<a (click)="closeFullScreen()" class="header__close">
<span class="icon icon_close"></span>
</a>
</p>
</header>
header.component.ts:
import { Component } from '#angular/core';
import { CrossDomainService } from '../../core';
#Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.less']
})
export class HeaderComponent {
constructor(private crossDomainService: CrossDomainService, private analyticsService: AnalyticsService) {}
closeFullScreen() {
this.crossDomainService.closeFullScreen();
}
}
ngOnInit lifecycle is only run when the view of the component is first rendered.
Since the old Tile Component is not destroyed and is always in the background even when the FullScreenComponent is displayed, the lifecycle hook never gets triggered even when you close the component.
( I am assuming you are not using the router to navigate, but use it as a popup since there is a close button as shown in the question )
Cannot help you isolate the issue or help you with suggestions unless you share some code. But the reason for ngOnInit not firing as per the question is because the component is not re-created.
Update :
I still can't realise why you need to trigger the ngOnInit ? If you just want to execute the code inside, make it a separate function say initSomething then call it inside ngOnInit to execute it the first time. Now if you just invoke this function on crossDomainService.closeFullScreen you get the desired effect.
To trigger the function whenever the closeFullScreen is called, you can create a Subject in the crossDomainService Service, and subscribe this subject it inside the ngOnInit(), and run the initSomething function mentioned above everytime it emits a value. Inside the closeFullScreen function, all you have to now do is do a Subject.next()
Pardon the brevity since I am away from my desk and typing on mobile, though the explanation should be enough to develop the code on your own.
One of the simple workaround would be to use changedetectorRef to hook up the initial state of component.
`import { Component, OnInit, ChangeDetectorRef } from '#angular/core';`
and insert it in constructor and you can keep OnInit function blank
constructor(){
this.changeDetectorRef.detectChanges();}
I'm new to rxjs, and I'm developing an angular multiselect list component that should render a long list of values (500+).
I'm rendering the list based on an UL, I'm iterating over an observable that will render the LI's.
I'm thinking about my options to avoid impacting the performance by rendering all the elements at once. But I don't know whether this is possible or not, and if it's possible what is the best operator to use.
The proposed solution:
On init I load all the data into an Observable. (src) and I'll take 100 elements from it and will put them on the target observable (The one that will be used to render the list)
Everytime that the user reaches the end of the list (the scrollEnd event fires) I'll load 100 elements more, until there are no more values in the src observable.
The emission of new values in the target observable will be triggered by the scrollEnd event.
Find my code below, I still need to implement the proposed solution, but I'm stuck at this point.
EDIT: I'm implementing #martin solution, but I'm still not able to make it work in my code. My first step was to replicate it in the code, to get the logged values, but the observable is completing immediately without producing any values.
Instead of triggering an event, I've added a subject. Everytime the scrollindEnd output emits, I will push a new value to the subject. The template has been modified to support this.
multiselect.component.ts
import { Component, AfterViewInit } from '#angular/core';
import { zip, Observable, fromEvent, range } from 'rxjs';
import { map, bufferCount, startWith, scan } from 'rxjs/operators';
import { MultiSelectService, ProductCategory } from './multiselect.service';
#Component({
selector: 'multiselect',
templateUrl: './multiselect.component.html',
styleUrls: ['./multiselect.component.scss']
})
export class MultiselectComponent implements AfterViewInit {
SLICE_SIZE = 100;
loadMore$: Observable<Event>;
numbers$ = range(450);
constructor() {}
ngAfterViewInit() {
this.loadMore$ = fromEvent(document.getElementsByTagName('button')[0], 'click');
zip(
this.numbers$.pipe(bufferCount(this.SLICE_SIZE)),
this.loadMore$.pipe(),
).pipe(
map(results => console.log(results)),
).subscribe({
next: v => console.log(v),
complete: () => console.log('complete ...'),
});
}
}
multiselect.component.html
<form action="#" class="multiselect-form">
<h3>Categories</h3>
<input type="text" placeholder="Search..." class="multiselect-form--search" tabindex="0"/>
<multiselect-list [categories]="categories$ | async" (scrollingFinished)="lazySubject.next($event)">
</multiselect-list>
<button class="btn-primary--large">Proceed</button>
</form>
multiselect-list.component.ts
import { Component, Input, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'multiselect-list',
templateUrl: './multiselect-list.component.html'
})
export class MultiselectListComponent {
#Output() scrollingFinished = new EventEmitter<any>();
#Input() categories: Array<string> = [];
constructor() {}
onScrollingFinished() {
this.scrollingFinished.emit(null);
}
}
multiselect-list.component.html
<ul class="multiselect-list" (scrollingFinished)="onScrollingFinished($event)">
<li *ngFor="let category of categories; let idx=index" scrollTracker class="multiselect-list--option">
<input type="checkbox" id="{{ category }}" tabindex="{{ idx + 1 }}"/>
<label for="{{ category }}">{{ category }}</label>
</li>
</ul>
NOTE: The scrollingFinished event is being triggered by the scrollTracker directive that holds the tracking logic. I'm bubbling the event from multiselect-list to the multiselect component.
Thanks in advance!
This example generates an array of 450 items and then splits them into chunks of 100. It first dumps the first 100 items and after every button click it takes another 100 and appends it to the previous results. This chain properly completes after loading all data.
I think you should be able to take this and use to for your problem. Just instead of button clicks use a Subject that will emit every time user scrolls to the bottom:
import { fromEvent, range, zip } from 'rxjs';
import { map, bufferCount, startWith, scan } from 'rxjs/operators';
const SLICE_SIZE = 100;
const loadMore$ = fromEvent(document.getElementsByTagName('button')[0], 'click');
const data$ = range(450);
zip(
data$.pipe(bufferCount(SLICE_SIZE)),
loadMore$.pipe(startWith(0)),
).pipe(
map(results => results[0]),
scan((acc, chunk) => [...acc, ...chunk], []),
).subscribe({
next: v => console.log(v),
complete: () => console.log('complete'),
});
Live demo: https://stackblitz.com/edit/rxjs-au9pt7?file=index.ts
If you're concerned about performance you should use trackBy for *ngFor to avoid re-rendering existing DOM elements but I guess you already know that.
Here is a live demo on Stackblitz.
If your component subscribes to an observable holding the whole list to be displayed, your service will have to hold this whole list and send a new one every time an item is added. Here is an implementation using this pattern. Since lists are passed by reference, each list pushed in the observable is simply a reference and not a copy of the list, so sending a new list is not a costly operation.
For the service, use a BehaviorSubject to inject your new items in your observable. You can get an observable from it using its asObservable() method. Use another property to hold your current list. Each time loadMore() is called, push the new items in your list, and then push this list in the subject, which will push it in the observable as well, and your components will rerender.
Here I am starting with a list holding all items (allCategories), every time loadMore() is called, a block of 100 items if taken and placed on the current list using Array.splice():
#Injectable({
providedIn: 'root'
})
export class MultiSelectService {
private categoriesSubject = new BehaviorSubject<Array<string>>([]);
categories$ = this.categoriesSubject.asObservable();
categories: Array<string> = [];
allCategories: Array<string> = Array.from({ length: 1000 }, (_, i) => `item #${i}`);
constructor() {
this.getNextItems();
this.categoriesSubject.next(this.categories);
}
loadMore(): void {
if (this.getNextItems()) {
this.categoriesSubject.next(this.categories);
}
}
getNextItems(): boolean {
if (this.categories.length >= this.allCategories.length) {
return false;
}
const remainingLength = Math.min(100, this.allCategories.length - this.categories.length);
this.categories.push(...this.allCategories.slice(this.categories.length, this.categories.length + remainingLength));
return true;
}
}
Then call the loadMore() method on your service from your multiselect component when the bottom is reached:
export class MultiselectComponent {
categories$: Observable<Array<string>>;
constructor(private dataService: MultiSelectService) {
this.categories$ = dataService.categories$;
}
onScrollingFinished() {
console.log('load more');
this.dataService.loadMore();
}
}
In your multiselect-list component, place the scrollTracker directive on the containing ul and not on the li:
<ul class="multiselect-list" scrollTracker (scrollingFinished)="onScrollingFinished()">
<li *ngFor="let category of categories; let idx=index" class="multiselect-list--option">
<input type="checkbox" id="{{ category }}" tabindex="{{ idx + 1 }}"/>
<label for="{{ category }}">{{ category }}</label>
</li>
</ul>
In order to detect a scroll to bottom and fire the event only once, use this logic to implement your scrollTracker directive:
#Directive({
selector: '[scrollTracker]'
})
export class ScrollTrackerDirective {
#Output() scrollingFinished = new EventEmitter<void>();
emitted = false;
#HostListener("window:scroll", [])
onScroll(): void {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight && !this.emitted) {
this.emitted = true;
this.scrollingFinished.emit();
} else if ((window.innerHeight + window.scrollY) < document.body.offsetHeight) {
this.emitted = false;
}
}
}
Hope that helps!
I need to use the swipeup/swipedown gestures in an Ionic 2 application. When I do
<div (swipe)='someFunction($event)'></div>
Then my someFunction(e) is being called, but only on horizontal slides -- therefore I'm unable to listen to swipes in up and down directions. (swipeup) and (swipedown) seem not to do anything at all. Do you have any idea whether this is possible at all with the Ionic beta?
Ionic 2 makes use hammerjs library to handle its gestures.
They’ve also built their own Gesture class that effectively acts as a wrapper to hammerjs: Gesture.ts.
So you can do your own directive like:
import {Directive, ElementRef, Input, OnInit, OnDestroy} from 'angular2/core'
import {Gesture} from 'ionic-angular/gestures/gesture'
declare var Hammer: any
/*
Class for the SwipeVertical directive (attribute (swipe) is only horizontal).
In order to use it you must add swipe-vertical attribute to the component.
The directives for binding functions are [swipeUp] and [swipeDown].
IMPORTANT:
[swipeUp] and [swipeDown] MUST be added in a component which
already has "swipe-vertical".
*/
#Directive({
selector: '[swipe-vertical]' // Attribute selector
})
export class SwipeVertical implements OnInit, OnDestroy {
#Input('swipeUp') actionUp: any;
#Input('swipeDown') actionDown: any;
private el: HTMLElement
private swipeGesture: Gesture
private swipeDownGesture: Gesture
constructor(el: ElementRef) {
this.el = el.nativeElement
}
ngOnInit() {
this.swipeGesture = new Gesture(this.el, {
recognizers: [
[Hammer.Swipe, {direction: Hammer.DIRECTION_VERTICAL}]
]
});
this.swipeGesture.listen()
this.swipeGesture.on('swipeup', e => {
this.actionUp()
})
this.swipeGesture.on('swipedown', e => {
this.actionDown()
})
}
ngOnDestroy() {
this.swipeGesture.destroy()
}
}
This code allows you to do something like this:
<div swipe-vertical [swipeUp]="mySwipeUpAction()" [swipeDown]="mySwipeDownAction()">
Just an update, Ionic now has gesture controls. see
http://ionicframework.com/docs/v2/components/#gestures
gestures return an $event object. You can probably use this data to check whether if it's a swipeup/swipedown event.
See $event screenshot (since I can't attach images yet ;) )