I am making custom component for dropdown. I have one config object which I am initializing in ngOnInit(), and I am combining the default configs and configs provided by user as an #Input(), But at run time from parent component, If I am making any changes in my config object, it is not updating in ngOnChanges() method of my child.
I tried this:
child component
#Input() config: MyConfig;
#Input() disabled: boolean
ngOnChanges() {
console.log('config', this.config); // this is not
console.log('disabled', this.disabled); // this is detecting
}
parent component html
<button (click)="changeConfig()">Change Config</button>
<app-child [config]="customConfig" [disabled]="newDisabled"></app-child>
parent component ts
newDisabled = false;
customConfig = {
value: 'code',
label: 'name',
floatLabel: 'Select'
};
changeConfig() {
this.customConfig.value = 'name';
this.newDisabled = true;
}
for disbale variable it is working, but for config it is not, Am I doing something wrong? please help
You problem is that you ngOnInit is setting the config variable to a new object. Since the #Input() is called once, this breaks your reference to the original object, and changes will not be detected.
You can fix this by using a setter and getter. I have added a stack blitz to demo this bellow.
Blockquote
parent component
import { ChangeDetectorRef, Component, VERSION } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
newDisabled = false;
customConfig = {
value: 'code',
label: 'name',
floatLabel: 'Select',
};
changeConfig1() {
this.customConfig.value = 'name1';
this.newDisabled = true;
console.log('Change Config 1');
}
changeConfig2() {
this.customConfig.value = 'name2';
this.newDisabled = true;
console.log('Change Config 2');
}
}
child component
import { Component, Input } from '#angular/core';
class MyConfig {}
#Component({
selector: 'hello',
template: `<h1> config: {{config | json}}</h1><h1> disabled: {{disabled}}</h1>`,
styles: [],
})
export class HelloComponent {
private _defaultConfig = {
key: 'default',
};
#Input() disabled: boolean;
private _config: MyConfig;
#Input() config: MyConfig;
set () {
if (!this.config) {
this.config = new MyConfig(); // it is a class
} else {
this._config = {
...this.config,
...this._defaultConfig,
};
}
}
get () {
return this._config;
}
ngOnChanges() {
console.log('config', this.config);
console.log('config', this._config);
console.log('disabled', this.disabled);
}
}
The problem is that the change detection is only triggered if the object customConfig is changed. In you example, only the value property is updated. What you can do is the following in the parent.component.ts:
changeConfig() {
this.customConfig = Object.assign(this.customConfig, { value: 'name'});
this.newDisabled = true;
}
This will create a new config object which contains the updated value property and all the other old properties of the old customConfig.
Input object are compared by reference, so if you want to reflect changes in your child component and trigger ngOnChanges do this:
changeConfig() {
this.customConfig = {...this.customConfig, value: 'name'};;
this.newDisabled = true;
}
And also move your below code from ngOnInit to ngOnChanges, chances are that at the time of initialisation input chagnes may not be available.
if (!this.config) {
this.config = new MyConfig(); // it is a class
} else {
this.config = {
...this._defaultConfig,
...this.config
};
}
Related
If I apply a child component (which uses ControlValueAccessor) in the parent and the write something in the child component, everything is passed on to the parent accordingly.
However if I try to write something in the parent component and then pass it on to the child component, nothing is available in the input. How can I fix this?
Just to be clear this is what the preferred behavior is supposed to be:
child string value = written in child
parent string value = written in child
but whenever I type in text in the parent input I get the following:
child string value = (empty)
parent string value = written in parent
[app.component.ts]
import { Component, VERSION } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
name = 'Angular ' + VERSION.major;
external = "";
}
[app.component.html]
<p>
Start editing to see some magic happen :)
</p>
<app-custom-input [(ngModel)]="external" name="externalVal"></app-custom-input>
<input [(ngModel)]="external"/>
external: {{ external }}
[custom-input.component.ts]
import { Component, forwardRef, HostBinding, Input, SimpleChanges } from "#angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "#angular/forms";
#Component({
selector: 'app-custom-input',
template: '<input [(ngModel)]="value"/>local: {{val}}',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
})
export class CustomInputComponent implements ControlValueAccessor {
onChange: any = () => {};
onTouch: any = () => {};
val = "";
set value(val) {
if (val !== undefined && this.val !== val) {
this.val = val;
this.onChange(val);
this.onTouch(val);
}
}
writeValue(value: any) {
this.value = value;
}
registerOnChange(fn: any) {
this.onChange = fn;
}
registerOnTouched(fn: any) {
this.onTouch = fn;
}
}
Also available at: https://stackblitz.com/edit/angular-ivy-ker4g5?file=src%2Fapp%2Fcustom-input%2Fcustom-input.component.ts
I think the problem is that you did not define a getter for value. So you'd have to modify your code as follows:
custom-input.component.ts
set value (val) {
/* ... */
}
get value () {
return this.val
}
This is a simplified example:
o = {
_n: null,
set name (v) {
this._n = v;
},
get name () {
return this._n;
}
}
// Setter
o.name = 'foo'
// Getter
// Without `get name()` -> `undefined`
o.name
I'm creating a reusable component which can be shown from any external component, but can be hidden using a function in same component, but somehow the property change in parent component is not updating child.
Here is the stackblitz for the same.
https://stackblitz.com/edit/angular-hfjkmu
I need "Show" button should show the component all the time and I can hide the component using "hide" button any time.
you need sync value from child to parent using Output
#Input()
show = false;
#Output()
showChange = new EventEmitter<boolean>();
constructor() { }
ngOnInit() {
}
hide(){
this.show = false;
this.showChange.emit(this.show);
}
<app-show-hide [(show)]="show"></app-show-hide>
The show property from child do not pointing to same prop in the parent comp, because it's primitive value.
I don't recommend to modify data that not belong to child component (reference type, eg: object, array), it can lead to unexpected behavior.
Online demo with reference type (be careful when modify ref type): https://stackblitz.com/edit/angular-vhxgpo?file=src%2Fapp%2Fshow-hide-obj%2Fshow-hide-obj.component.tsenter link description here
You have the problem because your child component modify Input value within your child component scope so no way parent component know the data is change
Your child component
export class ShowHideComponent implements OnInit {
#Input('show') show: boolean;
#Output() updateShowValue: EventEmitter<any> = new EventEmitter<
any
>();
constructor() { }
ngOnInit() {
console.log(this.show);
}
hide() {
this.updateShowValue.emit(!this.show);
}
}
In the app.component.html
<app-show-hide [show]="show" (updateShowValue)="update($event)"></app-show-hide>
And app.component.ts
export class AppComponent implements OnInit {
show:boolean = false;
ngOnInit() {
this.show = false;
console.log(this.show)
}
showComp(){
this.show = !this.show;
}
update(event) {
this.show = event;
}
}
You need to add an #Output in your child component, when you click the hide button (in the child component) you need to notify your parent component and change the value of show variable to false, this is done with the EventEmitter.
Changes to made are :
ShowHideComponent.ts
import { Component, OnInit, Input, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'app-show-hide',
templateUrl: './show-hide.component.html'
})
export class ShowHideComponent {
#Input('show') show : boolean;
#Output('') hideEE = new EventEmitter();
constructor() { }
hide(){
this.hideEE.emit(false);
}
}
AppComponent.ts
import { Component,OnInit } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html'
})
export class AppComponent {
show:boolean = false;
}
appComponent.html
<button type="button" (click)="show = true">Show</button>
<app-show-hide [show]="show" (hideEE)="show = $event"></app-show-hide>
stackblitz Link
I want to reload component after a button click from another component Angular 6.
As #MariyamMohammedJalil said you can use an EventEmitter to trigger the update of your first component.
See following sample:
first.component.ts
#Component({
selector: 'first-component',
template: '<div>{{label}}</label>
})
export class FirstComponent {
#Input() update: EventEmitter<string>;
label = 'First Component';
constructor() {}
ngOnInit() {
if (this.update) {
// Subscribe to the event emitter to receive an update event
this.update.subscribe((value: string) => {
this.refresh(value);
})
}
}
refresh(value: string) {
// Do your stuff here
this.label = value;
}
}
second.component.ts
#Component({
selector: 'second-component',
template: '<button (click)="updateFirstCmp()">Update First Component</button>'
})
export class SecondComponent {
#Input() update: EventEmitter<string>;
constructor(){}
updateFirstCmp() {
// Emit an event to update your first component
this.update.emit('Updated First Component');
}
}
And for example you should add following to your app.component.ts:
updateEventEmitter: EventEmitter<string>;
constructor() {
...
this.updateEventEmitter = new EventEmitter();
}
And in your app.component.html:
<first-component [update]="updateEventEmitter"></first-component>
<second-component [update]="updateEventEmitter"
Another way do solve your problem can be to enter the first.component as input parameter to the second.component to call the refresh function directly without EventEmitter. See following sample:
app.component.html
<first-component #firstComponent></first-component>
<second-component [firstCmp]="firstComponent"></second-component>
second.component.ts
#Component({
selector: 'second-component',
template: '<button (click)="updateFirstCmp()">Update First Component</button>'
})
export class SecondComponent {
#Input() firstCmp: FirstComponent;
constructor(){}
updateFirstCmp() {
// Update the first component directly
this.firstCmp.refresh('Updated First Component');
}
}
With this sample you don't need to subscribe to an update event, because you're not using an EventEmitter.
Is there a posibility to set variables dynamically?
My code looks like this. The if gets true but how do I (if possible) set the variable to true dynamically?
import { Component, OnInit, } from '#angular/core';
import {forEach} from "#angular/router/src/utils/collection";
#Component({
selector: 'app-menu',
templateUrl: './menu.component.html',
styleUrls: ['./menu.component.css']
})
export class MenuComponent implements OnInit {
menuContentSize = false;
menuContentBackground = false;
menuContentImages = false;
menuContentText = false;
menuContentFrame = false;
menuOptions: string[] = ['menuContentSize',
'menuContentBackground',
'menuContentImages',
'menuContentText',
'menuContentFrame'];
constructor() {
}
ngOnInit() {
}
menuOptionSelected(event){
this.menuOptions.forEach(function(element){
if(element == event){
// Set name of element(variable) to true
// In my dreamworld this.element = true; will be e.g. this.menuContentSize = true;
}
});
}
}
this.menuOptions.forEach(function(element){
needs to be
this.menuOptions.forEach((element) => {
if you want to use this to reference to the current component instance
See also https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
I'm not sure about the rest of your question.
I guess what you want is
this[element] = true;
which sets this.menuContentSize to true if element holds the string value 'menuContentSize'
I am trying to implement the tab view component of Prime NG. but my tabs are dynamic in nature ie.
So when the container is loaded it sends multiple AJAX requests for data inside the component.(Maybe the component is initialized multiple times?)
Another thing, in one of the components, moving mouse gives Thousands of errors on the console.
ERROR Error: Error trying to diff '[object Object]'. Only arrays and iterables are allowed
ERROR CONTEXT [object Object]
Not sure why. Used the same component in another place and there was no issue.
Even if I remove the dynamic nature of the components and just place 4 static tabs, everything works perfectly.(Right now the same 4 components are coming from server).
Html Template:
<div class="col-md-12 padding0">
<div class="tabViewWrapper">
<p-tabView (onChange)="handleChange($event)">
<p-tabPanel header="{{tab.tabName}}" *ngFor="let tab of tabs" >
<dynamic-component [componentData]="componentData"></dynamic-component>
</p-tabPanel>
</p-tabView>
<div>
</div>
Component:
#Component({
selector: 'tab-view',
templateUrl: './tab-view.component.html',
styleUrls: ['./tab-view.component.scss'],
encapsulation: ViewEncapsulation.None,
entryComponents: [GenericDataTableComponent, SingleEditCategoryExplorerComponent, AssetsDataTableComponent]
})
export class TabViewComponent implements OnInit {
private ngUnsubscribe: Subject<void> = new Subject<void>();
private componentData = null;
private tabs: Array<any>;
private index:number;
private disabledTabs:Array<any>;
private disabledTabsWhenMetaDataClicked:Array<any>;
versionConfig = {
url: AppSettingProperties.DATA_TABLE_VALUES.VERSIONS_URL,
dateLocale: AppSettingProperties.DATA_TABLE_VALUES.LOCALE,
header: AppSettingProperties.DATA_TABLE_VALUES.VERSIONS_HEADER
};
relatedConfig = {
url: AppSettingProperties.BASEURL + AppSettingProperties.DATA_TABLE_VALUES.RELATED_ENDPOINT,
header: AppSettingProperties.DATA_TABLE_VALUES.RELATED_HEADER
};
constructor(private assetDataLoadedService: AssetDataLoadedService, private assetDetailsService: AssetDetailsService, private assetDetailDataModel:AssetDetailDataModel) { }
#ViewChildren(DynamicContainerComponent) dynamicContainers: QueryList<DynamicContainerComponent>;
ngOnInit() {
this.disabledTabs = [];
//Set items to be disabled when Metadata button is clicked
this.disabledTabsWhenMetaDataClicked = [AppSettingProperties.TAB_RELATEDITEMS, AppSettingProperties.TAB_VERSIONS];
//Disable the tabs as per the condistions
this.disableTabsAsPerRequirement();
//Assigning tabs
this.tabs = this.assetDetailsService.systemTabs;
}
getInitialSelected(tab){
return this.selectedTab == this.tabs.indexOf(tab);
}
get selectedTab():number{
return this.index;
}
set selectedTab(val:number){
this.index = val;
var defaultTab = this.tabs[this.index]['tabName'];
if(!this.assetDetailDataModel.catalogId){
this.assetDataLoadedService.assetDetailPublisher.subscribe(data=>{
this.loadComponentByTab(defaultTab);
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
});
}
else{
this.loadComponentByTab(defaultTab);
}
}
handleChange(e) {
let tabName: string = e.originalEvent.currentTarget.innerText;
this.selectedTab = e.index;
//this.loadComponentByTab(tabName);
}
loadComponentByTab(tabName:string){
switch (tabName) {
case AppSettingProperties.TAB_METADATA:
this.componentData = { component: AssetsDataTableComponent, inputs: {} }
break;
case AppSettingProperties.TAB_CATEGORY:
let categoryConfig: object = {"catalog_id":this.assetDetailDataModel.catalogId,"item_id":this.assetDetailDataModel.assetId};
console.log(categoryConfig);
this.componentData = { component: SingleEditCategoryExplorerComponent, inputs: { tabConfig: categoryConfig } }
break;
case AppSettingProperties.TAB_RELATEDITEMS:
this.componentData = { component: GenericDataTableComponent, inputs: { tabConfig: this.relatedConfig } }
break;
case AppSettingProperties.TAB_VERSIONS:
this.componentData = { component: GenericDataTableComponent, inputs: { tabConfig: this.versionConfig } }
break;
}
}
}
Dynamic Component:
import { Component, Input, ViewContainerRef, ViewChild, ReflectiveInjector, ComponentFactoryResolver } from '#angular/core';
#Component({
selector: 'dynamic-component',
template: `<div #dynamicComponentContainer></div>`,
})
export class DynamicComponent {
private currentComponent = null;
#ViewChild('dynamicComponentContainer', { read: ViewContainerRef }) dynamicComponentContainer: ViewContainerRef;
constructor(private resolver: ComponentFactoryResolver) { }
// component: Class for the component you want to create
// inputs: An object with key/value pairs mapped to input name/input value
#Input() set componentData(data: { component: any, inputs: any }) {
console.log("Building Component Start");
if (!data) {
return;
}
// Inputs need to be in the following format to be resolved properly
let inputProviders = Object.keys(data.inputs).map((inputName) => { return { provide: inputName, useValue: data.inputs[inputName] }; });
let resolvedInputs = ReflectiveInjector.resolve(inputProviders);
// We create an injector out of the data we want to pass down and this components injector
let injector = ReflectiveInjector.fromResolvedProviders(resolvedInputs, this.dynamicComponentContainer.parentInjector);
// We create a factory out of the component we want to create
let factory = this.resolver.resolveComponentFactory(data.component);
// We create the component using the factory and the injector
let component = factory.create(injector);
// We insert the component into the dom container
this.dynamicComponentContainer.insert(component.hostView);
// We can destroy the old component is we like by calling destroy
if (this.currentComponent) {
this.currentComponent.destroy();
}
this.currentComponent = component;
console.log("Building Component Finish");
}
}
Another thing is that the console start in dynamic component is shown 8 times.
While console finish is shown 4-5 times.
Seems really weird behavior.
As #echonax wrote in comment.
This is because you are trying to iterate something that is not an array.
Most probably this.tabs.
You can try and write out {{tabs|json}} in a div instead of the *ngFor
Since your response takes sometime to load your DOM will have tabs variable as undefined array.
To solve this initialize the variable to an empty array as below
tabs:Array<any> = []
or inside the constructor as
constructor(){
this.tabs = [];
}