In a nutshell, I have a component which works as a text input. The parent component passes data into this component using @Input
bindings. When the change event gets fired via the @Output
binding, I perform some automatic validation in the parent component. This is to remove erroneous values and replace them with some sensible default for a better user experience.
Here is a basic version of the parent component
@Component({
selector: 'parent',
template: `
<div>
<child [value]="item.key1"
(valueChange)="onValueChange($event)">
</child>
</div>
`
})
export class Parent {
item = {
key1: 'test value',
key2: 'another test value'
};
onValueChange(newValue) {
// Perform some validation and set default
if (newValue.length > 4) {
newValue = newValue.substring(0, 4);
}
this.item.key1 = newValue;
}
}
And the child component
@Component({
selector: 'child',
template: `
<div>
<input type="text"
[(ngModel)]="value"
(blur)="onBlur($event)" />
</div>
`
})
export class Child {
@Input() value: string;
@Output() valueChange = new EventEmitter();
onBlur() {
this.valueChange.emit(this.value);
}
}
See here for a Plunker example
The issue I am having is as follows:
When entering a value in the child input and firing the blur
event, the new value is bubbled up to the parent and the validation is applied - if the validation causes the value to get modified, it bubbles back down to the child's value correctly - happy days.
However, if the first 4 characters stay the same, and you append additional characters, upon blur
the validation will still be applied and the parent's value will get updated correctly, but the child will preserve the "invalid" value - no more happy days.
So it looks to me like Angular isn't detecting the change in the parents data (fair enough because it hasn't technically changed) so it isn't sending the "latest" value back down to the child.
My question is, how can I get the child text input to always show the correct value from the parent, even if it technically hasn't "changed"?
better solution
@Jota.Toledo's good comment made me realise that my approach, although it did serve as a quick workaround for me at the time, it's not a good one, so I actually went and made some changes to my project that can work for you as well, also following his suggestion of
Delegating that "validation" logic into the child component
while keeping the validation definition in the parent as a function that's passed to the child as an @Input
param.
This way I'd give the parent 2 public vars
and change onValueChange
function to only update the item.key1 as it will be already validated.
In the child add a new @Input
param (validation) of type Function
and use that function to validate the newValue inside the onBlur, before emiting the value to the parent.
I have the feeling that what I've written here might "sound" a bit confusing so I'm adding the code for what I'm trying to explain.
Parent
@Component({
selector: 'parent',
template: `
<div>
<p><b>This is the parent component</b></p>
<child [value]="item.key1"
[validation]="validation"
(valueChange)="onValueChange($event)">
</child>
<p><b>Variable Value:</b> {{item | json}} </p>
</div>
`
})
export class Parent {
item = {
key1: 'test value',
key2: 'another test value'
};
validation = (newValue) => {
if (newValue.length > 4) {
newValue = newValue.substring(0, 4);
}
return newValue;
}
onValueChange(newValue) {
this.item.key1 = newValue;
}
}
Child (leaving the template part out because it's unchanged)
export class Child {
@Input() value: string;
@Input() validation: Function;
@Output() valueChange = new EventEmitter();
onBlur() {
this.value = this.validation(this.value)
this.valueChange.emit(this.value);
}
}
previous answer (not a good approach)
I had a similar problem in the past and the way I solved was to clear my var and then giving it the final value inside a setTimeout
without specifying the milliseconds.
in your parent component it would look something like this:
onValueChange(newValue) {
console.log('Initial: ' + newValue);
if (newValue.length > 4) {
newValue = newValue.substring(0, 4);
}
console.log('Final: ' + newValue);
this.item.key1 = '';
setTimeout(() => {
this.item.key1 = newValue;
});
}