Passing void events from parent component to child component - javascript

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.

Related

Angular: How to check change in queryParams before ngOnDestroy is called

I would like to conditionally execute some code in ngOnDestroy based on changes in current route.
Route is changed from /foo to /login?logout=true, and this change is triggered outside of Foo component.
In ngOnInit I am subscribing to queryParam changes, to update correctly loggingOut flag.
My problem is, that ngOnDestroy is called before next handler of queryParam, so the loggingOut has incorrect value.
export class FooComponent implements OnInit, OnDestroy {
loggingOut = false;
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.route.queryParamMap.subscribe(queryParams => {
this.loggingOut = queryParams.get('logout') === 'true';
});
}
ngOnDestroy(): void {
if (this.loggingOut) {
// do this
} else {
// do that
}
}
}
Seems this is intended behavior from lifecycle POV, so have following question:
Is there a way to check route changes before ngOnDestory is called?
If possible, please add link to documentation describing, how are lifecycle hooks (especially ngOnDestory) called with respect to navigation changes?
Thanks.
My problem is, that ngOnDestroy is called before next handler of queryParam
componentDestroyed = false;
ngOnInit(): void {
this.route.queryParamMap.subscribe(queryParams => {
if (!this.componentDestroyed)
this.loggingOut = queryParams.get('logout') === 'true';
else {
// Do here what you wanted to do in ngOnDestroy
}
});
}
ngOnDestroy(): void {
this.componentDestroyed = true;
}
Would this fix your problem?

ngOnChanges do not display changes expected in a variable

i am trying to understand the callback ngOnChanges() so i created the below posted example. but at the compile time despite the interface Post
has values for its attributes title and content respectively, however, i do not receive any logs from ngOnChanges
please let me know how to use correctly
app.component.ts:
import { Component, OnInit, OnChanges, SimpleChanges,Output, EventEmitter } from '#angular/core';
export interface Post {
title:string;
content:string;
}
#Component({
selector: 'app-post-create',
templateUrl: './post-create.component.html',
styleUrls: ['./post-create.component.css']
})
export class PostCreateComponent implements OnInit {
#Output() post : Post;
#Output() onPostSubmittedEvtEmitter: EventEmitter<Post> = new EventEmitter<Post>();
constructor() {
this.post = {} as Post;
}
ngOnInit(): void {
}
ngOnChanges(changes: SimpleChanges) {
for (let changedProperty in changes) {
console.log("ngOnChanges->: changes[changedProperty].previousValue: " + changes[changedProperty].previousValue);
console.log("ngOnChanges->: changes[changedProperty].currentValue):" + changes[changedProperty].currentValue);
}
}
onSubmitPost(post: Post) {
this.post = {
title: this.post.title,
content: this.post.content
};
this.onPostSubmittedEvtEmitter.emit(this.post);
console.log("onSubmitPost->: post.title: " + post.title);
console.log("onSubmitPost->: post.content:" + post.content);
}
}
update 05.04.2021
as recommended i have added the ngOnChanges to observe changes in a prpoperty annotated with Input decorator as follows:
#Input() postsToAddToList: Post[] = [];
now, when I compile the code i add some values, i receive the following logs from ngOnChanges :
ngOnChanges->: changes[changedProperty].previousValue: undefined
post-list.component.ts:20 ngOnChanges->: changes[changedProperty].currentValue):
but the problem is when i keep adding more values, i do not receive any logs from the ngOnChanges
please let me know why despite i keep adding more values that result in changing the contents of the object that is decorated with #Input??!
post-list.component.ts:
import { Component, Input,OnInit, OnChanges, SimpleChanges,Output, EventEmitter } from '#angular/core';
import { Post } from '../post-create/post-create.component';
#Component({
selector: 'app-post-list',
templateUrl: './post-list.component.html',
styleUrls: ['./post-list.component.css']
})
export class PostListComponent implements OnInit {
constructor() {}
#Input() postsToAddToList: Post[] = [];
ngOnInit(): void {}
ngOnChanges(changes: SimpleChanges) {
for (let changedProperty in changes) {
console.log("ngOnChanges->: changes[changedProperty].previousValue: " + changes[changedProperty].previousValue);
console.log("ngOnChanges->: changes[changedProperty].currentValue):" + changes[changedProperty].currentValue);
}
}
}
ngOnChanges() only gets called when component's inputs changed from the parent component(fields that marked with #Input decorator). But you have #Output fields. The idea of ngOnChanges() is to react to changes that were done by the parent.
Following your business logic, you can handle whatever you want straight in onSubmitPost.
Answer for the update 05.04.2021
You add values to the array itself. Since the link to the array hasn't changed, ngOnChanges() does not catch these changes. But if you put new link to the component and do the following in the parent:
component:
this.yourArrInTheParent = [...this.yourArrInTheParent];
template:
<app-post-lis [postsToAddToList]="yourArrInTheParent"></app-post-lis>
Now value that you passed to the input changed and you will see the changes in the ngOnChanges(). The same goes for objects if you change object's property, angular won't see it as a change in ngOnChanges() since it only detects changes in #Input() values.
In order to catch those changes, you can use ngDoCheck hook. But it is power consuming, bear in mind not to perform heavy calculations there.
I think you are doing in correct way. Its just you missing to implement onChanges class. In latest Angular versions it straight throws error but in older version it does not.
Try this.
export class PostListComponent implements OnInit, OnChanges{
constructor() {}
#Input() postsToAddToList: Post[] = [];
ngOnInit(): void {}
ngOnChanges(changes: SimpleChanges) {
for (let changedProperty in changes) {
console.log("ngOnChanges->: changes[changedProperty].previousValue: " +
changes[changedProperty].previousValue);
console.log("ngOnChanges->: changes[changedProperty].currentValue):" +
changes[changedProperty].currentValue);
}
}
}
As already pointed out by #Vadzim Lisakovich
ngOnChanges() only gets called when component's inputs changed from
the parent component
Now, the thing is that the input is compared using === operator i.e. shallow comparison. If you add something to the post array, the reference to the array stays the same thus no event is triggered.
To fix that you can implement ngDoCheck() or replace the reference.
Here is a very similar question to yours:
Angular2 change detection: ngOnChanges not firing for nested object
And of cause the documentation:
https://angular.io/guide/lifecycle-hooks#docheck

angular 8 how to get and set values and access the value from any page? [duplicate]

This question already has an answer here:
How to share data between components using a service properly?
(1 answer)
Closed 2 years ago.
hello i am using angular 8 and i would like to know how can i access the set value in any page ?
my code
class.ts
export class testClass {
get test():string{
return this.sexe;
}
set test(val:string){
this.sexe = val;
}
}
in clild.ts
import { testClass } from '../class';
export class Child{
constructor (private test:testClass){}
test (){
this.test.test = "hello";
}
in parent.js
import { testClass } from '../class';
export class Parent{
constructor (private test:testClass){}
test (){
console.log(test.test);
}
}
in app.module.ts
import { testClass } from '../class';
providers: [testClass],
what am i doing wrang to get "test undifined" in parent.js
Not to sure what you mean by setting and getting the value in any page? I'm assuming you mean component?
If so I'd use a service like so
#Injectable({
providedIn: 'root'
})
export class ExampleService{
private _value: any;
private _valueObs$ = new BehaviorSubject(null);
set setValue(newValue: any): void{
this._value = newValue;
}
get getNewValue(): any{
return this._value;
}
set setObservableValue(newValue: any): void{
this._valueObs$.next(newValue)
}
get getNewObservableValue(): any{
return this._valueObs$;
}
}
There are two approaches in the above method, the first is a pretty standard set and get, the second is utilising something known as a Subject, I'll touch on the difference in the next section.
To then use this service in any component
#Component({
selector: 'example',
})
export class ExampleComponent implements OnInit {
newValue: any;
constructor(private readonly exampleService: ExampleService
) { }
ngOnInit(): void {
this.getObservableExampleValue();
}
getExampleServiceValue(): any {
this.exampleService.getNewValue;
}
setExampleServiceNewValue(value: any): void {
this.exampleService.setNewValue = value;
}
getObservableExampleValue() {
this.exampleService.getNewObservableValue.subscribe((newObsValue) => {
this.newValue = newObsValue
})
}
setObservableExampleValue(value: any): void{
this.exampleService.setObservableValue(value);
}
ngOnDestroy(){
this.exampleService.getNewObservableValue.unsubscribe();
}
}
So I wont go into detail on the standard setValue & getNewValue, you can invoke them how you see fit.
Now the second approach is great if you want several components to be aware of a particular value at one time, so lets say we set the _valueObs$ with the setObservableValue method, and we have used this service in 5 different components, all 5 of those components will receive that value, very handy right?
Now you'll notice it's important that we actually invoke the getNewObservableValue so we can open the stream, normally you'd do this on the ngOnInit so the components template/code can have access to the value, assuming your looking to use the value straight away, otherwise you can invoke it at a later date, the way subscribing/observable's work is a bit like a tap.
Imagine you have a tap, and you turn it on - Known as subscribing
this.exampleService.getNewObservableValue.subscribe((newObsValue) => {
this.newValue = newObsValue
})
Well the tap is turned on and now emits a stream of water or again in this case a stream of data, so every time you set a new value, the new piece of data will come through that stream and will automatically update the this.newValue within your component.
But it's also important to turn the tap off! We don't want to be wasting water when we are done using it, this is when we unsubscribe when the component is no longer being used so
ngOnDestroy(){
this.exampleService.getNewObservableValue.unsubscribe();
}
This is to prevent what is known as a memory leak, which is beyond the scope of this answer, know to learn more about Rxjs I'd read some documentation - https://www.learnrxjs.io/ or watch some youtube videos there are plenty of tutorials out there!
Hopefully I've explained comprehensively enough if not feel free to comment.
You have to use a service.
The services are initialized when the app starts, and remain so until it stops. Passing a value through a service allows you to access it anywhere you call the service.
So if you had the following:
#Injectable()
export class ExampleService {
public varIWant: string = 'I wan't to use this anywhere.'
}
You can access it in your components, by doing:
import { ExampleService } from '../my/path/to/service'
export class Parent {
constructor(private exampleService: ExampleService) { }
public setVarAsLocal: string = this.exampleService.varIWant;
public changeServiceVariable() {
this.setVarAsLocal = 'New Value For String';
this.exampleService.varIWant = this.setVarAsLocal;
}
}
And that's it. As long as the instance is running the value will hold;

ionic page data does not update

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.

NgOnChanges overrides form control value when user types

I have autocomplete form control:
#Component({
selector: 'app-autocomplete',
templateUrl: './app-autocomplete.view.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AutoCompleteFilterComponent implements OnInit, OnDestroy, OnChanges {
#Input() value: any;
#Output() onChanged = new EventEmitter();
autoCompleteControl: FormControl = new FormControl();
private autoCompleteControlSubscription: Subscription;
constructor() { }
ngOnInit(): void {
this.autoCompleteControl.setValue(this.value, { emitEvent: false });
this.autoCompleteControlSubscription = this.autoCompleteControl.valueChanges
.pipe(
skipUndefined(),
filter(value => value.length >= 3),
distinctUntilChanged(),
debounceTime(350),
map(value => {
this.onChanged.emit(value.trim());
})
).subscribe();
}
ngOnChanges(changes: SimpleChanges): void {
if (!changes.value.firstChange) {
this.autoCompleteControl.setValue(changes.value.currentValue);
}
}
ngOnDestroy(): void {
if (this.autoCompleteControlSubscription) {
this.autoCompleteControlSubscription.unsubscribe();
}
}
I get initial value from store and pass it as #Input variable:
this.value$ = this._store$.select(s=>s.value);
<app-autocomplete [value]="value$ | async"></app-autocomplete>
The problem that I ran into is:
Component loads and I pass initial value from the store.
User types something in input text field.
User stops typing for 350ms (debounce time).
I emit value to the parent and use an Action + Reducer to keep the value in the store.
this.value$ Observable reacts on store change and triggers ngOnChange method.
User continue typing.
Value from the store overwrites what user has already typed.
For example user typed "stri", then made short pause, then typed "string", but "store" value overwrites his "string" value and he got "stri" value that I put into the "store" before.
Has anyone come across this before? The only solution we come up with is to check the focus and don't set new value.
You're subscribing to changes in ngOnInit:
this.autoCompleteControlSubscription = this.autoCompleteControl.valueChanges
And ngOnChanges too.
this.autoCompleteControl.setValue(changes.value.currentValue);
I'm going to take a shot at what you're trying to do:
On init you may want to patchValue and then setup the subscription so they do not interfere with each other.
If you want to patch value without triggering the form's valueChanges then patch without event:
this.form.controls[control].patchValue(value, { emitEvent: false });
Take a look at how I'm doing this with my own ControlValueAccessor control component on StackBlitz. I set the initial control value (if there is any) with writeValue

Categories