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;
Related
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.
I'm making a pop-up component that I want to use in several of my other components, so I made a popup.service that enable the component to be loaded through *ngIf inside other components. This is creating a problem for me since the PopupComponent is a separate entity and I'm unsure how to pass data from the child component(PopupComponent) to its respective parents.
Atm the loading looks like this in ParentComponent.ts:
public openPopup(order_id: string, invoice_id: string): void{
this.load_popup=this.popupService.openPopup(order_id, "selected_order", invoice_id, "selected_invoice");
}
And ParentComponent.html:
<app-popup *ngIf="load_popup"></app-popup>
And it loads like a charm, the problem is in closing it. The close button is located on the PopupComponent, is there an efficient way to have the Child Component (PopupComponent) to affect a variable in the Parent Component ie. ParentComponent.load_popup=false?
My other thought was dynamically loading the component, however I have no idea on how to do that. I was fidgeting around with using the PopupService and putting something like this in it:
import { Injectable, ComponentRef } from '#angular/core';
import {PopupComponent} from '../popup/popup.component';
#Injectable({
providedIn: 'root'
})
export class PopupService {
popup_ref: ComponentRef<PopupComponent>
constructor(
) { }
//Implemented in orderoverviewcomponent, invoicecomponent, and placeordercomponent
public openPopup(id1:string, storage_label1:string, id2:string, storage_label2:string): Boolean{
if (id1){
localStorage.setItem(storage_label1, JSON.stringify(id1));
}
if (id2){
localStorage.setItem(storage_label2, JSON.stringify(id2));
}
this.popup_ref.initiate(); //this line is a made up example of loading the component
return true;
}
public closePopup(storage_label1: string, storage_label2:string): Boolean{
if(storage_label1){
localStorage.removeItem(storage_label1);
}
if(storage_label2){
localStorage.removeItem(storage_label2);
}
this.popup_ref.destroy();
return false;
}
}
Where this.popup_ref.destroy(); would ideally destroy PopupComponent, but when I did that I got a "cannot read property of undefined" on the popup_ref, I'm having trouble declaring it, the syntax seems a bit tricky.
The problem also remains that i need a function to load the component, the opposite of .destroy(), if this is possible I would much prefer it over loading and destroying with *ngIf.
Edit: Partially solved it by just using a boolean in the service as the trigger for *ngIf, is there a way to do a function load and destroy on a component still?
You can bind an EventEmitter() to your component to invoke a function in the parent component.
<app-popup [onClose]="load_popup = false" *ngIf="load_popup"></app-popup>
Then inside of your app-popup component:
#Output onClose = new EventEmitter();
public closePopup(/* code emitted for brevity */) {
/* code emitted for brevity */
this.onClose.emit(); //Call the parent function (in this case: 'load_popup = false')
}
It's important to know that you can pass entire functions to the bound function, and you can even pass variables back to the parent from the child:
[onClose]="myFunction($event)"
this.onClose.emit(DATA HERE);
As an aside, since you're using Angular; I would suggest looking into using Modals for popup dialogue boxes. You can see a good example here:
https://ng-bootstrap.github.io/#/components/modal/examples
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 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()