In our Angular 2 application we use the Combobox component from Kendo. This component is wrapped into a component created dynamically during the runtime. The code of the creation is very simple :
let factory = this.resolver
.resolveComponentFactory(ComboboxComponent);
nextTo.createComponent(factory);
The nextTo variable represents where Angular has to create the component.
#ViewChild('container', { read: ViewContainerRef })
container: ViewContainerRef;
The container variable represents a div in the HTML Template.
NB : the component is created during the ngAfterViewInit event. No errors are thrown during the creation.
The Kendo component is properly instanciated and initialized, but when we affect data after the creation, the component seems to don't recognize the binding, and do nothing. Any ideas ?
HTML of the component :
<kendo-combobox [data]="listItems"></kendo-combobox>
TypeScript :
#Component({
templateUrl: `combobox.component.html`,
selector: 'combobox',
styleUrls: [
'combobox.component.css'
]
})
export class ComboboxComponent extends FieldComponent {
public listItems: Array<string> = ["X-Small", "Small", "Medium", "Large", "X-Large", "2X-Large"];
}
NB2 : FieldComponent is an abstract class we use for global actions for all of our components.
EDIT1 : I finally manage to find what's the problem, but I can't say what's wrong. When I inspect the DOM, I can see that a <div role='combobox'> is hidden, and this is the combobox which contain all data. So why have I a second combobox shown with no data ?
I suspect that change detection for the component is not triggered in this case.
createComponent returns a ComponentRef which has the change detector associated with that component. You can try calling detectChanges() of that ChangeDetectorRef once the dynamic component is created.
I've found what cause this strange behavior. Due to the very first beginning of the project, we use Kendo JQuery for our components, and we use kendo.all.js. I don't really know why, but it seems that kendo.all.js interfers into the kendo-combobox HTML template of the new Angular component, and it inject some intempestive HTML which cause the strange behavior.
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
I have a parent component which has two templates (Stackblitz):
the first contains a simple data-bound text
the second contains a child component which updates data model (from its constructor and ngOnInit in this demo)
Here is the parent component template:
<ng-template [ngIf]="true">
<div>
<b>Value from TemplateComponent: </b>{{ dataService.value | async }}
</div>
</ng-template>
<ng-template [ngIf]="true">
<template-component></template-component>
</ng-template>
Both parent and child components are ChangeDetectionStrategy.OnPush and this is a requirement.
The issue is that the data updates starting from ngOnInit (and later ones) of the child component don't get picked up by the change detection in the parent component. This causes the parent component in the demo to display:
Value from TemplateComponent: value from constructor
instead of
Value from TemplateComponent: value from ngOnInit
since the CD picks up an update on constructor stage only, but not in ngOnInit and later:
export class TemplateComponent implements OnInit {
public constructor(public readonly dataService: DataService) {
this.dataService.setValue("value from constructor");
}
public ngOnInit(): void {
// This update is NOT picked up by CD in parent component
this.dataService.setValue("value from ngOnInit");
}
}
Except this approach with markForCheck(), I've also tried a simple #Output event in the child, but it obviously doesn't work, since the child is created via ng-template and actually is not a direct child of the parent component.
Could someone give an idea of what is the cleanest and Angular's OnPush-friendly approach for passing data to the parent from ng-template created control?
P.S.: By
the cleanest and Angular's OnPush-friendly approach
, I mean something that keeps all parts (components and service loosely-coupled), for example, without involving setTimeout() or other dirty fixes.
You've grasped the idea of using observables, async pipes and OnPush strategy, but the problem is that you have to go a little deeper and understand how angular component lifecycle events work.
Let's see what happens (simplified version)
Parent component constructor()
Child component constructor()
Parent component onInit() - evaluate #Input templates and ngIf directives
Child component onInit() - trigger changes to some properties used in parent
Parent afterViewInit()
Once onInit in child component is called, the directives in parent component were already evaluated! From angular perspective content inside [ngIf] template is already what it is suppose to be and there's need to run change detection again or update anything.
You shouldn't change data that is used in parent component from onInit() in child component, otherwise you will surely break something.
If you wait until all components are fully instantiated and trigger change, then all views will be updated.
eg. in your child component:
setTimeout(()=>this.dataService.setValue("hello"),1000)
That also works, if you try to markForCheck in ngAfterViewInit in your parent component
ngAfterViewInit(): void {
this.changeDetector.markForCheck();
}
or if you won't use *ngIf the value will be re-evaluated.
<div><b>Value from regular div: </b>{{ dataService.value | async }}</div>
All of mentioned workarounds are not advisable, just don't change data from child component's onInit and you will be fine.
I have a scenario where I need to pass a list of different "components" to another angular component. I'm passing the list of components as an array using the #Input. How can I render those components in the HTML?
It would be helpful to see some code to have an idea about what you're trying to accomplish.
From the documentation, #Input()
An Input property is a settable property annotated with an #Input decorator. Values flow into the property when it is data bound with a property binding
An #Input() annotation is used for passing data. It's not used for passing components to another component.
To display another component inside another component, reference the nested component's selector inside the parent component. The selector is part of component metadata
selector: A CSS selector that tells Angular to create and insert an instance of this component wherever it finds the corresponding tag in template HTML. For example, if an app's HTML contains , then Angular inserts an instance of the HeroListComponent view between those tags.
app-hero-list.component.ts
Create a component and give a selector in the metadata.
#Component({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
providers: [ HeroService ]
})
app.component.html
Then you can reference that selector as a pair of HTML tags in another component's template.
<app-hero-list></app-hero-list>
Have a look at the component metadata link referenced above and the Angular Tutorial for some more information about displaying components in other components.
If you need a more detailed answer, please update your question with a code example.
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;