[SalesForce] Lightning Components – SObject attribute change event only sent to descendants

I'm seeing some unexpected behaviour when passing an SObject attribute through multiple descendant components, in that when I update a field on the SObject the change event is fired for each descendant component, but not the ancestors.

I've dwindled this down to the following component nesting:

Level 1
-> Level 2
-> Level 3

I have two attributes – one an account SObject and one a regular String. I set the default value for these in my Level 1 component, and pass them as attributes to Level 2 and thence to Level 3.

Here are the three components and their controllers – they are pretty simple in terms of what they do and all provide much the same functionality:

Level 1 Component:

<aura:component implements="force:appHostable">
    <aura:attribute name="acc" type="Account"
                        default="{ 'sobjectType': 'Account',
                                'Name': 'Name'
                                 }" />
    <aura:handler name="change" value="{!v.acc.Name}" action="{!c.accChanged}"/>    
    <aura:handler name="change" value="{!v.str}" action="{!c.strChanged}"/>    

    <aura:attribute name="str" type="String" default="Str"/>
    <aura:handler name="change" value="{!v.items}" action="{!c.itemsChange}"/>
    <div>Level 1 name : {!v.acc.Name}</div>
    <div>Level 1 Str : {!v.str}</div>
    <div><ui:button press="{!c.changeName}" label="Press to Change"/></div>

    <bblightning:Level2 acc="{!v.acc}" str="{!v.str}" />
</aura:component>

Level 1 Controller:

({
    changeName : function(component, event, helper) {
        component.set('v.acc.Name', 'Name from 1!');
        component.set('v.str', 'Str from 1!');
    },
    accChanged : function (){
        console.log('Level 1 : Account changed');
    },
    strChanged : function (){
        console.log('Level 1 : String changed');
    }
})

Level 2 Component:

<aura:component >
    <aura:attribute name="acc" type="Account"/>
    <aura:attribute name="str" type="String"/>

    <aura:handler name="change" value="{!v.acc.Name}" action="{!c.accChanged}"/>    
    <aura:handler name="change" value="{!v.str}" action="{!c.strChanged}"/>    

    <div>Level 2 name : {!v.acc.Name}</div>
    <div>Level 2 Str : {!v.str}</div>
    <div><ui:button press="{!c.changeName}" label="Press to Change"/></div>

    <bblightning:Level3 acc="{!v.acc}" str="{!v.str}" />
</aura:component>

Level 2 Controller:

({
    changeName : function(component, event, helper) {
        component.set('v.acc.Name', 'Name from 2!');
        component.set('v.str', 'Str from 2!');
    },
    accChanged : function (){
        console.log('Level 2 : Account changed');
    },
    strChanged : function (){
        console.log('Level 2 : String changed');
    }

})

Level 3 Component:

<aura:component >
    <aura:attribute name="acc" type="Account"/>
    <aura:attribute name="str" type="String"/>

    <aura:handler name="change" value="{!v.acc.Name}" action="{!c.accChanged}"/>    
    <aura:handler name="change" value="{!v.str}" action="{!c.strChanged}"/>    

    <div>Level 3 name : {!v.acc.Name}</div>
    <div>Level 3 Str : {!v.str}</div>
    <div><ui:button press="{!c.changeName}" label="Press to Change"/></div>
</aura:component>

Level 3 Controller :

({
    changeName : function(component, event, helper) {
        component.set('v.acc.Name', 'Name from 3!');
        component.set('v.str', 'Str from 3!');
    },
    accChanged : function (){
        console.log('Level 3 : Account changed');
    },
    strChanged : function (){
        console.log('Level 3 : String changed');
    }

})

Accessing Level 1 shows the following output, which the default attributes are being passed correctly through at setup:

enter image description here

Pressing the level 1 button correctly updates the attributes and cascades down through the descendants:

enter image description here

and the console.log output shows the change events firing as expected:

enter image description here

However, reloading the page and pressing the Level 3 button only updates the String attribute in the ancestors, not the Account:

enter image description here

and the console.log output confirms that the change event for the Account object is not being received by the ancestors:

enter image description here

My initial thought was that the issue here was that I'm changing a field on a complex object which doesn't fire the change event as that is tied to the object reference itself, but that doesn't match up with the fact that it does fire to descendants through multiple levels.

I'm currently working around this by passing the individual fields through as attributes, but it means parent component(s) has to know more about the workings of the child component(s) than I'd like.

Best Answer

Thanks Bob.

I can confirm the issue you are reporting: change handlers are not fired up the chain when members of attribute type "object" are modified. Here is a workaround:

Change:

component.set('v.acc.Name', 'Name from 3!');

To:

var acc = component.get('v.acc');
acc.Name = 'Name from 3!';
component.set('v.acc', acc);

And everything will work.

Here is why you are seeing this behavior, and why the workaround is working:

  • The rule to remember: all changes are propagated down, but only propagated up when they affect the root of the attribute.

  • About the workaround: because component.get('v.acc') is returning a pointer to the object (this is JavaScript after all), we are not not copying the data, and it is not less efficient than calling component.get('v.acc.Name'). Sure it is more verbose, but not less efficient!

Related Topic