I'm trying to detect changes on 'draft' Object from the parent component but ngOnChange() doesn't fire.
this is my try but it is not even enter the debugger
#Input() draft: Contribution;
ngOnChanges(changes: SimpleChanges) {
debugger
if (changes.draft && changes.draft.currentValue) {
this.loadDraft();
}
}
You could detect all the changes for draft in the child component where draft is declared, and then you could emit an EventEmitter to the parent notifying that like the following:
child component
#Component({
...,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnChanges {
#Input() draft!: Contribution;
#Output() change = new EventEmitter<Contribution>();
ngOnChanges(): void {
console.log('draft changed: ', this.draft);
this.change.emit(this.draft);
}
}
parent component
#Component({
...
template: '<app-child [draft]="data" (change)="onChildChange($event)"></app-child>'
})
export class ParentComponent {
data: Contribution = {...};
ngOnInit(): void {
// for triggering the ngOnChanges from the child
setTimeout(() => this.data = {...anotherObject }, 2500)
}
onChildChange(event: Contribution): void {
console.log('data changed...', event);
}
}
demo
Related
I hope not to get dom directly.Not use document.querySelector、ViewChild...
I need to create an Observable and mount the internal variables this.subscribe = subscribe to the component instance. I think this is not good, very jumping.
import { Component, OnDestroy, OnInit } from '#angular/core';
import { Observable, Subscriber, Subscription } from 'rxjs';
#Component({
selector: 'app-root',
template: '<button (click)="onClick()">button</button>',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
subscribe!: Subscriber<void>
subscription!: Subscription
count: number = 0
ngOnInit(): void {
this.subscription = new Observable<void>((subscribe) => {
this.subscribe = subscribe
})
.subscribe(() => {
console.log('update', ++this.count)
})
}
ngOnDestroy(): void {
this.subscription?.unsubscribe()
}
onClick() {
this.subscribe.next()
}
}
It looks like you haven't discovered Subject yet because you have sort of reinvented it :-)
Basically Subject is an object that you can subscribe to as an Observable, but you can also push values through by calling its .next() method.
export class AppComponent implements OnInit, OnDestroy {
click$ = new Subject<void>()
subscription!: Subscription
count: number = 0
ngOnInit(): void {
this.subscription = this.click$.subscribe(() => {
console.log('update', ++this.count)
})
}
ngOnDestroy(): void {
this.subscription?.unsubscribe()
}
onClick() {
this.click$.next()
}
}
You could define your count as an observable by using the scan operator like this:
export class AppComponent implements OnInit, OnDestroy {
subscription!: Subscription
private click$ = new Subject<void>()
count$: Observable<number> = this.click$.pipe(
scan(previous => previous + 1, 0),
tap(count => console.log('update', count))
)
ngOnInit(): void {
this.subscription = this.count$.subscribe()
}
ngOnDestroy(): void {
this.subscription?.unsubscribe()
}
onClick() {
this.click$.next()
}
}
In many cases you don't need to subscribe in your component, you can use the async pipe in your template instead. This alleviates the need to keep track of Subscription and also doesn't require implementing OnInit and OnDestroy:
export class AppComponent {
private click$ = new Subject<void>()
count$ = this.click$.pipe(
scan(previous => previous + 1, 0),
tap(count => console.log('update', count))
);
onClick() {
this.click$.next()
}
}
Then in your template, do something like:
<p> {{ count$ | async }} </p>
Well you already have an observable, (click) is an event emitter which extends an RxJs subject. So (click)="onClick()" is telling your component to subscribe to the click event emitter of the button with your onClick function, what extra observable do you need?
I have a parent component which observes child component's Output event emitter (topicsChanged).
Parent component:
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnInit,
Output
} from "#angular/core";
import { Language } from "../language";
import { TemplateTopic } from "../template-topic";
#Component({
selector: "at-main-topics",
templateUrl: "./main-topics.component.html",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MainTopicsComponent implements OnInit {
#Input() templates: string[] = [];
#Input() templateTopics: TemplateTopic[] = [];
#Input() languages: Language[] = [];
#Output() templateTopicChanged = new EventEmitter<TemplateTopic>();
constructor() {}
ngOnInit(): void {}
get availableTemplateTopics(): TemplateTopic[] {
return this.templates
.map(x => +x)
.map(template => {
const existingTopic = this.templateTopics.find(
x => x.template === template
);
return (
existingTopic ||
{ //observer will disappear for this empty created object.
template: template,
topics: []
}
);
});
}
onTopicsChanged(templateTopic: TemplateTopic) {
// This will not be triggered for 3rd template which is created in availableTemplateTopics getter, because it doesn't exist in initial data (templateTopics)
this.templateTopicChanged.emit(templateTopic);
}
}
<at-template-topic *ngFor="let templateTopic of availableTemplateTopics"
[templateTopic]="templateTopic"
[languages]="languages"
(topicsChanged)="onTopicsChanged($event)">
</at-template-topic>
In one strange case, this event emitter loses it's parent component's observer. That is - in child component I am opening a dialog. Before dialog is opened, if I inspect the emitter, the observer is there, but once the dialog is closed, observer is gone.
Child component:
import { Component, EventEmitter, Input, OnInit, Output } from '#angular/core';
import { MatDialog } from '#angular/material/dialog';
import { Language } from '../language';
import { TemplateTopic } from '../template-topic';
import { Topic } from '../topic';
import { TranslationDialogModel } from '../translation-dialog.model';
import { TranslationDialogComponent } from '../translation-dialog/translation-dialog.component';
#Component({
selector: 'at-template-topic',
templateUrl: './template-topic.component.html'
})
export class TemplateTopicComponent implements OnInit {
#Input() templateTopic: TemplateTopic;
#Input() languages: Language[] = [];
#Output() topicsChanged = new EventEmitter<TemplateTopic>();
private dialogTitle: string = 'lorem ipsum'
constructor(
private dialog: MatDialog
) { }
ngOnInit(): void {
}
onCreateTopic(): void {
this.openDialog();
}
onEditTopic(topic: Topic): void {
this.openDialog(topic);
}
private openDialog(topic?: Topic): void {
// this.topicsChanged always has observer at this point
const dialogRef = this.dialog.open(TranslationDialogComponent, {
data: {
pageTitle: this.dialogTitle,
availableLanguages: this.languages,
translations: topic?.translations
} as TranslationDialogModel
});
dialogRef
.beforeClosed()
.subscribe(translations => {
if (translations) {
if (topic) {
topic.translations = translations;
topic.title = translations[0].title;
} else {
this.templateTopic.topics.push({ translations, title: translations[0].title })
}
// When called via onCreateTopic method for a category which was created as an empty placeholder, this.topicsChanged has lost it's observer. However if category had initial data, then observer is still there.
this.topicsChanged.emit(this.templateTopic);
}
})
}
}
There is nothing shady going in the dialog, it simply returns some data and that's it. This is somehow connected to the getter get availableTemplateTopics in parent component from which list of child components are created. In getter there is a list of templates representing each child component which is either populated from already existing data or an empty placeholder is created. And the issue is with the empty placeholder objects.
Snippet:
get availableTemplateTopics(): TemplateTopic[] {
return this.templates
.map(x => +x)
.map(template => {
const existingTopic = this.templateTopics.find(
x => x.template === template
);
return (
existingTopic ||
{ //observer will disappear for this empty created object.
template: template,
topics: []
}
);
});
}
I found that I can solve all of this simply by moving the getter logic one level up, but I would still like to understand this weird behavior. How can observer disappear just like that and how is it connected to the getter?
Stackblitz for full code: https://stackblitz.com/edit/angular-kjewu7?file=src/app/main-topics/main-topics.component.ts
problem
TypeError: Cannot read property 'nativeElement' of undefined
component1
<div id="formkeyRefId" #formkeyRefId (click)="test.reloadContent(data.value)> </div>
<ng-container>
<app-component2 #test [data]="data.value" ></app-component2>
</ng-container>
</div>
export class Component1 implements OnInit, AfterViewInit {
#ViewChild('formkeyRefId', { static: false }) formkeyRefId: ElementRef;
constructor() { }
ngOnInit() { }
ngAfterViewInit() {
console.log(this.formkeyRefId.nativeElement);
this.formkeyRefId.nativeElement.click();
setTimeout(() => {
console.log(this.formkeyRefId.nativeElement.id);
this.formkeyRefId.nativeElement.click();
}, 5000);
}
component 2 app-component2
import { Component, Input, OnInit, Output, EventEmitter ,ElementRef, ViewChild,AfterViewInit } from '#angular/core';
export class Component2 implements OnInit,AfterContentInit {
#ViewChild('formkeyRefId',{static: false}) formkeyRefId: ElementRef;
constructor(public myElement: ElementRef){}
this.afterViewInitExecuted = false;
ngAfterContentInit() {
this.afterViewInitExecuted = true;
}
formEventEmitter(event) {
if (event && this.afterViewInitExecuted) {
setTimeout(() => {
console.log(this.formkeyRefId.nativeElement);
}, 5000);
//this.formkeyRefId.nativeElement.click();
//const el: HTMLElement = this.myDiv.nativeElement;
//e1.click();
}
reloadContent() {
getdata();
}
}
tried to test whether id exist in another component
ngAfterViewInit() {
this.afterViewInitExecuted = true;
console.log(this.formkeyRefId); Output Undefined
}
I Need to Trigger click function reloadContent in component 1 .
How to Access Id form another component using #formkeyRefId
I can access elementref in same component but not in another component.
any suggestion is most welcome.
It appears your click event is missing a closing quote.
Change:
(click)="test.reloadContent(data.value)
To:
(click)="test.reloadContent(data.value)"
I think the function is being executed before the lifecilycle AfterViewInit executes.
Try adding a flag indicating that the AfterViewInit have already been executed.
import { Component, Input, OnInit, Output, EventEmitter ,ElementRef, ViewChild,AfterViewInit } from '#angular/core';
export class Component2 implements OnInit {
#ViewChild('formkeyRefId',{static: false}) formkeyRefId: ElementRef;
prívate afterViewInitExecuted = false;
constructor(public myElement: ElementRef){}
ngAfterViewInit() {
this.afterViewInitExecuted = true;
}
formEventEmitter(event) {
if (event && this.afterViewInitExecuted) {
this.formkeyRefId.nativeElement.click();
//const el: HTMLElement = this.myDiv.nativeElement;
//e1.click();
}
reloadContent() {
getdata();
}
}
I'm trying to handle an event from NgLoopDirective within the method EV of NgDNDirective, by passing the EventEmitter object by reference and calling .subscribe() as described within the code below:
import { Directive, Input, ElementRef, Renderer2, EventEmitter } from '#angular/core';
#Directive({
selector: '[ngDN]'
})
export class NgDNDirective {
private dn: number = -1
private ev: EventEmitter<void>;
#Input() set ngDN(dn: number) {
this.dn = dn
}
#Input() set EV(ref: {ev: EventEmitter<void>}) {
console.log('waiting for ev')
ref.ev.subscribe(() => {
console.log('data-num:', this.dn)
this.renderer.setAttribute(this.elRef, 'data-num', this.dn.toString())
})
}
constructor(private elRef: ElementRef,
private renderer: Renderer2) {}
}
#Directive({
selector: '[ngLoop]'
})
export class NgLoopDirective {
#Input() set ngLoop(iter_count: number) {
this.container.clear()
for (let i=0; i<iter_count; i++) {
let ev: EventEmitter<void> = new EventEmitter<void>()
let ref = {ev: ev}
this.container.createEmbeddedView(this.template, {index: i, ev: ref})
ev.emit()
}
}
constructor(private template: TemplateRef<any>,
private container: ViewContainerRef) {}
}
This is the used HTML code:
<ng-template [ngLoop]="10" let-i="index" let-ref="ev">
</ng-template>
When I debug the code under the console - I get only this message displayed:
waiting for ev
meaning that the event was not handled successfully as console.log('data-num:', this.dn) wasn't called.
What's supposed to cause the problem?
Angular runs change detection for NgDNDirective after you have emitted event.
So either run change detection manually:
const view = this.container.createEmbeddedView(this.template, {index: i, ev: ref});
view.detectChanges();
Plunker Example
or use ReplaySubject instead of EventEmitter
Plunker Example
I have a parent component
#Component({
selector: 'mve-trace-multi-filter',
template: `
<child [itemsReady]="itemsReady$ | async"></child>
`
})
export class ParentComponent {
itemsReady$ : Subject<any> = new Subject();
ngOnInit() {
this.store.select('state').subscribe(data => this.itemsReady$.next(data));
}
}
And a child component with onPush
changeDetection: ChangeDetectionStrategy.OnPush
export class ChildComponent {
#Input() set itemsReady( items ) {
console.log('change', items);
}
}
}
The problem is that when the next() method is run, there is not change detection in the child component. I am excepting the child component to be updated because of the async pipe. What is wrong?
I saw the log as null in the first time but that's it.