Quick overview
I have a "Terminal" Component which should be able to be used multiple times all over my application. This component should also be able to put into a "read only" mode where you pass a single command you'd like the terminal to fire and it will display the output. I am trying to have this component be able to be refreshed by many things; data updating else where, user events, etc. To achieve this, currently, I am using RxJS subjects as an #Input into the terminal component which when updated fires the subscribed functions. This works for the first user click (see bellow) but after that the subject doesn't update again. I suspect this is due to the "object" not updating, there for angular doesn't register the change and my whole idea falls apart.
Can I fix this? or do I need to redesign this "Terminal" component?
Code
terminal.component.ts
export class TerminalComponent implements OnInit, OnDestroy {
constructor() {}
$destroy = new Subject();
terminalOutput = '';
// Command input (if you want the terminal to only fire one command)
#Input() command = '';
$command: BehaviorSubject<string> = new BehaviorSubject('');
// Refresh terminal input
$refresh: Subject<void> = new Subject();
#Input() set refresh(value: Subject<void>) {
this.$refresh = value;
}
// ReadOnly Input
#Input() readOnly = false;
ngOnInit(): void {
this.$refresh
.pipe(
takeUntil(this.$destroy),
tap(() => {
const lastCommand = this.$command.getValue();
if (lastCommand) {
console.log('Refreshing, last command is:', lastCommand);
}
})
)
.subscribe();
//...
}
//...
}
parent.component.html
<h1>Home</h1>
<app-terminal command="ls" [refresh]="$refreshSubject"></app-terminal>
<button (click)="refreshTest()">Refresh</button>
parent.component.ts
export class ParentComponent implements OnInit {
$refreshSubject: Subject<void> = new Subject();
constructor() {}
ngOnInit(): void {}
refreshTest(): void {
console.log('Refreshing');
this.$refreshSubject.next();
}
}
So I found the problem, it was another bug in my code that was causing the RxJS tap to not fire after the first time.
ngOnInit(): void {
this.$refresh
.pipe(
takeUntil(this.$destroy),
tap(() => {
const lastCommand = this.$command.getValue();
if (lastCommand) {
console.log('Refreshing, last command is:', lastCommand);
throw new Error("Example Error");
// ^^^^ This will cause future Subject emits to not fire as this function has failed.
}
})
)
.subscribe();
//...
}
Side note: I think the question title should be changed to better suit the real problem with my code, and there for have better SEO. However if this is a completely rubbish question then I think it should be deleted.
So say i have page one:
This page contains multiple variables and a constructor. it could look something like this:
export class TestPage implements OnInit {
testInt: number;
testString: string;
constructor(private someService: SomeService) {
}
ngOnInit() {
this.testInt = this.someService.getInt();
this.testString = this.someService.getLongText();
}
}
Now when this page loads it correctly sets the values.
Now say that I change page and on this page, I change some of the values in the service.
When I then come pack to this TestPage it hasn't updated the values.
Does this have something to do with caching? or with push state?
How can I make sure that the page is "reloaded" ?
Try using RxJS.
#Injectable({...})
class SomeService {
private _testInt: BehaviorSubject<number> = new BehaviorSubject<number>(0); // initial value 0
setTestInt(value: number) {
this._testInt.next(value);
}
getTestInt(): Observable<number> {
return this._testInt.asObservable();
}
}
#Component({...})
class TestPage implements OnInit {
public testInt: number;
public testInt$: Observable<number>;
private subscription: Subscription;
constructor(private someService: SomeService) {}
ngOnInit() {
// one way
this.testInt$ = this.someService.getTestInt();
// or another
this.subscription = this.someService.getTestInt()
.subscribe((value: number) => {
this.testInt = value;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
in the HTML:
<p>{{ testInt }}</p>
<p>{{ testInt$ | async }}</p>
If you are subscribing to a Observable, make sure you unsubscribe after the usage (usually On Destroy lifecycle hook).
Async Pipe does that out of the box.
Or try the ionViewWillEnter lifecycle hook.
As you can see in the official documentation:
ngOnInit will only fire each time the page is freshly created, but not when navigated back to the page.
For instance, navigating between each page in a tabs interface will only call each page's ngOnInit method once, but not on subsequent visits.
ngOnDestroy will only fire when a page "popped". link That means that Page is cached, yes. Assigning value On Init will set the value only the first time page is visited and therefore not updated.
I created an Angular issue:
https://github.com/angular/angular/issues/20471
Also started a proposal in gist:
https://gist.github.com/matthewharwood/23ea18c8509b8056813d3c3e7df0d1b2
Hey I was messing with the
Angular/cdk directive which works outside of ngZones and #angular/animations. I was wonder if you could tell me if a particular layout thrashing optimization is taken into account? My secret weapon for performance has always been to use fastdom library for effects like parallax scrolling. I've yet to test perf because I have a feeling that angular already has methods similar too fastdom, that I just don't know about.
Question:
Does Angular have any method for scheduling measures and mutates of the DOM? If so, how? If not, would it be wise to implement fastdom into libraries like cdk and animations, if so again, could you give me some examples to help me communicate this better to the angular team?
Anyways describing the problem is a bit hard to explain; however, lets look at the fastdom and I'll move to angular afterwards.
Fastdom works like this:
fastdom.measure(() => {
console.log('measure');
});
fastdom.mutate(() => {
console.log('mutate');
});
fastdom.measure(() => {
console.log('measure');
});
fastdom.mutate(() => {
console.log('mutate');
});
Would output:
measure
measure
mutate
mutate
When looking at cdk:
https://sourcegraph.com/github.com/angular/material2#master/-/blob/src/cdk/scrolling/scrollable.ts
doesn't seem to have kind of measure wrapper unless ngZones is it.
ngOnInit() {
this._scrollListener = this._ngZone.runOutsideAngular(() => {
return this._renderer.listen(this.getElementRef().nativeElement, 'scroll', (event: Event) => {
this._elementScrolled.next(event);
});
});
this._scroll.register(this);
}
Test case:
Imagine you had a element on a page that would translate based on the current scroll position.
You could implement this in something like: (Pseudo code)
// index.html
<body [cdkScrollable]>
<some-component></some-component>
</body>
// some-component.ts
#Component({
selector: 'some-component'
})
export class SomeComponent implement OnInit, AfterViewInit {
#ViewChild(CdkScrollable) child: cdkScrollable;
private _el: any;
constructor(private _elementRef: ElementRef, private _renderer: Renderer2){}
ngOnInit() {
this._el = this._renderer.selectRootElement(this._elementRef);
}
ngAfterViewInit() {
this._makeAnimation()
}
private _makeAnimation() {
// first build the animation
const myAnimation = this._builder.build([
style({ top: 0 }),
animate(1000, style({ top: `${cdkScrollable. elementScrolled(). scrollTop + 100}px` }))
]);
// then create a player from it
const player = myAnimation.create(this._el.nativeElement);
player.play();
}
}
Over something like this if you used fast dom:
// this should need optimization to run outside of angular too.
#Component({
selector: 'some-other-component'
})
export class SomeOtherComponent {
private _el: any;
constructor(private _elementRef: ElementRef, private _renderer: Renderer2){}
public ngOnInit() {
this._el = this._renderer.selectRootElement(this._elementRef);
this.renderLoop_();
}
private renderLoop_() {
this.renderScrollBar_();
fd.measure(() => this.renderLoop_());
}
private renderScrollBar() {
fd.measure(() => {
const scroll = window.scrollY + 100;
fd.mutate(() => {
this._el.top = `${scroll}px`;
});
});
}
}
I'm pretty certain that this kind of optimization isn't included unless this._ngZone.runOutsideAngular is that optimization. Does anyone have any insight into the presence of optimization?
I am trying to use Publish and Subscribe from ionic-angular.
subscribe.js
this.events.subscribe('done:eventA', (userEventData) => {
//Perform some operations
this.startEventB();
this.events.unsubscribe('done:eventA'); <---
}
this.events.subscribe('done:eventB', (userEventData) => {
//Perform some operations
this.startEventA();
}
this.events.subscribe('done:eventA', (userEventData) => {
//Perform some operations
this.startEventC();
}
startEventB(){
this.events.publish('done:eventB', data);
}
startEventA(){
this.events.publish('done:eventA', data);
}
The first time Event A is publish I want to perform startEventB()
The second time Event A is publish I want to perform startEventC() so I tried to unsubscribe from the first part.
But when I unsubscribe, all my subscriptions are gone.
Can I know whats a good way to subscribe and unsubscribe from events?
If you don't pass a second parameter to the unsubscribe, all subscribers are removed. If you want to unsubscribe a given subscriber, you must subscribe and unsubscribe the very same object, as explained here.
Another solution I prefer, is to share a service between the components that has one of the RxJS subject types. The source class can publish using a service function that does next on the subject, and the target class can observe the stream obtaining the subject as an observable.
Here you have an example of this kind of service:
import {Injectable} from '#angular/core';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {Booking} from '../../common/model/booking';
#Injectable()
export class BookingEventsService
{
private _newChannel: Subject<Booking> = new Subject<Booking>();
constructor()
{
}
publishNew( booking: Booking )
{
this._newChannel.next( booking );
}
newChannel(): Observable<Booking>
{
return this._newChannel.asObservable();
}
}
Hope it helps. Regards,
Xavi
I have built an app that uses router 3.0.0-beta.1 to switch between app sections. I also use location.go() to emulate the switch between subsections of the same page. I used <base href="/"> and a few URL rewrite rules in order to redirect all routes to index.html in case of page refresh. This allows the router to receive the requested subsection as a URL param. Basically I have managed to avoid using the HashLocationStrategy.
routes.ts
export const routes: RouterConfig = [
{
path: '',
redirectTo: '/catalog',
pathMatch: 'full'
},
{
path: 'catalog',
component: CatalogComponent
},
{
path: 'catalog/:topCategory',
component: CatalogComponent
},
{
path: 'summary',
component: SummaryComponent
}
];
If I click on a subsection in the navigation bar 2 things happen:
logation.go() updates the URL with the necessary string in order to indicate the current subsection
A custom scrollTo() animation scrolls the page at the top of the requested subsection.
If I refresh the page I am using the previously defined route and extract the necessary parameter to restore scroll to the requested subsection.
this._activatedRoute.params
.map(params => params['topCategory'])
.subscribe(topCategory => {
if (typeof topCategory !== 'undefined' &&
topCategory !== null
) {
self.UiState.startArrowWasDismised = true;
self.UiState.selectedTopCategory = topCategory;
}
});
All works fine except when I click the back button. If previous page was a different section, the app router behaves as expected. However if the previous page/url was a subsection, the url changes to the previous one, but nothing happens in the UI. How can I detect if the back button was pressed in order to invoke the scrollTo() function to do it's job again?
Most answers I saw relly on the event onhashchange, but this event does not get fired in my app since I have no hash in the URL afterall...
I don't know if the other answers are dated, but neither of them worked well for me in Angular 7. What I did was add an Angular event listener by importing it into my component:
import { HostListener } from '#angular/core';
and then listening for popstate on the window object (as Adrian recommended):
#HostListener('window:popstate', ['$event'])
onPopState(event) {
console.log('Back button pressed');
}
This worked for me.
Another alternative for this issue would be to subscribe to the events emitted by the Angular Router service. Since we are dealing with routing, it seems to me that using Router events makes more sense.
constructor(router: Router) {
router.events
.subscribe((event: NavigationStart) => {
if (event.navigationTrigger === 'popstate') {
// Perform actions
}
});
}
I would like to note that popstate happens when pressing back and forward on the browser. So in order to do this efficiently, you would have to find a way to determine which one is occurring. For me, that was just using the event object of type NavigationStart which gives information about where the user is coming from and where they are going to.
To detect browser back button click
import platformlocation from '#angular/common and place the below code in your constructor :
constructor(location: PlatformLocation) {
location.onPopState(() => {
alert(window.location);
}); }
This is the latest update for Angular 13
You have to first import NavigationStart from the angular router
import { NavigationStart, Router } from '#angular/router';
Then add the following code to the constructor
constructor(private router: Router) {
router.events.forEach((event) => {
if(event instanceof NavigationStart) {
if (event.navigationTrigger === 'popstate') {
/* Do something here */
}
}
});
}
Angular documentation states directly in PlatformLocation class...
This class should not be used directly by an application developer.
I used LocationStrategy in the constructor
constructor(location: LocationStrategy) {
location.onPopState(() => {
alert(window.location);
});
}
A great clean way is to import 'fromEvent' from rxjs and use it this way.
fromEvent(window, 'popstate')
.subscribe((e) => {
console.log(e, 'back button');
});
Using onpopstate event did the trick:
window.addEventListener('popstate',
// Add your callback here
() => self.events.scrollToTopCategory.emit({ categId: self.state.selectedTopCategory })
);
I agree with Adrian Moisa answer,
but you can use "more Angular 2 way" using class PlatformLocation by injecting to your component or service, then you can define onPopState callback this way:
this.location.onPopState(()=>{
// your code here...
this.logger.debug('onpopstate event');
});
Simpler way - Link
import { PlatformLocation } from '#angular/common';
import { NgbModal } from '#ng-bootstrap/ng-bootstrap';
...
constructor(
private platformLocation: PlatformLocation ,
private modalService: NgbModal
)
{
platformLocation.onPopState(() => this.modalService.dismissAll());
}
I honestly don't know your use case. And the thread is quite old. But this was the first hit on Google. And if someone else is looking for this with Angular 2 and ui-router (just as you are using).
It's not technically detecting the back button. It's more detecting whether you as a developer triggered the state change or whether the user updated the URL themselves.
You can add custom options to state changes, this can be done via uiSref and via stateService.go. In your transitions, you can check whether this option is set. (It won't be set on back button clicks and such).
Using ui-sref
<a uiSref="destination-name" [uiOptions]="{custom: {viaSref: true}}">Bar</a>
Using state.go
import {StateService} from '#uirouter/core';
...
#Component(...)
export class MyComponent {
constructor(
private stateService: StateService
) {}
public myAction(): void {
this.stateService.go('destination-name', {}, {custom: {viaGo: true}});
}
}
You can detect it in any transition hook, for example onSucces!
import {Transition, TransitionOptions, TransitionService} from '#uirouter/core';
...
#Component(...)
export class MyComponent implements OnInit {
constructor(
private transitionService: TransitionService
) {}
public ngOnInit(): void {
this.transitionService.onSuccess({}, (transition: Transition) => this.handleTransition(Transition));
}
private handleTransition(transition: Transition): void {
let transitionOptions: TransitionOptions = transition.options();
if (transitionOptions.custom?.viaSref) {
console.log('viaSref!');
return;
}
if (transitionOptions.custom?.viaGo) {
console.log('viaGo!');
return;
}
console.log('User transition?');
}
}
You can check the size of "window.history", if the size is 1 you can't go back.
isCanGoBack(){
window.history.length > 1;
}