I have a component which contains <ng-template #placeholder></ng-template> in its template to insert components dynamically. In OnInit a "loadComponents" method is called which uses the viewRef, but is unable on the second time it loads:
#ViewChild("placeholder", { read: ViewContainerRef, static: false })
private viewRef: ViewContainerRef;
private loadComponents() {
if (this.viewRef) {
this.viewRef.clear();
this.criteria.forEach((criterion) => {
if (Object.keys(this.componentMap).includes(criterion.type)) {
const componentFactory = this.cfr.resolveComponentFactory(
this.componentMap[criterion.type]
);
const componentRef = this.viewRef.createComponent(componentFactory);
}
});
}
}
This component is a form for a search component which it includes.
After starting the search, the search component shows the data below and has links to the detail views. But when I open the detail view and go back to the search page -- which holds the search criteria in a store -- the viewRef is undefined.
When I check it "later" with the Angular Tools then viewRef is defined. So I guess it is injected after OnInit.
Any ideas how to fix this?
I've got a solution. I created a new sub component which wraps the placeholder. I did not need ChangeDetection OnPush in the view ref. But the loadComponents not gets triggered in ngOnChanges.
Related
EDIT 2: This appears to be my general problem, and solution (using setTimeout so Angular's lifecycle can happen). I'll either close this or post an answer to my own question when I can.
See EDIT for a simpler repro that doesn't involve Subjects/Observables but is essentially the same problem.
I have a parent component that's responsible for fetching data from a service.
export class ParentComponent implements OnInit {
public mySubject: Subject<Foo[]> = new Subject<Foo[]>();
public buttonClicked = false;
private currentValues: Foo[] = null;
constructor(private SomeService myService) { }
this.myService.get().subscribe(values => {
this.mySubject.next(values); // Does NOT work when a child component is hidden, as expected.
this.currentValues = values; // Keep value so we can manually fire later.
});
private buttonClickHandler() {
this.buttonClicked = true;
this.mySubject.next(this.currentValues);
}
}
This data is subscribed to in the HTML by a child component. This component is hidden by default via *ngIf, and only becomes visible on a button click:
<app-child-component [values]="mySubject.asObservable()" *ngif="buttonClicked" />
In the parent component above you see I'm trying to pass the current available data to the child by invoking next() when the component is made visible in some way:
this.mySubject.next(this.currentValues);
This does not work when initially un-hiding the component via *ngIf. If I click the button a second time, which then calls next() again, then it works as expected. But when Angular is in the current context of un-hiding something, observables aren't getting their data. (This also happens when things are unhidden by other means, but the result is the same: If in the same method, the subject/data passing does not work; the component has to already be visible as of the method call.)
I'm guessing the binding to the observable is not happening until after *ngIf shows the child component, after the method call resolves. Is there some place I can hook into that I can then pass child data down?
EDIT for clarification: I don't believe this is an issue of Subject vs. BehaviorSubject. I'm not having issue passing the data. The issue is that the data-passing (confirmed via console.log()) is not occurring at all in the first place. It's not that the child component is receiving a null value. The subscription just isn't firing to the child.
I found I can reproduce this in a simpler fashion too: Trying to select an element in the DOM of *ngIf HTML reveals undefined if I make *ngIf's value true within the same Angular method.
<div *ngIf="buttonClicked">
<div id="someElement">Foo</div>
</div>
public someMethod(): void {
this.buttonClicked = true;
const container = document.getElementById('someElement'); // DOES NOT WORK if this.buttonClicked was false at the start of this method!
}
You going to need to use a BehaviourSubject instead of Subject, which emits the previously set value initially.
What is the difference between Subject and BehaviorSubject?
General
I have a problem. My component does not re-render without calling changeDetectorRef.markForCheck method.
I have an autocomplete. When input changes I send some async request (just simple HttpClient service and get method). After that, I fill in some internal state.
The code
Note markForCheck call. If I remove this line: nothing works. I noticed that if I remove it and if I click somewhere outside of the component, I see re-render. Right in time when I click somewhere component is re-rendered.
By the way, I realized that markForCheck is working by accident. I just tried something and it worked. I got info about CD mechanisms and CD service from some articles.
Here is my main component:
#Component({
selector: 'tags-auto-complete',
template: `
<tags-internal-auto-complete-input
// ....
(inputChanged)="onInputChange($event);"
></tags-internal-auto-complete-input>
<tags-internal-auto-complete-results
[data]="queryResultsTags"
// ....
></tags-internal-auto-complete-results>
`,
})
export class TagsAutoCompleteContainerComponent implements OnInit {
inputChanged = new Subject<string>();
queryResultsTags: Tag[] = [];
constructor(
private tagsService: TagsService,
private changeDetectorRef: ChangeDetectorRef,
) {}
onInputChange(query: string): void {
this.inputChanged.next(query);
}
ngOnInit() {
this.inputChanged
.filter(inputValue => inputValue.length > 0)
.debounceTime(400)
.switchMap(query => this.tagsService.getTagsList({ query }))
.do(() => this.changeDetectorRef.markForCheck()); // note this
.subscribe((tags: Tag[]) => (this.queryResultsTags = tags)) // here I change the input of inner component
}
// ...
Here is child component (tags-internal-auto-complete-results):
#Component({
selector: 'tags-internal-auto-complete-results',
template: `
<div class="container">
<span *ngFor="let tag of data" (click)="selectTag.emit(tag);" class="tag">
{{tag.name}}
</span>
</div>
`,
styleUrls: ['./results.styles.css'],
})
export class TagsAutoCompleteResultsComponent {
#Input() data: Tag[] = [];
#Output() selectTag = new EventEmitter<Tag>();
}
These are just fragments. Whole code is available on GitHub.
main component
inner component
By the way, I have another component (selected tags block) and I have input showLoader in it. It has exactly same problem.
My thoughts
Probably problem somehow connected to the zones mechanism. From some articles I know, that zone.js monkey-patches some events or XHR calls. And my case is XHR call (I didn't dive deep into HttpClient but it must just make an HTTP call).
What I want
I want to understand why changes are not detecting out of the box there (so I will use markForCheck and I will be ok) or I want to find a mistake in my code.
Hope you will help me there.
It's due to a ChangeDetectionStrategy.OnPush added on a parent component.
In that parent, if the references of his inputs don't change, his subtree components will not be checked for change.
In my Angular app, I have a component with a function that opens a dialog overlay. I am trying to figure out how to pass some data from the originating component to this dialog component (EnrollingProcessComponent). This is not a parent-child relationship, so I can't use Input() or [] binding here.
Also, because multiple instances could cause issues I won't get into here, we're not using a service to get and share data between components. So I can't inject a service to get the same instance in the dialog component (EnrollingProcessComponent) either.
So, all that said, I need to somehow pass this data (which is simply an email address) from the originating component to the dialog component. I assume I should be able to do this by passing it as a parameter, but so far I'm not able to get it to work (i.e., when I console out the value in the originating component, I get the value. But when consoling that value out in the dialog (EnrollingProcessComponent) component, I get 'undefined').
I use a click() event to open the dialog component:
<button *ngIf="canBeEnrolled()" md-menu-item
(click)="onContactClicked()">
<span class="md-menu-text">Initiate Contact</span>
</button>
And the function that's triggered looks like this:
public onContactClicked(primaryContactEmail): void {
primaryContactEmail = this.getPrimaryContactEmail();
console.log(this.getPrimaryContactEmail());
console.log('onContactClicked engaged on: ' + new Date());
// Create dialog
let dialogRef: MdDialogRef<EnrollingProcessComponent>
= this.dialog.open(EnrollingProcessComponent, {disableClose: true});
}
How can I pass the result of the getPrimaryContactEmail(), which is an email address, from the originating component to the component fired when the dialog opens?
You can pass values to the component instance via the data property of MdDialogConfig options like this:
primaryContactEmail = this.getPrimaryContactEmail();
let dialogRef: MdDialogRef<EnrollingProcessComponent>
= this.dialog.open(EnrollingProcessComponent, {
disableClose: true,
data: { primaryContactEmail: primaryContactEmail }
});
You would then need to inject MD_DIALOG_DATA into the component EnrollingProcessComponent component, which would allow you to access any passed data, in this case a property named primaryContactEmail:
import {MdDialogRef, MD_DIALOG_DATA} from '#angular/material';
#Component({
selector: 'example-dialog',
templateUrl: 'example-dialog.html',
})
export class DialogResultExampleDialog {
constructor(
public dialogRef: MdDialogRef<DialogResultExampleDialog>,
#Inject(MD_DIALOG_DATA) public data: any) {
console.log(this.data);
console.log(this.data.primaryContactEmail);
}
}
Here is a plunker demonstrating the functionality. Check the console when you open the dialog to see the data being loggable.
If you need to pass the value back to the parent component you could use md-dialog-close or close() to pass the value back up.
I've added closing the dialog from within the component via close(value: any) and passing a value to the parent calling component. Ignore the initial errors on load, those were present on the base unaltered example.
Hopefully that helps!
In my Angular2 app, on UI input a component is loaded which pulls data from a web service.
I want to reload the aptSearchComponent when the user input changes. Although the new data is fetched from the service base on the input, the component is not reloaded.
The input is in the headerComponent, when the user inputs some search criteria and hits enter, data is passed to the sharedService and routed to aptSearchComponent, where data is pulled from the shared service and results are displayed.
The headerComponent template stays at the top and the aptSearchcomponent template is displayed below it.
#Component({
selector: 'app-header',
template: `
<div class="mdl-textfield__expandable-holder">
<input class="mdl-textfield__input" type="text" id="search" (keyup.enter)="Search($event)">
</div>
`,
})
export class HeaderComponent {
public apartments: Object[];
constructor(private apartmentService: ApartmentService,private router: Router,private sharedService: SharedService) {
this.apartmentService=apartmentService;
this.sharedService=sharedService;
}
Search(event){
this.apartmentService.searchApt2(event.target.value).subscribe(res => {this.sharedService.temp = res
this.router.navigate(['AptSearch'])});
}
}
How can I reload the component in Angular 2. Basically the data in this.aptDetails is changed, but template is still shows the old data.
export class AptSearchComponent implements OnInit {
aptDetails: any;
constructor(private apartmentService: ApartmentService, private sharedService: SharedService,private zone:NgZone) {
this.apartmentService = apartmentService;
}
ngOnInit(){
this.aptDetails = this.sharedService.temp;
JSON.stringify(console.log(this.aptDetails)); //the data here is changed based on input, but the template is not refreshed and I still see the previous result.
}
}
I tried the following in constructor() but no luck
this.zone.run(()=>this.aptDetails=this.sharedService.temp);
I am using RC4, and polyfills in not imported.
I resolved this by using #Input and ngOnChanges() hook in the child component. will share the detailed answer if anybody needs it.
To reload it you can remove it with a simple trick.
Put an *ngIf on the component and set it to true initially.
When you want to remove it set it to false, and then using setTimeout flick it back to true instantly. This will remove it and then recreate it.
When you recreate it pass the new parameters you want to pass in from the parent component.
(Angular2 used to use this trick to reset a form, I'm not sure if a better way is available now but during RC this was the correct approach).
Change detection only work if the property reference changed.
You must reset aptDetails before updating it.
this.aptDetails = {} // or whatever type it is
this.aptDetails = this.sharedService.temp;
In angular docs there is a topic about listening for child events from parents. That's fine. But my purpose is something reverse!. In my app there is an 'admin.component' that holds the layout view of admin page (sidebar menu,task bar, status etc..).
In this parent component I configured router system for changing the main view between other pages of administrator.
The problem is for saving things after change, the user clicks on save button in task bar (that is placed in admin.component) and the child component must listen to that click event for doing save staff.
For the sake of posterity, just thought I'd mention the more conventional solution to this: Simply obtain a reference to the ViewChild then call one of its methods directly.
#Component({
selector: 'app-child'
})
export class ChildComponent {
notifyMe() {
console.log('Event Fired');
}
}
#Component({
selector: 'app-parent',
template: `<app-child #child></app-child>`
})
export class ParentComponent {
#ViewChild('child')
private child: ChildComponent;
ngOnInit() {
this.child.notifyMe();
}
}
I think that this doc could be helpful to you:
https://angular.io/docs/ts/latest/cookbook/component-communication.html
In fact you could leverage an observable / subject that the parent provides to its children. Something like that:
#Component({
(...)
template: `
<child [parentSubject]="parentSubject"></child>
`,
directives: [ ChildComponent ]
})
export class ParentComponent {
parentSubject:Subject<any> = new Subject();
notifyChildren() {
this.parentSubject.next('some value');
}
}
The child component can simply subscribe on this subject:
#Component({
(...)
})
export class ChildComponent {
#Input()
parentSubject:Subject<any>;
ngOnInit() {
this.parentSubject.subscribe(event => {
// called when the notifyChildren method is
// called in the parent component
});
}
ngOnDestroy() {
// needed if child gets re-created (eg on some model changes)
// note that subsequent subscriptions on the same subject will fail
// so the parent has to re-create parentSubject on changes
this.parentSubject.unsubscribe();
}
}
Otherwise, you could leverage a shared service containing such a subject in a similar way...
A more bare bones approach might be possible here if I understand the question correctly. Assumptions --
OP has a save button in the parent component
The data that needs to be saved is in the child components
All other data that the child component might need can be accessed from services
In the parent component
<button type="button" (click)="prop1=!prop1">Save Button</button>
<app-child-component [setProp]='prop1'></app-child-component>
And in the child ..
prop1:boolean;
#Input()
set setProp(p: boolean) {
// -- perform save function here
}
This simply sends the button click to the child component. From there the child component can save the data independently.
EDIT: if data from the parent template also needs to be passed along with the button click, that is also possible with this approach. Let me know if that is the case and I will update the code samples.
For those who are getting Cannot read property 'notifyMe' of undefined
Try calling the method inside ngAfterViewInit() intead of ngOnInit()