Search code examples
javascriptangularangular-template

Angular parent to child binding change detection


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"?


Solution

  • 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

    • a object (item)
    • a function (validation)

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