Set value in child component that uses ControlValueAccessor from parent component - javascript

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

Related

How to update object data of parent in child

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
};
}

Initial counter value not displaying on ChangeDetectionPush strategy

I am writing a simple counter. It has start,stop, toggle functionality in parent (app) and displaying changed value in child (counter) component using ChangeDetectionStrategy.OnPush.
Issue I am facing is not able to display initial counter value in child component on load.
Below are screenshot and code.
app.component.ts
import { Component } from '#angular/core';
import {BehaviorSubject} from 'rxjs';
#Component({
selector: 'app-root',
template: `<h1>Change Detection</h1>
<button (click)="start()">Start</button>
<button (click)="stop()">Stop</button>
<button (click)="toggleCD()">Toggle CD</button>
<hr>
<counter [data]="data$" [notifier]="notifier$"></counter>`,
})
export class AppComponent {
_counter = 0;
_interval;
_cdEnabled = false;
data$ = new BehaviorSubject({counter: 0});
notifier$ = new BehaviorSubject(false);
start() {
if (!this._interval) {
this._interval = setInterval((() => {
this.data$.next({counter: ++this._counter});
}), 10);
}
}
stop() {
clearInterval(this._interval);
this._interval = null;
}
toggleCD(){
this._cdEnabled = !this._cdEnabled;
this.notifier$.next(this._cdEnabled);
}
}
counter.component.ts
import {Component, Input, ChangeDetectionStrategy, OnInit, ChangeDetectorRef} from '#angular/core';
import {Observable} from 'rxjs/index';
#Component({
selector: 'counter',
template: `Items: {{_data.counter}}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent implements OnInit {
#Input() data: Observable<any>;
#Input() notifier: Observable<boolean>;
_data: any;
constructor(private cd: ChangeDetectorRef) {}
ngOnInit() {
this.data.subscribe((value) => {
/**
Below this._data.counter is showing 0 in console.log but
not in template
**/
this._data = value;
this.cd.markForCheck();
});
this.cd.detach();
this.notifier.subscribe((value) => {
if (value) {
this.cd.reattach();
} else {
this.cd.detach();
}
});
}
}
I'm using Angular 6.1.0
your AppComponent data$ is a BehaviorSubject, which you have given an initial value. your CounterComponent data expects an Observable, which you subscribe to. The defaulted BehaviorSubject does not fire until it changes. to get the value you have to query it upon load:
#Input() data: BehaviorSubject<any>;
ngOnInit() {
this._data = this.data.value; // get the initial value from the subject
this.data.subscribe((value) => {
this._data = value;
this.cd.markForCheck();
}
);
should do the trick.

Component props are not changed when function is called from a child component

When a method is called from a child component, the Propos of parent component are not changed in angular.io.
I have this method in dashboard.component:
listPeers(){
this.loading = true;
this.localStorage.getItem("payload").subscribe( (storage : string) =>{
this.jwt = storage;
this.userService.listPeers(this.jwt).subscribe( (res) => {
this.loading = false;
if(res.success){
console.log(res.peers)
this.peers = res.peers;
this.listUsersCrm();
}else{
this.snackBar.openFromComponent(ToastComponent, {
data: res.msg
});
this.router.navigate(['/notfound']);
}
})
})
}
And on "commit-changes.component" I call this method:
import { Component, OnInit} from '#angular/core';
import { DashboardComponent } from "../dashboard/dashboard.component";
#Component({
providers: [DashboardComponent],
selector: 'app-commit-changes',
templateUrl: './commit-changes.component.html',
styleUrls: ['./commit-changes.component.scss']
})
export class CommitChangesComponent implements OnInit {
constructor(private dashBoardComponent : DashboardComponent) { }
ngOnInit() {
}
discardChanges(){
this.dashBoardComponent.listPeers();
}
}
This method is already called, but the props from parent (dashboard.component) are not changed.
Note: The method listPeers is calling API to list peers stored on BD and set peers props.
this.peers = res.peers;
What's the correctly way to executed this method and apply changes of props?

Setting variables dynamically

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'

Why Pipe doesn't work correctly in Angular2?

The task is simple, it is necessary that the input was entered only numbers below a certain number. I did so:
export class MaxNumber implements PipeTransform{
transform(value, [maxNumber]) {
value = value.replace(/[^\d]+/g,'');
value = value > maxNumber?maxNumber:value;
return value;
}
}
and then in the template called something like:
<input type="text" [ngModel]="obj.count | maxNumber:1000" (ngModelChange)="obj.count=$event" />
But it works very strange click.
I probably misunderstand something. I would be grateful if someone will explain that behavior.
I think that you need rather a custom value accessor. This way you will be able to check the value before setting it in the ngModel. This way you obj.count won't be upper than 1000.
Here is a sample implementation:
const CUSTOM_VALUE_ACCESSOR = new Provider(
NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => MaxNumberAccessor), multi: true});
#Directive({
selector: 'input',
host: {'(input)': 'customOnChange($event.target.value)'},
providers: [ CUSTOM_VALUE_ACCESSOR ]
})
export class MaxNumberAccessor implements ControlValueAccessor {
onChange = (_) => {};
onTouched = () => {};
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
writeValue(value: any): void {
var normalizedValue = (value == null) ? '' : value;
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue);
}
customOnChange(val) {
var maxNumber = 1000;
val = val.replace(/[^\d]+/g,'');
val = val > maxNumber?maxNumber:val;
this.onChange(val);
}
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
There is nothing to do in your component to use it than setting this directive into its directives attribute:
#Component({
selector: 'my-app',
template: `
<div>
<h2>Hello {{name}}</h2>
Number: <input type="text" [(ngModel)]="obj.count" />
<p>Actual model value: {{obj.count}}</p>
</div>
`,
directives: [MaxNumberAccessor]
})
export class App {
(...)
}
Corresponding plunkr: https://plnkr.co/edit/7e87xZoEHnnm82OYP84o?p=preview.

Categories